From cdd600b4098cbc997109bac77502da8b613782f3 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 23 Jul 2025 17:30:37 -0500 Subject: [PATCH 01/21] Add ClickMap control. --- korman/properties/modifiers/game_gui.py | 28 +++++++++++++++++++++++++ korman/ui/modifiers/game_gui.py | 7 ++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/korman/properties/modifiers/game_gui.py b/korman/properties/modifiers/game_gui.py index fa3f2cf1..2353c595 100644 --- a/korman/properties/modifiers/game_gui.py +++ b/korman/properties/modifiers/game_gui.py @@ -372,6 +372,34 @@ def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): self.mouse_click_anims.export(exporter, bo, so, ctrl, ctrl.addAnimationKey, "animName") +class PlasamGameGuiClickMapModifier(PlasmaModifierProperties, _GameGuiMixin): + pl_id = "gui_clickmap" + pl_depends = {"gui_control"} + pl_page_types = {"gui"} + + bl_category = "GUI" + bl_label = "GUI ClickMap (ex)" + bl_description = "XXX" + + report_while: Set[str] = EnumProperty( + name="Report While", + description="", + items=[ + ("kMouseDragged", "Dragging", ""), + ("kMouseHovered", "Hovering", ""), + ], + options={"ENUM_FLAG"} + ) + + def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, so: Optional[plSceneObject] = None) -> pfGUIClickMapCtrl: + return exporter.mgr.find_create_object(pfGUIClickMapCtrl, bl=bo, so=so) + + def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): + ctrl = self.get_control(exporter, bo, so) + for report in self.report_while: + ctrl.setFlag(getattr(pfGUIClickMapCtrl, report), True) + + class PlasmaGameGuiDialogModifier(PlasmaModifierProperties, _GameGuiMixin): pl_id = "gui_dialog" pl_page_types = {"gui"} diff --git a/korman/ui/modifiers/game_gui.py b/korman/ui/modifiers/game_gui.py index 6be792f9..63be3b2f 100644 --- a/korman/ui/modifiers/game_gui.py +++ b/korman/ui/modifiers/game_gui.py @@ -21,7 +21,7 @@ from .. import ui_list if TYPE_CHECKING: - from ...properties.modifiers.game_gui import GameGuiAnimation, GameGuiAnimationGroup + from ...properties.modifiers.game_gui import * class GuiAnimListUI(bpy.types.UIList): def _iter_target_names(self, item: GameGuiAnimation): @@ -94,6 +94,11 @@ def gui_button(modifier, layout, context): col.prop_search(modifier, "mouse_over_sound", soundemit, "sounds", text="Mouse Over", icon="SPEAKER") col.prop_search(modifier, "mouse_off_sound", soundemit, "sounds", text="Mouse Off", icon="SPEAKER") +def gui_clickmap(modifier: PlasamGameGuiClickMapModifier, layout, context): + sub = layout.row() + sub.label("Report When:") + sub.prop(modifier, "report_while") + def gui_control(modifier, layout, context): split = layout.split() col = split.column() From e1460de221197db7e5bbb64ec2542a205d6f78b9 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 23 Jul 2025 20:28:22 -0500 Subject: [PATCH 02/21] Use a dict for GUI sounds. --- korman/properties/modifiers/game_gui.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/korman/properties/modifiers/game_gui.py b/korman/properties/modifiers/game_gui.py index 2353c595..7516f814 100644 --- a/korman/properties/modifiers/game_gui.py +++ b/korman/properties/modifiers/game_gui.py @@ -34,9 +34,9 @@ class _GameGuiMixin: @property - def gui_sounds(self) -> Iterable[Tuple[str, int]]: - """Overload to automatically export GUI sounds on the control. This should return an iterable - of tuple attribute name and sound index. + def gui_sounds(self) -> Dict[str, int]: + """Overload to automatically export GUI sounds on the control. + This should return a dict of string attribute names to indices. """ return [] @@ -91,7 +91,7 @@ def sanity_check(self, exporter): # Blow up on invalid sounds soundemit = self.id_data.plasma_modifiers.soundemit - for attr_name, _ in self.gui_sounds: + for attr_name in self.gui_sounds: sound_name = getattr(self, attr_name) if not sound_name: continue @@ -158,7 +158,7 @@ def convert_gui_sounds(self, exporter: Exporter, ctrl: pfGUIControlMod, ctrl_mod # NOTE that zero is a special value here meaning no sound, so we need to offset the sounds # that we get from the emitter modifier by +1. sound_indices = {} - for attr_name, gui_sound_idx in ctrl_mod.gui_sounds: + for attr_name, gui_sound_idx in ctrl_mod.gui_sounds.items(): sound_name = getattr(ctrl_mod, attr_name) if not sound_name: continue @@ -344,13 +344,13 @@ def _update_notify_type(self, context): ) @property - def gui_sounds(self): - return ( - ("mouse_down_sound", pfGUIButtonMod.kMouseDown), - ("mouse_up_sound", pfGUIButtonMod.kMouseUp), - ("mouse_over_sound", pfGUIButtonMod.kMouseOver), - ("mouse_off_sound", pfGUIButtonMod.kMouseOff), - ) + def gui_sounds(self) -> Dict[str, int]: + return { + "mouse_down_sound": pfGUIButtonMod.kMouseDown, + "mouse_up_sound": pfGUIButtonMod.kMouseUp, + "mouse_over_sound": pfGUIButtonMod.kMouseOver, + "mouse_off_sound": pfGUIButtonMod.kMouseOff, + } def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, so: Optional[plSceneObject] = None) -> pfGUIButtonMod: return exporter.mgr.find_create_object(pfGUIButtonMod, bl=bo, so=so) From 32d90d41bc4c027788e327de664dcf09ed7a9ff0 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 23 Jul 2025 20:53:29 -0500 Subject: [PATCH 03/21] Abstract away GUI sound UI drawing. --- korman/ui/modifiers/game_gui.py | 46 +++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/korman/ui/modifiers/game_gui.py b/korman/ui/modifiers/game_gui.py index 63be3b2f..86396979 100644 --- a/korman/ui/modifiers/game_gui.py +++ b/korman/ui/modifiers/game_gui.py @@ -71,6 +71,30 @@ def _gui_anim(name: str, group: GameGuiAnimationGroup, layout, context): col.prop(anim, "target_material") col.prop(anim, "target_texture") +def _gui_sounds(modifier, layout, context, sounds: Dict[str, str]): + box = layout.box() + row = box.row(align=True) + if hasattr(modifier, "show_expanded_sounds"): + exicon = "TRIA_DOWN" if modifier.show_expanded_sounds else "TRIA_RIGHT" + row.prop(modifier, "show_expanded_sounds", text="", icon=exicon, emboss=False) + + row.label("Sound Effects") + if not getattr(modifier, "show_expanded_sounds", True): + return + + soundemit = modifier.id_data.plasma_modifiers.soundemit + col = box.column() + col.active = soundemit.enabled + for sound_attr, label_text in sounds.items(): + sound_name = getattr(modifier, sound_attr) + if sound_name: + sound = next((i for i in soundemit.sounds if i.name == sound_name), None) + alert = sound is None + else: + alert = False + + col.alert = alert + col.prop_search(modifier, sound_attr, soundemit, "sounds", text=label_text, icon="ERROR" if alert else "SPEAKER") def gui_button(modifier, layout, context): row = layout.row() @@ -80,19 +104,15 @@ def gui_button(modifier, layout, context): _gui_anim("Mouse Click", modifier.mouse_click_anims, layout, context) _gui_anim("Mouse Over", modifier.mouse_over_anims, layout, context) - box = layout.box() - row = box.row(align=True) - exicon = "TRIA_DOWN" if modifier.show_expanded_sounds else "TRIA_RIGHT" - row.prop(modifier, "show_expanded_sounds", text="", icon=exicon, emboss=False) - row.label("Sound Effects") - if modifier.show_expanded_sounds: - col = box.column() - soundemit = modifier.id_data.plasma_modifiers.soundemit - col.active = soundemit.enabled - col.prop_search(modifier, "mouse_down_sound", soundemit, "sounds", text="Mouse Down", icon="SPEAKER") - col.prop_search(modifier, "mouse_up_sound", soundemit, "sounds", text="Mouse Up", icon="SPEAKER") - col.prop_search(modifier, "mouse_over_sound", soundemit, "sounds", text="Mouse Over", icon="SPEAKER") - col.prop_search(modifier, "mouse_off_sound", soundemit, "sounds", text="Mouse Off", icon="SPEAKER") + _gui_sounds( + modifier, layout, context, + { + "mouse_down_sound": "Mouse Down", + "mouse_up_sound": "Mouse Up", + "mouse_over_sound": "Mouse Over", + "mouse_off_sound": "Mouse Off", + } + ) def gui_clickmap(modifier: PlasamGameGuiClickMapModifier, layout, context): sub = layout.row() From eea54e493cd5fd28bc6bc9214b358b7f53e197e0 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 24 Jul 2025 12:05:27 -0500 Subject: [PATCH 04/21] Implement GUI checkbox controls. --- korman/properties/modifiers/game_gui.py | 70 +++++++++++++++++++++++++ korman/ui/modifiers/game_gui.py | 15 ++++++ 2 files changed, 85 insertions(+) diff --git a/korman/properties/modifiers/game_gui.py b/korman/properties/modifiers/game_gui.py index 7516f814..ba45e259 100644 --- a/korman/properties/modifiers/game_gui.py +++ b/korman/properties/modifiers/game_gui.py @@ -372,6 +372,76 @@ def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): self.mouse_click_anims.export(exporter, bo, so, ctrl, ctrl.addAnimationKey, "animName") + +class PlasmaGameGuiCheckBoxModifier(PlasmaModifierProperties, _GameGuiMixin): + pl_id = "gui_checkbox" + pl_depends = {"gui_control"} + pl_page_types = {"gui"} + + bl_category = "GUI" + bl_label = "GUI Checkbox (ex)" + bl_description = "XXX" + bl_object_types = {"MESH"} + + def _update_notify_type(self, context): + # It doesn't make sense to have no notify type at all selected, so + # default to at least one option. + if not self.notify_type: + self.notify_type = {"DOWN"} + + anims: GameGuiAnimationGroup = PointerProperty(type=GameGuiAnimationGroup) + show_expanded_sounds: bool = BoolProperty(options={"HIDDEN"}) + + checked: bool = BoolProperty( + name="Checked by Default", + description="Does the checkbox default to checked?", + options=set() + ) + + mouse_down_sound: str = StringProperty( + name="Mouse Down SFX", + description="Sound played when the mouse button is down", + options=set() + ) + + mouse_up_sound: str = StringProperty( + name="Mouse Up SFX", + description="Sound played when the mouse button is released", + options=set() + ) + + mouse_over_sound: str = StringProperty( + name="Mouse Over SFX", + description="Sound played when the mouse moves over the GUI button", + options=set() + ) + + mouse_off_sound: str = StringProperty( + name="Mouse Off SFX", + description="Sound played when the mouse moves off of the GUI button", + options=set() + ) + + @property + def gui_sounds(self) -> Dict[str, int]: + return { + "mouse_down_sound": pfGUICheckBoxCtrl.kMouseDown, + "mouse_up_sound": pfGUICheckBoxCtrl.kMouseUp, + "mouse_over_sound": pfGUICheckBoxCtrl.kMouseOver, + "mouse_off_sound": pfGUICheckBoxCtrl.kMouseOff, + } + + def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, so: Optional[plSceneObject] = None) -> pfGUICheckBoxCtrl: + return exporter.mgr.find_create_object(pfGUICheckBoxCtrl, bl=bo, so=so) + + def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): + ctrl = self.get_control(exporter, bo, so) + ctrl.setFlag(pfGUIControlMod.kWantsInterest, True) + ctrl.checked = self.checked + + self.anims.export(exporter, bo, so, ctrl, ctrl.addAnimKey, "animName") + + class PlasamGameGuiClickMapModifier(PlasmaModifierProperties, _GameGuiMixin): pl_id = "gui_clickmap" pl_depends = {"gui_control"} diff --git a/korman/ui/modifiers/game_gui.py b/korman/ui/modifiers/game_gui.py index 86396979..4f00648c 100644 --- a/korman/ui/modifiers/game_gui.py +++ b/korman/ui/modifiers/game_gui.py @@ -114,6 +114,21 @@ def gui_button(modifier, layout, context): } ) +def gui_checkbox(modifier: PlasmaGameGuiCheckBoxModifier, layout, context): + layout.prop(modifier, "checked") + + _gui_anim("Check", modifier.anims, layout, context) + + _gui_sounds( + modifier, layout, context, + { + "mouse_down_sound": "Mouse Down", + "mouse_up_sound": "Mouse Up", + "mouse_over_sound": "Mouse Over", + "mouse_off_sound": "Mouse Off", + } + ) + def gui_clickmap(modifier: PlasamGameGuiClickMapModifier, layout, context): sub = layout.row() sub.label("Report When:") From 8541357629b24d50ac279867cd7c21590e965e23 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 24 Jul 2025 12:58:58 -0500 Subject: [PATCH 05/21] Add GUI Drag Bar. --- korman/properties/modifiers/game_gui.py | 21 +++++++++++++++++++++ korman/ui/modifiers/game_gui.py | 3 +++ 2 files changed, 24 insertions(+) diff --git a/korman/properties/modifiers/game_gui.py b/korman/properties/modifiers/game_gui.py index ba45e259..1010e3ea 100644 --- a/korman/properties/modifiers/game_gui.py +++ b/korman/properties/modifiers/game_gui.py @@ -470,6 +470,27 @@ def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): ctrl.setFlag(getattr(pfGUIClickMapCtrl, report), True) +class PlasmaGameGuiDragBarModifier(PlasmaModifierProperties, _GameGuiMixin): + pl_id = "gui_dragbar" + pl_depends = {"gui_control"} + pl_page_types = {"gui"} + + bl_category = "GUI" + bl_label = "GUI Drag Bar (ex)" + bl_description = "XXX" + + def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, so: Optional[plSceneObject] = None) -> pfGUIDragBarCtrl: + return exporter.mgr.find_create_object(pfGUIDragBarCtrl, bl=bo, so=so) + + def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): + ctrl = self.get_control(exporter, bo, so) + ctrl.setFlag(pfGUIControlMod.kBetterHitTesting, True) + + @property + def requires_actor(self): + return True + + class PlasmaGameGuiDialogModifier(PlasmaModifierProperties, _GameGuiMixin): pl_id = "gui_dialog" pl_page_types = {"gui"} diff --git a/korman/ui/modifiers/game_gui.py b/korman/ui/modifiers/game_gui.py index 4f00648c..56762af2 100644 --- a/korman/ui/modifiers/game_gui.py +++ b/korman/ui/modifiers/game_gui.py @@ -149,6 +149,9 @@ def gui_control(modifier, layout, context): row.active = col.active and modifier.proc == "console_command" row.prop(modifier, "console_command") +def gui_dragbar(modifier: PlasmaGameGuiDragBarModifier, layout, context): + layout.label("Drag Bars have no settings.") + def gui_dialog(modifier, layout, context): row = layout.row(align=True) row.prop(modifier, "camera_object") From 5a32a038c6553c4d8ebee86ab308d0ca81c71f7b Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 24 Jul 2025 17:37:34 -0500 Subject: [PATCH 06/21] GUI DynTexts can't share materials. This also adds a helper to ensure that MRO works correctly for mixin classes. There are a few places in PlasmaModifierProperties where type hints are commented out because of this. We should go back and apply this helper for that. --- korman/properties/modifiers/__init__.py | 11 +++++++++++ korman/properties/modifiers/game_gui.py | 18 ++++++++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/korman/properties/modifiers/__init__.py b/korman/properties/modifiers/__init__.py index 87e35bea..bdda951e 100644 --- a/korman/properties/modifiers/__init__.py +++ b/korman/properties/modifiers/__init__.py @@ -27,6 +27,17 @@ from .sound import * from .water import * +# Check our mixins to ensure that the subclasses have them first in their MRO. +_mod_mixins = [game_gui._GameGuiMixin] +for mixin in _mod_mixins: + for sub in mixin.__subclasses__(): + mro = sub.__mro__ + if mro.index(mixin) > mro.index(PlasmaModifierProperties): + raise ImportError( + f"{sub.__name__} base class {mixin.__name__} isn't properly " + "overriding PlasmaModifierProperties!" + ) + class PlasmaModifiers(bpy.types.PropertyGroup): def determine_next_id(self): """Gets the ID for the next modifier in the UI""" diff --git a/korman/properties/modifiers/game_gui.py b/korman/properties/modifiers/game_gui.py index 1010e3ea..845c9c26 100644 --- a/korman/properties/modifiers/game_gui.py +++ b/korman/properties/modifiers/game_gui.py @@ -33,6 +33,12 @@ from ..prop_world import PlasmaAge, PlasmaPage class _GameGuiMixin: + @property + def copy_material(self) -> bool: + # If this control uses a dynamic text map, then its contents are unique. + # Therefore, we need to copy the material. + return self.requires_dyntext + @property def gui_sounds(self) -> Dict[str, int]: """Overload to automatically export GUI sounds on the control. @@ -100,7 +106,7 @@ def sanity_check(self, exporter): raise ExportError(f"'{self.id_data.name}': Invalid '{attr_name}' GUI Sound '{sound_name}'") -class PlasmaGameGuiControlModifier(PlasmaModifierProperties, _GameGuiMixin): +class PlasmaGameGuiControlModifier(_GameGuiMixin, PlasmaModifierProperties): pl_id = "gui_control" pl_page_types = {"gui"} @@ -287,7 +293,7 @@ def export( add_func(i) -class PlasmaGameGuiButtonModifier(PlasmaModifierProperties, _GameGuiMixin): +class PlasmaGameGuiButtonModifier(_GameGuiMixin, PlasmaModifierProperties): pl_id = "gui_button" pl_depends = {"gui_control"} pl_page_types = {"gui"} @@ -373,7 +379,7 @@ def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): -class PlasmaGameGuiCheckBoxModifier(PlasmaModifierProperties, _GameGuiMixin): +class PlasmaGameGuiCheckBoxModifier(_GameGuiMixin, PlasmaModifierProperties): pl_id = "gui_checkbox" pl_depends = {"gui_control"} pl_page_types = {"gui"} @@ -442,7 +448,7 @@ def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): self.anims.export(exporter, bo, so, ctrl, ctrl.addAnimKey, "animName") -class PlasamGameGuiClickMapModifier(PlasmaModifierProperties, _GameGuiMixin): +class PlasamGameGuiClickMapModifier(_GameGuiMixin, PlasmaModifierProperties): pl_id = "gui_clickmap" pl_depends = {"gui_control"} pl_page_types = {"gui"} @@ -470,7 +476,7 @@ def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): ctrl.setFlag(getattr(pfGUIClickMapCtrl, report), True) -class PlasmaGameGuiDragBarModifier(PlasmaModifierProperties, _GameGuiMixin): +class PlasmaGameGuiDragBarModifier(_GameGuiMixin, PlasmaModifierProperties): pl_id = "gui_dragbar" pl_depends = {"gui_control"} pl_page_types = {"gui"} @@ -491,7 +497,7 @@ def requires_actor(self): return True -class PlasmaGameGuiDialogModifier(PlasmaModifierProperties, _GameGuiMixin): +class PlasmaGameGuiDialogModifier(_GameGuiMixin, PlasmaModifierProperties): pl_id = "gui_dialog" pl_page_types = {"gui"} From 8c91d4eebe3e66c1d6f3ea223e60e7f0fe5920de Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 11 Aug 2025 20:49:39 -0500 Subject: [PATCH 07/21] Allow specifying a DynaTextMap for GUI controls. --- korman/idprops.py | 9 ++++++ korman/properties/modifiers/game_gui.py | 42 +++++++++++++++++++++++-- korman/properties/modifiers/render.py | 11 +------ korman/ui/modifiers/game_gui.py | 5 +++ 4 files changed, 55 insertions(+), 12 deletions(-) diff --git a/korman/idprops.py b/korman/idprops.py index d593c038..9865e847 100644 --- a/korman/idprops.py +++ b/korman/idprops.py @@ -133,6 +133,15 @@ def poll_empty_objects(self, value): def poll_mesh_objects(self, value): return value.type == "MESH" +def poll_object_dyntexts(self, value): + 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) + def poll_softvolume_objects(self, value): return value.plasma_modifiers.softvolume.enabled diff --git a/korman/properties/modifiers/game_gui.py b/korman/properties/modifiers/game_gui.py index 845c9c26..2f084aaa 100644 --- a/korman/properties/modifiers/game_gui.py +++ b/korman/properties/modifiers/game_gui.py @@ -142,6 +142,16 @@ class PlasmaGameGuiControlModifier(_GameGuiMixin, PlasmaModifierProperties): description="", options=set() ) + texture = PointerProperty( + name="Texture", + description="The texture to draw GUI content on", + type=bpy.types.Texture, + poll=idprops.poll_object_dyntexts + ) + + def sanity_check(self, exporter: Exporter): + if self.requires_dyntext and self.texture is None: + raise ExportError(f"'{self.id_data.name}': GUI Control requires a Texture to draw onto.") def convert_gui_control(self, exporter: Exporter, ctrl: pfGUIControlMod, bo: bpy.types.Object, so: plSceneObject): ctrl.tagID = self.tag_id @@ -177,6 +187,23 @@ def convert_gui_sounds(self, exporter: Exporter, ctrl: pfGUIControlMod, ctrl_mod if sound_indices: ctrl.soundIndices = [sound_indices.get(i, 0) for i in range(max(sound_indices) + 1)] + def convert_gui_dyntext(self, exporter: Exporter, ctrl: pfGUIControlMod, ctrl_mod: _GameGuiMixin, bo: bpy.types.Object, so: plSceneObject): + if not ctrl_mod.requires_dyntext: + return + + layers = tuple(exporter.mesh.material.get_layers(bo=bo, tex=self.texture)) + num_layers = len(layers) + if num_layers > 1: + exporter.report.warn(f"GUI Texture '{self.texture.name}' mapped to {len(layers)} Plasma Layers. This can only be 1.") + elif num_layers == 0: + raise ExportError(f"'{bo.name}': Unable to lookup GUI Texture!") + + ctrl.dynTextLayer = layers[0] + ctrl.dynTextMap = layers[0].object.texture + + # This is basically the blockRGB flag on the DynaTextMap + ctrl.setFlag(pfGUIControlMod.kXparentBgnd, self.texture.use_alpha) + def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): ctrl_mods = list(self.iterate_control_modifiers()) if not ctrl_mods: @@ -184,8 +211,15 @@ def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): exporter.report.warn("This modifier has no effect because no GUI control modifiers are present!") for ctrl_mod in ctrl_mods: ctrl_obj = ctrl_mod.get_control(exporter, bo, so) - self.convert_gui_control(exporter, ctrl_obj, bo, so) - self.convert_gui_sounds(exporter, ctrl_obj, ctrl_mod) + if ctrl_obj is not None: + self.convert_gui_control(exporter, ctrl_obj, bo, so) + self.convert_gui_sounds(exporter, ctrl_obj, ctrl_mod) + + def post_export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): + for ctrl_mod in self.iterate_control_modifiers(): + ctrl_obj = ctrl_mod.get_control(exporter, bo, so) + if ctrl_obj is not None: + self.convert_gui_dyntext(exporter, ctrl_obj, ctrl_mod, bo, so) @property def has_gui_proc(self) -> bool: @@ -198,6 +232,10 @@ def is_game_gui_control(cls) -> bool: # or may not be used by other controls. This just helps fill out the other modifiers. return False + @property + def requires_dyntext(self) -> bool: + return any((i.requires_dyntext for i in self.iterate_control_modifiers())) + class GameGuiAnimation(bpy.types.PropertyGroup): def _poll_target_object(self, value): diff --git a/korman/properties/modifiers/render.py b/korman/properties/modifiers/render.py index 6debded5..922cd8ca 100644 --- a/korman/properties/modifiers/render.py +++ b/korman/properties/modifiers/render.py @@ -645,19 +645,10 @@ class PlasmaLocalizedTextModifier(PlasmaModifierProperties, PlasmaModifierLogicW 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) + poll=idprops.poll_object_dyntexts) font_face = StringProperty(name="Font Face", default="Arial", diff --git a/korman/ui/modifiers/game_gui.py b/korman/ui/modifiers/game_gui.py index 56762af2..df58250c 100644 --- a/korman/ui/modifiers/game_gui.py +++ b/korman/ui/modifiers/game_gui.py @@ -142,6 +142,11 @@ def gui_control(modifier, layout, context): col = split.column() col.prop(modifier, "tag_id") + col = layout.column() + col.active = modifier.requires_dyntext + col.alert = modifier.requires_dyntext and modifier.texture is None + col.prop(modifier, "texture") + col = layout.column() col.active = modifier.has_gui_proc col.prop(modifier, "proc") From c846f41a9037005bf6f0cd0e1b8877b9a71dfeea Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 11 Aug 2025 20:55:02 -0500 Subject: [PATCH 08/21] Add GUI Color Scheme Modifier. --- korman/properties/modifiers/game_gui.py | 124 ++++++++++++++++++++++++ korman/ui/modifiers/game_gui.py | 22 +++++ 2 files changed, 146 insertions(+) diff --git a/korman/properties/modifiers/game_gui.py b/korman/properties/modifiers/game_gui.py index 2f084aaa..b8679f7e 100644 --- a/korman/properties/modifiers/game_gui.py +++ b/korman/properties/modifiers/game_gui.py @@ -105,6 +105,122 @@ def sanity_check(self, exporter): if sound is None: raise ExportError(f"'{self.id_data.name}': Invalid '{attr_name}' GUI Sound '{sound_name}'") + @property + def wants_colorscheme(self) -> bool: + return self.requires_dyntext + + +class PlasmaGameGuiColorSchemeModifier(_GameGuiMixin, PlasmaModifierProperties): + pl_id = "gui_colorscheme" + pl_page_types = {"gui"} + + bl_category = "GUI" + bl_label = "Color Scheme (ex)" + bl_description = "XXX" + bl_icon = "COLOR" + + foreground_color = FloatVectorProperty( + name="Foreground", + description="", + default=(1.0, 1.0, 1.0, 1.0), + min=0.0, max=1.0, + subtype="COLOR", + size=4, + options=set() + ) + background_color = FloatVectorProperty( + name="Background", + description="", + default=(0.0, 0.0, 0.0, 0.0), + min=0.0, max=1.0, + subtype="COLOR", + size=4, + options=set() + ) + selection_foreground_color = FloatVectorProperty( + name="Selection Foreground", + description="", + default=(1.0, 1.0, 1.0, 1.0), + min=0.0, max=1.0, + subtype="COLOR", + size=4, + options=set() + ) + selection_background_color = FloatVectorProperty( + name="Selection Background", + description="", + default=(0.0, 0.0, 0.0, 0.0), + min=0.0, max=1.0, + subtype="COLOR", + size=4, + options=set() + ) + + font_face: str = StringProperty( + name="Font Face", + description="", + default="Arial", + options=set() + ) + font_size: int = IntProperty( + name="Size", + description="", + default=12, + subtype="UNSIGNED", + soft_min=8, + min=1, + step=2, + options=set() + ) + font_style = EnumProperty( + name="Style", + description="", + items=[ + ("kFontBold", "Bold", ""), + ("kFontItalic", "Italic", ""), + ("kFontShadowed", "Shadowed", ""), + ], + options={"ENUM_FLAG"} + ) + + def convert_colorscheme(self) -> pfGUIColorScheme: + scheme = pfGUIColorScheme() + scheme.foreColor = hsColorRGBA(*self.foreground_color) + scheme.backColor = hsColorRGBA(*self.background_color) + scheme.selForeColor = hsColorRGBA(*self.selection_foreground_color) + scheme.selBackColor = hsColorRGBA(*self.selection_background_color) + scheme.fontFace = self.font_face + scheme.fontSize = self.font_size + for flag in self.font_style: + scheme.fontFlags |= getattr(pfGUIColorScheme, flag) + return scheme + + def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): + scheme_targets: Iterable[_GameGuiMixin] = ( + getattr(self.id_data.plasma_modifiers, i.pl_id) + for i in _GameGuiMixin.__subclasses__() + ) + scheme_targets: List[_GameGuiMixin] = [i for i in scheme_targets if i.wants_colorscheme] + + if not scheme_targets: + exporter.report.warn("This modifier has no effect because no GUI modifiers want a color scheme!") + return + + # Internally, libHSPlasma will steal the color scheme that we give to pfGUIControlMods, + # so we need to give each control a unique color scheme object. Dialogs will copy, but + # that's a less common case. + for i in scheme_targets: + ctrl = i.get_control(exporter, i.id_data) + if ctrl is not None: + ctrl.colorScheme = self.convert_colorscheme() + + @classmethod + def is_game_gui_control(cls): + # This is just an optional field on the GUI control itself. + # It's also on dialogs themselves, so we separate it from + # the main control. + return False + class PlasmaGameGuiControlModifier(_GameGuiMixin, PlasmaModifierProperties): pl_id = "gui_control" @@ -557,6 +673,10 @@ class PlasmaGameGuiDialogModifier(_GameGuiMixin, PlasmaModifierProperties): options=set() ) + def get_control(self, exporter: Exporter, bo = None, so = None) -> pfGUIDialogMod: + # This isn't really a control, but we may need this. + return exporter.mgr.find_create_object(pfGUIDialogMod, bl=bo, so=so) + def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): # Find all of the visible objects in the GUI page for use in hither/yon raycast and # camera matrix calculations. @@ -646,3 +766,7 @@ def post_export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObjec ctrl_key = control.key exporter.report.msg(f"GUIDialog '{bo.name}': [{control.ClassName()}] '{ctrl_key.name}'") dialog.addControl(ctrl_key) + + @property + def wants_colorscheme(self) -> bool: + return True diff --git a/korman/ui/modifiers/game_gui.py b/korman/ui/modifiers/game_gui.py index df58250c..17d83358 100644 --- a/korman/ui/modifiers/game_gui.py +++ b/korman/ui/modifiers/game_gui.py @@ -134,6 +134,28 @@ def gui_clickmap(modifier: PlasamGameGuiClickMapModifier, layout, context): sub.label("Report When:") sub.prop(modifier, "report_while") +def gui_colorscheme( + modifier: PlasmaGameGuiColorSchemeModifier, + layout: bpy.types.UILayout, + context: bpy.types.Context +) -> None: + split = layout.split() + + col = split.column() + col.prop(modifier, "foreground_color") + col.prop(modifier, "background_color") + + col = split.column() + col.prop(modifier, "selection_foreground_color") + col.prop(modifier, "selection_background_color") + + layout.separator() + layout.prop(modifier, "font_face") + + row = layout.row() + row.prop_menu_enum(modifier, "font_style") + row.prop(modifier, "font_size") + def gui_control(modifier, layout, context): split = layout.split() col = split.column() From 2ff690ce41d94141e1d90799c111bffb03f39e4d Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 11 Aug 2025 20:56:15 -0500 Subject: [PATCH 09/21] Add GUI TextBox Modifier. --- korman/exporter/locman.py | 35 +++++++- korman/properties/modifiers/game_gui.py | 111 +++++++++++++++++++++++- korman/properties/modifiers/gui.py | 23 +++-- korman/properties/modifiers/render.py | 2 +- korman/ui/modifiers/game_gui.py | 13 +++ 5 files changed, 173 insertions(+), 11 deletions(-) diff --git a/korman/exporter/locman.py b/korman/exporter/locman.py index f38ca050..71c1093e 100644 --- a/korman/exporter/locman.py +++ b/korman/exporter/locman.py @@ -13,6 +13,8 @@ # You should have received a copy of the GNU General Public License # along with Korman. If not, see . +from __future__ import annotations + import bpy from PyHSPlasma import * @@ -21,7 +23,7 @@ import itertools from pathlib import Path import re -from typing import NamedTuple, Union +from typing import * from xml.sax.saxutils import escape as xml_escape import weakref @@ -91,6 +93,24 @@ def add_string(self, set_name, element_name, language, value): self._strings[set_name][element_name][language] = value + def get_localized_string(self, translations: Dict[str, str]): + # If there's only an English translation, just output this string directly. + if translations.keys() == {"English"}: + return translations["English"] + + ignored_translations = frozenset(translations.keys()) - _SP_LANGUAGES + if ignored_translations: + self._report.warn( + f"These translations are not supported in single player: " + f"{', '.join(ignored_translations)}" + ) + + return "".join( + f"${lang[0:2]}${value}" + for lang, value in translations.items() + if lang in _SP_LANGUAGES + ) + @contextmanager def _generate_file(self, filename, **kwargs): if self._exporter is not None: @@ -234,20 +254,27 @@ def run(self): def _run_harvest_journals(self): from ..properties.modifiers import TranslationMixin + def iter_subclasses(cls): + for i in cls.__subclasses__(): + yield i + if i.__subclasses__(): + yield from iter_subclasses(i) + + 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: - for mod_type in filter(None, (getattr(j, "pl_id", None) for j in TranslationMixin.__subclasses__())): + for mod_type in filter(None, (getattr(j, "pl_id", None) for j in iter_subclasses(TranslationMixin))): modifier = getattr(i.plasma_modifiers, mod_type) if modifier.enabled: - translations = [j for j in modifier.translations if j.text_id is not None] + translations = [j for j in modifier.translations if j.text] if not translations: self._report.error(f"'{i.name}': No content translations available. The localization will not be exported.") for j in translations: - self.add_string(modifier.localization_set, modifier.key_name, j.language, j.text_id) + self.add_string(modifier.localization_set, modifier.key_name, j.language, j.text) inc_progress() def _run_generate(self): diff --git a/korman/properties/modifiers/game_gui.py b/korman/properties/modifiers/game_gui.py index b8679f7e..55f57abe 100644 --- a/korman/properties/modifiers/game_gui.py +++ b/korman/properties/modifiers/game_gui.py @@ -26,12 +26,36 @@ from ...exporter import ExportError from .base import PlasmaModifierProperties +from .gui import ( + _DEFAULT_LANGUAGE_NAME, languages, + TranslationItem, TranslationMixin +) from ... import idprops if TYPE_CHECKING: from ...exporter import Exporter from ..prop_world import PlasmaAge, PlasmaPage + +class GameGuiTranslationItem(TranslationItem, bpy.types.PropertyGroup): + language = EnumProperty( + name="Language", + description="Language of this translation", + items=languages, + default=_DEFAULT_LANGUAGE_NAME, + options=set() + ) + value = StringProperty( + name="Text", + description="", + options=set() + ) + + @property + def text(self) -> str: + return self.value + + class _GameGuiMixin: @property def copy_material(self) -> bool: @@ -647,7 +671,92 @@ def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): ctrl.setFlag(pfGUIControlMod.kBetterHitTesting, True) @property - def requires_actor(self): + def requires_actor(self) -> bool: + return True + + +class PlasmaGameGuiTextBoxModifier(_GameGuiMixin, TranslationMixin, PlasmaModifierProperties): + pl_id = "gui_textbox" + pl_depends = {"gui_control"} + pl_page_types = {"gui"} + + bl_category = "GUI" + bl_label = "GUI Text Box (ex)" + bl_description = "XXX" + bl_icon = "SYNTAX_OFF" + bl_object_types = {"MESH"} + + _JUSTIFICATION_LUT = { + "center": pfGUITextBoxMod.kCenterJustify, + "right": pfGUITextBoxMod.kRightJustify, + } + + justification: str = EnumProperty( + name="Justification", + description="", + items=[ + ("left", "Left", ""), + ("center", "Center", ""), + ("right", "Right", ""), + ], + options=set() + ) + + text_translations = CollectionProperty( + name="Translations", + type=GameGuiTranslationItem, + 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 convert_string(self, exporter: Exporter) -> str: + with exporter.report.indent(): + exporter.report.msg("Converting legacy GUI localization...") + value = exporter.locman.get_localized_string( + { i.language: i.text for i in self.translations if i.text } + ) + exporter.report.msg(value) + return value + + def export_localization(self, exporter: Exporter): + # Only MOUL, EoA, and Hex Isle have pfLocalization support in GUIs. + # Otherwise, this translation mixin does something we don't actually want. + ctrl = self.get_control(exporter, self.id_data) + if exporter.mgr.getVer() >= pvMoul: + super().export_localization(exporter) + ctrl.localizationPath = f"{exporter.age_name}.{self.localization_set}.{self.key_name}" + else: + ctrl.text = self.convert_string(exporter) + + def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, so: Optional[plSceneObject] = None) -> pfGUITextBoxMod: + return exporter.mgr.find_create_object(pfGUITextBoxMod, bl=bo, so=so) + + def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): + ctrl = self.get_control(exporter, bo, so) + ctrl.setFlag(pfGUIControlMod.kIntangible, True) + + just_flag = self._JUSTIFICATION_LUT.get(self.justification) + if just_flag is not None: + ctrl.setFlag(just_flag, True) + + @property + def localization_set(self) -> str: + return "GUI" + + @property + def translations(self) -> Iterable[GameGuiTranslationItem]: + return self.text_translations + + @property + def requires_dyntext(self): return True diff --git a/korman/properties/modifiers/gui.py b/korman/properties/modifiers/gui.py index 0016e4d3..eb65ce72 100644 --- a/korman/properties/modifiers/gui.py +++ b/korman/properties/modifiers/gui.py @@ -87,7 +87,16 @@ def export(self, exporter, bo, so): exporter.mesh.material.export_prepared_image(owner=ilmod, image=item.image, allowed_formats={"JPG", "PNG"}, extension="hsm") -class PlasmaJournalTranslation(bpy.types.PropertyGroup): +class TranslationItem: + if TYPE_CHECKING: + language: str + + @property + def text(self) -> Optional[Union[str, bpy.types.Text]]: + raise NotImplementedError + + +class PlasmaJournalTranslation(TranslationItem, bpy.types.PropertyGroup): def _poll_nonpytext(self, value): return not value.name.endswith(".py") @@ -102,15 +111,19 @@ def _poll_nonpytext(self, value): poll=_poll_nonpytext, options=set()) + @property + def text(self) -> Optional[bpy.types.Text]: + return self.text_id + class TranslationMixin: def export_localization(self, exporter): - translations = [i for i in self.translations if i.text_id is not None] + translations = [i for i in self.translations if i.text is not None] if not translations: exporter.report.error(f"'{self.id_data.name}': '{self.bl_label}' No content translations available. The localization will not be exported.") return for i in translations: - exporter.locman.add_string(self.localization_set, self.key_name, i.language, i.text_id) + exporter.locman.add_string(self.localization_set, self.key_name, i.language, i.text) def _get_translation(self): # Ensure there is always a default (read: English) translation available. @@ -143,11 +156,11 @@ def _set_translation(self, value): self.active_translation_index = idx @property - def localization_set(self): + def localization_set(self) -> str: raise RuntimeError("TranslationMixin subclass needs a localization set getter!") @property - def translations(self): + def translations(self) -> Iterable[TranslationItem]: raise RuntimeError("TranslationMixin subclass needs a translation getter!") diff --git a/korman/properties/modifiers/render.py b/korman/properties/modifiers/render.py index 922cd8ca..6a6b1429 100644 --- a/korman/properties/modifiers/render.py +++ b/korman/properties/modifiers/render.py @@ -624,7 +624,7 @@ def want_rt_lights(self): return False -class PlasmaLocalizedTextModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz, TranslationMixin): +class PlasmaLocalizedTextModifier(TranslationMixin, PlasmaModifierProperties, PlasmaModifierLogicWiz): pl_id = "dynatext" pl_page_types = {"gui", "room"} diff --git a/korman/ui/modifiers/game_gui.py b/korman/ui/modifiers/game_gui.py index 17d83358..b4dc17c4 100644 --- a/korman/ui/modifiers/game_gui.py +++ b/korman/ui/modifiers/game_gui.py @@ -179,6 +179,19 @@ def gui_control(modifier, layout, context): def gui_dragbar(modifier: PlasmaGameGuiDragBarModifier, layout, context): layout.label("Drag Bars have no settings.") +def gui_textbox(modifier: PlasmaGameGuiTextBoxModifier, layout, context): + layout.prop(modifier, "justification") + + sub = layout.column() + sub.prop(modifier, "active_translation") + try: + translation = modifier.text_translations[modifier.active_translation_index] + except Exception as e: + sub.label(text="Error (see console)", icon="ERROR") + print(e) + else: + sub.prop(translation, "value") + def gui_dialog(modifier, layout, context): row = layout.row(align=True) row.prop(modifier, "camera_object") From 8fa4b2f6384936dec3b122960b5d9dcab44b8272 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 11 Aug 2025 20:57:35 -0500 Subject: [PATCH 10/21] Add icons to some GUI Modifiers. --- korman/properties/modifiers/game_gui.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/korman/properties/modifiers/game_gui.py b/korman/properties/modifiers/game_gui.py index 55f57abe..1631dd2b 100644 --- a/korman/properties/modifiers/game_gui.py +++ b/korman/properties/modifiers/game_gui.py @@ -479,6 +479,7 @@ class PlasmaGameGuiButtonModifier(_GameGuiMixin, PlasmaModifierProperties): bl_category = "GUI" bl_label = "GUI Button (ex)" bl_description = "XXX" + bl_icon = "BUTS" bl_object_types = {"FONT", "MESH"} def _update_notify_type(self, context): @@ -565,6 +566,7 @@ class PlasmaGameGuiCheckBoxModifier(_GameGuiMixin, PlasmaModifierProperties): bl_category = "GUI" bl_label = "GUI Checkbox (ex)" bl_description = "XXX" + bl_icon = "CHECKBOX_HLT" bl_object_types = {"MESH"} def _update_notify_type(self, context): @@ -634,6 +636,7 @@ class PlasamGameGuiClickMapModifier(_GameGuiMixin, PlasmaModifierProperties): bl_category = "GUI" bl_label = "GUI ClickMap (ex)" bl_description = "XXX" + bl_icon = "HAND" report_while: Set[str] = EnumProperty( name="Report While", @@ -767,6 +770,7 @@ class PlasmaGameGuiDialogModifier(_GameGuiMixin, PlasmaModifierProperties): bl_category = "GUI" bl_label = "GUI Dialog (ex)" bl_description = "XXX" + bl_icon = "SPLITSCREEN" camera_object: bpy.types.Object = PointerProperty( name="GUI Camera", From e5aee1d06ca1f0cb6fb4720c334e20031c41d1e6 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 12 Aug 2025 12:13:51 -0500 Subject: [PATCH 11/21] Add GUI Radio Group Modifier. --- korman/operators/op_toolbox.py | 18 ++++ korman/properties/modifiers/game_gui.py | 108 ++++++++++++++++++++++-- korman/ui/modifiers/game_gui.py | 5 ++ 3 files changed, 125 insertions(+), 6 deletions(-) diff --git a/korman/operators/op_toolbox.py b/korman/operators/op_toolbox.py index a5ffb64b..c236ddd8 100644 --- a/korman/operators/op_toolbox.py +++ b/korman/operators/op_toolbox.py @@ -160,6 +160,24 @@ def execute(self, context): return {"FINISHED"} +class PlasmaSelectRadioGroupCheckboxesOperator(bpy.types.Operator): + bl_idname = "object.plasma_select_radio_group" + bl_label = "Select Radio Group" + bl_description = "Selects all checkboxes in a radio group" + + def execute(self, context): + active_object = context.active_object + for i in context.scene.objects: + cb_mod = i.plasma_modifiers.gui_checkbox + cb_rg = cb_mod.radio_group + i.select = ( + (cb_mod.enabled and cb_rg is not None and cb_rg.name == active_object.name) + or + (i.name == active_object.name) + ) + return {"FINISHED"} + + class PlasmaToggleAllPlasmaObjectsOperator(ToolboxOperator, bpy.types.Operator): bl_idname = "object.plasma_toggle_all_objects" bl_label = "Toggle All Plasma Objects" diff --git a/korman/properties/modifiers/game_gui.py b/korman/properties/modifiers/game_gui.py index 1631dd2b..af06442c 100644 --- a/korman/properties/modifiers/game_gui.py +++ b/korman/properties/modifiers/game_gui.py @@ -578,11 +578,7 @@ def _update_notify_type(self, context): anims: GameGuiAnimationGroup = PointerProperty(type=GameGuiAnimationGroup) show_expanded_sounds: bool = BoolProperty(options={"HIDDEN"}) - checked: bool = BoolProperty( - name="Checked by Default", - description="Does the checkbox default to checked?", - options=set() - ) + checked_value: bool = BoolProperty(options={"HIDDEN"}) mouse_down_sound: str = StringProperty( name="Mouse Down SFX", @@ -608,6 +604,56 @@ def _update_notify_type(self, context): options=set() ) + def _poll_radio_group(self, object: bpy.types.Object): + if object.plasma_object.page == self.id_data.plasma_object.page: + if object.plasma_modifiers.gui_radio_group.enabled: + return True + return False + + def _iter_other_checkboxes(self, context: bpy.types.Context) -> Iterator[Self]: + if self.radio_group is None: + return + rg_mod = self.radio_group.plasma_modifiers.gui_radio_group + for i in rg_mod.iter_checkbox_mods(context): + if i.id_data.name != self.id_data.name: + yield i + + def _get_checked(self) -> bool: + # Short circuit if we don't think we're checked + if not self.checked_value: + return False + + if self.radio_group is not None: + others = self._iter_other_checkboxes(bpy.context) + if any(i.checked_value for i in others): + return False + + return self.checked_value + + def _set_checked(self, value: bool) -> None: + if not value: + self.checked_value = False + return + + for i in self._iter_other_checkboxes(bpy.context): + i.checked_value = False + self.checked_value = True + + checked: bool = BoolProperty( + name="Checked", + description="Whether or not the checkbox is checked by default", + get=_get_checked, + set=_set_checked, + options=set() + ) + + radio_group = PointerProperty( + name="Radio Group", + description="", + type=bpy.types.Object, + poll=_poll_radio_group + ) + @property def gui_sounds(self) -> Dict[str, int]: return { @@ -665,19 +711,69 @@ class PlasmaGameGuiDragBarModifier(_GameGuiMixin, PlasmaModifierProperties): bl_category = "GUI" bl_label = "GUI Drag Bar (ex)" bl_description = "XXX" + bl_icon = "ARROW_LEFTRIGHT" def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, so: Optional[plSceneObject] = None) -> pfGUIDragBarCtrl: return exporter.mgr.find_create_object(pfGUIDragBarCtrl, bl=bo, so=so) def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): ctrl = self.get_control(exporter, bo, so) - ctrl.setFlag(pfGUIControlMod.kBetterHitTesting, True) @property def requires_actor(self) -> bool: return True +class PlasmaGameGuiRadioGroupModifier(_GameGuiMixin, PlasmaModifierProperties): + pl_id = "gui_radio_group" + pl_depends = {"gui_control"} + pl_page_types = {"gui"} + + bl_category = "GUI" + bl_label = "GUI Radio Group (ex)" + bl_description = "XXX" + bl_icon = "RADIOBUT_ON" + + allow_no_selection = BoolProperty( + name="Allow No Selection", + description="Allows no check boxes to be checked", + options=set() + ) + + def get_control(self, exporter: Exporter, bo = None, so = None) -> pfGUIRadioGroupCtrl: + return exporter.mgr.find_create_object(pfGUIRadioGroupCtrl, bl=bo, so=so) + + def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject) -> None: + ctrl = self.get_control(exporter, bo, so) + ctrl.setFlag(pfGUIRadioGroupCtrl.kAllowNoSelection, self.allow_no_selection) + active_cbs = ( + i for i in self.iter_checkbox_mods(bpy.context) + if i.id_data.plasma_object.enabled + ) + for i, cb_mod in enumerate(active_cbs): + exporter.report.msg(f"Found checkbox '{cb_mod.id_data.name}'") + ctrl.addControl(cb_mod.get_control(exporter, i.id_data).key) + if cb_mod.checked: + ctrl.defaultValue = i + + def iter_checkbox_mods(self, context: bpy.types.Context) -> Iterator[PlasmaGameGuiCheckBoxModifier]: + # This is really not the fastest way to do this. The fastest way would be for us + # to maintain a list of the checkbox children here. But that means the user could + # try to add a single checkbox to multiple radio groups. That seems silly, but it + # feels like a problem waiting to happen. So, instead, we'll set the radio group + # on the checkboxes themselves to prevent that tomfoolery. It does mean the export + # will be slightly slower because we have to iterate all of the objects in the scene + # to find checkboxes, but it should be negligible. + for i in context.scene.objects: + checkbox_mod: PlasmaGameGuiCheckBoxModifier = i.plasma_modifiers.gui_checkbox + if not checkbox_mod.enabled: + continue + + rg = checkbox_mod.radio_group + if rg is not None and rg.name == self.id_data.name: + yield checkbox_mod + + class PlasmaGameGuiTextBoxModifier(_GameGuiMixin, TranslationMixin, PlasmaModifierProperties): pl_id = "gui_textbox" pl_depends = {"gui_control"} diff --git a/korman/ui/modifiers/game_gui.py b/korman/ui/modifiers/game_gui.py index b4dc17c4..cc4f04ef 100644 --- a/korman/ui/modifiers/game_gui.py +++ b/korman/ui/modifiers/game_gui.py @@ -115,6 +115,7 @@ def gui_button(modifier, layout, context): ) def gui_checkbox(modifier: PlasmaGameGuiCheckBoxModifier, layout, context): + layout.prop(modifier, "radio_group", icon="RADIOBUT_ON") layout.prop(modifier, "checked") _gui_anim("Check", modifier.anims, layout, context) @@ -179,6 +180,10 @@ def gui_control(modifier, layout, context): def gui_dragbar(modifier: PlasmaGameGuiDragBarModifier, layout, context): layout.label("Drag Bars have no settings.") +def gui_radio_group(modifier: PlasmaGameGuiRadioGroupModifier, layout, context): + layout.operator("object.plasma_select_radio_group", icon="RESTRICT_SELECT_OFF") + layout.prop(modifier, "allow_no_selection") + def gui_textbox(modifier: PlasmaGameGuiTextBoxModifier, layout, context): layout.prop(modifier, "justification") From 67d84081160b8b11714f6d46e038a2d122215df0 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 12 Aug 2025 13:38:00 -0500 Subject: [PATCH 12/21] Add GUI Dynamic Display Modifier. --- korman/idprops.py | 7 ++++ korman/properties/modifiers/game_gui.py | 50 +++++++++++++++++++++++++ korman/ui/modifiers/game_gui.py | 4 ++ 3 files changed, 61 insertions(+) diff --git a/korman/idprops.py b/korman/idprops.py index 9865e847..d9a925b9 100644 --- a/korman/idprops.py +++ b/korman/idprops.py @@ -142,6 +142,13 @@ def poll_object_dyntexts(self, value): obj_materials = frozenset(filter(None, (i.material for i in self.id_data.material_slots))) return bool(tex_materials & obj_materials) +def poll_object_image_textures(self, value): + if value.type != "IMAGE": + 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) + def poll_softvolume_objects(self, value): return value.plasma_modifiers.softvolume.enabled diff --git a/korman/properties/modifiers/game_gui.py b/korman/properties/modifiers/game_gui.py index af06442c..3615ba00 100644 --- a/korman/properties/modifiers/game_gui.py +++ b/korman/properties/modifiers/game_gui.py @@ -724,6 +724,56 @@ def requires_actor(self) -> bool: return True +class PlasmaGameGuiDynamicDisplayModifier(_GameGuiMixin, PlasmaModifierProperties): + pl_id = "gui_dynamic_display" + pl_depends = {"gui_control"} + pl_page_types = {"gui"} + + bl_category = "GUI" + bl_label = "GUI Dynamic Display (ex)" + bl_description = "XXX" + bl_icon = "TPAINT_HLT" + + texture = PointerProperty( + name="Texture", + description="Texture this GUI control can modify", + type=bpy.types.Texture, + poll=idprops.poll_object_image_textures + ) + + def sanity_check(self, exporter): + if self.texture is None: + raise ExportError(f"'{self.id_data.name}': GUI Dynamic Display Modifier requires a Texture!") + + def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, so: Optional[plSceneObject] = None) -> pfGUIDynDisplayCtrl: + return exporter.mgr.find_create_object(pfGUIDynDisplayCtrl, bl=bo, so=so) + + def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject) -> None: + ctrl = self.get_control(exporter, bo, so) + + layers = exporter.mesh.material.get_layers(bo, tex=self.texture) + materials = exporter.mesh.material.get_materials(bo) + for layer in layers: + ctrl.addLayer(layer) + tex_key = layer.object.texture + + # It is completely possible and legal to have a plMipmap. That + # happens on journal covers. We're provided a default cover + # texture that could be swapped out. + if tex_key is not None and tex_key.type == plFactory.kDynamicTextMap: + ctrl.addTextMap(tex_key) + + # This is a little lazy, but GUIs are so uncommon that we + # don't need to sweat efficiency. + for material in materials: + bottom_iter = (i.object.bottomOfStack for i in material.object.layers) + if layer.object.bottomOfStack in bottom_iter: + # PlasmaMax unconditionally adds materials, but that seems + # a little wasteful. + if material not in ctrl.materials: + ctrl.addMaterial(material) + + class PlasmaGameGuiRadioGroupModifier(_GameGuiMixin, PlasmaModifierProperties): pl_id = "gui_radio_group" pl_depends = {"gui_control"} diff --git a/korman/ui/modifiers/game_gui.py b/korman/ui/modifiers/game_gui.py index cc4f04ef..47addca3 100644 --- a/korman/ui/modifiers/game_gui.py +++ b/korman/ui/modifiers/game_gui.py @@ -180,6 +180,10 @@ def gui_control(modifier, layout, context): def gui_dragbar(modifier: PlasmaGameGuiDragBarModifier, layout, context): layout.label("Drag Bars have no settings.") +def gui_dynamic_display(modifier: PlasmaGameGuiDynamicDisplayModifier, layout, context): + layout.alert = modifier.texture is None + layout.prop(modifier, "texture") + def gui_radio_group(modifier: PlasmaGameGuiRadioGroupModifier, layout, context): layout.operator("object.plasma_select_radio_group", icon="RESTRICT_SELECT_OFF") layout.prop(modifier, "allow_no_selection") From 7f8f15e7fbe2d582e093e6bb4f6a9911b69e72e2 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 12 Aug 2025 13:46:42 -0500 Subject: [PATCH 13/21] Minor nitpicks for general controls. --- korman/properties/modifiers/game_gui.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/korman/properties/modifiers/game_gui.py b/korman/properties/modifiers/game_gui.py index 3615ba00..7d9d7cc8 100644 --- a/korman/properties/modifiers/game_gui.py +++ b/korman/properties/modifiers/game_gui.py @@ -68,7 +68,7 @@ def gui_sounds(self) -> Dict[str, int]: """Overload to automatically export GUI sounds on the control. This should return a dict of string attribute names to indices. """ - return [] + return {} def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, so: Optional[plSceneObject] = None) -> Optional[pfGUIControlMod]: return None @@ -266,7 +266,7 @@ class PlasmaGameGuiControlModifier(_GameGuiMixin, PlasmaModifierProperties): description="", default=True, options=set() - ) + ) proc = EnumProperty( name="Notification Procedure", description="", @@ -304,6 +304,8 @@ def convert_gui_control(self, exporter: Exporter, ctrl: pfGUIControlMod, bo: bpy handler = pfGUIConsoleCmdProc() handler.command = self.console_command ctrl.handler = handler + else: + raise ValueError(self.proc) def convert_gui_sounds(self, exporter: Exporter, ctrl: pfGUIControlMod, ctrl_mod: _GameGuiMixin): soundemit = ctrl_mod.id_data.plasma_modifiers.soundemit From 73971f0ea45f7dc7cbf762cab5dcf74aa7487970 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 12 Aug 2025 16:50:34 -0500 Subject: [PATCH 14/21] Standardize "better hit testing" option. --- korman/properties/modifiers/game_gui.py | 43 +++++++++++++++++++++++-- korman/ui/modifiers/game_gui.py | 4 +++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/korman/properties/modifiers/game_gui.py b/korman/properties/modifiers/game_gui.py index 7d9d7cc8..10aef41e 100644 --- a/korman/properties/modifiers/game_gui.py +++ b/korman/properties/modifiers/game_gui.py @@ -57,6 +57,10 @@ def text(self) -> str: class _GameGuiMixin: + @property + def allow_better_hit_testing(self) -> bool: + return False + @property def copy_material(self) -> bool: # If this control uses a dynamic text map, then its contents are unique. @@ -288,12 +292,25 @@ class PlasmaGameGuiControlModifier(_GameGuiMixin, PlasmaModifierProperties): type=bpy.types.Texture, poll=idprops.poll_object_dyntexts ) + hit_testing = EnumProperty( + name="Hit Testing", + description="", + items=[ + ("bounding_box", "Bounding Box", ""), + ("hull", "2D Convex Hull", ""), + ], + options=set() + ) def sanity_check(self, exporter: Exporter): if self.requires_dyntext and self.texture is None: raise ExportError(f"'{self.id_data.name}': GUI Control requires a Texture to draw onto.") - def convert_gui_control(self, exporter: Exporter, ctrl: pfGUIControlMod, bo: bpy.types.Object, so: plSceneObject): + def convert_gui_control( + self, exporter: Exporter, + ctrl: pfGUIControlMod, ctrl_mod: _GameGuiMixin, + bo: bpy.types.Object, so: plSceneObject + ) -> None: ctrl.tagID = self.tag_id ctrl.visible = self.visible if self.proc == "default": @@ -307,6 +324,12 @@ def convert_gui_control(self, exporter: Exporter, ctrl: pfGUIControlMod, bo: bpy else: raise ValueError(self.proc) + if ctrl_mod.allow_better_hit_testing: + ctrl.setFlag( + pfGUIControlMod.kBetterHitTesting, + self.hit_testing == "hull" + ) + def convert_gui_sounds(self, exporter: Exporter, ctrl: pfGUIControlMod, ctrl_mod: _GameGuiMixin): soundemit = ctrl_mod.id_data.plasma_modifiers.soundemit if not ctrl_mod.gui_sounds or not soundemit.enabled: @@ -354,7 +377,7 @@ def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): for ctrl_mod in ctrl_mods: ctrl_obj = ctrl_mod.get_control(exporter, bo, so) if ctrl_obj is not None: - self.convert_gui_control(exporter, ctrl_obj, bo, so) + self.convert_gui_control(exporter, ctrl_obj, ctrl_mod, bo, so) self.convert_gui_sounds(exporter, ctrl_obj, ctrl_mod) def post_export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): @@ -363,6 +386,10 @@ def post_export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObjec if ctrl_obj is not None: self.convert_gui_dyntext(exporter, ctrl_obj, ctrl_mod, bo, so) + @property + def allow_better_hit_testing(self) -> bool: + return any((i.allow_better_hit_testing for i in self.iterate_control_modifiers())) + @property def has_gui_proc(self) -> bool: return any((i.has_gui_proc for i in self.iterate_control_modifiers())) @@ -530,6 +557,10 @@ def _update_notify_type(self, context): options=set() ) + @property + def allow_better_hit_testing(self): + return True + @property def gui_sounds(self) -> Dict[str, int]: return { @@ -656,6 +687,10 @@ def _set_checked(self, value: bool) -> None: poll=_poll_radio_group ) + @property + def allow_better_hit_testing(self): + return True + @property def gui_sounds(self) -> Dict[str, int]: return { @@ -715,6 +750,10 @@ class PlasmaGameGuiDragBarModifier(_GameGuiMixin, PlasmaModifierProperties): bl_description = "XXX" bl_icon = "ARROW_LEFTRIGHT" + @property + def allow_better_hit_testing(self): + return True + def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, so: Optional[plSceneObject] = None) -> pfGUIDragBarCtrl: return exporter.mgr.find_create_object(pfGUIDragBarCtrl, bl=bo, so=so) diff --git a/korman/ui/modifiers/game_gui.py b/korman/ui/modifiers/game_gui.py index 47addca3..da7b5f8f 100644 --- a/korman/ui/modifiers/game_gui.py +++ b/korman/ui/modifiers/game_gui.py @@ -170,6 +170,10 @@ def gui_control(modifier, layout, context): col.alert = modifier.requires_dyntext and modifier.texture is None col.prop(modifier, "texture") + col = layout.column() + col.active = modifier.allow_better_hit_testing + col.prop(modifier, "hit_testing") + col = layout.column() col.active = modifier.has_gui_proc col.prop(modifier, "proc") From aa8295dae4f056c0dea986d86fdcb7f15349fe5d Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 12 Aug 2025 16:51:31 -0500 Subject: [PATCH 15/21] Add GUI Progress Control Modifier. --- korman/properties/modifiers/game_gui.py | 92 +++++++++++++++++++++++++ korman/ui/modifiers/game_gui.py | 17 +++++ 2 files changed, 109 insertions(+) diff --git a/korman/properties/modifiers/game_gui.py b/korman/properties/modifiers/game_gui.py index 10aef41e..a936da72 100644 --- a/korman/properties/modifiers/game_gui.py +++ b/korman/properties/modifiers/game_gui.py @@ -815,6 +815,48 @@ def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject) -> ctrl.addMaterial(material) +class PlasmaGameGuiProgressControlModifier(_GameGuiMixin, PlasmaModifierProperties): + pl_id = "gui_progress" + pl_depends = {"gui_control", "gui_value"} + pl_page_types = {"gui"} + + bl_category = "GUI" + bl_label = "GUI Progress Control (ex)" + bl_description = "XXX" + bl_icon = "SETTINGS" + + anims: GameGuiAnimationGroup = PointerProperty(type=GameGuiAnimationGroup) + show_expanded_sounds: bool = BoolProperty(options={"HIDDEN"}) + animate_sound: str = StringProperty( + name="Animate Sound", + description="Sound played as the progress control animates", + options=set() + ) + direction: str = EnumProperty( + name="Direction", + description="Which direction the animation should play", + items=[ + ("foreward", "Foreward", "Play animation such that 100% progress is the end"), + ("backward", "Backward", "Play the animation such that 100% progress is the beginning"), + ], + options=set() + ) + + @property + def gui_sounds(self) -> Dict[str, int]: + return { + "animate_sound": pfGUIProgressCtrl.kAnimateSound, + } + + def get_control(self, exporter: Exporter, bo = None, so = None) -> pfGUIProgressCtrl: + return exporter.mgr.find_create_object(pfGUIProgressCtrl, bl=bo, so=so) + + def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject) -> None: + ctrl = self.get_control(exporter, bo, so) + self.anims.export(exporter, bo, so, ctrl, ctrl.addAnimKey, "animName") + ctrl.setFlag(pfGUIProgressCtrl.kReverseValues, self.direction == "backward") + + class PlasmaGameGuiRadioGroupModifier(_GameGuiMixin, PlasmaModifierProperties): pl_id = "gui_radio_group" pl_depends = {"gui_control"} @@ -950,6 +992,56 @@ def requires_dyntext(self): return True +class PlasmaGameGuiValueControlModifier(_GameGuiMixin, PlasmaModifierProperties): + pl_id = "gui_value" + pl_depends = {"gui_control"} + pl_page_types = {"gui"} + + bl_category = "GUI" + bl_label = "GUI Value Control (ex)" + bl_description = "XXX" + bl_icon = "LINENUMBERS_ON" + + min_value = FloatProperty( + name="Min", + description="Minimum Value", + options=set() + ) + + max_value = FloatProperty( + name="Max", + description="Maximum Value", + default=10.0, + min=-10000.0, + max=10000.0, + options=set() + ) + + step = FloatProperty( + name="Step", + description="", + default=1.0, + options=set() + ) + + @property + def has_gui_proc(self) -> bool: + return False + + @classmethod + def is_game_gui_control(cls) -> bool: + # This is a base class + return False + + def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): + for ctrl_mod in self.iterate_control_modifiers(): + ctrl = ctrl_mod.get_control(exporter, bo, so) + if isinstance(ctrl, pfGUIValueCtrl): + ctrl.min = min(self.min_value, self.max_value) + ctrl.max = max(self.min_value, self.max_value) + ctrl.step = self.step + + class PlasmaGameGuiDialogModifier(_GameGuiMixin, PlasmaModifierProperties): pl_id = "gui_dialog" pl_page_types = {"gui"} diff --git a/korman/ui/modifiers/game_gui.py b/korman/ui/modifiers/game_gui.py index da7b5f8f..5cdaa59d 100644 --- a/korman/ui/modifiers/game_gui.py +++ b/korman/ui/modifiers/game_gui.py @@ -188,6 +188,16 @@ def gui_dynamic_display(modifier: PlasmaGameGuiDynamicDisplayModifier, layout, c layout.alert = modifier.texture is None layout.prop(modifier, "texture") +def gui_progress(modifier: PlasmaGameGuiProgressControlModifier, layout, context): + layout.prop(modifier, "direction") + _gui_anim("Animation", modifier.anims, layout, context) + _gui_sounds( + modifier, layout, context, + { + "animate_sound": "Animation", + } + ) + def gui_radio_group(modifier: PlasmaGameGuiRadioGroupModifier, layout, context): layout.operator("object.plasma_select_radio_group", icon="RESTRICT_SELECT_OFF") layout.prop(modifier, "allow_no_selection") @@ -205,6 +215,13 @@ def gui_textbox(modifier: PlasmaGameGuiTextBoxModifier, layout, context): else: sub.prop(translation, "value") +def gui_value(modifier: PlasmaGameGuiValueControlModifier, layout, context): + row = layout.row(align=True) + row.alert = modifier.min_value >= modifier.max_value + row.prop(modifier, "min_value") + row.prop(modifier, "step") + row.prop(modifier, "max_value") + def gui_dialog(modifier, layout, context): row = layout.row(align=True) row.prop(modifier, "camera_object") From 78d991346e94718985607bad3c2780dcfc8e430c Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 12 Aug 2025 17:20:53 -0500 Subject: [PATCH 16/21] Standardize more GUI flags. --- korman/properties/modifiers/game_gui.py | 55 +++++++++++++++++++++++-- korman/ui/modifiers/game_gui.py | 4 ++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/korman/properties/modifiers/game_gui.py b/korman/properties/modifiers/game_gui.py index a936da72..d4773a62 100644 --- a/korman/properties/modifiers/game_gui.py +++ b/korman/properties/modifiers/game_gui.py @@ -61,6 +61,10 @@ class _GameGuiMixin: def allow_better_hit_testing(self) -> bool: return False + @property + def allow_text_scaling(self) -> bool: + return self.requires_dyntext + @property def copy_material(self) -> bool: # If this control uses a dynamic text map, then its contents are unique. @@ -81,6 +85,10 @@ def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, def has_gui_proc(self) -> bool: return True + @property + def intangible(self) -> bool: + return False + def iterate_control_modifiers(self) -> Iterator[_GameGuiMixin]: pl_mods = self.id_data.plasma_modifiers yield from ( @@ -137,6 +145,10 @@ def sanity_check(self, exporter): def wants_colorscheme(self) -> bool: return self.requires_dyntext + @property + def wants_interest(self) -> bool: + return False + class PlasmaGameGuiColorSchemeModifier(_GameGuiMixin, PlasmaModifierProperties): pl_id = "gui_colorscheme" @@ -301,6 +313,11 @@ class PlasmaGameGuiControlModifier(_GameGuiMixin, PlasmaModifierProperties): ], options=set() ) + scale_text: bool = BoolProperty( + name="Scale Text", + description="Scale text up as game resolution increases", + options=set() + ) def sanity_check(self, exporter: Exporter): if self.requires_dyntext and self.texture is None: @@ -329,6 +346,14 @@ def convert_gui_control( pfGUIControlMod.kBetterHitTesting, self.hit_testing == "hull" ) + if ctrl_mod.allow_text_scaling: + ctrl.setFlag( + pfGUIControlMod.kScaleTextWithResolution, + self.scale_text + ) + + ctrl.setFlag(pfGUIControlMod.kIntangible, ctrl_mod.intangible) + ctrl.setFlag(pfGUIControlMod.kWantsInterest, ctrl_mod.wants_interest) def convert_gui_sounds(self, exporter: Exporter, ctrl: pfGUIControlMod, ctrl_mod: _GameGuiMixin): soundemit = ctrl_mod.id_data.plasma_modifiers.soundemit @@ -390,6 +415,10 @@ def post_export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObjec def allow_better_hit_testing(self) -> bool: return any((i.allow_better_hit_testing for i in self.iterate_control_modifiers())) + @property + def allow_text_scaling(self) -> bool: + return any((i.allow_text_scaling for i in self.iterate_control_modifiers())) + @property def has_gui_proc(self) -> bool: return any((i.has_gui_proc for i in self.iterate_control_modifiers())) @@ -575,7 +604,6 @@ def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): ctrl = self.get_control(exporter, bo, so) - ctrl.setFlag(pfGUIControlMod.kWantsInterest, True) if self.notify_type == {"UP"}: ctrl.notifyType = pfGUIButtonMod.kNotifyOnUp @@ -589,6 +617,9 @@ def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): self.mouse_over_anims.export(exporter, bo, so, ctrl, ctrl.addMouseOverKey, "mouseOverAnimName") self.mouse_click_anims.export(exporter, bo, so, ctrl, ctrl.addAnimationKey, "animName") + @property + def wants_interest(self): + return True class PlasmaGameGuiCheckBoxModifier(_GameGuiMixin, PlasmaModifierProperties): @@ -705,11 +736,14 @@ def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): ctrl = self.get_control(exporter, bo, so) - ctrl.setFlag(pfGUIControlMod.kWantsInterest, True) ctrl.checked = self.checked self.anims.export(exporter, bo, so, ctrl, ctrl.addAnimKey, "animName") + @property + def wants_interest(self): + return True + class PlasamGameGuiClickMapModifier(_GameGuiMixin, PlasmaModifierProperties): pl_id = "gui_clickmap" @@ -764,6 +798,10 @@ def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): def requires_actor(self) -> bool: return True + @property + def wants_interest(self): + return True + class PlasmaGameGuiDynamicDisplayModifier(_GameGuiMixin, PlasmaModifierProperties): pl_id = "gui_dynamic_display" @@ -814,6 +852,10 @@ def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject) -> if material not in ctrl.materials: ctrl.addMaterial(material) + @property + def intangible(self): + return True + class PlasmaGameGuiProgressControlModifier(_GameGuiMixin, PlasmaModifierProperties): pl_id = "gui_progress" @@ -889,6 +931,10 @@ def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject) -> if cb_mod.checked: ctrl.defaultValue = i + @property + def intangible(self): + return True + def iter_checkbox_mods(self, context: bpy.types.Context) -> Iterator[PlasmaGameGuiCheckBoxModifier]: # This is really not the fastest way to do this. The fastest way would be for us # to maintain a list of the checkbox children here. But that means the user could @@ -973,12 +1019,15 @@ def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): ctrl = self.get_control(exporter, bo, so) - ctrl.setFlag(pfGUIControlMod.kIntangible, True) just_flag = self._JUSTIFICATION_LUT.get(self.justification) if just_flag is not None: ctrl.setFlag(just_flag, True) + @property + def intangible(self): + return True + @property def localization_set(self) -> str: return "GUI" diff --git a/korman/ui/modifiers/game_gui.py b/korman/ui/modifiers/game_gui.py index 5cdaa59d..7a26283b 100644 --- a/korman/ui/modifiers/game_gui.py +++ b/korman/ui/modifiers/game_gui.py @@ -162,6 +162,10 @@ def gui_control(modifier, layout, context): col = split.column() col.prop(modifier, "visible") + col = split.column() + col.active = modifier.allow_text_scaling + col.prop(modifier, "scale_text") + col = split.column() col.prop(modifier, "tag_id") From 006463d4b9849fea895400fe3db854132814a856 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 12 Aug 2025 17:27:28 -0500 Subject: [PATCH 17/21] Allow the GUI Control Modifier to be applied to Empties. Radio Groups currently need to be a dedicated object. A radio group is intangible, so an empty is a good choice for that, IMO. --- korman/properties/modifiers/game_gui.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/korman/properties/modifiers/game_gui.py b/korman/properties/modifiers/game_gui.py index d4773a62..721c42df 100644 --- a/korman/properties/modifiers/game_gui.py +++ b/korman/properties/modifiers/game_gui.py @@ -269,7 +269,8 @@ class PlasmaGameGuiControlModifier(_GameGuiMixin, PlasmaModifierProperties): bl_category = "GUI" bl_label = "GUI Control (ex)" bl_description = "XXX" - bl_object_types = {"FONT", "MESH"} + bl_object_types = {"EMPTY", "FONT", "MESH"} + bl_icon = "FULLSCREEN" tag_id = IntProperty( name="Tag ID", From 4976b18a09bb52314556c5f427d0b07906e7fdc8 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sat, 16 Aug 2025 13:39:10 -0500 Subject: [PATCH 18/21] Add GUI Input Box Modifier. This is two components in PlasmaMax... The edit box and multi line edit box. IMO, the two are almost identical, so it's best to use one modifier and just add a single or multi line flag. --- korman/properties/modifiers/game_gui.py | 86 +++++++++++++++++++++++++ korman/ui/modifiers/game_gui.py | 7 ++ 2 files changed, 93 insertions(+) diff --git a/korman/properties/modifiers/game_gui.py b/korman/properties/modifiers/game_gui.py index 721c42df..c4dbdabf 100644 --- a/korman/properties/modifiers/game_gui.py +++ b/korman/properties/modifiers/game_gui.py @@ -149,6 +149,10 @@ def wants_colorscheme(self) -> bool: def wants_interest(self) -> bool: return False + @property + def wants_special_keys(self) -> bool: + return False + class PlasmaGameGuiColorSchemeModifier(_GameGuiMixin, PlasmaModifierProperties): pl_id = "gui_colorscheme" @@ -354,6 +358,7 @@ def convert_gui_control( ) ctrl.setFlag(pfGUIControlMod.kIntangible, ctrl_mod.intangible) + ctrl.setFlag(pfGUIControlMod.kTakesSpecialKeys, ctrl_mod.wants_special_keys) ctrl.setFlag(pfGUIControlMod.kWantsInterest, ctrl_mod.wants_interest) def convert_gui_sounds(self, exporter: Exporter, ctrl: pfGUIControlMod, ctrl_mod: _GameGuiMixin): @@ -858,6 +863,82 @@ def intangible(self): return True +class PlasmaGameGuiInputBoxModifier(_GameGuiMixin, PlasmaModifierProperties): + pl_id = "gui_input" + pl_depends = {"gui_control"} + pl_page_types = {"gui"} + + bl_category = "GUI" + bl_label = "GUI Input Box (ex)" + bl_description = "XXX" + bl_icon = "SYNTAX_ON" + bl_object_types = {"MESH"} + + lines = EnumProperty( + name="Box Type", + description="", + items=[ + ("single", "Single Line", "A single line edit box"), + ("multi", "Multi Line", "A multiple line text box"), + ], + options=set() + ) + + def _poll_scroll_ctrl(self, value: bpy.types.Object) -> bool: + if value.plasma_object.page != self.id_data.plasma_object.page: + return False + return value.plasma_modifiers.gui_value.enabled + + scroll_control = PointerProperty( + name="Scroll Control", + description="", + poll=_poll_scroll_ctrl, + type=bpy.types.Object + ) + + def sanity_check(self, exporter: Exporter): + if self.scroll_control is not None: + value_controls = list( + self.scroll_control.plasma_modifiers.gui_value.iterate_value_modifiers() + ) + num_value_controls = len(value_controls) + if num_value_controls != 1: + raise ExportError( + f"'{self.id_data.name}': Scroll control '{self.id_data.name}' is invalid. " + f"Expected exactly 1 value control, found {num_value_controls}." + ) + + def get_control( + self, exporter: Exporter, bo = None, so = None + ) -> Union[pfGUIEditBoxMod, pfGUIMultiLineEditCtrl]: + if self.lines == "single": + return exporter.mgr.find_create_object(pfGUIEditBoxMod, bl=bo, so=so) + elif self.lines == "multi": + return exporter.mgr.find_create_object(pfGUIMultiLineEditCtrl, bl=bo, so=so) + else: + raise ValueError(self.lines) + + def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject) -> None: + ctrl = self.get_control(exporter, bo, so) + if isinstance(ctrl, pfGUIMultiLineEditCtrl) and self.scroll_control is not None: + ctrl.scrollCtrl = next( + self.scroll_control.plasma_modifiers.gui_value.iterate_value_modifiers(), + None + ) + + @property + def requires_dyntext(self): + return True + + @property + def wants_interest(self): + return True + + @property + def wants_special_keys(self): + return True + + class PlasmaGameGuiProgressControlModifier(_GameGuiMixin, PlasmaModifierProperties): pl_id = "gui_progress" pl_depends = {"gui_control", "gui_value"} @@ -1083,6 +1164,11 @@ def is_game_gui_control(cls) -> bool: # This is a base class return False + def iterate_value_modifiers(self) -> Iterator[_GameGuiMixin]: + for i in self.iterate_control_modifiers(): + if self.pl_id in getattr(i, "pl_depends", set()): + yield i + def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): for ctrl_mod in self.iterate_control_modifiers(): ctrl = ctrl_mod.get_control(exporter, bo, so) diff --git a/korman/ui/modifiers/game_gui.py b/korman/ui/modifiers/game_gui.py index 7a26283b..30a245cb 100644 --- a/korman/ui/modifiers/game_gui.py +++ b/korman/ui/modifiers/game_gui.py @@ -192,6 +192,13 @@ def gui_dynamic_display(modifier: PlasmaGameGuiDynamicDisplayModifier, layout, c layout.alert = modifier.texture is None layout.prop(modifier, "texture") +def gui_input(modifier: PlasmaGameGuiInputBoxModifier, layout, context): + layout.prop(modifier, "lines") + + row = layout.row() + row.active = modifier.lines == "multi" + row.prop(modifier, "scroll_control", icon="LINENUMBERS_ON") + def gui_progress(modifier: PlasmaGameGuiProgressControlModifier, layout, context): layout.prop(modifier, "direction") _gui_anim("Animation", modifier.anims, layout, context) From 7ceca68e83e431194ba98c2435dda41a9dd24ada Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sat, 16 Aug 2025 13:41:21 -0500 Subject: [PATCH 19/21] Add `pfGUIDraggableMod` support to the Draggable Modifier. I'm not 100% sure what the difference is between DragBar and Draggable is in terms of when and how they are used. Draggables have some extra reporting features and can be used by buttons. Unfortunately, there is only *one* Draggable in all of Uru... the pod map in the Ae'gura Museum, and it's not a draggable button. Even though I don't quite understand how it all fits together, I think it's fine to simply add the support - this Game GUI stuff is still all explicitly "experimental," so here be dragons, yo. --- korman/properties/modifiers/game_gui.py | 76 +++++++++++++++++++++++-- korman/ui/modifiers/game_gui.py | 11 +++- 2 files changed, 80 insertions(+), 7 deletions(-) diff --git a/korman/properties/modifiers/game_gui.py b/korman/properties/modifiers/game_gui.py index c4dbdabf..28908d6e 100644 --- a/korman/properties/modifiers/game_gui.py +++ b/korman/properties/modifiers/game_gui.py @@ -592,6 +592,21 @@ def _update_notify_type(self, context): options=set() ) + def _poll_control_draggable(self, value: bpy.types.Object) -> bool: + if value.plasma_object.page != self.id_data.plasma_object.page: + return False + draggable_mod = value.plasma_modifiers.gui_draggable + if not draggable_mod.enabled: + return False + return draggable_mod.drag_target == "control" + + draggable: bpy.types.Object = PointerProperty( + name="Draggable", + description="", + type=bpy.types.Object, + poll=_poll_control_draggable + ) + @property def allow_better_hit_testing(self): return True @@ -608,6 +623,12 @@ def gui_sounds(self) -> Dict[str, int]: def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, so: Optional[plSceneObject] = None) -> pfGUIButtonMod: return exporter.mgr.find_create_object(pfGUIButtonMod, bl=bo, so=so) + def sanity_check(self, exporter): + if self.draggable is not None: + draggable_mod = self.draggable.plasma_modifiers.gui_draggable + if draggable_mod.drag_target != "control": + raise ExportError(f"'{self.id_data.name}': Draggable must target a control!") + def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): ctrl = self.get_control(exporter, bo, so) @@ -623,6 +644,13 @@ def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): self.mouse_over_anims.export(exporter, bo, so, ctrl, ctrl.addMouseOverKey, "mouseOverAnimName") self.mouse_click_anims.export(exporter, bo, so, ctrl, ctrl.addAnimationKey, "animName") + # I'm not 100% sure what a draggable attached to a button is useful for. + # The Plasma code has basically no comments and doesn't seem "right" to me, + # but maybe it will be useful to somone. + if self.draggable: + draggable_mod = self.draggable.plasma_modifiers.gui_draggable + ctrl.draggable = draggable_mod.get_control(exporter, self.draggable).key + @property def wants_interest(self): return True @@ -780,25 +808,63 @@ def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): ctrl.setFlag(getattr(pfGUIClickMapCtrl, report), True) -class PlasmaGameGuiDragBarModifier(_GameGuiMixin, PlasmaModifierProperties): - pl_id = "gui_dragbar" +class PlasmaGameGuiDraggableModifier(_GameGuiMixin, PlasmaModifierProperties): + pl_id = "gui_draggable" pl_depends = {"gui_control"} pl_page_types = {"gui"} bl_category = "GUI" - bl_label = "GUI Drag Bar (ex)" + bl_label = "GUI Dragable (ex)" bl_description = "XXX" bl_icon = "ARROW_LEFTRIGHT" + drag_target = EnumProperty( + name="Drag Target", + description="", + items=[ + ("dialog", "Parent Dialog", "Drag the entire dialog"), + ("control", "Control", "Drag just this control"), + ], + options=set() + ) + + report_dragging = BoolProperty( + name="Report While Dragging", + description="Call the notification procedure during dragging (as opposed to only at the begin/end of dragging)", + options=set() + ) + hide_cursor = BoolProperty( + name="Hide Cursor", + description="Hide the cursor while dragging", + options=set() + ) + snap_back = BoolProperty( + name="Snap Back", + description="Snap the control back to its original position when the mouse goes up", + options=set() + ) + @property def allow_better_hit_testing(self): return True - def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, so: Optional[plSceneObject] = None) -> pfGUIDragBarCtrl: - return exporter.mgr.find_create_object(pfGUIDragBarCtrl, bl=bo, so=so) + def get_control( + self, exporter: Exporter, + bo: Optional[bpy.types.Object] = None, so: Optional[plSceneObject] = None + ) -> Union[pfGUIDragBarCtrl, pfGUIDraggableMod]: + if self.drag_target == "dialog": + return exporter.mgr.find_create_object(pfGUIDragBarCtrl, bl=bo, so=so) + elif self.drag_target == "control": + return exporter.mgr.find_create_object(pfGUIDraggableMod, bl=bo, so=so) + else: + raise ValueError(self.drag_target) def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): ctrl = self.get_control(exporter, bo, so) + if isinstance(ctrl, pfGUIDraggableMod): + ctrl.setFlag(pfGUIDraggableMod.kReportDragging, self.report_dragging) + ctrl.setFlag(pfGUIDraggableMod.kHideCursorWhileDragging, self.hide_cursor) + ctrl.setFlag(pfGUIDraggableMod.kAlwaysSnapBackToStart, self.snap_back) @property def requires_actor(self) -> bool: diff --git a/korman/ui/modifiers/game_gui.py b/korman/ui/modifiers/game_gui.py index 30a245cb..57088b4f 100644 --- a/korman/ui/modifiers/game_gui.py +++ b/korman/ui/modifiers/game_gui.py @@ -100,6 +100,7 @@ def gui_button(modifier, layout, context): row = layout.row() row.label("Notify On:") row.prop(modifier, "notify_type") + layout.prop(modifier, "draggable") _gui_anim("Mouse Click", modifier.mouse_click_anims, layout, context) _gui_anim("Mouse Over", modifier.mouse_over_anims, layout, context) @@ -185,8 +186,14 @@ def gui_control(modifier, layout, context): row.active = col.active and modifier.proc == "console_command" row.prop(modifier, "console_command") -def gui_dragbar(modifier: PlasmaGameGuiDragBarModifier, layout, context): - layout.label("Drag Bars have no settings.") +def gui_draggable(modifier: PlasmaGameGuiDraggableModifier, layout, context): + layout.prop(modifier, "drag_target") + + row = layout.row() + row.active = modifier.drag_target == "control" + row.prop(modifier, "report_dragging") + row.prop(modifier, "hide_cursor") + row.prop(modifier, "snap_back") def gui_dynamic_display(modifier: PlasmaGameGuiDynamicDisplayModifier, layout, context): layout.alert = modifier.texture is None From d7cbb1895701cb54ddffa3668fc6b7bbd9882531 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sat, 16 Aug 2025 14:06:08 -0500 Subject: [PATCH 20/21] Narrowly allow multiple GUI Modifiers. --- korman/properties/modifiers/game_gui.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/korman/properties/modifiers/game_gui.py b/korman/properties/modifiers/game_gui.py index 28908d6e..c8726072 100644 --- a/korman/properties/modifiers/game_gui.py +++ b/korman/properties/modifiers/game_gui.py @@ -123,13 +123,24 @@ def sanity_check(self, exporter): if our_page is None or our_page.page_type != "gui": raise ExportError(f"'{self.id_data.name}': {self.bl_label} Modifier must be in a GUI page!") - # Only one Game GUI Control per object. Continuously check this because objects can be - # generated/mutated during the pre-export phase. - modifiers = self.id_data.plasma_modifiers - controls = [i for i in self.iterate_control_subclasses() if getattr(modifiers, i.pl_id).enabled] - num_controls = len(controls) - if num_controls > 1: - raise ExportError(f"'{self.id_data.name}': Only 1 GUI Control modifier is allowed per object. We found {num_controls}.") + # Previously, only one Game GUI control per object was allowed. Now, + # we will allow multiple controls, but only ONE of them can be tangible. + # So, it's ok to have a draggable text box, or have your radio group + # modifier attached to the first checkbox (but you probably don't want + # to do that because then the tag IDs will be all yucky). + # Anyway, we need to check this continually throughout the export progress + # in case some pre_export() functions generate something illegal. + all_gui_mods = list(self.iterate_control_modifiers()) + num_tangible = len([i for i in all_gui_mods if not i.intangible]) + if num_tangible > 1: + control_msg = "\n".join( + f"'{i.bl_label}': {'Intangible' if i.intangible else 'Tangible'}" for i in all_gui_mods + ) + raise ExportError( + f"'{self.id_data.name}': Only 1 tangible Game GUI Control modifier is allowed per object. We found:\n" + f"{control_msg}\n" + f"That's {num_tangible} tangible controls!" + ) # Blow up on invalid sounds soundemit = self.id_data.plasma_modifiers.soundemit From a9aa27331ab1af489f1c91823bd3781772e616bb Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sat, 16 Aug 2025 14:13:01 -0500 Subject: [PATCH 21/21] Make `GameGuiMixin.get_control()` less bugprone. I've already had to rewrite history once because I was fetching controls on the wrong objects. So, let's remove that footgun. --- korman/properties/modifiers/game_gui.py | 79 ++++++++++++------------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/korman/properties/modifiers/game_gui.py b/korman/properties/modifiers/game_gui.py index c8726072..a87d7d60 100644 --- a/korman/properties/modifiers/game_gui.py +++ b/korman/properties/modifiers/game_gui.py @@ -78,7 +78,7 @@ def gui_sounds(self) -> Dict[str, int]: """ return {} - def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, so: Optional[plSceneObject] = None) -> Optional[pfGUIControlMod]: + def get_control(self, exporter: Exporter) -> Optional[pfGUIControlMod]: return None @property @@ -265,7 +265,7 @@ def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): # so we need to give each control a unique color scheme object. Dialogs will copy, but # that's a less common case. for i in scheme_targets: - ctrl = i.get_control(exporter, i.id_data) + ctrl = i.get_control(exporter) if ctrl is not None: ctrl.colorScheme = self.convert_colorscheme() @@ -417,14 +417,14 @@ def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): exporter.report.msg(str(list(self.iterate_control_subclasses()))) exporter.report.warn("This modifier has no effect because no GUI control modifiers are present!") for ctrl_mod in ctrl_mods: - ctrl_obj = ctrl_mod.get_control(exporter, bo, so) + ctrl_obj = ctrl_mod.get_control(exporter) if ctrl_obj is not None: self.convert_gui_control(exporter, ctrl_obj, ctrl_mod, bo, so) self.convert_gui_sounds(exporter, ctrl_obj, ctrl_mod) def post_export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): for ctrl_mod in self.iterate_control_modifiers(): - ctrl_obj = ctrl_mod.get_control(exporter, bo, so) + ctrl_obj = ctrl_mod.get_control(exporter) if ctrl_obj is not None: self.convert_gui_dyntext(exporter, ctrl_obj, ctrl_mod, bo, so) @@ -631,8 +631,8 @@ def gui_sounds(self) -> Dict[str, int]: "mouse_off_sound": pfGUIButtonMod.kMouseOff, } - def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, so: Optional[plSceneObject] = None) -> pfGUIButtonMod: - return exporter.mgr.find_create_object(pfGUIButtonMod, bl=bo, so=so) + def get_control(self, exporter: Exporter) -> pfGUIButtonMod: + return exporter.mgr.find_create_object(pfGUIButtonMod, bl=self.id_data) def sanity_check(self, exporter): if self.draggable is not None: @@ -641,7 +641,7 @@ def sanity_check(self, exporter): raise ExportError(f"'{self.id_data.name}': Draggable must target a control!") def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): - ctrl = self.get_control(exporter, bo, so) + ctrl = self.get_control(exporter) if self.notify_type == {"UP"}: ctrl.notifyType = pfGUIButtonMod.kNotifyOnUp @@ -660,7 +660,7 @@ def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): # but maybe it will be useful to somone. if self.draggable: draggable_mod = self.draggable.plasma_modifiers.gui_draggable - ctrl.draggable = draggable_mod.get_control(exporter, self.draggable).key + ctrl.draggable = draggable_mod.get_control(exporter).key @property def wants_interest(self): @@ -776,11 +776,11 @@ def gui_sounds(self) -> Dict[str, int]: "mouse_off_sound": pfGUICheckBoxCtrl.kMouseOff, } - def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, so: Optional[plSceneObject] = None) -> pfGUICheckBoxCtrl: - return exporter.mgr.find_create_object(pfGUICheckBoxCtrl, bl=bo, so=so) + def get_control(self, exporter: Exporter) -> pfGUICheckBoxCtrl: + return exporter.mgr.find_create_object(pfGUICheckBoxCtrl, bl=self.id_data) def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): - ctrl = self.get_control(exporter, bo, so) + ctrl = self.get_control(exporter) ctrl.checked = self.checked self.anims.export(exporter, bo, so, ctrl, ctrl.addAnimKey, "animName") @@ -810,11 +810,11 @@ class PlasamGameGuiClickMapModifier(_GameGuiMixin, PlasmaModifierProperties): options={"ENUM_FLAG"} ) - def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, so: Optional[plSceneObject] = None) -> pfGUIClickMapCtrl: - return exporter.mgr.find_create_object(pfGUIClickMapCtrl, bl=bo, so=so) + def get_control(self, exporter: Exporter) -> pfGUIClickMapCtrl: + return exporter.mgr.find_create_object(pfGUIClickMapCtrl, bl=self.id_data) def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): - ctrl = self.get_control(exporter, bo, so) + ctrl = self.get_control(exporter) for report in self.report_while: ctrl.setFlag(getattr(pfGUIClickMapCtrl, report), True) @@ -861,17 +861,16 @@ def allow_better_hit_testing(self): def get_control( self, exporter: Exporter, - bo: Optional[bpy.types.Object] = None, so: Optional[plSceneObject] = None ) -> Union[pfGUIDragBarCtrl, pfGUIDraggableMod]: if self.drag_target == "dialog": - return exporter.mgr.find_create_object(pfGUIDragBarCtrl, bl=bo, so=so) + return exporter.mgr.find_create_object(pfGUIDragBarCtrl, bl=self.id_data) elif self.drag_target == "control": - return exporter.mgr.find_create_object(pfGUIDraggableMod, bl=bo, so=so) + return exporter.mgr.find_create_object(pfGUIDraggableMod, bl=self.id_data) else: raise ValueError(self.drag_target) def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): - ctrl = self.get_control(exporter, bo, so) + ctrl = self.get_control(exporter) if isinstance(ctrl, pfGUIDraggableMod): ctrl.setFlag(pfGUIDraggableMod.kReportDragging, self.report_dragging) ctrl.setFlag(pfGUIDraggableMod.kHideCursorWhileDragging, self.hide_cursor) @@ -907,11 +906,11 @@ def sanity_check(self, exporter): if self.texture is None: raise ExportError(f"'{self.id_data.name}': GUI Dynamic Display Modifier requires a Texture!") - def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, so: Optional[plSceneObject] = None) -> pfGUIDynDisplayCtrl: - return exporter.mgr.find_create_object(pfGUIDynDisplayCtrl, bl=bo, so=so) + def get_control(self, exporter: Exporter) -> pfGUIDynDisplayCtrl: + return exporter.mgr.find_create_object(pfGUIDynDisplayCtrl, bl=self.id_data) def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject) -> None: - ctrl = self.get_control(exporter, bo, so) + ctrl = self.get_control(exporter) layers = exporter.mesh.material.get_layers(bo, tex=self.texture) materials = exporter.mesh.material.get_materials(bo) @@ -986,17 +985,17 @@ def sanity_check(self, exporter: Exporter): ) def get_control( - self, exporter: Exporter, bo = None, so = None + self, exporter: Exporter ) -> Union[pfGUIEditBoxMod, pfGUIMultiLineEditCtrl]: if self.lines == "single": - return exporter.mgr.find_create_object(pfGUIEditBoxMod, bl=bo, so=so) + return exporter.mgr.find_create_object(pfGUIEditBoxMod, bl=self.id_data) elif self.lines == "multi": - return exporter.mgr.find_create_object(pfGUIMultiLineEditCtrl, bl=bo, so=so) + return exporter.mgr.find_create_object(pfGUIMultiLineEditCtrl, bl=self.id_data) else: raise ValueError(self.lines) def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject) -> None: - ctrl = self.get_control(exporter, bo, so) + ctrl = self.get_control(exporter) if isinstance(ctrl, pfGUIMultiLineEditCtrl) and self.scroll_control is not None: ctrl.scrollCtrl = next( self.scroll_control.plasma_modifiers.gui_value.iterate_value_modifiers(), @@ -1049,11 +1048,11 @@ def gui_sounds(self) -> Dict[str, int]: "animate_sound": pfGUIProgressCtrl.kAnimateSound, } - def get_control(self, exporter: Exporter, bo = None, so = None) -> pfGUIProgressCtrl: - return exporter.mgr.find_create_object(pfGUIProgressCtrl, bl=bo, so=so) + def get_control(self, exporter: Exporter) -> pfGUIProgressCtrl: + return exporter.mgr.find_create_object(pfGUIProgressCtrl, bl=self.id_data) def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject) -> None: - ctrl = self.get_control(exporter, bo, so) + ctrl = self.get_control(exporter) self.anims.export(exporter, bo, so, ctrl, ctrl.addAnimKey, "animName") ctrl.setFlag(pfGUIProgressCtrl.kReverseValues, self.direction == "backward") @@ -1074,11 +1073,11 @@ class PlasmaGameGuiRadioGroupModifier(_GameGuiMixin, PlasmaModifierProperties): options=set() ) - def get_control(self, exporter: Exporter, bo = None, so = None) -> pfGUIRadioGroupCtrl: - return exporter.mgr.find_create_object(pfGUIRadioGroupCtrl, bl=bo, so=so) + def get_control(self, exporter: Exporter) -> pfGUIRadioGroupCtrl: + return exporter.mgr.find_create_object(pfGUIRadioGroupCtrl, bl=self.id_data) def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject) -> None: - ctrl = self.get_control(exporter, bo, so) + ctrl = self.get_control(exporter) ctrl.setFlag(pfGUIRadioGroupCtrl.kAllowNoSelection, self.allow_no_selection) active_cbs = ( i for i in self.iter_checkbox_mods(bpy.context) @@ -1086,7 +1085,7 @@ def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject) -> ) for i, cb_mod in enumerate(active_cbs): exporter.report.msg(f"Found checkbox '{cb_mod.id_data.name}'") - ctrl.addControl(cb_mod.get_control(exporter, i.id_data).key) + ctrl.addControl(cb_mod.get_control(exporter).key) if cb_mod.checked: ctrl.defaultValue = i @@ -1166,18 +1165,18 @@ def convert_string(self, exporter: Exporter) -> str: def export_localization(self, exporter: Exporter): # Only MOUL, EoA, and Hex Isle have pfLocalization support in GUIs. # Otherwise, this translation mixin does something we don't actually want. - ctrl = self.get_control(exporter, self.id_data) + ctrl = self.get_control(exporter) if exporter.mgr.getVer() >= pvMoul: super().export_localization(exporter) ctrl.localizationPath = f"{exporter.age_name}.{self.localization_set}.{self.key_name}" else: ctrl.text = self.convert_string(exporter) - def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, so: Optional[plSceneObject] = None) -> pfGUITextBoxMod: - return exporter.mgr.find_create_object(pfGUITextBoxMod, bl=bo, so=so) + def get_control(self, exporter: Exporter) -> pfGUITextBoxMod: + return exporter.mgr.find_create_object(pfGUITextBoxMod, bl=self.id_data) def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): - ctrl = self.get_control(exporter, bo, so) + ctrl = self.get_control(exporter) just_flag = self._JUSTIFICATION_LUT.get(self.justification) if just_flag is not None: @@ -1248,7 +1247,7 @@ def iterate_value_modifiers(self) -> Iterator[_GameGuiMixin]: def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): for ctrl_mod in self.iterate_control_modifiers(): - ctrl = ctrl_mod.get_control(exporter, bo, so) + ctrl = ctrl_mod.get_control(exporter) if isinstance(ctrl, pfGUIValueCtrl): ctrl.min = min(self.min_value, self.max_value) ctrl.max = max(self.min_value, self.max_value) @@ -1278,9 +1277,9 @@ class PlasmaGameGuiDialogModifier(_GameGuiMixin, PlasmaModifierProperties): options=set() ) - def get_control(self, exporter: Exporter, bo = None, so = None) -> pfGUIDialogMod: + def get_control(self, exporter: Exporter) -> pfGUIDialogMod: # This isn't really a control, but we may need this. - return exporter.mgr.find_create_object(pfGUIDialogMod, bl=bo, so=so) + return exporter.mgr.find_create_object(pfGUIDialogMod, bl=self.id_data) def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): # Find all of the visible objects in the GUI page for use in hither/yon raycast and @@ -1367,7 +1366,7 @@ def post_export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObjec if obj.plasma_modifiers.gui_control.enabled ) for control_modifier in control_modifiers: - control = control_modifier.get_control(exporter, control_modifier.id_data) + control = control_modifier.get_control(exporter) ctrl_key = control.key exporter.report.msg(f"GUIDialog '{bo.name}': [{control.ClassName()}] '{ctrl_key.name}'") dialog.addControl(ctrl_key)