From 20ccfa87f9ae4bc3ead2863066a53a6acdce30e9 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 18 Feb 2024 18:12:53 -0500 Subject: [PATCH] Ensure note popups don't clobber each other. Fail fast if someone sets up multiple note popups with conflicting settings. This will allow us to obey the principle of least surprise. --- korman/exporter/convert.py | 6 ++++-- korman/exporter/gui.py | 8 ++++++++ korman/properties/modifiers/anim.py | 2 +- korman/properties/modifiers/avatar.py | 2 +- korman/properties/modifiers/game_gui.py | 2 +- korman/properties/modifiers/gui.py | 8 ++++++-- korman/properties/modifiers/logic.py | 2 +- korman/properties/modifiers/render.py | 4 ++-- korman/properties/modifiers/sound.py | 2 +- 9 files changed, 25 insertions(+), 11 deletions(-) diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index 95173c3..bace2de 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -229,7 +229,7 @@ class Exporter: for mod in bl_obj.plasma_modifiers.modifiers: fn = getattr(mod, "sanity_check", None) if fn is not None: - fn() + fn(self) inc_progress() self.report.msg("... Age is grinning and holding a spatula. Must be OK, then.") @@ -502,7 +502,9 @@ class Exporter: # Wow, recursively generated objects. Aren't you special? with indent(): for mod in temporary.plasma_modifiers.modifiers: - mod.sanity_check() + fn = getattr(mod, "sanity_check", None) + if fn is not None: + fn(self) do_pre_export(temporary) return temporary diff --git a/korman/exporter/gui.py b/korman/exporter/gui.py index 10c9842..bf9b952 100644 --- a/korman/exporter/gui.py +++ b/korman/exporter/gui.py @@ -49,10 +49,12 @@ class GuiConverter: if TYPE_CHECKING: _parent: weakref.ref[Exporter] = ... + _pages: Dict[str, Any] = ... _mods_exported: Set[str] = ... def __init__(self, parent: Optional[Exporter] = None): self._parent = weakref.ref(parent) if parent is not None else None + self._pages = {} self._mods_exported = set() # Go ahead and prepare the GUI transparent material for future use. @@ -206,6 +208,12 @@ class GuiConverter: w2c[2, i] *= -1.0 return PostEffectModMatrices(c2w, w2c) + def check_pre_export(self, name: str, **kwargs): + previous = self._pages.setdefault(name, kwargs) + if previous != kwargs: + diff = set(previous.items()) - set(kwargs.items()) + raise ExportError(f"GUI Page '{name}' has target modifiers with conflicting settings:\n{diff}") + def create_note_gui(self, gui_page: str, gui_camera: bpy.types.Object): if not gui_page in self._mods_exported: guidialog_object = utils.create_empty_object(f"{gui_page}_NoteDialog") diff --git a/korman/properties/modifiers/anim.py b/korman/properties/modifiers/anim.py index 684ea91..8db48fb 100644 --- a/korman/properties/modifiers/anim.py +++ b/korman/properties/modifiers/anim.py @@ -42,7 +42,7 @@ class ActionModifier: return None raise ExportError("'{}': Object has an animation modifier but is not animated".format(bo.name)) - def sanity_check(self) -> None: + def sanity_check(self, exporter) -> None: if not self.id_data.plasma_object.has_animation_data: raise ExportError("'{}': Has an animation modifier but no animation data.", self.id_data.name) diff --git a/korman/properties/modifiers/avatar.py b/korman/properties/modifiers/avatar.py index 5dc4b59..17cc9eb 100644 --- a/korman/properties/modifiers/avatar.py +++ b/korman/properties/modifiers/avatar.py @@ -189,7 +189,7 @@ class PlasmaSittingBehavior(idprops.IDPropObjectMixin, PlasmaModifierProperties, # This should be an empty, really... return True - def sanity_check(self): + def sanity_check(self, exporter): # The user absolutely MUST specify a clickable or this won't export worth crap. if self.clickable_object is None: raise ExportError("'{}': Sitting Behavior's clickable object is invalid".format(self.key_name)) diff --git a/korman/properties/modifiers/game_gui.py b/korman/properties/modifiers/game_gui.py index 9f44a18..d930264 100644 --- a/korman/properties/modifiers/game_gui.py +++ b/korman/properties/modifiers/game_gui.py @@ -70,7 +70,7 @@ class _GameGuiMixin: def requires_dyntext(self) -> bool: return False - def sanity_check(self): + def sanity_check(self, exporter): age: PlasmaAge = bpy.context.scene.world.plasma_age # Game GUI modifiers must be attached to objects in a GUI page, ONLY diff --git a/korman/properties/modifiers/gui.py b/korman/properties/modifiers/gui.py index 8a24eee..5b06df2 100644 --- a/korman/properties/modifiers/gui.py +++ b/korman/properties/modifiers/gui.py @@ -660,7 +660,7 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz share.link_input(share_anim_stage, "stage", "stage_refs") share.link_output(linkingnode, "hosts", "shareBookSeek") - def sanity_check(self): + def sanity_check(self, exporter): if self.clickable is None: raise ExportError("{}: Linking Book modifier requires a clickable!", self.id_data.name) if self.seek_point is None: @@ -724,11 +724,15 @@ class PlasmaNotePopupModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz): if self.id_data.type == "MESH": return self.id_data - def sanity_check(self): + def sanity_check(self, exporter: Exporter): page_type = helpers.get_page_type(self.id_data.plasma_object.page) if page_type != "room": raise ExportError(f"Note Popup modifiers should be in a 'room' page, not a '{page_type}' page!") + # It's OK if multiple note popups point to the same GUI page, + # they just need to have the same camera. + exporter.gui.check_pre_export(self.gui_page, pl_id="note_popup", camera=self.gui_camera) + def pre_export(self, exporter: Exporter, bo: bpy.types.Object): # The GUI converter will debounce duplicate GUI dialogs. yield from exporter.gui.create_note_gui(self.gui_page, self.gui_camera) diff --git a/korman/properties/modifiers/logic.py b/korman/properties/modifiers/logic.py index 1dec936..675b4fe 100644 --- a/korman/properties/modifiers/logic.py +++ b/korman/properties/modifiers/logic.py @@ -154,7 +154,7 @@ class PlasmaTelescope(PlasmaModifierProperties, PlasmaModifierLogicWiz): type=bpy.types.Object, poll=idprops.poll_camera_objects) - def sanity_check(self): + def sanity_check(self, exporter): if self.camera_object is None: raise ExportError(f"'{self.id_data.name}': Telescopes must specify a camera!") diff --git a/korman/properties/modifiers/render.py b/korman/properties/modifiers/render.py index 44c05c2..ec0645e 100644 --- a/korman/properties/modifiers/render.py +++ b/korman/properties/modifiers/render.py @@ -120,7 +120,7 @@ class PlasmaBlendMod(PlasmaModifierProperties): for i in (j.blend_onto for j in self.dependencies if j.blend_onto is not None and j.enabled): yield i - def sanity_check(self): + def sanity_check(self, exporter): if self.has_circular_dependency: raise ExportError("'{}': Circular Render Dependency detected!".format(self.id_data.name)) @@ -770,7 +770,7 @@ class PlasmaLocalizedTextModifier(PlasmaModifierProperties, PlasmaModifierLogicW def localization_set(self): return "DynaTexts" - def sanity_check(self): + def sanity_check(self, exporter): if self.texture is None: raise ExportError("'{}': Localized Text modifier requires a texture", self.id_data.name) diff --git a/korman/properties/modifiers/sound.py b/korman/properties/modifiers/sound.py index 394afc2..477427e 100644 --- a/korman/properties/modifiers/sound.py +++ b/korman/properties/modifiers/sound.py @@ -533,7 +533,7 @@ class PlasmaSoundEmitter(PlasmaModifierProperties): stereize_left = PointerProperty(type=bpy.types.Object, options={"HIDDEN", "SKIP_SAVE"}) stereize_right = PointerProperty(type=bpy.types.Object, options={"HIDDEN", "SKIP_SAVE"}) - def sanity_check(self): + def sanity_check(self, exporter): modifiers = self.id_data.plasma_modifiers # Sound emitters can potentially export sounds to more than one emitter SceneObject. Currently,