diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index 6b4cdb1..b7b2a21 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -69,6 +69,7 @@ class Exporter: self.report.progress_add_step("Collecting Objects") self.report.progress_add_step("Verify Competence") self.report.progress_add_step("Touching the Intangible") + self.report.progress_add_step("Unifying Superstrings") self.report.progress_add_step("Harvesting Actors") if self._op.lighting_method != "skip": etlight.LightBaker.add_progress_steps(self.report) @@ -98,6 +99,9 @@ class Exporter: # In other words, generate any ephemeral Blender objects that need to be exported. self._pre_export_scene_objects() + # Step 2.3: Run through all the objects and export localization. + self._export_localization() + # Step 2.5: Run through all the objects we collected in Step 2 and see if any relationships # that the artist made requires something to have a CoordinateInterface self._harvest_actors() @@ -248,6 +252,18 @@ class Exporter: return ci return so.coord.object + def _export_localization(self): + self.report.progress_advance() + self.report.progress_range = len(self._objects) + inc_progress = self.report.progress_increment + + self.report.msg("\nExporting localization...") + + for bl_obj in self._objects: + for mod in filter(lambda x: hasattr(x, "export_localization"), bl_obj.plasma_modifiers.modifiers): + mod.export_localization(self) + inc_progress() + def _export_scene_objects(self): self.report.progress_advance() self.report.progress_range = len(self._objects) diff --git a/korman/exporter/locman.py b/korman/exporter/locman.py index 8439a76..13418b7 100644 --- a/korman/exporter/locman.py +++ b/korman/exporter/locman.py @@ -16,6 +16,7 @@ import bpy from PyHSPlasma import * +from collections import defaultdict from contextlib import contextmanager import itertools from pathlib import Path @@ -45,24 +46,16 @@ class LocalizationConverter: self._age_name = kwargs.get("age_name") self._path = kwargs.get("path") self._version = kwargs.get("version") - 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() - return True - - 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 - if self._exporter is not None and self._exporter().mgr.getVer() <= pvPots: - return False - return True + self._strings = defaultdict(lambda: defaultdict(dict)) + + def add_string(self, set_name, element_name, language, value, indent=0): + self._report.msg("Accepted '{}' translation for '{}'.", element_name, language, indent=indent) + if isinstance(value, bpy.types.Text): + if value.is_modified: + self._report.warn("'{}' translation for '{}' is modified on the disk but not reloaded in Blender.", + element_name, language, indent=indent) + value = value.as_string() + self._strings[set_name][element_name][language] = value @contextmanager def _generate_file(self, filename, **kwargs): @@ -96,8 +89,9 @@ class LocalizationConverter: stream.write(contents.encode("windows-1252", "replace")) return True - for journal_name, translations in self._journals.items(): - self._report.msg("Copying Journal '{}'", journal_name, indent=1) + locs = itertools.chain(self._strings["Journals"].items(), self._strings["DynaTexts"].items()) + for journal_name, translations in locs: + self._report.msg("Copying localization '{}'", 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.", @@ -121,30 +115,20 @@ class LocalizationConverter: self._report.port("No 'English' nor any other suitable default translation available", indent=2) def _generate_loc_files(self): - set_LUT = { - "Journals": self._journals - } - - # Merge in any manual strings, but error if dupe sets are encountered. - special_sets, string_sets = frozenset(set_LUT.keys()), frozenset(self._strings.keys()) - intersection = special_sets & string_sets - assert not intersection, "Duplicate localization sets: {}".format(" ".join(intersection)) - set_LUT.update(self._strings) - - if not any(itertools.chain.from_iterable(set_LUT.values())): + if not self._strings: return method = bpy.context.scene.world.plasma_age.localization_method if method == "single_file": - self._generate_loc_file("{}.loc".format(self._age_name), set_LUT) + self._generate_loc_file("{}.loc".format(self._age_name), self._strings) elif method in {"database", "database_back_compat"}: # Where the strings are set -> element -> language: str, we want language -> set -> element: str # This is so we can mimic pfLocalizationEditor's English.loc pathing. - database = {} - for set_name, elements in set_LUT.items(): + database = defaultdict(lambda: defaultdict(dict)) + for set_name, elements in self._strings.items(): for element_name, translations in elements.items(): for language_name, value in translations.items(): - database.setdefault(language_name, {}).setdefault(set_name, {})[element_name] = value + database[language_name][set_name][element_name] = value for language_name, sets in database.items(): self._generate_loc_file("{}{}.loc".format(self._age_name, language_name), sets, language_name) @@ -200,7 +184,7 @@ class LocalizationConverter: 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("Harvesting Translations") self._report.progress_add_step("Generating Localization") self._report.progress_start("Exporting Localization Data") @@ -212,20 +196,23 @@ class LocalizationConverter: self._report.raise_errors() def _run_harvest_journals(self): + from ..properties.modifiers import TranslationMixin + 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) + for mod_type in filter(None, (getattr(j, "pl_id", None) for j in TranslationMixin.__subclasses__())): + modifier = getattr(i.plasma_modifiers, mod_type) + if modifier.enabled: + translations = [j for j in modifier.translations if j.text_id is not None] + if not translations: + self._report.error("'{}': No content translations available. The localization will not be exported.", + i.name, indent=2) + for j in translations: + self.add_string(modifier.localization_set, modifier.key_name, j.language, j.text_id, indent=1) inc_progress() def _run_generate(self): diff --git a/korman/properties/modifiers/base.py b/korman/properties/modifiers/base.py index 247d2df..3ed56b1 100644 --- a/korman/properties/modifiers/base.py +++ b/korman/properties/modifiers/base.py @@ -57,6 +57,15 @@ class PlasmaModifierProperties(bpy.types.PropertyGroup): """ pass + # Commented out to prevent conflicts with TranslationMixin overload. + """ + def export_localization(self, exporter): + '''This is an auxiliary export phase that should only convert localization data. PRP objects + are in an undefined state and therefore should not be used. + ''' + pass + """ + @property def face_sort(self): """Indicates that the geometry's faces should be sorted by the engine""" diff --git a/korman/properties/modifiers/gui.py b/korman/properties/modifiers/gui.py index 53b15c0..d4921d2 100644 --- a/korman/properties/modifiers/gui.py +++ b/korman/properties/modifiers/gui.py @@ -123,6 +123,15 @@ class PlasmaJournalTranslation(bpy.types.PropertyGroup): class TranslationMixin: + def export_localization(self, exporter): + translations = [i for i in self.translations if i.text_id is not None] + if not translations: + exporter.report.error("'{}': '{}' No content translations available. The localization will not be exported.", + self.id_data.name, self.bl_label, indent=1) + return + for i in translations: + exporter.locman.add_string(self.localization_set, self.key_name, i.language, i.text_id, indent=1) + 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.translations) @@ -153,6 +162,10 @@ class TranslationMixin: else: self.active_translation_index = idx + @property + def localization_set(self): + raise RuntimeError("TranslationMixin subclass needs a localization set getter!") + @property def translations(self): raise RuntimeError("TranslationMixin subclass needs a translation getter!") @@ -214,15 +227,6 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz 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: with utils.bmesh_object("{}_Journal_ClkRgn".format(self.key_name)) as (rgn_obj, bm): bmesh.ops.create_cube(bm, size=(6.0)) @@ -305,12 +309,16 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz locpath = nodes.new("PlasmaAttribStringNode") locpath.link_output(journalnode, "pfm", "LocPath") - locpath.value = "{}.Journals.{}".format(age_name, self.key_name) + locpath.value = "{}.{}.{}".format(age_name, self.localization_set, self.key_name) guitype = nodes.new("PlasmaAttribStringNode") guitype.link_output(journalnode, "pfm", "GUIType") guitype.value = self.book_type + @property + def localization_set(self): + return "Journals" + @property def requires_actor(self): # We are too late in the export to be harvested automatically, so let's be explicit diff --git a/korman/properties/modifiers/render.py b/korman/properties/modifiers/render.py index bf686a9..a5dbb15 100644 --- a/korman/properties/modifiers/render.py +++ b/korman/properties/modifiers/render.py @@ -702,11 +702,6 @@ class PlasmaLocalizedTextModifier(PlasmaModifierProperties, PlasmaModifierLogicW def pre_export(self, exporter, bo): yield self.convert_logic(bo, age_name=exporter.age_name, version=exporter.mgr.getVer()) - def export(self, exporter, bo, so): - # TODO: This should probably be pulled out into its own export pass for locs - for i in filter(None, self.translations): - exporter.locman.add_string("DynaTexts", self.key_name, i.language, i.text_id, indent=2) - def logicwiz(self, bo, tree, *, age_name, version): # Rough justice. If the dynamic text map texture doesn't request alpha, then we'll want # to explicitly clear it to the material's diffuse color. This will allow artists to trivially @@ -722,7 +717,7 @@ class PlasmaLocalizedTextModifier(PlasmaModifierProperties, PlasmaModifierLogicW 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) - loc_path = self.key_name if version <= pvPots else "{}.DynaTexts.{}".format(age_name, self.key_name) + 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", target_object=bo, material=material, texture=self.texture) @@ -745,6 +740,10 @@ class PlasmaLocalizedTextModifier(PlasmaModifierProperties, PlasmaModifierLogicW self._create_python_attribute(pfm_node, "clearColorB", value=clear_color[2]) self._create_python_attribute(pfm_node, "clearColorA", value=1.0) + @property + def localization_set(self): + return "DynaTexts" + def sanity_check(self): if self.texture is None: raise ExportError("'{}': Localized Text modifier requires a texture", self.id_data.name)