diff --git a/korman/exporter/material.py b/korman/exporter/material.py index b96d228..bceb1aa 100644 --- a/korman/exporter/material.py +++ b/korman/exporter/material.py @@ -799,7 +799,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/properties/modifiers/gui.py b/korman/properties/modifiers/gui.py index 164405e..53b15c0 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,50 @@ 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 _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 translations(self): + raise RuntimeError("TranslationMixin subclass needs a translation getter!") + + +class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz, TranslationMixin): pl_id = "journalbookmod" bl_category = "GUI" @@ -156,36 +194,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 +201,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): @@ -307,6 +316,11 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz # 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 6b39dfe..bf686a9 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): @@ -604,6 +605,151 @@ 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 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 + # 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 "{}.DynaTexts.{}".format(age_name, 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) + + 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 3d78870..cc2d5fb 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")