Browse Source

Merge pull request #276 from Hoikas/loc_mark_deux

Add "Localized Text" Modifier
pull/282/head
Adam Johnson 3 years ago committed by GitHub
parent
commit
f0dd0c5b2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      korman/exporter/convert.py
  2. 143
      korman/exporter/locman.py
  3. 7
      korman/exporter/material.py
  4. 8
      korman/nodes/node_python.py
  5. 8
      korman/operators/op_export.py
  6. 33
      korman/properties/modifiers/base.py
  7. 114
      korman/properties/modifiers/gui.py
  8. 147
      korman/properties/modifiers/render.py
  9. 45
      korman/ui/modifiers/render.py
  10. 1
      korman/ui/ui_world.py

16
korman/exporter/convert.py

@ -69,6 +69,7 @@ class Exporter:
self.report.progress_add_step("Collecting Objects") self.report.progress_add_step("Collecting Objects")
self.report.progress_add_step("Verify Competence") self.report.progress_add_step("Verify Competence")
self.report.progress_add_step("Touching the Intangible") self.report.progress_add_step("Touching the Intangible")
self.report.progress_add_step("Unifying Superstrings")
self.report.progress_add_step("Harvesting Actors") self.report.progress_add_step("Harvesting Actors")
if self._op.lighting_method != "skip": if self._op.lighting_method != "skip":
etlight.LightBaker.add_progress_steps(self.report) 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. # In other words, generate any ephemeral Blender objects that need to be exported.
self._pre_export_scene_objects() 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 # 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 # that the artist made requires something to have a CoordinateInterface
self._harvest_actors() self._harvest_actors()
@ -248,6 +252,18 @@ class Exporter:
return ci return ci
return so.coord.object 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): def _export_scene_objects(self):
self.report.progress_advance() self.report.progress_advance()
self.report.progress_range = len(self._objects) self.report.progress_range = len(self._objects)

143
korman/exporter/locman.py

@ -14,17 +14,26 @@
# along with Korman. If not, see <http://www.gnu.org/licenses/>. # along with Korman. If not, see <http://www.gnu.org/licenses/>.
import bpy import bpy
from PyHSPlasma import *
from collections import defaultdict
from contextlib import contextmanager 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 .explosions import NonfatalExportError
from .. import korlib from .. import korlib
from . import logger 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"} _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: class LocalizationConverter:
def __init__(self, exporter=None, **kwargs): def __init__(self, exporter=None, **kwargs):
if exporter is not None: if exporter is not None:
@ -37,20 +46,16 @@ class LocalizationConverter:
self._age_name = kwargs.get("age_name") self._age_name = kwargs.get("age_name")
self._path = kwargs.get("path") self._path = kwargs.get("path")
self._version = kwargs.get("version") self._version = kwargs.get("version")
self._journals = {} self._strings = defaultdict(lambda: defaultdict(dict))
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): def add_string(self, set_name, element_name, language, value, indent=0):
trans_set = self._strings.setdefault(set_name, {}) self._report.msg("Accepted '{}' translation for '{}'.", element_name, language, indent=indent)
trans_element = trans_set.setdefault(element_name, {}) if isinstance(value, bpy.types.Text):
trans_element[language] = value 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 @contextmanager
def _generate_file(self, filename, **kwargs): def _generate_file(self, filename, **kwargs):
@ -68,12 +73,12 @@ class LocalizationConverter:
finally: finally:
handle.close() handle.close()
def _generate_journal_texts(self): def _generate_text_files(self):
age_name = self._age_name age_name = self._age_name
def write_journal_file(language, file_name, contents): def write_text_file(language, file_name, contents):
try:
with self._generate_file(dirname="ageresources", filename=file_name) as stream: with self._generate_file(dirname="ageresources", filename=file_name) as stream:
try:
stream.write(contents.encode("windows-1252")) stream.write(contents.encode("windows-1252"))
except UnicodeEncodeError: 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.", self._report.warn("Translation '{}': Contents contains characters that cannot be used in this version of Plasma. They will appear as a '?' in game.",
@ -84,8 +89,9 @@ class LocalizationConverter:
stream.write(contents.encode("windows-1252", "replace")) stream.write(contents.encode("windows-1252", "replace"))
return True return True
for journal_name, translations in self._journals.items(): locs = itertools.chain(self._strings["Journals"].items(), self._strings["DynaTexts"].items())
self._report.msg("Copying Journal '{}'", journal_name, indent=1) for journal_name, translations in locs:
self._report.msg("Copying localization '{}'", journal_name, indent=1)
for language_name, value in translations.items(): for language_name, value in translations.items():
if language_name not in _SP_LANGUAGES: 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.", 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 continue
suffix = "_{}".format(language_name.lower()) if language_name != "English" else "" suffix = "_{}".format(language_name.lower()) if language_name != "English" else ""
file_name = "{}--{}{}.txt".format(age_name, journal_name, suffix) 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 # Ensure that default (read: "English") journal is available
if "English" not in translations: if "English" not in translations:
@ -102,55 +108,69 @@ class LocalizationConverter:
if language_name is not None: if language_name is not None:
file_name = "{}--{}.txt".format(age_name, journal_name) file_name = "{}--{}.txt".format(age_name, journal_name)
# If you manage to screw up this badly... Well, I am very sorry. # 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", self._report.warn("No 'English' translation available, so '{}' will be used as the default",
language_name, indent=2) language_name, indent=2)
else: else:
self._report.port("No 'English' nor any other suitable default translation available", indent=2) self._report.port("No 'English' nor any other suitable default translation available", indent=2)
def _generate_loc_file(self): def _generate_loc_files(self):
# Only generate this junk if needed if not self._strings:
if not self._strings and not self._journals:
return 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 <agename>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): def write_line(value, *args, **kwargs):
# tabs suck, then you die... # tabs suck, then you die...
whitespace = " " * kwargs.pop("indent", 0) whitespace = " " * kwargs.pop("indent", 0)
if args or kwargs: if args or kwargs:
value = value.format(*args, **kwargs) value = value.format(*args, **kwargs)
line = "".join((whitespace, value, "\n")) line = "".join((whitespace, value, "\n"))
stream.write(line.encode("utf-16_le")) stream.write(line.encode("utf-8"))
age_name = self._age_name def iter_element(element):
enc = plEncryptedStream.kEncAes if self._version == pvEoa else None if language_name is None:
file_name = "{}.loc".format(age_name) yield from element.items()
with self._generate_file(file_name, enc=enc) as stream: else:
# UTF-16 little endian byte order mark yield language_name, element
stream.write(b"\xFF\xFE")
write_line("<?xml version=\"1.0\" encoding=\"utf-16\"?>") enc = plEncryptedStream.kEncAes if self._version == pvEoa else None
with self._generate_file(filename, enc=enc) as stream:
write_line("<?xml version=\"1.0\" encoding=\"utf-8\"?>")
write_line("<localizations>") write_line("<localizations>")
write_line("<age name=\"{}\">", age_name, indent=1) write_line("<age name=\"{}\">", self._age_name, indent=1)
# Arbitrary strings defined by something like a GUI or a node tree for set_name, elements in sets.items():
for set_name, elements in self._strings.items():
write_line("<set name=\"{}\">", set_name, indent=2) write_line("<set name=\"{}\">", set_name, indent=2)
for element_name, translations in elements.items(): for element_name, value in elements.items():
write_line("<element name=\"{}\">", element_name, indent=3) write_line("<element name=\"{}\">", element_name, indent=3)
for language_name, value in translations.items(): for translation_language, translation_value in iter_element(value):
write_line("<translation language=\"{language}\">{translation}</translation>", if _ESHTML_REGEX.search(translation_value):
language=language_name, translation=xml_escape(value), indent=4) encoded_value = "<![CDATA[{}]]>".format(translation_value)
write_line("</element>", indent=3) else:
write_line("</set>", indent=2) encoded_value = xml_escape(translation_value)
# Journals
if self._journals:
write_line("<set name=\"Journals\">", indent=2)
for journal_name, translations in self._journals.items():
write_line("<element name=\"{}\">", journal_name, indent=3)
for language_name, value in translations.items():
write_line("<translation language=\"{language}\">{translation}</translation>", write_line("<translation language=\"{language}\">{translation}</translation>",
language=language_name, translation=xml_escape(value), indent=4) language=translation_language, translation=encoded_value, indent=4)
write_line("</element>", indent=3) write_line("</element>", indent=3)
write_line("</set>", indent=2) write_line("</set>", indent=2)
@ -164,7 +184,7 @@ class LocalizationConverter:
loc_path = str(Path(self._path) / "dat" / "{}.loc".format(self._age_name)) loc_path = str(Path(self._path) / "dat" / "{}.loc".format(self._age_name))
log = logger.ExportVerboseLogger if age_props.verbose else logger.ExportProgressLogger log = logger.ExportVerboseLogger if age_props.verbose else logger.ExportProgressLogger
with korlib.ConsoleToggler(age_props.show_console), log(loc_path) as self._report: 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_add_step("Generating Localization")
self._report.progress_start("Exporting Localization Data") self._report.progress_start("Exporting Localization Data")
@ -176,20 +196,23 @@ class LocalizationConverter:
self._report.raise_errors() self._report.raise_errors()
def _run_harvest_journals(self): def _run_harvest_journals(self):
from ..properties.modifiers import TranslationMixin
objects = bpy.context.scene.objects objects = bpy.context.scene.objects
self._report.progress_advance() self._report.progress_advance()
self._report.progress_range = len(objects) self._report.progress_range = len(objects)
inc_progress = self._report.progress_increment inc_progress = self._report.progress_increment
for i in objects: for i in objects:
journal = i.plasma_modifiers.journalbookmod for mod_type in filter(None, (getattr(j, "pl_id", None) for j in TranslationMixin.__subclasses__())):
if journal.enabled: modifier = getattr(i.plasma_modifiers, mod_type)
translations = [j for j in journal.journal_translations if j.text_id is not None] if modifier.enabled:
translations = [j for j in modifier.translations if j.text_id is not None]
if not translations: if not translations:
self._report.error("Journal '{}': No content translations available. The journal will not be exported.", self._report.error("'{}': No content translations available. The localization will not be exported.",
i.name, indent=2) i.name, indent=2)
for j in translations: for j in translations:
self.add_journal(journal.key_name, j.language, j.text_id, indent=1) self.add_string(modifier.localization_set, modifier.key_name, j.language, j.text_id, indent=1)
inc_progress() inc_progress()
def _run_generate(self): def _run_generate(self):
@ -198,6 +221,6 @@ class LocalizationConverter:
def save(self): def save(self):
if self._version > pvPots: if self._version > pvPots:
self._generate_loc_file() self._generate_loc_files()
else: else:
self._generate_journal_texts() self._generate_text_files()

7
korman/exporter/material.py

@ -805,7 +805,12 @@ class MaterialConverter:
# when the exporter tells us to finalize all our shit # when the exporter tells us to finalize all our shit
if texture.image is None: if texture.image is None:
dtm = self._mgr.find_create_object(plDynamicTextMap, name="{}_DynText".format(layer.key.name), bl=bo) 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.visWidth = int(layer_props.dynatext_resolution)
dtm.visHeight = int(layer_props.dynatext_resolution) dtm.visHeight = int(layer_props.dynatext_resolution)
layer.texture = dtm.key layer.texture = dtm.key

8
korman/nodes/node_python.py

@ -138,6 +138,14 @@ class PlasmaAttributeArguments(bpy.types.PropertyGroup):
class PlasmaAttribute(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_id = IntProperty()
attribute_type = StringProperty() attribute_type = StringProperty()
attribute_name = StringProperty() attribute_name = StringProperty()

8
korman/operators/op_export.py

@ -96,6 +96,14 @@ class PlasmaAgeExportOperator(ExportOperator, bpy.types.Operator):
"default": "as_requested", "default": "as_requested",
"options": set()}), "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", "export_active": (BoolProperty, {"name": "INTERNAL: Export currently running",
"default": False, "default": False,
"options": {"SKIP_SAVE"}}), "options": {"SKIP_SAVE"}}),

33
korman/properties/modifiers/base.py

@ -17,7 +17,7 @@ import bpy
from bpy.props import * from bpy.props import *
import abc import abc
from typing import Any, Dict, Generator from typing import Any, Dict, Generator, Optional
class PlasmaModifierProperties(bpy.types.PropertyGroup): class PlasmaModifierProperties(bpy.types.PropertyGroup):
@property @property
@ -57,6 +57,15 @@ class PlasmaModifierProperties(bpy.types.PropertyGroup):
""" """
pass 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 @property
def face_sort(self): def face_sort(self):
"""Indicates that the geometry's faces should be sorted by the engine""" """Indicates that the geometry's faces should be sorted by the engine"""
@ -143,6 +152,28 @@ class PlasmaModifierLogicWiz:
pfm_node.update() pfm_node.update()
return pfm_node 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 @abc.abstractmethod
def logicwiz(self, bo, tree): def logicwiz(self, bo, tree):
pass pass

114
korman/properties/modifiers/gui.py

@ -14,11 +14,13 @@
# along with Korman. If not, see <http://www.gnu.org/licenses/>. # along with Korman. If not, see <http://www.gnu.org/licenses/>.
import bpy import bpy
import math
import bmesh import bmesh
from bpy.props import *
import mathutils import mathutils
import math
from pathlib import Path from pathlib import Path
from bpy.props import *
from PyHSPlasma import * from PyHSPlasma import *
from ...addon_prefs import game_versions from ...addon_prefs import game_versions
@ -113,14 +115,63 @@ class PlasmaJournalTranslation(bpy.types.PropertyGroup):
items=languages, items=languages,
default=_DEFAULT_LANGUAGE_NAME, default=_DEFAULT_LANGUAGE_NAME,
options=set()) options=set())
text_id = PointerProperty(name="Journal Contents", text_id = PointerProperty(name="Contents",
description="Text data block containing the journal's contents for this language", description="Text data block containing the text for this language",
type=bpy.types.Text, type=bpy.types.Text,
poll=_poll_nonpytext, poll=_poll_nonpytext,
options=set()) 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" pl_id = "journalbookmod"
bl_category = "GUI" bl_category = "GUI"
@ -156,36 +207,6 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
type=bpy.types.Object, type=bpy.types.Object,
poll=idprops.poll_mesh_objects) 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", journal_translations = CollectionProperty(name="Journal Translations",
type=PlasmaJournalTranslation, type=PlasmaJournalTranslation,
options=set()) options=set())
@ -193,7 +214,8 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
active_translation = EnumProperty(name="Language", active_translation = EnumProperty(name="Language",
description="Language of this translation", description="Language of this translation",
items=languages, items=languages,
get=_get_translation, set=_set_translation, get=TranslationMixin._get_translation,
set=TranslationMixin._set_translation,
options=set()) options=set())
def pre_export(self, exporter, bo): def pre_export(self, exporter, bo):
@ -205,15 +227,6 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
bo.name, version, indent=2) bo.name, version, indent=2)
return 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: if self.clickable_region is None:
with utils.bmesh_object("{}_Journal_ClkRgn".format(self.key_name)) as (rgn_obj, bm): with utils.bmesh_object("{}_Journal_ClkRgn".format(self.key_name)) as (rgn_obj, bm):
bmesh.ops.create_cube(bm, size=(6.0)) bmesh.ops.create_cube(bm, size=(6.0))
@ -296,17 +309,26 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
locpath = nodes.new("PlasmaAttribStringNode") locpath = nodes.new("PlasmaAttribStringNode")
locpath.link_output(journalnode, "pfm", "LocPath") 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 = nodes.new("PlasmaAttribStringNode")
guitype.link_output(journalnode, "pfm", "GUIType") guitype.link_output(journalnode, "pfm", "GUIType")
guitype.value = self.book_type guitype.value = self.book_type
@property
def localization_set(self):
return "Journals"
@property @property
def requires_actor(self): def requires_actor(self):
# We are too late in the export to be harvested automatically, so let's be explicit # We are too late in the export to be harvested automatically, so let's be explicit
return True return True
@property
def translations(self):
# Backwards compatibility thunk.
return self.journal_translations
linking_pfms = { linking_pfms = {
pvPots : { pvPots : {

147
korman/properties/modifiers/render.py

@ -19,10 +19,11 @@ from bpy.props import *
import functools import functools
from PyHSPlasma import * from PyHSPlasma import *
from .base import PlasmaModifierProperties, PlasmaModifierUpgradable from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz, PlasmaModifierUpgradable
from ...exporter.etlight import _NUM_RENDER_LAYERS from ...exporter.etlight import _NUM_RENDER_LAYERS
from ...exporter import utils from ...exporter import utils
from ...exporter.explosions import ExportError from ...exporter.explosions import ExportError
from .gui import languages, PlasmaJournalTranslation, TranslationMixin
from ... import idprops from ... import idprops
class PlasmaBlendOntoObject(bpy.types.PropertyGroup): class PlasmaBlendOntoObject(bpy.types.PropertyGroup):
@ -609,6 +610,150 @@ class PlasmaLightingMod(PlasmaModifierProperties):
return False 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): class PlasmaShadowCasterMod(PlasmaModifierProperties):
pl_id = "rtshadow" pl_id = "rtshadow"

45
korman/ui/modifiers/render.py

@ -97,6 +97,51 @@ def decal_receive(modifier, layout, context):
layout.alert = decal_mgr is None layout.alert = decal_mgr is None
layout.prop_search(mgr_ref, "name", scene, "decal_managers", icon="BRUSH_DATA") 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): def fademod(modifier, layout, context):
layout.prop(modifier, "fader_type") layout.prop(modifier, "fader_type")

1
korman/ui/ui_world.py

@ -276,6 +276,7 @@ class PlasmaAgePanel(AgeButtonsPanel, bpy.types.Panel):
layout.separator() layout.separator()
layout.prop(age, "envmap_method") layout.prop(age, "envmap_method")
layout.prop(age, "lighting_method") layout.prop(age, "lighting_method")
layout.prop(age, "localization_method")
layout.prop(age, "python_method") layout.prop(age, "python_method")
layout.prop(age, "texcache_method") layout.prop(age, "texcache_method")

Loading…
Cancel
Save