diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py
index fbc7726..a10f0a8 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 979fe4a..13418b7 100644
--- a/korman/exporter/locman.py
+++ b/korman/exporter/locman.py
@@ -14,17 +14,26 @@
# along with Korman. If not, see .
import bpy
+from PyHSPlasma import *
+
+from collections import defaultdict
from contextlib import contextmanager
+import itertools
+from pathlib import Path
+import re
+from xml.sax.saxutils import escape as xml_escape
+import weakref
+
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
_SP_LANGUAGES = {"English", "French", "German", "Italian", "Spanish"}
+# Detects if there are any Plasma esHTML tags in the translated data. If so, we store
+# as CDATA instead of XML encoding the entry.
+_ESHTML_REGEX = re.compile("<.+>")
+
class LocalizationConverter:
def __init__(self, exporter=None, **kwargs):
if exporter is not None:
@@ -37,20 +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 = {}
+ self._strings = defaultdict(lambda: defaultdict(dict))
- 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 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):
@@ -68,24 +73,25 @@ class LocalizationConverter:
finally:
handle.close()
- def _generate_journal_texts(self):
+ def _generate_text_files(self):
age_name = self._age_name
- def write_journal_file(language, file_name, contents):
- try:
- with self._generate_file(dirname="ageresources", filename=file_name) as stream:
+ def write_text_file(language, file_name, contents):
+ with self._generate_file(dirname="ageresources", filename=file_name) as stream:
+ try:
stream.write(contents.encode("windows-1252"))
- except UnicodeEncodeError:
- self._report.warn("Translation '{}': Contents contains characters that cannot be used in this version of Plasma. They will appear as a '?' in game.",
- language, indent=2)
-
- # Yes, there are illegal characters... As a stopgap, we will export the file with
- # replacement characters ("?") just so it'll work dammit.
- 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)
+ except UnicodeEncodeError:
+ self._report.warn("Translation '{}': Contents contains characters that cannot be used in this version of Plasma. They will appear as a '?' in game.",
+ language, indent=2)
+
+ # Yes, there are illegal characters... As a stopgap, we will export the file with
+ # replacement characters ("?") just so it'll work dammit.
+ stream.write(contents.encode("windows-1252", "replace"))
+ return True
+
+ 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.",
@@ -93,7 +99,7 @@ class LocalizationConverter:
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)
+ write_text_file(language_name, file_name, value)
# Ensure that default (read: "English") journal is available
if "English" not in translations:
@@ -102,55 +108,69 @@ class LocalizationConverter:
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):
+ if write_text_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:
+ def _generate_loc_files(self):
+ 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), 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 = 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[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)
+
+ # Generate an empty localization file to defeat any old ones from Korman 0.11 (and lower)
+ if method == "database_back_compat":
+ self._generate_loc_file("{}.loc".format(self._age_name), {})
+ else:
+ raise RuntimeError("Unexpected localization method {}".format(method))
+
+ def _generate_loc_file(self, filename, sets, language_name=None):
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"))
+ stream.write(line.encode("utf-8"))
- age_name = self._age_name
- enc = plEncryptedStream.kEncAes if self._version == pvEoa else None
- file_name = "{}.loc".format(age_name)
- with self._generate_file(file_name, enc=enc) as stream:
- # UTF-16 little endian byte order mark
- stream.write(b"\xFF\xFE")
+ def iter_element(element):
+ if language_name is None:
+ yield from element.items()
+ else:
+ yield language_name, element
- write_line("")
+ enc = plEncryptedStream.kEncAes if self._version == pvEoa else None
+ with self._generate_file(filename, enc=enc) as stream:
+ write_line("")
write_line("")
- write_line("", age_name, indent=1)
+ write_line("", self._age_name, indent=1)
- # Arbitrary strings defined by something like a GUI or a node tree
- for set_name, elements in self._strings.items():
+ for set_name, elements in sets.items():
write_line("", set_name, indent=2)
- for element_name, translations in elements.items():
+ for element_name, value 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():
+ for translation_language, translation_value in iter_element(value):
+ if _ESHTML_REGEX.search(translation_value):
+ encoded_value = "".format(translation_value)
+ else:
+ encoded_value = xml_escape(translation_value)
write_line("{translation}",
- language=language_name, translation=xml_escape(value), indent=4)
+ language=translation_language, translation=encoded_value, indent=4)
write_line("", indent=3)
write_line("", indent=2)
@@ -164,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")
@@ -176,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):
@@ -198,6 +221,6 @@ class LocalizationConverter:
def save(self):
if self._version > pvPots:
- self._generate_loc_file()
+ self._generate_loc_files()
else:
- self._generate_journal_texts()
+ self._generate_text_files()
diff --git a/korman/exporter/material.py b/korman/exporter/material.py
index ae8e9aa..c3b8fc3 100644
--- a/korman/exporter/material.py
+++ b/korman/exporter/material.py
@@ -805,7 +805,12 @@ class MaterialConverter:
# when the exporter tells us to finalize all our shit
if texture.image is None:
dtm = self._mgr.find_create_object(plDynamicTextMap, name="{}_DynText".format(layer.key.name), bl=bo)
- dtm.hasAlpha = texture.use_alpha
+ if texture.use_alpha:
+ dtm.hasAlpha = True
+ if not state.blendFlags & hsGMatState.kBlendMask:
+ state.blendFlags |= hsGMatState.kBlendAlpha
+ else:
+ dtm.hasAlpha = False
dtm.visWidth = int(layer_props.dynatext_resolution)
dtm.visHeight = int(layer_props.dynatext_resolution)
layer.texture = dtm.key
diff --git a/korman/nodes/node_python.py b/korman/nodes/node_python.py
index 87d90c6..dd0b94a 100644
--- a/korman/nodes/node_python.py
+++ b/korman/nodes/node_python.py
@@ -138,6 +138,14 @@ class PlasmaAttributeArguments(bpy.types.PropertyGroup):
class PlasmaAttribute(bpy.types.PropertyGroup):
+ # This is thy lookup helper
+ type_LUT = {
+ bool: "ptAttribBoolean",
+ float: "ptAttribFloat",
+ int: "ptAttribInt",
+ str: "ptAttribString",
+ }
+
attribute_id = IntProperty()
attribute_type = StringProperty()
attribute_name = StringProperty()
diff --git a/korman/operators/op_export.py b/korman/operators/op_export.py
index 7ccfa8b..5f41e18 100644
--- a/korman/operators/op_export.py
+++ b/korman/operators/op_export.py
@@ -96,6 +96,14 @@ class PlasmaAgeExportOperator(ExportOperator, bpy.types.Operator):
"default": "as_requested",
"options": set()}),
+ "localization_method": (EnumProperty, {"name": "Localization",
+ "description": "Specifies how localization data should be exported",
+ "items": [("database", "Localization Database", "A per-language database compatible with pfLocalizationEditor"),
+ ("database_back_compat", "Localization Database (Compat Mode)", "A per-language database compatible with pfLocalizationEditor and Korman <=0.11"),
+ ("single_file", "Single File", "A single file database, as in Korman <=0.11")],
+ "default": "database",
+ "options": set()}),
+
"export_active": (BoolProperty, {"name": "INTERNAL: Export currently running",
"default": False,
"options": {"SKIP_SAVE"}}),
diff --git a/korman/properties/modifiers/base.py b/korman/properties/modifiers/base.py
index 4752988..1e4827b 100644
--- a/korman/properties/modifiers/base.py
+++ b/korman/properties/modifiers/base.py
@@ -17,7 +17,7 @@ import bpy
from bpy.props import *
import abc
-from typing import Any, Dict, Generator
+from typing import Any, Dict, Generator, Optional
class PlasmaModifierProperties(bpy.types.PropertyGroup):
@property
@@ -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"""
@@ -143,6 +152,28 @@ class PlasmaModifierLogicWiz:
pfm_node.update()
return pfm_node
+ def _create_python_attribute(self, pfm_node, attribute_name: str, attribute_type: Optional[str] = None, **kwargs):
+ """Creates and links a Python Attribute Node to the Python File Node given by `pfm_node`.
+ This will automatically handle simple attribute types such as numbers and strings, however,
+ for object linkage, you should specify the optional `attribute_type` to ensure the proper
+ attribute type is found. For attribute nodes that require multiple values, the `value` may
+ be set to None and handled in your code."""
+ from ...nodes.node_python import PlasmaAttribute, PlasmaAttribNodeBase
+ if attribute_type is None:
+ assert len(kwargs) == 1 and "value" in kwargs, \
+ "In order to deduce the attribute_type, exactly one attribute value must be passed as a kw named `value`"
+ attribute_type = PlasmaAttribute.type_LUT.get(kwargs["value"].__class__)
+ node_cls = next((i for i in PlasmaAttribNodeBase.__subclasses__() if attribute_type in i.pl_attrib), None)
+ assert node_cls is not None, "'{}': Unable to find attribute node type for '{}' ('{}')".format(
+ self.id_data.name, attribute_name, attribute_type
+ )
+
+ node = pfm_node.id_data.nodes.new(node_cls.bl_idname)
+ node.link_output(pfm_node, "pfm", attribute_name)
+ for i, j in kwargs.items():
+ setattr(node, i, j)
+ return node
+
@abc.abstractmethod
def logicwiz(self, bo, tree):
pass
diff --git a/korman/properties/modifiers/gui.py b/korman/properties/modifiers/gui.py
index 164405e..d4921d2 100644
--- a/korman/properties/modifiers/gui.py
+++ b/korman/properties/modifiers/gui.py
@@ -14,11 +14,13 @@
# along with Korman. If not, see .
import bpy
-import math
import bmesh
+from bpy.props import *
import mathutils
+
+import math
from pathlib import Path
-from bpy.props import *
+
from PyHSPlasma import *
from ...addon_prefs import game_versions
@@ -113,14 +115,63 @@ class PlasmaJournalTranslation(bpy.types.PropertyGroup):
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",
+ text_id = PointerProperty(name="Contents",
+ description="Text data block containing the text for this language",
type=bpy.types.Text,
poll=_poll_nonpytext,
options=set())
-class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz):
+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)
+ if translation.language == _DEFAULT_LANGUAGE_NAME), (None, None))
+ if default is None:
+ default_idx = len(self.translations)
+ default = self.translations.add()
+ default.language = _DEFAULT_LANGUAGE_NAME
+ if self.active_translation_index < len(self.translations):
+ language = self.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.translations)
+ if translation.language == language_name), None)
+ if idx is None:
+ self.active_translation_index = len(self.translations)
+ translation = self.translations.add()
+ translation.language = language_name
+ 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!")
+
+
+class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz, TranslationMixin):
pl_id = "journalbookmod"
bl_category = "GUI"
@@ -156,36 +207,6 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
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())
@@ -193,7 +214,8 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
active_translation = EnumProperty(name="Language",
description="Language of this translation",
items=languages,
- get=_get_translation, set=_set_translation,
+ get=TranslationMixin._get_translation,
+ set=TranslationMixin._set_translation,
options=set())
def pre_export(self, exporter, bo):
@@ -205,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))
@@ -296,17 +309,26 @@ 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
return True
+ @property
+ def translations(self):
+ # Backwards compatibility thunk.
+ return self.journal_translations
+
linking_pfms = {
pvPots : {
diff --git a/korman/properties/modifiers/render.py b/korman/properties/modifiers/render.py
index 5abd2e8..04bb8ae 100644
--- a/korman/properties/modifiers/render.py
+++ b/korman/properties/modifiers/render.py
@@ -19,10 +19,11 @@ from bpy.props import *
import functools
from PyHSPlasma import *
-from .base import PlasmaModifierProperties, PlasmaModifierUpgradable
+from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz, PlasmaModifierUpgradable
from ...exporter.etlight import _NUM_RENDER_LAYERS
from ...exporter import utils
from ...exporter.explosions import ExportError
+from .gui import languages, PlasmaJournalTranslation, TranslationMixin
from ... import idprops
class PlasmaBlendOntoObject(bpy.types.PropertyGroup):
@@ -609,6 +610,150 @@ 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" },
+)
+
+class PlasmaLocalizedTextModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz, TranslationMixin):
+ pl_id = "dynatext"
+
+ bl_category = "Render"
+ bl_label = "Localized Text"
+ bl_description = ""
+ bl_icon = "TEXT"
+
+ translations = CollectionProperty(name="Translations",
+ type=PlasmaJournalTranslation,
+ options=set())
+ active_translation_index = IntProperty(options={"HIDDEN"})
+ active_translation = EnumProperty(name="Language",
+ description="Language of this translation",
+ items=languages,
+ get=TranslationMixin._get_translation,
+ set=TranslationMixin._set_translation,
+ options=set())
+
+ def _poll_dyna_text(self, value: bpy.types.Texture) -> bool:
+ if value.type != "IMAGE":
+ return False
+ if value.image is not None:
+ return False
+ tex_materials = frozenset(value.users_material)
+ obj_materials = frozenset(filter(None, (i.material for i in self.id_data.material_slots)))
+ return bool(tex_materials & obj_materials)
+
+ texture = PointerProperty(name="Texture",
+ description="The texture to write the localized text on",
+ type=bpy.types.Texture,
+ poll=_poll_dyna_text)
+
+ font_face = StringProperty(name="Font Face",
+ default="Arial",
+ options=set())
+ font_size = IntProperty(name="Font Size",
+ default=12,
+ min=0, soft_max=72,
+ options=set())
+ font_color = FloatVectorProperty(name="Font Color",
+ default=(0.0, 0.0, 0.0, 1.0),
+ min=0.0, max=1.0,
+ subtype="COLOR", size=4,
+ options=set())
+
+ # Using individual properties for better UI documentation
+ margin_top = IntProperty(name="Margin Top",
+ min=-4096, soft_min=0, max=4096,
+ options=set())
+ margin_left = IntProperty(name="Margin Left",
+ min=-4096, soft_min=0, max=4096,
+ options=set())
+ margin_bottom = IntProperty(name="Margin Bottom",
+ min=-4096, soft_min=0, max=4096,
+ options=set())
+ margin_right = IntProperty(name="Margin Right",
+ min=-4096, soft_min=0, max=4096,
+ options=set())
+
+ justify = EnumProperty(name="Justification",
+ items=[("left", "Left", ""),
+ ("center", "Center", ""),
+ ("right", "Right", "")],
+ default="left",
+ options=set())
+ line_spacing = IntProperty(name="Line Spacing",
+ default=0,
+ soft_min=0, soft_max=10,
+ options=set())
+
+ def pre_export(self, exporter, bo):
+ yield self.convert_logic(bo, age_name=exporter.age_name, version=exporter.mgr.getVer())
+
+ 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
+ # add text surfaces directly to objects, opposed to where Cyan tends to use a separate
+ # transparent object over the background object.
+ if not self.texture.use_alpha:
+ material_filter = lambda slot: slot and slot.material and self.texture in (i.texture for i in slot.material.texture_slots if i)
+ for slot in filter(material_filter, bo.material_slots):
+ self._create_nodes(bo, tree, age_name=age_name, version=version,
+ material=slot.material, clear_color=slot.material.diffuse_color)
+ else:
+ 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)
+ 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)
+ self._create_python_attribute(pfm_node, "locPath", value=loc_path)
+ self._create_python_attribute(pfm_node, "fontFace", value=self.font_face)
+ self._create_python_attribute(pfm_node, "fontSize", value=self.font_size)
+ self._create_python_attribute(pfm_node, "fontColorR", value=self.font_color[0])
+ self._create_python_attribute(pfm_node, "fontColorG", value=self.font_color[1])
+ self._create_python_attribute(pfm_node, "fontColorB", value=self.font_color[2])
+ self._create_python_attribute(pfm_node, "fontColorA", value=self.font_color[3])
+ self._create_python_attribute(pfm_node, "marginTop", value=self.margin_top)
+ self._create_python_attribute(pfm_node, "marginLeft", value=self.margin_left)
+ self._create_python_attribute(pfm_node, "marginBottom", value=self.margin_bottom)
+ self._create_python_attribute(pfm_node, "marginRight", value=self.margin_right)
+ self._create_python_attribute(pfm_node, "justify", value=self.justify)
+
+ if clear_color is not None:
+ self._create_python_attribute(pfm_node, "clearColorR", value=clear_color[0])
+ self._create_python_attribute(pfm_node, "clearColorG", value=clear_color[1])
+ 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)
+
+
class PlasmaShadowCasterMod(PlasmaModifierProperties):
pl_id = "rtshadow"
diff --git a/korman/ui/modifiers/render.py b/korman/ui/modifiers/render.py
index 8b90792..fe17821 100644
--- a/korman/ui/modifiers/render.py
+++ b/korman/ui/modifiers/render.py
@@ -97,6 +97,51 @@ def decal_receive(modifier, layout, context):
layout.alert = decal_mgr is None
layout.prop_search(mgr_ref, "name", scene, "decal_managers", icon="BRUSH_DATA")
+def dynatext(modifier, layout, context):
+ col = layout.column()
+ col.alert = modifier.texture is None
+ col.prop(modifier, "texture")
+ if modifier.texture is None:
+ col.label("You must specify a blank image texture to draw on.", icon="ERROR")
+
+ split = layout.split()
+ col = split.column()
+ col.label("Content Translations:")
+ col.prop(modifier, "active_translation", text="")
+ # This should never fail...
+ try:
+ translation = modifier.translations[modifier.active_translation_index]
+ except Exception as e:
+ col.label(text="Error (see console)", icon="ERROR")
+ print(e)
+ else:
+ col.prop(translation, "text_id", text="")
+
+ col = split.column()
+ col.label("Font:")
+ sub = col.row()
+ sub.alert = not modifier.font_face.strip()
+ sub.prop(modifier, "font_face", text="", icon="OUTLINER_DATA_FONT")
+ col.prop(modifier, "font_size", text="Size")
+
+ layout.separator()
+ split = layout.split()
+ col = split.column(align=True)
+ if modifier.texture is not None:
+ col.alert = modifier.margin_top + modifier.margin_bottom >= int(modifier.texture.plasma_layer.dynatext_resolution)
+ col.prop(modifier, "margin_top")
+ col.prop(modifier, "margin_bottom")
+ col = split.column(align=True)
+ if modifier.texture is not None:
+ col.alert = modifier.margin_left + modifier.margin_right >= int(modifier.texture.plasma_layer.dynatext_resolution)
+ col.prop(modifier, "margin_left")
+ col.prop(modifier, "margin_right")
+
+ layout.separator()
+ flow = layout.column_flow(columns=2)
+ flow.prop_menu_enum(modifier, "justify")
+ flow.prop(modifier, "line_spacing")
+
def fademod(modifier, layout, context):
layout.prop(modifier, "fader_type")
diff --git a/korman/ui/ui_world.py b/korman/ui/ui_world.py
index 04e3d9a..4fb0792 100644
--- a/korman/ui/ui_world.py
+++ b/korman/ui/ui_world.py
@@ -276,6 +276,7 @@ class PlasmaAgePanel(AgeButtonsPanel, bpy.types.Panel):
layout.separator()
layout.prop(age, "envmap_method")
layout.prop(age, "lighting_method")
+ layout.prop(age, "localization_method")
layout.prop(age, "python_method")
layout.prop(age, "texcache_method")