From 2d3d9495ddc2faaf8be292b1e3e4059862ab7c2c Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 6 Jun 2017 21:35:51 -0400 Subject: [PATCH 01/14] Implement an object name -> ID Property converter This effectively bumps the minimum requirement to Blender 2.79. Furthermore, any blendfiles saved with ID Datablock properties will crash earlier versions of Blender. You have been warned... After approximately 24 hours of writing, rewriting, and cursing Blender, this appears to be the most flexible way of magically upgrading our old string properties to ID Datablock properties. The general hacky-ness is due to limitations in Blender's API. Here's how it works... In your property group (node, modifier, etc) you will need to implement the classmethod `_idprop_mapping`. This will map new ID Datablock property attribute names to old string property attribute names. Further, you will need to implement the `_idprop_sources` method. This will map string property attribute names to a collection to fetch the ID from. If you have specific filtering rules to follow, they can be implemented here :). To ensure code sanity, any attempts to access string properties that have been marked as converted will now fail with an AttributeError. Happy haxxoring! --- korman/__init__.py | 2 +- korman/idprops.py | 118 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 korman/idprops.py diff --git a/korman/__init__.py b/korman/__init__.py index 5d516e1..093df40 100644 --- a/korman/__init__.py +++ b/korman/__init__.py @@ -22,7 +22,7 @@ from . import operators bl_info = { "name": "Korman", "author": "Guild of Writers", - "blender": (2, 78, 0), + "blender": (2, 79, 0), "location": "File > Import-Export", "description": "Exporter for Cyan Worlds' Plasma Engine", "warning": "beta", diff --git a/korman/idprops.py b/korman/idprops.py new file mode 100644 index 0000000..3342044 --- /dev/null +++ b/korman/idprops.py @@ -0,0 +1,118 @@ +# This file is part of Korman. +# +# Korman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Korman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Korman. If not, see . + +import bpy +from bpy.props import * + +class IDPropMixin: + """ + So, here's the rub. + + In Blender 2.79, we finally get the ability to use native Blender ID Datablock properties in Python. + This is great! It will allow us to specify other objects (Blender Objects, Materials, Textures) in + our plugin as pointer properties. Further, we can even specify a poll method to create a 'search list' + of valid options. + + Naturally, there are some cons. The con here is that we've been storing object NAMES in string properties + for several releases now. Therefore, the purpose of this class is simple... It is a mixin to be + used for silently upgrading these object name properties to ID Properties. You will need to override + the _idprop_mapping and _idprop_sources methods in your class. The mixin will handle upgrading + the properties when a derived class is touched. + + Unfortunately, it is not possible to easily batch convert everything on load or save, due to issues + in the way Blender's Python API functions. Long story short: PropertyGroups do not execute __new__ + or __init__. Furthermore, Blender's UI does not appreciate having ID Datablocks return from + __getattribute__. To make matters worse, all properties are locked in a read-only state during + the UI draw stage. + """ + + def __getattribute__(self, attr): + _getattribute = super().__getattribute__ + + # Let's make sure no one is trying to access an old version... + if attr in _getattribute("_idprop_mapping")().values(): + raise AttributeError("'{}' has been deprecated... Please use the ID Property".format(attr)) + + # I have some bad news for you... Unfortunately, this might have been called + # during Blender's draw() context. Blender locks all properties during the draw loop. + # HOWEVER!!! There is a solution. Upon inspection of the Blender source code, however, it + # appears this restriction is temporarily suppressed during property getters... So let's get + # a property that executes a getter :D + # ... + # ... + # But why not simply proxy requests here, you ask? Ah, young grasshopper... This is the + # fifth time I have (re-)written this code. Trust me when I say, 'tis a boondoggle. + assert _getattribute("idprops_upgraded") + + # Must be something regular. Just super it. + return super().__getattribute__(attr) + + def __setattr__(self, attr, value): + idprops = super().__getattribute__("_idprop_mapping")() + + # Disallow any attempts to set the old string property + if attr in idprops.values(): + raise AttributeError("'{}' has been deprecated... Please use the ID Property".format(attr)) + + # Inappropriate touching? + super().__getattribute__("_try_upgrade_idprops")() + + # Now, pass along our update + super().__setattr__(attr, value) + + @classmethod + def register(cls): + if hasattr(super(), "register"): + super().register() + + cls.idprops_upgraded = BoolProperty(name="INTERNAL: ID Property Upgrader HACK", + description="HAAAX *throws CRT monitor*", + get=cls._try_upgrade_idprops, + options={"HIDDEN"}) + cls.idprops_upgraded_value = BoolProperty(name="INTERNAL: ID Property Upgrade Status", + description="Have old StringProperties been upgraded to ID Datablock Properties?", + default=False, + options={"HIDDEN"}) + for str_prop in cls._idprop_mapping().values(): + setattr(cls, str_prop, StringProperty(description="deprecated")) + + def _try_upgrade_idprops(self): + _getattribute = super().__getattribute__ + + if not _getattribute("idprops_upgraded_value"): + idprop_map = _getattribute("_idprop_mapping")() + strprop_src = _getattribute("_idprop_sources")() + + for idprop_name, strprop_name in idprop_map.items(): + if not super().is_property_set(strprop_name): + continue + strprop_value = _getattribute(strprop_name) + idprop_value = strprop_src[strprop_name].get(strprop_value, None) + super().__setattr__(idprop_name, idprop_value) + super().property_unset(strprop_name) + super().__setattr__("idprops_upgraded_value", True) + + # you should feel like this now... https://youtu.be/1JBSs6MQJeI?t=33s + return True + + +class IDPropObjectMixin(IDPropMixin): + """Like IDPropMixin, but with the assumption that all IDs can be found in bpy.data.objects""" + + def _idprop_sources(self): + # NOTE: bad problems result when using super() here, so we'll manually reference object + cls = object.__getattribute__(self, "__class__") + idprops = cls._idprop_mapping() + return { i: bpy.data.objects for i in idprops.values() } From 7503676d74e19b68bc2871017410aafef9898861 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 7 Jun 2017 14:19:56 -0400 Subject: [PATCH 02/14] Convert water modifier fake-ID props to ID props I have also added some poll helpers to the idprops module so every time we use an ID Datablock Property, we won't have to reinvent the wheel. I also noticed that PointerProperties have to be a direct descendent of `bpy.types.ID` otherwise Blender crashes. This kind of makes sense and is not a huge issue to have to work around. --- korman/idprops.py | 10 +++ korman/properties/modifiers/water.py | 107 +++++++++++++++------------ korman/ui/modifiers/water.py | 12 +-- 3 files changed, 75 insertions(+), 54 deletions(-) diff --git a/korman/idprops.py b/korman/idprops.py index 3342044..032b895 100644 --- a/korman/idprops.py +++ b/korman/idprops.py @@ -116,3 +116,13 @@ class IDPropObjectMixin(IDPropMixin): cls = object.__getattribute__(self, "__class__") idprops = cls._idprop_mapping() return { i: bpy.data.objects for i in idprops.values() } + + +def poll_empty_objects(self, value): + return value.type == "EMPTY" + +def poll_mesh_objects(self, value): + return value.type == "MESH" + +def poll_envmap_textures(self, value): + return isinstance(value, bpy.types.EnvironmentMapTexture) diff --git a/korman/properties/modifiers/water.py b/korman/properties/modifiers/water.py index 0424e3d..78e250d 100644 --- a/korman/properties/modifiers/water.py +++ b/korman/properties/modifiers/water.py @@ -20,8 +20,9 @@ from PyHSPlasma import * from .base import PlasmaModifierProperties from ...exporter import ExportError, ExportAssertionError +from ... import idprops -class PlasmaSwimRegion(PlasmaModifierProperties, bpy.types.PropertyGroup): +class PlasmaSwimRegion(idprops.IDPropObjectMixin, PlasmaModifierProperties, bpy.types.PropertyGroup): pl_id = "swimregion" bl_category = "Water" @@ -35,9 +36,10 @@ class PlasmaSwimRegion(PlasmaModifierProperties, bpy.types.PropertyGroup): "STRAIGHT": plSwimStraightCurrentRegion, } - region_name = StringProperty(name="Region", - description="Swimming detector region", - options=set()) + region = PointerProperty(name="Region", + description="Swimming detector region", + type=bpy.types.Object, + poll=idprops.poll_mesh_objects) down_buoyancy = FloatProperty(name="Downward Buoyancy", description="Distance the avatar sinks into the water", @@ -78,9 +80,10 @@ class PlasmaSwimRegion(PlasmaModifierProperties, bpy.types.PropertyGroup): description="Current velocity far from the region center", min=-100.0, max=100.0, default=0.0, options=set()) - current_object = StringProperty(name="Current Object", - description="Object whose Y-axis defines the direction of the current", - options=set()) + current = PointerProperty(name="Current Object", + description="Object whose Y-axis defines the direction of the current", + type=bpy.types.Object, + poll=idprops.poll_empty_objects) def export(self, exporter, bo, so): swimIface = self.get_key(exporter, so).object @@ -99,12 +102,9 @@ class PlasmaSwimRegion(PlasmaModifierProperties, bpy.types.PropertyGroup): swimIface.nearVel = self.near_velocity swimIface.farVel = self.far_velocity if isinstance(swimIface, (plSwimCircularCurrentRegion, plSwimStraightCurrentRegion)): - if not self.current_object: + if self.current is None: raise ExportError("Swimming Surface '{}' does not specify a current object".format(bo.name)) - current_bo = bpy.data.objects.get(self.current_object, None) - if current_bo is None: - raise ExportError("Swimming Surface '{}' specifies an invalid current object '{}'".format(bo.name, self.current_object)) - swimIface.currentObj = exporter.mgr.find_create_key(plSceneObject, bl=current_bo) + swimIface.currentObj = exporter.mgr.find_create_key(plSceneObject, bl=self.current) # The surface needs bounds for LOS -- this is generally a flat plane, or I would think... # NOTE: If the artist has this on a WaveSet, they probably intend for the avatar to swim on @@ -122,16 +122,13 @@ class PlasmaSwimRegion(PlasmaModifierProperties, bpy.types.PropertyGroup): physical.LOSDBs |= plSimDefs.kLOSDBSwimRegion # Detector region bounds - if self.region_name: - region_bo = bpy.data.objects.get(self.region_name, None) - if region_bo is None: - raise ExportError("Swim Surface '{}' references invalid region '{}'".format(bo.name, self.region_name)) - region_so = exporter.mgr.find_create_object(plSceneObject, bl=region_bo) + if self.region is not None: + region_so = exporter.mgr.find_create_object(plSceneObject, bl=self.region) # Good news: if this phys has already been exported, this is basically a noop - det_name = "{}_SwimDetector".format(self.region_name) - bounds = region_bo.plasma_modifiers.collision.bounds - simIface, physical = exporter.physics.generate_physical(region_bo, region_so, bounds, det_name) + det_name = "{}_SwimDetector".format(self.region.name) + bounds = self.region.plasma_modifiers.collision.bounds + simIface, physical = exporter.physics.generate_physical(self.region, region_so, bounds, det_name) physical.memberGroup = plSimDefs.kGroupDetector physical.reportGroup |= 1 << plSimDefs.kGroupAvatar @@ -166,25 +163,34 @@ class PlasmaSwimRegion(PlasmaModifierProperties, bpy.types.PropertyGroup): return exporter.mgr.find_create_key(pClass, bl=self.id_data, so=so) def harvest_actors(self): - if self.current_type != "NONE" and self.current_object: - return set((self.current_object,)) + if self.current_type != "NONE" and self.current: + return set((self.current.name,)) return set() + @classmethod + def _idprop_mapping(cls): + return {"current": "current_object", + "region": "region_name"} + -class PlasmaWaterModifier(PlasmaModifierProperties, bpy.types.PropertyGroup): +class PlasmaWaterModifier(idprops.IDPropMixin, PlasmaModifierProperties, bpy.types.PropertyGroup): pl_id = "water_basic" bl_category = "Water" bl_label = "Basic Water" bl_description = "Basic water properties" - wind_object_name = StringProperty(name="Wind Object", - description="Object whose Y axis represents the wind direction") + wind_object = PointerProperty(name="Wind Object", + description="Object whose Y axis represents the wind direction", + type=bpy.types.Object, + poll=idprops.poll_empty_objects) wind_speed = FloatProperty(name="Wind Speed", description="Magnitude of the wind", default=1.0) - envmap_name = StringProperty(name="EnvMap", - description="Texture defining an environment map for this water object") + envmap = PointerProperty(name="EnvMap", + description="Texture defining an environment map for this water object", + type=bpy.types.Texture, + poll=idprops.poll_envmap_textures) envmap_radius = FloatProperty(name="Environment Sphere Radius", description="How far away the first object you want to see is", min=5.0, max=10000.0, @@ -232,12 +238,9 @@ class PlasmaWaterModifier(PlasmaModifierProperties, bpy.types.PropertyGroup): def export(self, exporter, bo, so): waveset = exporter.mgr.find_create_object(plWaveSet7, name=bo.name, so=so) - if self.wind_object_name: - wind_obj = bpy.data.objects.get(self.wind_object_name, None) - if wind_obj is None: - raise ExportError("{}: Wind Object '{}' not found".format(bo.name, self.wind_object_name)) - if wind_obj.plasma_object.enabled and wind_obj.plasma_modifiers.animation.enabled: - waveset.refObj = exporter.mgr.find_create_key(plSceneObject, bl=wind_obj) + if self.wind_object: + if self.wind_object.plasma_object.enabled and self.wind_object.plasma_modifiers.animation.enabled: + waveset.refObj = exporter.mgr.find_create_key(plSceneObject, bl=self.wind_object) waveset.setFlag(plWaveSet7.kHasRefObject, True) # This is much like what happened in PyPRP @@ -259,15 +262,9 @@ class PlasmaWaterModifier(PlasmaModifierProperties, bpy.types.PropertyGroup): state.depthFalloff = hsVector3(self.depth_opacity, self.depth_reflection, self.depth_wave) # Environment Map - if self.envmap_name: - texture = bpy.data.textures.get(self.envmap_name, None) - if texture is None: - raise ExportError("{}: Texture '{}' not found".format(self.key_name, self.envmap_name)) - if texture.type != "ENVIRONMENT_MAP": - raise ExportError("{}: Texture '{}' is not an ENVIRONMENT MAP".format(self.key_name, self.envmap_name)) - + if self.envmap: # maybe, just maybe, we're absuing our privledges? - dem = exporter.mesh.material.export_dynamic_env(bo, None, texture, plDynamicEnvMap) + dem = exporter.mesh.material.export_dynamic_env(bo, None, self.envmap, plDynamicEnvMap) waveset.envMap = dem.key state.envCenter = dem.position state.envRefresh = dem.refreshRate @@ -293,15 +290,30 @@ class PlasmaWaterModifier(PlasmaModifierProperties, bpy.types.PropertyGroup): if not mods.water_shore.enabled: mods.water_shore.convert_default(state) + @classmethod + def _idprop_mapping(cls): + return {"wind_object": "wind_object_name", + "envmap": "envmap_name"} + + def _idprop_sources(self): + return {"wind_object_name": bpy.data.objects, + "envmap_name": bpy.data.textures} + @property def key_name(self): return "{}_WaveSet7".format(self.id_data.name) -class PlasmaShoreObject(bpy.types.PropertyGroup): +class PlasmaShoreObject(idprops.IDPropObjectMixin, bpy.types.PropertyGroup): display_name = StringProperty(name="Display Name") - object_name = StringProperty(name="Shore Object", - description="Object that waves crash upon") + shore_object = PointerProperty(name="Shore Object", + description="Object that waves crash upon", + type=bpy.types.Object, + poll=idprops.poll_mesh_objects) + + @classmethod + def _idprop_mapping(cls): + return {"shore_object": "object_name"} class PlasmaWaterShoreModifier(PlasmaModifierProperties): @@ -365,10 +377,9 @@ class PlasmaWaterShoreModifier(PlasmaModifierProperties): wavestate = waveset.state for i in self.shores: - shore = bpy.data.objects.get(i.object_name, None) - if shore is None: - raise ExportError("'{}': Shore Object '{}' does not exist".format(self.key_name, i.object_name)) - waveset.addShore(exporter.mgr.find_create_key(plSceneObject, bl=shore)) + if i.shore_object is None: + raise ExportError("'{}': Shore Object for '{}' is invalid".format(self.key_name, i.display_name)) + waveset.addShore(exporter.mgr.find_create_key(plSceneObject, bl=i.shore_object)) wavestate.wispiness = self.wispiness / 100.0 wavestate.minColor = hsColorRGBA(*self.shore_tint, alpha=(self.shore_opacity / 100.0)) diff --git a/korman/ui/modifiers/water.py b/korman/ui/modifiers/water.py index 1b9d63c..f6dec6f 100644 --- a/korman/ui/modifiers/water.py +++ b/korman/ui/modifiers/water.py @@ -19,9 +19,9 @@ def swimregion(modifier, layout, context): split = layout.split() col = split.column() col.label("Detector Region:") - col.prop_search(modifier, "region_name", bpy.data, "objects", text="") + col.prop(modifier, "region", text="") - region_bo = bpy.data.objects.get(modifier.region_name, None) + region_bo = modifier.region col = split.column() col.enabled = region_bo is not None bounds_src = region_bo if region_bo is not None else modifier.id_data @@ -52,11 +52,11 @@ def swimregion(modifier, layout, context): col.prop(modifier, "near_velocity", text="Near") col.prop(modifier, "far_velocity", text="Far") - layout.prop_search(modifier, "current_object", bpy.data, "objects") + layout.prop(modifier, "current") def water_basic(modifier, layout, context): - layout.prop_search(modifier, "wind_object_name", bpy.data, "objects") - layout.prop_search(modifier, "envmap_name", bpy.data, "textures") + layout.prop(modifier, "wind_object") + layout.prop(modifier, "envmap") row = layout.row() row.prop(modifier, "wind_speed") @@ -128,7 +128,7 @@ def water_shore(modifier, layout, context): # Display the active shore if modifier.shores: shore = modifier.shores[modifier.active_shore_index] - layout.prop_search(shore, "object_name", bpy.data, "objects", icon="MESH_DATA") + layout.prop(shore, "shore_object", icon="MESH_DATA") split = layout.split() col = split.column() From 904f9b8f7795beed78e7260f129a322d5d1d14ef Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 7 Jun 2017 14:42:52 -0400 Subject: [PATCH 03/14] Update anim modifiers to ID props --- korman/idprops.py | 6 ++++++ korman/properties/modifiers/anim.py | 15 +++++++++++---- korman/ui/modifiers/anim.py | 6 +++--- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/korman/idprops.py b/korman/idprops.py index 032b895..d856edf 100644 --- a/korman/idprops.py +++ b/korman/idprops.py @@ -118,6 +118,12 @@ class IDPropObjectMixin(IDPropMixin): return { i: bpy.data.objects for i in idprops.values() } +def poll_animated_objects(self, value): + if value.animation_data is not None: + if value.animation_data.action is not None: + return True + return False + def poll_empty_objects(self, value): return value.type == "EMPTY" diff --git a/korman/properties/modifiers/anim.py b/korman/properties/modifiers/anim.py index 320c184..341e9cd 100644 --- a/korman/properties/modifiers/anim.py +++ b/korman/properties/modifiers/anim.py @@ -19,6 +19,7 @@ from PyHSPlasma import * from .base import PlasmaModifierProperties from ...exporter import ExportError, utils +from ... import idprops def _convert_frame_time(frame_num): fps = bpy.context.scene.render.fps @@ -92,9 +93,15 @@ class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties): atcanim.loopEnd = atcanim.end -class AnimGroupObject(bpy.types.PropertyGroup): - object_name = StringProperty(name="Child Animation", - description="Object whose action is a child animation") +class AnimGroupObject(idprops.IDPropObjectMixin, bpy.types.PropertyGroup): + child_anim = PointerProperty(name="Child Animation", + description="Object whose action is a child animation", + type=bpy.types.Object, + poll=idprops.poll_animated_objects) + + @classmethod + def _idprop_mapping(cls): + return {"child_anim": "object_name"} class PlasmaAnimationGroupModifier(ActionModifier, PlasmaModifierProperties): @@ -123,7 +130,7 @@ class PlasmaAnimationGroupModifier(ActionModifier, PlasmaModifierProperties): agmaster.msgForwarder = msgfwd.key agmaster.isGrouped, agmaster.isGroupMaster = True, True for i in self.children: - child_bo = bpy.data.objects.get(i.object_name, None) + child_bo = i.child_anim if child_bo is None: msg = "Animation Group '{}' specifies an invalid object '{}'. Ignoring..." exporter.report.warn(msg.format(self.key_name, i.object_name), ident=2) diff --git a/korman/ui/modifiers/anim.py b/korman/ui/modifiers/anim.py index 402b7c2..a17f0c4 100644 --- a/korman/ui/modifiers/anim.py +++ b/korman/ui/modifiers/anim.py @@ -45,8 +45,8 @@ def animation(modifier, layout, context): class GroupListUI(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): - label = item.object_name if item.object_name else "[No Child Specified]" - icon = "ACTION" if item.object_name else "ERROR" + label = item.child_anim.name if item.child_anim is not None else "[No Child Specified]" + icon = "ACTION" if item.child_anim is not None else "ERROR" layout.label(text=label, icon=icon) @@ -68,7 +68,7 @@ def animation_group(modifier, layout, context): op.index = modifier.active_child_index if modifier.children: - layout.prop_search(modifier.children[modifier.active_child_index], "object_name", bpy.data, "objects", icon="ACTION") + layout.prop(modifier.children[modifier.active_child_index], "child_anim", icon="ACTION") class LoopListUI(bpy.types.UIList): From 1fe8ecc02d4ad5155496366a720868b403e3ad3f Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 7 Jun 2017 15:33:29 -0400 Subject: [PATCH 04/14] Update Avatar Modifiers and Condition Nodes --- korman/helpers.py | 6 +-- korman/nodes/node_conditions.py | 68 ++++++++++++++++----------- korman/properties/modifiers/avatar.py | 33 ++++++++----- korman/properties/modifiers/region.py | 2 +- korman/ui/modifiers/avatar.py | 8 ++-- 5 files changed, 70 insertions(+), 47 deletions(-) diff --git a/korman/helpers.py b/korman/helpers.py index 1bc0151..6ae2bf2 100644 --- a/korman/helpers.py +++ b/korman/helpers.py @@ -51,9 +51,9 @@ class TemporaryObject: def ensure_power_of_two(value): return pow(2, math.floor(math.log(value, 2))) -def find_modifier(boname, modid): - """Given a Blender Object name, finds a given modifier and returns it or None""" - bo = bpy.data.objects.get(boname, None) + +def find_modifier(bo, modid): + """Given a Blender Object, finds a given modifier and returns it or None""" if bo is not None: # if they give us the wrong modid, it is a bug and an AttributeError return getattr(bo.plasma_modifiers, modid) diff --git a/korman/nodes/node_conditions.py b/korman/nodes/node_conditions.py index d4fec71..d44363d 100644 --- a/korman/nodes/node_conditions.py +++ b/korman/nodes/node_conditions.py @@ -21,8 +21,9 @@ from PyHSPlasma import * from .node_core import * from ..properties.modifiers.physics import bounds_types +from .. import idprops -class PlasmaClickableNode(PlasmaNodeBase, bpy.types.Node): +class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node): bl_category = "CONDITIONS" bl_idname = "PlasmaClickableNode" bl_label = "Clickable" @@ -31,8 +32,10 @@ class PlasmaClickableNode(PlasmaNodeBase, bpy.types.Node): # These are the Python attributes we can fill in pl_attrib = {"ptAttribActivator", "ptAttribActivatorList", "ptAttribNamedActivator"} - clickable = StringProperty(name="Clickable", - description="Mesh that is clickable") + clickable_object = PointerProperty(name="Clickable", + description="Mesh object that is clickable", + type=bpy.types.Object, + poll=idprops.poll_mesh_objects) bounds = EnumProperty(name="Bounds", description="Clickable's bounds (NOTE: only used if your clickable is not a collider)", items=bounds_types, @@ -63,7 +66,7 @@ class PlasmaClickableNode(PlasmaNodeBase, bpy.types.Node): ]) def draw_buttons(self, context, layout): - layout.prop_search(self, "clickable", bpy.data, "objects", icon="MESH_DATA") + layout.prop(self, "clickable_object", icon="MESH_DATA") layout.prop(self, "bounds") def export(self, exporter, parent_bo, parent_so): @@ -127,27 +130,31 @@ class PlasmaClickableNode(PlasmaNodeBase, bpy.types.Node): # First: look up the clickable mesh. if it is not specified, then it's this BO. # We do this because we might be exporting from a BO that is not actually the clickable object. # Case: sitting modifier (exports from sit position empty) - if self.clickable: - clickable_bo = bpy.data.objects.get(self.clickable, None) - if clickable_bo is None: - self.raise_error("invalid Clickable object: '{}'".format(self.clickable)) - clickable_so = exporter.mgr.find_create_object(plSceneObject, bl=clickable_bo) - return (clickable_bo, clickable_so) + if self.clickable_object: + clickable_so = exporter.mgr.find_create_object(plSceneObject, bl=self.clickable_object) + return (self.clickable_object, clickable_so) else: return (None, parent_so) def harvest_actors(self): - return (self.clickable,) + if self.clickable_object: + return (self.clickable_object.name,) + @classmethod + def _idprop_mapping(cls): + return {"clickable_object": "clickable"} -class PlasmaClickableRegionNode(PlasmaNodeBase, bpy.types.Node): + +class PlasmaClickableRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node): bl_category = "CONDITIONS" bl_idname = "PlasmaClickableRegionNode" bl_label = "Clickable Region Settings" bl_width_default = 200 - region = StringProperty(name="Region", - description="Object that defines the region mesh") + region_object = PointerProperty(name="Region", + description="Object that defines the region mesh", + type=bpy.types.Object, + poll=idprops.poll_mesh_objects) bounds = EnumProperty(name="Bounds", description="Physical object's bounds (NOTE: only used if your clickable is not a collider)", items=bounds_types, @@ -161,14 +168,14 @@ class PlasmaClickableRegionNode(PlasmaNodeBase, bpy.types.Node): ]) def draw_buttons(self, context, layout): - layout.prop_search(self, "region", bpy.data, "objects", icon="MESH_DATA") + layout.prop(self, "region_object", icon="MESH_DATA") layout.prop(self, "bounds") def convert_subcondition(self, exporter, parent_bo, parent_so, logicmod): # REMEMBER: parent_so doesn't have to be the actual region scene object... - region_bo = bpy.data.objects.get(self.region, None) + region_bo = self.region_object if region_bo is None: - self.raise_error("invalid Region object: '{}'".format(self.region)) + self.raise_error("invalid Region") region_so = exporter.mgr.find_create_key(plSceneObject, bl=region_bo).object # Try to figure out the appropriate bounds type for the region.... @@ -198,6 +205,10 @@ class PlasmaClickableRegionNode(PlasmaNodeBase, bpy.types.Node): objinbox_key.object.satisfied = True logicmod.addCondition(objinbox_key) + @classmethod + def _idprop_mapping(cls): + return {"region_object": "region"} + class PlasmaClickableRegionSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): bl_color = (0.412, 0.0, 0.055, 1.0) @@ -310,7 +321,7 @@ class PlasmaVolumeReportNode(PlasmaNodeBase, bpy.types.Node): row.prop(self, "threshold", text="") -class PlasmaVolumeSensorNode(PlasmaNodeBase, bpy.types.Node): +class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node): bl_category = "CONDITIONS" bl_idname = "PlasmaVolumeSensorNode" bl_label = "Region Sensor" @@ -320,8 +331,10 @@ class PlasmaVolumeSensorNode(PlasmaNodeBase, bpy.types.Node): pl_attrib = {"ptAttribActivator", "ptAttribActivatorList", "ptAttribNamedActivator"} # Region Mesh - region = StringProperty(name="Region", - description="Object that defines the region mesh") + region_object = PointerProperty(name="Region", + description="Object that defines the region mesh", + type=bpy.types.Object, + poll=idprops.poll_mesh_objects) bounds = EnumProperty(name="Bounds", description="Physical object's bounds", items=bounds_types) @@ -364,11 +377,13 @@ class PlasmaVolumeSensorNode(PlasmaNodeBase, bpy.types.Node): layout.prop(self, "report_on") # Okay, if they changed the name of the ObData, that's THEIR problem... - layout.prop_search(self, "region", bpy.data, "objects", icon="MESH_DATA") + layout.prop(self, "region_object", icon="MESH_DATA") layout.prop(self, "bounds") def get_key(self, exporter, parent_so): bo = self.region_object + if bo is None: + self.raise_error("Region cannot be empty") so = exporter.mgr.find_create_object(plSceneObject, bl=bo) rgn_enter, rgn_exit = None, None @@ -393,6 +408,8 @@ class PlasmaVolumeSensorNode(PlasmaNodeBase, bpy.types.Node): def export(self, exporter, bo, parent_so): # We need to ensure we export to the correct SO region_bo = self.region_object + if region_bo is None: + self.raise_error("Region cannot be empty") region_so = exporter.mgr.find_create_object(plSceneObject, bl=region_bo) interface = exporter.mgr.find_create_object(plInterfaceInfoModifier, name=self.key_name, so=region_so) @@ -457,12 +474,9 @@ class PlasmaVolumeSensorNode(PlasmaNodeBase, bpy.types.Node): logicmod.addCondition(volKey) return logicKey - @property - def region_object(self): - phys_bo = bpy.data.objects.get(self.region, None) - if phys_bo is None: - self.raise_error("invalid Region object: '{}'".format(self.region)) - return phys_bo + @classmethod + def _idprop_mapping(cls): + return {"region_object": "region"} @property def report_enters(self): diff --git a/korman/properties/modifiers/avatar.py b/korman/properties/modifiers/avatar.py index 76527e3..81c0457 100644 --- a/korman/properties/modifiers/avatar.py +++ b/korman/properties/modifiers/avatar.py @@ -20,13 +20,14 @@ from PyHSPlasma import * from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz from ...exporter.explosions import ExportError from ...helpers import find_modifier +from ... import idprops sitting_approach_flags = [("kApproachFront", "Front", "Approach from the font"), ("kApproachLeft", "Left", "Approach from the left"), ("kApproachRight", "Right", "Approach from the right"), ("kApproachRear", "Rear", "Approach from the rear guard")] -class PlasmaSittingBehavior(PlasmaModifierProperties, PlasmaModifierLogicWiz): +class PlasmaSittingBehavior(idprops.IDPropObjectMixin, PlasmaModifierProperties, PlasmaModifierLogicWiz): pl_id = "sittingmod" bl_category = "Avatar" @@ -39,10 +40,14 @@ class PlasmaSittingBehavior(PlasmaModifierProperties, PlasmaModifierLogicWiz): default={"kApproachFront", "kApproachLeft", "kApproachRight"}, options={"ENUM_FLAG"}) - clickable_obj = StringProperty(name="Clickable", - description="Object that defines the clickable area") - region_obj = StringProperty(name="Region", - description="Object that defines the region mesh") + clickable_object = PointerProperty(name="Clickable", + description="Object that defines the clickable area", + type=bpy.types.Object, + poll=idprops.poll_mesh_objects) + region_object = PointerProperty(name="Region", + description="Object that defines the region mesh", + type=bpy.types.Object, + poll=idprops.poll_mesh_objects) facing_enabled = BoolProperty(name="Avatar Facing", description="The avatar must be facing the clickable's Y-axis", @@ -53,8 +58,7 @@ class PlasmaSittingBehavior(PlasmaModifierProperties, PlasmaModifierLogicWiz): def export(self, exporter, bo, so): # The user absolutely MUST specify a clickable or this won't export worth crap. - clickable_obj = bpy.data.objects.get(self.clickable_obj, None) - if clickable_obj is None: + if self.clickable_object is None: raise ExportError("'{}': Sitting Behavior's clickable object is invalid".format(self.key_name)) # Generate the logic nodes now @@ -65,7 +69,7 @@ class PlasmaSittingBehavior(PlasmaModifierProperties, PlasmaModifierLogicWiz): def harvest_actors(self): if self.facing_enabled: - return (self.clickable_obj,) + return (self.clickable_object.name,) return () def logicwiz(self, bo): @@ -81,16 +85,16 @@ class PlasmaSittingBehavior(PlasmaModifierProperties, PlasmaModifierLogicWiz): # Clickable clickable = nodes.new("PlasmaClickableNode") clickable.link_output(sittingmod, "satisfies", "condition") - clickable.clickable = self.clickable_obj - clickable.bounds = find_modifier(self.clickable_obj, "collision").bounds + clickable.clickable_object = self.clickable_object + clickable.bounds = find_modifier(self.clickable_object, "collision").bounds # Avatar Region (optional) - region_phys = find_modifier(self.region_obj, "collision") + region_phys = find_modifier(self.region_object, "collision") if region_phys is not None: region = nodes.new("PlasmaClickableRegionNode") region.link_output(clickable, "satisfies", "region") region.name = "ClickableAvRegion" - region.region = self.region_obj + region.region_object = self.region_object region.bounds = region_phys.bounds # Facing Target (optional) @@ -105,6 +109,11 @@ class PlasmaSittingBehavior(PlasmaModifierProperties, PlasmaModifierLogicWiz): # facing target conditional for us. isn't that nice? clickable.find_input_socket("facing").allow_simple = False + @classmethod + def _idprop_mapping(cls): + return {"clickable_object": "clickable_obj", + "region_object": "region_obj"} + @property def key_name(self): return "{}_SitBeh".format(self.id_data.name) diff --git a/korman/properties/modifiers/region.py b/korman/properties/modifiers/region.py index 8101cd9..fbc5e42 100644 --- a/korman/properties/modifiers/region.py +++ b/korman/properties/modifiers/region.py @@ -88,7 +88,7 @@ class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierLogicWiz): # Region Sensor volsens = nodes.new("PlasmaVolumeSensorNode") volsens.name = "RegionSensor" - volsens.region = bo.name + volsens.region_object = bo volsens.bounds = self.bounds volsens.find_input_socket("enter").allow = True volsens.find_input_socket("exit").allow = True diff --git a/korman/ui/modifiers/avatar.py b/korman/ui/modifiers/avatar.py index 5e8d010..31cc508 100644 --- a/korman/ui/modifiers/avatar.py +++ b/korman/ui/modifiers/avatar.py @@ -21,14 +21,14 @@ def sittingmod(modifier, layout, context): layout.row().prop(modifier, "approach") col = layout.column() - col.prop_search(modifier, "clickable_obj", bpy.data, "objects", icon="MESH_DATA") - clickable = find_modifier(modifier.clickable_obj, "collision") + col.prop(modifier, "clickable_object", icon="MESH_DATA") + clickable = find_modifier(modifier.clickable_object, "collision") if clickable is not None: col.prop(clickable, "bounds") col = layout.column() - col.prop_search(modifier, "region_obj", bpy.data, "objects", icon="MESH_DATA") - region = find_modifier(modifier.region_obj, "collision") + col.prop(modifier, "region_object", icon="MESH_DATA") + region = find_modifier(modifier.region_object, "collision") if region is not None: col.prop(region, "bounds") From 9ec511493ac3edf4771a7413e9f6639229767ae7 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 7 Jun 2017 15:48:53 -0400 Subject: [PATCH 05/14] Update Logic Modifiers to ID props --- korman/properties/modifiers/logic.py | 29 ++++++++++++++++------------ korman/ui/modifiers/logic.py | 2 +- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/korman/properties/modifiers/logic.py b/korman/properties/modifiers/logic.py index 340bf15..339fd9e 100644 --- a/korman/properties/modifiers/logic.py +++ b/korman/properties/modifiers/logic.py @@ -19,29 +19,31 @@ from PyHSPlasma import * from .base import PlasmaModifierProperties from ...exporter import ExportError +from ... import idprops game_versions = [("pvPrime", "Ages Beyond Myst (63.11)", "Targets the original Uru (Live) game"), ("pvPots", "Path of the Shell (63.12)", "Targets the most recent offline expansion pack"), ("pvMoul", "Myst Online: Uru Live (70)", "Targets the most recent online game")] -class PlasmaVersionedNodeTree(bpy.types.PropertyGroup): +class PlasmaVersionedNodeTree(idprops.IDPropMixin, bpy.types.PropertyGroup): name = StringProperty(name="Name") version = EnumProperty(name="Version", description="Plasma versions this node tree exports under", items=game_versions, options={"ENUM_FLAG"}, default=set(list(zip(*game_versions))[0])) - node_tree_name = StringProperty(name="Node Tree", - description="Node Tree to export") + node_tree = PointerProperty(name="Node Tree", + description="Node Tree to export", + type=bpy.types.NodeTree) node_name = StringProperty(name="Node Ref", description="Attach a reference to this node") - @property - def node_tree(self): - try: - return bpy.data.node_groups[self.node_tree_name] - except KeyError: - raise ExportError("Node Tree '{}' does not exist!".format(self.node_tree_name)) + @classmethod + def _idprop_mapping(cls): + return {"node_tree": "node_tree_name"} + + def _idprop_sources(self): + return {"node_tree_name": bpy.data.node_groups} class PlasmaAdvancedLogic(PlasmaModifierProperties): @@ -60,19 +62,22 @@ class PlasmaAdvancedLogic(PlasmaModifierProperties): for i in self.logic_groups: our_versions = [globals()[j] for j in i.version] if version in our_versions: + if i.node_tree is None: + raise ExportError("'{}': Advanced Logic is missing a node tree for '{}'".format(bo.name, i.version)) + # If node_name is defined, then we're only adding a reference. We will make sure that # the entire node tree is exported once before the post_export step, however. if i.node_name: - exporter.want_node_trees[i.node_tree_name] = (bo, so) + exporter.want_node_trees[i.node_tree.name] = (bo, so) node = i.node_tree.nodes.get(i.node_name, None) if node is None: - raise ExportError("Node '{}' does not exist in '{}'".format(i.node_name, i.node_tree_name)) + raise ExportError("Node '{}' does not exist in '{}'".format(i.node_name, i.node_tree.name)) # We are going to assume get_key will do the adding correctly. Single modifiers # should fetch the appropriate SceneObject before doing anything, so this will # be a no-op in that case. Multi modifiers should accept any SceneObject, however node.get_key(exporter, so) else: - exporter.node_trees_exported.add(i.node_tree_name) + exporter.node_trees_exported.add(i.node_tree.name) i.node_tree.export(exporter, bo, so) def harvest_actors(self): diff --git a/korman/ui/modifiers/logic.py b/korman/ui/modifiers/logic.py index 9dc27f1..c96f675 100644 --- a/korman/ui/modifiers/logic.py +++ b/korman/ui/modifiers/logic.py @@ -39,7 +39,7 @@ def advanced_logic(modifier, layout, context): if modifier.logic_groups: logic = modifier.logic_groups[modifier.active_group_index] layout.row().prop_menu_enum(logic, "version") - layout.prop_search(logic, "node_tree_name", bpy.data, "node_groups", icon="NODETREE") + layout.prop(logic, "node_tree", icon="NODETREE") try: layout.prop_search(logic, "node_name", logic.node_tree, "nodes", icon="NODE") except: From 4de9ca072770f5016d4ceefa6120dade31788929 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 7 Jun 2017 16:34:46 -0400 Subject: [PATCH 06/14] Update SoftVolumes to newfangled ID properties --- korman/idprops.py | 3 +++ korman/nodes/node_softvolume.py | 22 +++++++++------ korman/properties/modifiers/region.py | 39 ++++++++++++++++----------- korman/ui/modifiers/region.py | 2 +- 4 files changed, 42 insertions(+), 24 deletions(-) diff --git a/korman/idprops.py b/korman/idprops.py index d856edf..903c54a 100644 --- a/korman/idprops.py +++ b/korman/idprops.py @@ -130,5 +130,8 @@ def poll_empty_objects(self, value): def poll_mesh_objects(self, value): return value.type == "MESH" +def poll_softvolume_objects(self, value): + return value.plasma_modifiers.softvolume.enabled + def poll_envmap_textures(self, value): return isinstance(value, bpy.types.EnvironmentMapTexture) diff --git a/korman/nodes/node_softvolume.py b/korman/nodes/node_softvolume.py index 5eae4c1..bf68cc9 100644 --- a/korman/nodes/node_softvolume.py +++ b/korman/nodes/node_softvolume.py @@ -19,6 +19,7 @@ from collections import OrderedDict from PyHSPlasma import * from .node_core import PlasmaNodeBase, PlasmaNodeSocketBase, PlasmaTreeOutputNodeBase +from .. import idprops class PlasmaSoftVolumeOutputNode(PlasmaTreeOutputNodeBase, bpy.types.Node): bl_category = "SV" @@ -71,7 +72,7 @@ class PlasmaSoftVolumePropertiesNode(PlasmaNodeBase, bpy.types.Node): softvolume.outsideStrength = self.outside_strength / 100 -class PlasmaSoftVolumeReferenceNode(PlasmaNodeBase, bpy.types.Node): +class PlasmaSoftVolumeReferenceNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node): bl_category = "SV" bl_idname = "PlasmaSoftVolumeReferenceNode" bl_label = "Soft Region" @@ -84,19 +85,24 @@ class PlasmaSoftVolumeReferenceNode(PlasmaNodeBase, bpy.types.Node): }), ]) - soft_object = StringProperty(name="Soft Volume", - description="Object whose Soft Volume modifier we should use") + soft_volume = PointerProperty(name="Soft Volume", + description="Object whose Soft Volume modifier we should use", + type=bpy.types.Object, + poll=idprops.poll_softvolume_objects) def draw_buttons(self, context, layout): - layout.prop_search(self, "soft_object", bpy.data, "objects", icon="OBJECT_DATA", text="") + layout.prop(self, "soft_volume", text="") def get_key(self, exporter, so): - softvol = bpy.data.objects.get(self.soft_object, None) - if softvol is None: - self.raise_error("Volume Object '{}' not found".format(self.soft_object)) + if self.soft_volume is None: + self.raise_error("Invalid SoftVolume object reference") # Don't use SO here because that's the tree owner's SO. This soft region will find or create # its own SceneObject. Yay! - return softvol.plasma_modifiers.softvolume.get_key(exporter) + return self.soft_volume.plasma_modifiers.softvolume.get_key(exporter) + + @classmethod + def _idprop_mapping(cls): + return {"soft_volume": "soft_object"} class PlasmaSoftVolumeInvertNode(PlasmaNodeBase, bpy.types.Node): diff --git a/korman/properties/modifiers/region.py b/korman/properties/modifiers/region.py index fbc5e42..f63ab44 100644 --- a/korman/properties/modifiers/region.py +++ b/korman/properties/modifiers/region.py @@ -19,6 +19,7 @@ from PyHSPlasma import * from ...exporter import ExportError from ...helpers import TemporaryObject +from ... import idprops from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz from .physics import bounds_types @@ -145,7 +146,7 @@ class PlasmaPanicLinkRegion(PlasmaModifierProperties): return True -class PlasmaSoftVolume(PlasmaModifierProperties): +class PlasmaSoftVolume(idprops.IDPropMixin, PlasmaModifierProperties): pl_id = "softvolume" bl_category = "Region" @@ -156,8 +157,9 @@ class PlasmaSoftVolume(PlasmaModifierProperties): use_nodes = BoolProperty(name="Use Nodes", description="Make this a node-based Soft Volume", default=False) - node_tree_name = StringProperty(name="Node Tree", - description="Node Tree detailing soft volume logic") + node_tree = PointerProperty(name="Node Tree", + description="Node Tree detailing soft volume logic", + type=bpy.types.NodeTree) # Basic invert = BoolProperty(name="Invert", @@ -179,9 +181,10 @@ class PlasmaSoftVolume(PlasmaModifierProperties): so = exporter.mgr.find_create_object(plSceneObject, bl=self.id_data) if self.use_nodes: - output = self.node_tree.find_output("PlasmaSoftVolumeOutputNode") + tree = self.get_node_tree() + output = tree.find_output("PlasmaSoftVolumeOutputNode") if output is None: - raise ExportError("SoftVolume '{}' Node Tree '{}' has no output node!".format(self.key_name, self.node_tree)) + raise ExportError("SoftVolume '{}' Node Tree '{}' has no output node!".format(self.key_name, tree.name)) return output.get_key(exporter, so) else: pClass = plSoftVolumeInvert if self.invert else plSoftVolumeSimple @@ -221,13 +224,19 @@ class PlasmaSoftVolume(PlasmaModifierProperties): sv.volume = isect def _export_sv_nodes(self, exporter, bo, so): - if self.node_tree_name not in exporter.node_trees_exported: - exporter.node_trees_exported.add(self.node_tree_name) - self.node_tree.export(exporter, bo, so) - - @property - def node_tree(self): - tree = bpy.data.node_groups.get(self.node_tree_name, None) - if tree is None: - raise ExportError("SoftVolume '{}': Node Tree '{}' does not exist!".format(self.key_name, self.node_tree_name)) - return tree + tree = self.get_node_tree() + if tree.name not in exporter.node_trees_exported: + exporter.node_trees_exported.add(tree.name) + tree.export(exporter, bo, so) + + def get_node_tree(self): + if self.node_tree is None: + raise ExportError("SoftVolume '{}' does not specify a valid Node Tree!".format(self.key_name)) + return self.node_tree + + @classmethod + def _idprop_mapping(cls): + return {"node_tree": "node_tree_name"} + + def _idprop_sources(self): + return {"node_tree_name": bpy.data.node_groups} diff --git a/korman/ui/modifiers/region.py b/korman/ui/modifiers/region.py index ea58627..ea82bd6 100644 --- a/korman/ui/modifiers/region.py +++ b/korman/ui/modifiers/region.py @@ -28,7 +28,7 @@ def softvolume(modifier, layout, context): row = layout.row() row.prop(modifier, "use_nodes", text="", icon="NODETREE") if modifier.use_nodes: - row.prop_search(modifier, "node_tree_name", bpy.data, "node_groups") + row.prop(modifier, "node_tree") else: row.label("Simple Soft Volume") From b6418a557b75cb7a238e57200389337b5a9b8179 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 7 Jun 2017 18:16:29 -0400 Subject: [PATCH 07/14] Update Render mods to new ID props --- korman/exporter/etlight.py | 7 +- korman/idprops.py | 3 + korman/operators/op_lightmap.py | 2 - korman/properties/modifiers/render.py | 94 +++++++++++++++++---------- korman/ui/modifiers/render.py | 18 ++--- 5 files changed, 72 insertions(+), 52 deletions(-) diff --git a/korman/exporter/etlight.py b/korman/exporter/etlight.py index 0f1c428..730248c 100644 --- a/korman/exporter/etlight.py +++ b/korman/exporter/etlight.py @@ -146,12 +146,9 @@ class LightBaker: def _generate_lightgroup(self, bo, user_lg=None): """Makes a new light group for the baking process that excludes all Plasma RT lamps""" - - if user_lg is not None: - user_lg = bpy.data.groups.get(user_lg) shouldibake = (user_lg is not None and bool(user_lg.objects)) - mesh = bo.data + for material in mesh.materials: if material is None: # material is not assigned to this material... (why is this even a thing?) @@ -245,7 +242,7 @@ class LightBaker: uv_textures = mesh.uv_textures # Create a special light group for baking - if not self._generate_lightgroup(bo, modifier.light_group): + if not self._generate_lightgroup(bo, modifier.lights): return False # We need to ensure that we bake onto the "BlahObject_LIGHTMAPGEN" image diff --git a/korman/idprops.py b/korman/idprops.py index 903c54a..f7ea5b2 100644 --- a/korman/idprops.py +++ b/korman/idprops.py @@ -133,5 +133,8 @@ def poll_mesh_objects(self, value): def poll_softvolume_objects(self, value): return value.plasma_modifiers.softvolume.enabled +def poll_visregion_objects(self, value): + return value.plasma_modifiers.visregion.enabled + def poll_envmap_textures(self, value): return isinstance(value, bpy.types.EnvironmentMapTexture) diff --git a/korman/operators/op_lightmap.py b/korman/operators/op_lightmap.py index 5581718..e67e241 100644 --- a/korman/operators/op_lightmap.py +++ b/korman/operators/op_lightmap.py @@ -32,8 +32,6 @@ class LightmapAutobakePreviewOperator(_LightingOperator, bpy.types.Operator): bl_label = "Preview Lightmap" bl_options = {"INTERNAL"} - light_group = StringProperty(name="Light Group") - def __init__(self): super().__init__() diff --git a/korman/properties/modifiers/render.py b/korman/properties/modifiers/render.py index dcad670..cf9247f 100644 --- a/korman/properties/modifiers/render.py +++ b/korman/properties/modifiers/render.py @@ -22,6 +22,7 @@ from .base import PlasmaModifierProperties from ...exporter.etlight import _NUM_RENDER_LAYERS from ...exporter import utils from ...exporter.explosions import ExportError +from ... import idprops class PlasmaFadeMod(PlasmaModifierProperties): @@ -81,7 +82,7 @@ class PlasmaFadeMod(PlasmaModifierProperties): mod.farTrans = self.far_trans -class PlasmaFollowMod(PlasmaModifierProperties): +class PlasmaFollowMod(idprops.IDPropObjectMixin, PlasmaModifierProperties): pl_id = "followmod" bl_category = "Render" @@ -108,8 +109,9 @@ class PlasmaFollowMod(PlasmaModifierProperties): ("kFollowObject", "Object", "Follow an object"), ]) - leader_object = StringProperty(name="Leader Object", - description="Object to follow") + leader = PointerProperty(name="Leader Object", + description="Object to follow", + type=bpy.types.Object) def export(self, exporter, bo, so): fm = exporter.mgr.find_create_object(plFollowMod, so=so, name=self.key_name) @@ -122,21 +124,21 @@ class PlasmaFollowMod(PlasmaModifierProperties): if self.leader_type == "kFollowObject": # If this object is following another object, make sure that the # leader has been selected and is a valid SO. - if self.leader_object: - leader_obj = bpy.data.objects.get(self.leader_object, None) - if leader_obj is None: - raise ExportError("'{}': Follow's leader object is invalid".format(self.key_name)) - else: - fm.leader = exporter.mgr.find_create_key(plSceneObject, bl=leader_obj) + if self.leader: + fm.leader = exporter.mgr.find_create_key(plSceneObject, bl=self.leader) else: raise ExportError("'{}': Follow's leader object must be selected".format(self.key_name)) + @classmethod + def _idprop_mapping(cls): + return {"leader": "leader_object"} + @property def requires_actor(self): return True -class PlasmaLightMapGen(PlasmaModifierProperties): +class PlasmaLightMapGen(idprops.IDPropMixin, PlasmaModifierProperties): pl_id = "lightmap" bl_category = "Render" @@ -159,8 +161,9 @@ class PlasmaLightMapGen(PlasmaModifierProperties): size=_NUM_RENDER_LAYERS, default=((True,) * _NUM_RENDER_LAYERS)) - light_group = StringProperty(name="Light Group", - description="Group that defines the collection of lights to bake") + lights = PointerProperty(name="Light Group", + description="Group that defines the collection of lights to bake", + type=bpy.types.Group) uv_map = StringProperty(name="UV Texture", description="UV Texture used as the basis for the lightmap") @@ -208,6 +211,13 @@ class PlasmaLightMapGen(PlasmaModifierProperties): # Mmm... cheating mat_mgr.export_prepared_layer(layer, lightmap_im) + @classmethod + def _idprop_mapping(cls): + return {"lights": "light_group"} + + def _idprop_sources(self): + return {"light_group": bpy.data.groups} + @property def key_name(self): return "{}_LIGHTMAPGEN".format(self.id_data.name) @@ -317,7 +327,7 @@ class PlasmaShadowCasterMod(PlasmaModifierProperties): caster.castFlags |= plShadowCaster.kSelfShadow -class PlasmaViewFaceMod(PlasmaModifierProperties): +class PlasmaViewFaceMod(idprops.IDPropObjectMixin, PlasmaModifierProperties): pl_id = "viewfacemod" bl_category = "Render" @@ -340,8 +350,9 @@ class PlasmaViewFaceMod(PlasmaModifierProperties): ("kFacePlay", "Player", "Face the local player"), ("kFaceObj", "Object", "Face an object"), ]) - target_object = StringProperty(name="Target Object", - description="Object to face") + target = PointerProperty(name="Target Object", + description="Object to face", + type=bpy.types.Object) pivot_on_y = BoolProperty(name="Pivot on local Y", description="Swivel only around the local Y axis", @@ -376,12 +387,8 @@ class PlasmaViewFaceMod(PlasmaModifierProperties): if self.follow_mode == "kFaceObj": # If this swivel is following an object, make sure that the # target has been selected and is a valid SO. - if self.target_object: - target_obj = bpy.data.objects.get(self.target_object, None) - if target_obj is None: - raise ExportError("'{}': Swivel's target object is invalid".format(self.key_name)) - else: - vfm.faceObj = exporter.mgr.find_create_key(plSceneObject, bl=target_obj) + if self.target: + vfm.faceObj = exporter.mgr.find_create_key(plSceneObject, bl=self.target) else: raise ExportError("'{}': Swivel's target object must be selected".format(self.key_name)) @@ -395,12 +402,16 @@ class PlasmaViewFaceMod(PlasmaModifierProperties): if self.offset_local: vfm.setFlag(plViewFaceModifier.kOffsetLocal, True) + @classmethod + def _idprop_mapping(cls): + return {"target": "target_object"} + @property def requires_actor(self): return True -class PlasmaVisControl(PlasmaModifierProperties): +class PlasmaVisControl(idprops.IDPropObjectMixin, PlasmaModifierProperties): pl_id = "visregion" bl_category = "Render" @@ -412,8 +423,10 @@ class PlasmaVisControl(PlasmaModifierProperties): items=[("normal", "Normal", "Objects are only visible when the camera is inside this region"), ("exclude", "Exclude", "Objects are only visible when the camera is outside this region"), ("fx", "Special FX", "This is a list of objects used for special effects only")]) - softvolume = StringProperty(name="Region", - description="Object defining the SoftVolume for this VisRegion") + soft_region = PointerProperty(name="Region", + description="Object defining the SoftVolume for this VisRegion", + type=bpy.types.Object, + poll=idprops.poll_softvolume_objects) replace_normal = BoolProperty(name="Hide Drawables", description="Hides drawables attached to this region", default=True) @@ -430,21 +443,31 @@ class PlasmaVisControl(PlasmaModifierProperties): exporter.report.msg("[VisRegion] I'm a SoftVolume myself :)", indent=1) rgn.region = this_sv.get_key(exporter, so) else: - exporter.report.msg("[VisRegion] SoftVolume '{}'", self.softvolume, indent=1) - sv_bo = bpy.data.objects.get(self.softvolume, None) - if sv_bo is None: - raise ExportError("'{}': Invalid object '{}' for VisControl soft volume".format(bo.name, self.softvolume)) + if not self.soft_region: + raise ExportError("'{}': Visibility Control must have a Soft Volume selected".format(self.key_name)) + sv_bo = self.soft_region sv = sv_bo.plasma_modifiers.softvolume + exporter.report.msg("[VisRegion] SoftVolume '{}'", sv_bo.name, indent=1) if not sv.enabled: - raise ExportError("'{}': '{}' is not a SoftVolume".format(bo.name, self.softvolume)) + raise ExportError("'{}': '{}' is not a SoftVolume".format(self.key_name, sv_bo.name)) rgn.region = sv.get_key(exporter) rgn.setProperty(plVisRegion.kIsNot, self.mode == "exclude") + @classmethod + def _idprop_mapping(cls): + return {"soft_region": "softvolume"} + -class VisRegion(bpy.types.PropertyGroup): +class VisRegion(idprops.IDPropObjectMixin, bpy.types.PropertyGroup): enabled = BoolProperty(default=True) - region_name = StringProperty(name="Control", - description="Object defining a Plasma Visibility Control") + control_region = PointerProperty(name="Control", + description="Object defining a Plasma Visibility Control", + type=bpy.types.Object, + poll=idprops.poll_visregion_objects) + + @classmethod + def _idprop_mapping(cls): + return {"control_region": "region_name"} class PlasmaVisibilitySet(PlasmaModifierProperties): @@ -474,7 +497,6 @@ class PlasmaVisibilitySet(PlasmaModifierProperties): for region in self.regions: if not region.enabled: continue - rgn_bo = bpy.data.objects.get(region.region_name, None) - if rgn_bo is None: - raise ExportError("{}: Invalid VisControl '{}' in VisSet modifier".format(bo.name, region.region_name)) - addRegion(exporter.mgr.find_create_key(plVisRegion, bl=rgn_bo)) + if not region.control_region: + raise ExportError("{}: Not all Visibility Controls are set up properly in Visibility Set".format(bo.name)) + addRegion(exporter.mgr.find_create_key(plVisRegion, bl=region.control_region)) diff --git a/korman/ui/modifiers/render.py b/korman/ui/modifiers/render.py index 5020a54..f097f66 100644 --- a/korman/ui/modifiers/render.py +++ b/korman/ui/modifiers/render.py @@ -43,7 +43,7 @@ def followmod(modifier, layout, context): layout.row().prop(modifier, "follow_mode", expand=True) layout.prop(modifier, "leader_type") if modifier.leader_type == "kFollowObject": - layout.prop_search(modifier, "leader_object", bpy.data, "objects", icon="OUTLINER_OB_MESH") + layout.prop(modifier, "leader", icon="OUTLINER_OB_MESH") def lighting(modifier, layout, context): split = layout.split() @@ -84,11 +84,10 @@ def lighting(modifier, layout, context): def lightmap(modifier, layout, context): layout.row(align=True).prop(modifier, "quality", expand=True) layout.prop(modifier, "render_layers", text="Active Render Layers") - layout.prop_search(modifier, "light_group", bpy.data, "groups", icon="GROUP") + layout.prop(modifier, "lights") layout.prop_search(modifier, "uv_map", context.active_object.data, "uv_textures") operator = layout.operator("object.plasma_lightmap_preview", "Preview Lightmap", icon="RENDER_STILL") - operator.light_group = modifier.light_group # Kind of clever stuff to show the user a preview... # We can't show images, so we make a hidden ImageTexture called LIGHTMAPGEN_PREVIEW. We check @@ -117,7 +116,7 @@ def viewfacemod(modifier, layout, context): if modifier.preset_options == "Custom": layout.row().prop(modifier, "follow_mode") if modifier.follow_mode == "kFaceObj": - layout.prop_search(modifier, "target_object", bpy.data, "objects", icon="OUTLINER_OB_MESH") + layout.prop(modifier, "target", icon="OUTLINER_OB_MESH") layout.separator() layout.prop(modifier, "pivot_on_y") @@ -136,9 +135,10 @@ def viewfacemod(modifier, layout, context): class VisRegionListUI(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): - myIcon = "ERROR" if bpy.data.objects.get(item.region_name, None) is None else "OBJECT_DATA" - label = item.region_name if item.region_name else "[No Object Specified]" - layout.label(label, icon=myIcon) + if item.control_region is None: + layout.label("[No Object Specified]", icon="ERROR") + else: + layout.label(item.control_region.name, icon="OBJECT_DATA") layout.prop(item, "enabled", text="") @@ -156,7 +156,7 @@ def visibility(modifier, layout, context): op.index = modifier.active_region_index if modifier.regions: - layout.prop_search(modifier.regions[modifier.active_region_index], "region_name", bpy.data, "objects") + layout.prop(modifier.regions[modifier.active_region_index], "control_region") def visregion(modifier, layout, context): layout.prop(modifier, "mode") @@ -164,7 +164,7 @@ def visregion(modifier, layout, context): # Only allow SoftVolume spec if this is not an FX and this object is not an SV itself sv = modifier.id_data.plasma_modifiers.softvolume if modifier.mode != "fx" and not sv.enabled: - layout.prop_search(modifier, "softvolume", bpy.data, "objects") + layout.prop(modifier, "soft_region") # Other settings layout.prop(modifier, "replace_normal") From 1b3afbe8d4f504b968787b61c24ef7c69fe35495 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 7 Jun 2017 19:23:01 -0400 Subject: [PATCH 08/14] Convert sound modifiers to newfangled ID Props Unfortunately, sound indices do not match up directly with sound ID blocks, therefore, those remain string properties. --- korman/nodes/node_messages.py | 36 ++++++---- korman/operators/op_sound.py | 5 +- korman/properties/modifiers/sound.py | 104 +++++++++++++++------------ korman/ui/modifiers/sound.py | 14 ++-- 4 files changed, 86 insertions(+), 73 deletions(-) diff --git a/korman/nodes/node_messages.py b/korman/nodes/node_messages.py index c455742..1308c1d 100644 --- a/korman/nodes/node_messages.py +++ b/korman/nodes/node_messages.py @@ -21,6 +21,7 @@ from PyHSPlasma import * from .node_core import * from ..properties.modifiers.region import footstep_surfaces, footstep_surface_ids from ..exporter import ExportError +from .. import idprops class PlasmaMessageSocketBase(PlasmaNodeSocketBase): bl_color = (0.004, 0.282, 0.349, 1.0) @@ -484,14 +485,19 @@ class PlasmaSceneObjectMsgRcvrNode(PlasmaNodeBase, bpy.types.Node): return ref_so_key -class PlasmaSoundMsgNode(PlasmaMessageNode, bpy.types.Node): +class PlasmaSoundMsgNode(idprops.IDPropObjectMixin, PlasmaMessageNode, bpy.types.Node): bl_category = "MSG" bl_idname = "PlasmaSoundMsgNode" bl_label = "Sound" bl_width_default = 190 - object_name = StringProperty(name="Object", - description="Sound emitter object") + def _poll_sound_emitters(self, value): + return value.plasma_modifiers.soundemit.enabled + + emitter_object = PointerProperty(name="Object", + description="Sound emitter object", + type=bpy.types.Object, + poll=_poll_sound_emitters) sound_name = StringProperty(name="Sound", description="Sound datablock") @@ -540,20 +546,19 @@ class PlasmaSoundMsgNode(PlasmaMessageNode, bpy.types.Node): msg.setCmd(plSoundMsg.kAddCallbacks) def convert_message(self, exporter, so): - sound_bo = bpy.data.objects.get(self.object_name, None) - if sound_bo is None: - self.raise_error("'{}' is not a valid object".format(self.object_name)) - soundemit = sound_bo.plasma_modifiers.soundemit + if self.emitter_object is None: + self.raise_error("Sound emitter must be set") + soundemit = self.emitter_object.plasma_modifiers.soundemit if not soundemit.enabled: - self.raise_error("'{}' is not a valid Sound Emitter".format(self.object_name)) + self.raise_error("'{}' is not a valid Sound Emitter".format(self.emitter_object.name)) # Always test the specified audible for validity if self.sound_name and soundemit.sounds.get(self.sound_name, None) is None: - self.raise_error("Invalid Sound '{}' requested from Sound Emitter '{}'".format(self.sound_name, self.object_name)) + self.raise_error("Invalid Sound '{}' requested from Sound Emitter '{}'".format(self.sound_name, self.emitter_object.name)) # Remember that 3D stereo sounds are exported as two emitters... # But, if we only have one sound attached, who cares, we can just address the message to all - audible_key = exporter.mgr.find_create_key(plAudioInterface, bl=sound_bo) + audible_key = exporter.mgr.find_create_key(plAudioInterface, bl=self.emitter_object) indices = (-1,) if not self.sound_name or len(soundemit.sounds) == 1 else soundemit.get_sound_indices(self.sound_name) for idx in indices: msg = plSoundMsg() @@ -586,10 +591,9 @@ class PlasmaSoundMsgNode(PlasmaMessageNode, bpy.types.Node): yield msg def draw_buttons(self, context, layout): - layout.prop_search(self, "object_name", bpy.data, "objects") - bo = bpy.data.objects.get(self.object_name, None) - if bo is not None: - soundemit = bo.plasma_modifiers.soundemit + layout.prop(self, "emitter_object") + if self.emitter_object is not None: + soundemit = self.emitter_object.plasma_modifiers.soundemit if soundemit.enabled: layout.prop_search(self, "sound_name", soundemit, "sounds", icon="SOUND") else: @@ -608,6 +612,10 @@ class PlasmaSoundMsgNode(PlasmaMessageNode, bpy.types.Node): def has_callbacks(self): return True + @classmethod + def _idprop_mapping(cls): + return {"emitter_object": "object_name"} + class PlasmaTimerCallbackMsgNode(PlasmaMessageNode, bpy.types.Node): bl_category = "MSG" diff --git a/korman/operators/op_sound.py b/korman/operators/op_sound.py index 44b6adc..220222b 100644 --- a/korman/operators/op_sound.py +++ b/korman/operators/op_sound.py @@ -36,19 +36,16 @@ class PlasmaSoundOpenOperator(SoundOperator, bpy.types.Operator): def execute(self, context): # Check to see if the sound exists... Because the sneakily introduced bpy.data.sounds.load # check_existing doesn't tell us if it already exists... dammit... - # We don't want to take ownership forcefully if we don't have to. for i in bpy.data.sounds: if self.filepath == i.filepath: sound = i break else: sound = bpy.data.sounds.load(self.filepath) - sound.plasma_owned = True - sound.use_fake_user = True # Now do the stanky leg^H^H^H^H^H^H^H^H^H^H deed and put the sound on the mod dest = eval(self.data_path) - setattr(dest, self.sound_property, sound.name) + setattr(dest, self.sound_property, sound) return {"FINISHED"} def invoke(self, context, event): diff --git a/korman/properties/modifiers/sound.py b/korman/properties/modifiers/sound.py index cce45b3..ccbaa42 100644 --- a/korman/properties/modifiers/sound.py +++ b/korman/properties/modifiers/sound.py @@ -23,6 +23,7 @@ from PyHSPlasma import * from ... import korlib from .base import PlasmaModifierProperties from ...exporter import ExportError +from ... import idprops class PlasmaSfxFade(bpy.types.PropertyGroup): fade_type = EnumProperty(name="Type", @@ -38,9 +39,17 @@ class PlasmaSfxFade(bpy.types.PropertyGroup): options=set(), subtype="TIME", unit="TIME") -class PlasmaSound(bpy.types.PropertyGroup): - def _sound_picked(self, context): - if not self.sound_data: +class PlasmaSound(idprops.IDPropMixin, bpy.types.PropertyGroup): + def _get_name_proxy(self): + if self.sound is not None: + return self.sound.name + return "" + + def _set_name_proxy(self, value): + self.sound = bpy.data.sounds.get(value, None) + + # This is the actual pointer update callback + if not self.sound: self.name = "[Empty]" return @@ -54,24 +63,33 @@ class PlasmaSound(bpy.types.PropertyGroup): else: self.is_valid = True self.is_stereo = header.numChannels == 2 - self._update_name(context) + self._update_name() - def _update_name(self, context): + def _update_name(self, context=None): if self.is_stereo and self.channel != {"L", "R"}: - self.name = "{}:{}".format(self.sound_data, "L" if "L" in self.channel else "R") + self.name = "{}:{}".format(self._sound_name, "L" if "L" in self.channel else "R") else: - self.name = self.sound_data + self.name = self._sound_name enabled = BoolProperty(name="Enabled", default=True, options=set()) - sound_data = StringProperty(name="Sound", description="Sound Datablock", - options=set(), update=_sound_picked) + sound = PointerProperty(name="Sound", + description="Sound Datablock", + type=bpy.types.Sound) + + # This is needed because pointer properties do not seem to allow update CBs... Bug? + sound_data_proxy = StringProperty(name="Sound", + description="Name of sound datablock", + get=_get_name_proxy, + set=_set_name_proxy, + options=set()) is_stereo = BoolProperty(default=True, options={"HIDDEN"}) is_valid = BoolProperty(default=False, options={"HIDDEN"}) - soft_region = StringProperty(name="Soft Volume", + sfx_region = PointerProperty(name="Soft Volume", description="Soft region this sound can be heard in", - options=set()) + type=bpy.types.Object, + poll=idprops.poll_softvolume_objects) sfx_type = EnumProperty(name="Category", description="Describes the purpose of this sound", @@ -170,9 +188,9 @@ class PlasmaSound(bpy.types.PropertyGroup): def _convert_sound(self, exporter, so, pClass, wavHeader, dataSize, channel=None): if channel is None: - name = "Sfx-{}_{}".format(so.key.name, self.sound_data) + name = "Sfx-{}_{}".format(so.key.name, self._sound_name) else: - name = "Sfx-{}_{}:{}".format(so.key.name, self.sound_data, channel) + name = "Sfx-{}_{}:{}".format(so.key.name, self._sound_name, channel) exporter.report.msg("[{}] {}", pClass.__name__[2:], name, indent=1) sound = exporter.mgr.find_create_object(pClass, so=so, name=name) @@ -181,13 +199,10 @@ class PlasmaSound(bpy.types.PropertyGroup): sv_mod, sv_key = self.id_data.plasma_modifiers.softvolume, None if sv_mod.enabled: sv_key = sv_mod.get_key(exporter, so) - elif self.soft_region: - sv_bo = bpy.data.objects.get(self.soft_region, None) - if sv_bo is None: - raise ExportError("'{}': Invalid object '{}' for SoundEmit '{}' soft volume".format(self.id_data.name, self.soft_region, self.sound_data)) - sv_mod = sv_bo.plasma_modifiers.softvolume + elif self.sfx_region: + sv_mod = self.sfx_region.plasma_modifiers.softvolume if not sv_mod.enabled: - raise ExportError("'{}': SoundEmit '{}', '{}' is not a SoftVolume".format(self.id_data.name, self.sound_data, self.soft_region)) + raise ExportError("'{}': SoundEmit '{}', '{}' is not a SoftVolume".format(self.id_data.name, self._sound_name, self.sfx_region.name)) sv_key = sv_mod.get_key(exporter) if sv_key is not None: sv_key.object.listenState |= plSoftVolume.kListenCheck | plSoftVolume.kListenDirty | plSoftVolume.kListenRegistered @@ -305,21 +320,36 @@ class PlasmaSound(bpy.types.PropertyGroup): key = sound.key return key + @classmethod + def _idprop_mapping(cls): + return {"sound": "sound_data", + "sfx_region": "soft_region"} + + def _idprop_sources(self): + return {"sound_data": bpy.data.sounds, + "soft_region": bpy.data.objects} + @property def is_3d_stereo(self): return self.sfx_type == "kSoundFX" and self.channel == {"L", "R"} and self.is_stereo def _raise_error(self, msg): - raise ExportError("SoundEmitter '{}': Sound '{}' {}".format(self.id_data.name, self.sound_data, msg)) + if self.sound: + raise ExportError("SoundEmitter '{}': Sound '{}' {}".format(self.id_data.name, self.sound.name, msg)) + else: + raise ExportError("SoundEmitter '{}': {}".format(self.id_data.name, msg)) @property def _sound(self): - try: - sound = bpy.data.sounds.get(self.sound_data) - except: - self._raise_error("is not loaded") - else: - return sound + if not self.sound: + self._raise_error("has an invalid sound specified") + return self.sound + + @property + def _sound_name(self): + if self.sound: + return self.sound.name + return "" class PlasmaSoundEmitter(PlasmaModifierProperties): @@ -341,7 +371,7 @@ class PlasmaSoundEmitter(PlasmaModifierProperties): # Pass this off to each individual sound for conversion for i in self.sounds: - if i.sound_data and i.enabled: + if i.enabled: i.convert_sound(exporter, so, winaud) def get_sound_indices(self, name=None, sound=None): @@ -374,26 +404,6 @@ class PlasmaSoundEmitter(PlasmaModifierProperties): else: raise ValueError(name) - @classmethod - def register(cls): - bpy.types.Sound.plasma_owned = BoolProperty(default=False, options={"HIDDEN"}) - @property def requires_actor(self): return True - - -@persistent -def _toss_orphaned_sounds(scene): - used_sounds = set() - for i in bpy.data.objects: - soundemit = i.plasma_modifiers.soundemit - used_sounds.update((j.sound_data for j in soundemit.sounds)) - dead_sounds = [i for i in bpy.data.sounds if i.plasma_owned and i.name not in used_sounds] - for i in dead_sounds: - i.use_fake_user = False - i.user_clear() - bpy.data.sounds.remove(i) - -# collects orphaned Plasma owned sound datablocks -bpy.app.handlers.save_pre.append(_toss_orphaned_sounds) diff --git a/korman/ui/modifiers/sound.py b/korman/ui/modifiers/sound.py index becabc9..1997ed6 100644 --- a/korman/ui/modifiers/sound.py +++ b/korman/ui/modifiers/sound.py @@ -22,10 +22,8 @@ def _draw_fade_ui(modifier, layout, label): class SoundListUI(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): - if item.sound_data: - sound = bpy.data.sounds.get(item.sound_data) - icon = "SOUND" if sound is not None else "ERROR" - layout.prop(item, "name", emboss=False, icon=icon, text="") + if item.sound: + layout.prop(item, "name", emboss=False, icon="SOUND", text="") layout.prop(item, "enabled", text="") else: layout.label("[Empty]") @@ -51,13 +49,13 @@ def soundemit(modifier, layout, context): else: # Sound datablock picker row = layout.row(align=True) - row.prop_search(sound, "sound_data", bpy.data, "sounds", text="") + row.prop_search(sound, "sound_data_proxy", bpy.data, "sounds", text="") open_op = row.operator("sound.plasma_open", icon="FILESEL", text="") open_op.data_path = repr(sound) open_op.sound_property = "sound_data" # Pack/Unpack - data = bpy.data.sounds.get(sound.sound_data) + data = sound.sound if data is not None: if data.packed_file is None: row.operator("sound.plasma_pack", icon="UGLYPACKAGE", text="") @@ -65,7 +63,7 @@ def soundemit(modifier, layout, context): row.operator_menu_enum("sound.plasma_unpack", "method", icon="PACKAGE", text="") # If an invalid sound data block is spec'd, let them know about it. - if sound.sound_data and not sound.is_valid: + if data and not sound.is_valid: layout.label(text="Invalid sound specified", icon="ERROR") # Core Props @@ -102,4 +100,4 @@ def soundemit(modifier, layout, context): if not sv.enabled: col.separator() col.label("Soft Region:") - col.prop_search(sound, "soft_region", bpy.data, "objects", text="") + col.prop(sound, "sfx_region", text="") From ebe1b1bdb50d9120995787066d3f16de295e71f4 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 7 Jun 2017 19:43:02 -0400 Subject: [PATCH 09/14] Convert more logic nodes to newfangled ID props --- korman/nodes/node_logic.py | 69 ++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/korman/nodes/node_logic.py b/korman/nodes/node_logic.py index 4d6c72c..29f3885 100644 --- a/korman/nodes/node_logic.py +++ b/korman/nodes/node_logic.py @@ -20,8 +20,9 @@ from PyHSPlasma import * from .node_core import * from ..properties.modifiers.physics import bounds_types, bounds_type_index +from .. import idprops -class PlasmaExcludeRegionNode(PlasmaNodeBase, bpy.types.Node): +class PlasmaExcludeRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node): bl_category = "LOGIC" bl_idname = "PlasmaExcludeRegionNode" bl_label = "Exclude Region" @@ -31,17 +32,17 @@ class PlasmaExcludeRegionNode(PlasmaNodeBase, bpy.types.Node): pl_attribs = {"ptAttribExcludeRegion"} def _get_bounds(self): - bo = bpy.data.objects.get(self.region, None) - if bo is not None: - return bounds_type_index(bo.plasma_modifiers.collision.bounds) + if self.region_object is not None: + return bounds_type_index(self.region_object.plasma_modifiers.collision.bounds) return bounds_type_index("hull") def _set_bounds(self, value): - bo = bpy.data.objects.get(self.region, None) - if bo is not None: - bo.plasma_modifiers.collision.bounds = value + if self.region_object is not None: + self.region_object.plasma_modifiers.collision.bounds = value - region = StringProperty(name="Region", - description="Region object's name") + region_object = PointerProperty(name="Region", + description="Region object's name", + type=bpy.types.Object, + poll=idprops.poll_mesh_objects) bounds = EnumProperty(name="Bounds", description="Region bounds", items=bounds_types, @@ -74,56 +75,58 @@ class PlasmaExcludeRegionNode(PlasmaNodeBase, bpy.types.Node): ]) def draw_buttons(self, context, layout): - layout.prop_search(self, "region", bpy.data, "objects", icon="MESH_DATA") + layout.prop(self, "region_object", icon="MESH_DATA") layout.prop(self, "bounds") layout.prop(self, "block_cameras") def get_key(self, exporter, parent_so): - region_bo = bpy.data.objects.get(self.region, None) - if region_bo is None: - self.raise_error("invalid region object '{}'".format(self.region)) - return exporter.mgr.find_create_key(plExcludeRegionModifier, bl=region_bo, name=self.key_name) + if self.region_object is None: + self.raise_error("Region must be set") + return exporter.mgr.find_create_key(plExcludeRegionModifier, bl=self.region_object, name=self.key_name) def harvest_actors(self): - return [i.safepoint_name for i in self.find_input_sockets("safe_points")] + return [i.safepoint.name for i in self.find_input_sockets("safe_points") if i.safepoint is not None] def export(self, exporter, bo, parent_so): - region_bo = bpy.data.objects.get(self.region, None) - if region_bo is None: - self.raise_error("invalid region object '{}'".format(self.region)) - region_so = exporter.mgr.find_create_object(plSceneObject, bl=region_bo) - excludergn = exporter.mgr.find_create_object(plExcludeRegionModifier, so=region_so, name=self.key_name) + excludergn = self.get_key(exporter, parent_so).object excludergn.setFlag(plExcludeRegionModifier.kBlockCameras, self.block_cameras) + region_so = exporter.mgr.find_create_object(plSceneObject, bl=self.region_object) # Safe points for i in self.find_input_sockets("safe_point"): - if not i.safepoint_name: - continue - safept = bpy.data.objects.get(i.safepoint_name, None) - if safept is None: - self.raise_error("invalid SafePoint '{}'".format(i.safepoint_name)) - excludergn.addSafePoint(exporter.mgr.find_create_key(plSceneObject, bl=safept)) + safept = i.safepoint_object + if safept: + excludergn.addSafePoint(exporter.mgr.find_create_key(plSceneObject, bl=safept)) # Ensure the region is exported - phys_name = "{}_XRgn".format(self.region) - simIface, physical = exporter.physics.generate_physical(region_bo, region_so, self.bounds, phys_name) + phys_name = "{}_XRgn".format(self.region_object.name) + simIface, physical = exporter.physics.generate_physical(self.region_object, region_so, self.bounds, phys_name) simIface.setProperty(plSimulationInterface.kPinned, True) physical.setProperty(plSimulationInterface.kPinned, True) physical.LOSDBs |= plSimDefs.kLOSDBUIBlockers + @classmethod + def _idprop_mapping(cls): + return {"region_object": "region"} -class PlasmaExcludeSafePointSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): + +class PlasmaExcludeSafePointSocket(idprops.IDPropObjectMixin, PlasmaNodeSocketBase, bpy.types.NodeSocket): bl_color = (0.0, 0.0, 0.0, 0.0) - safepoint_name = StringProperty(name="Safe Point", - description="A point outside of this exclude region to move the avatar to") + safepoint_object = PointerProperty(name="Safe Point", + description="A point outside of this exclude region to move the avatar to", + type=bpy.types.Object) def draw(self, context, layout, node, text): - layout.prop_search(self, "safepoint_name", bpy.data, "objects", icon="EMPTY_DATA") + layout.prop(self, "safepoint_object", icon="EMPTY_DATA") + + @classmethod + def _idprop_mapping(cls): + return {"safepoint_object": "safepoint_name"} @property def is_used(self): - return bpy.data.objects.get(self.safepoint_name, None) is not None + return self.safepoint_object is not None class PlasmaExcludeMessageSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): From 5344a652c4b4fb2e2bc2d0e3aaff80c453973be5 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 7 Jun 2017 21:51:27 -0400 Subject: [PATCH 10/14] Convert python nodes to ID props --- korman/nodes/node_python.py | 92 ++++++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 26 deletions(-) diff --git a/korman/nodes/node_python.py b/korman/nodes/node_python.py index 969e853..1938e9e 100644 --- a/korman/nodes/node_python.py +++ b/korman/nodes/node_python.py @@ -19,6 +19,7 @@ from pathlib import Path from PyHSPlasma import * from .node_core import * +from .. import idprops _single_user_attribs = { "ptAttribBoolean", "ptAttribInt", "ptAttribFloat", "ptAttribString", "ptAttribDropDownList", @@ -419,7 +420,7 @@ class PlasmaAttribNumericNode(PlasmaAttribNodeBase, bpy.types.Node): return self.value_float -class PlasmaAttribObjectNode(PlasmaAttribNodeBase, bpy.types.Node): +class PlasmaAttribObjectNode(idprops.IDPropObjectMixin, PlasmaAttribNodeBase, bpy.types.Node): bl_category = "PYTHON" bl_idname = "PlasmaAttribObjectNode" bl_label = "Object Attribute" @@ -427,8 +428,9 @@ class PlasmaAttribObjectNode(PlasmaAttribNodeBase, bpy.types.Node): pl_attrib = ("ptAttribSceneobject", "ptAttribSceneobjectList", "ptAttribAnimation", "ptAttribSwimCurrent", "ptAttribWaveSet") - object_name = StringProperty(name="Object", - description="Object containing the required data") + target_object = PointerProperty(name="Object", + description="Object containing the required data", + type=bpy.types.Object) def init(self, context): super().init(context) @@ -436,7 +438,7 @@ class PlasmaAttribObjectNode(PlasmaAttribNodeBase, bpy.types.Node): self.outputs[0].link_limit = 1 def draw_buttons(self, context, layout): - layout.prop_search(self, "object_name", bpy.data, "objects", text=self.attribute_name) + layout.prop(self, "target_object", text=self.attribute_name) def get_key(self, exporter, so): attrib = self.to_socket @@ -444,9 +446,9 @@ class PlasmaAttribObjectNode(PlasmaAttribNodeBase, bpy.types.Node): self.raise_error("must be connected to a Python File node!") attrib = attrib.attribute_type - bo = bpy.data.objects.get(self.object_name, None) + bo = self.target_object if bo is None: - self.raise_error("invalid object specified: '{}'".format(self.object_name)) + self.raise_error("Target object must be specified") ref_so_key = exporter.mgr.find_create_key(plSceneObject, bl=bo) ref_so = ref_so_key.object @@ -467,6 +469,10 @@ class PlasmaAttribObjectNode(PlasmaAttribNodeBase, bpy.types.Node): self.raise_error("water modifier not enabled on '{}'".format(self.object_name)) return exporter.mgr.find_create_key(plWaveSet7, so=ref_so, bl=bo) + @classmethod + def _idprop_mapping(cls): + return {"target_object": "object_name"} + class PlasmaAttribStringNode(PlasmaAttribNodeBase, bpy.types.Node): bl_category = "PYTHON" @@ -486,7 +492,7 @@ class PlasmaAttribStringNode(PlasmaAttribNodeBase, bpy.types.Node): self.value = attrib.simple_value -class PlasmaAttribTextureNode(PlasmaAttribNodeBase, bpy.types.Node): +class PlasmaAttribTextureNode(idprops.IDPropMixin, PlasmaAttribNodeBase, bpy.types.Node): bl_category = "PYTHON" bl_idname = "PlasmaAttribTextureNode" bl_label = "Texture Attribute" @@ -494,8 +500,31 @@ class PlasmaAttribTextureNode(PlasmaAttribNodeBase, bpy.types.Node): pl_attrib = ("ptAttribMaterial", "ptAttribMaterialList", "ptAttribDynamicMap", "ptAttribMaterialAnimation") - material_name = StringProperty(name="Material") - texture_name = StringProperty(name="Texture") + + def _poll_texture(self, value): + if self.material is not None: + # is this the type of dealio that we're looking for? + attrib = self.to_socket + if attrib is not None: + attrib = attrib.attribute_type + if attrib == "ptAttribDynamicMap": + if not self._is_dyntext(value): + return False + elif attrib == "ptAttribMaterialAnimation": + if not self._is_animated(self.material, value): + return False + + # must be a legal option... but is it a member of this material? + return value.name in self.material.texture_slots + return False + + material = PointerProperty(name="Material", + description="Material the texture is attached to", + type=bpy.types.Material) + texture = PointerProperty(name="Texture", + description="Texture to expose to Python", + type=bpy.types.Texture, + poll=_poll_texture) def init(self, context): super().init(context) @@ -509,36 +538,47 @@ class PlasmaAttribTextureNode(PlasmaAttribNodeBase, bpy.types.Node): layout.prop_search(self, "texture_name", material, "texture_slots") def get_key(self, exporter, so): - material = bpy.data.materials.get(self.material_name, None) - if material is None: - self.raise_error("invalid Material '{}'".format(self.material_name)) - tex_slot = material.texture_slots.get(self.texture_name, None) - if tex_slot is None: - self.raise_error("invalid Texture '{}'".format(self.texture_name)) + if self.material is None: + self.raise_error("Material must be specified") + if self.texture is None: + self.raise_error("Texture must be specified") + attrib = self.to_socket if attrib is None: self.raise_error("must be connected to a Python File node!") attrib = attrib.attribute_type - - # Helpers - texture = tex_slot.texture - is_animated = ((material.animation_data is not None and material.animation_data.action is not None) - or (texture.animation_data is not None and texture.animation_data.action is not None)) - is_dyntext = texture.type == "IMAGE" and texture.image is None + material = self.material + texture = self.texture # Your attribute stuff here... if attrib == "ptAttribDynamicMap": - if not is_dyntext: + if not self._is_dyntext(material, texture): self.raise_error("Texture '{}' is not a Dynamic Text Map".format(self.texture_name)) - name = "{}_{}_DynText".format(self.material_name, self.texture_name) + name = "{}_{}_DynText".format(material.name, texture.name) return exporter.mgr.find_create_key(plDynamicTextMap, name=name, so=so) - elif is_animated: - name = "{}_{}_LayerAnim".format(self.material_name, self.texture_name) + elif self._is_animated(material, texture): + name = "{}_{}_LayerAnim".format(material_name, texture.name) return exporter.mgr.find_create_key(plLayerAnimation, name=name, so=so) else: - name = "{}_{}".format(self.material_name, self.texture_name) + name = "{}_{}".format(material.name, texture.name) return exporter.mgr.find_create_key(plLayer, name=name, so=so) + @classmethod + def _idprop_mapping(cls): + return {"material": "material_name", + "texture": "texture_name"} + + def _idprop_sources(self): + return {"material_name": bpy.data.materials, + "texture_name": bpy.data.textures} + + def _is_animated(self, material, texture): + return ((material.animation_data is not None and material.animation_data.action is not None) + or (texture.animation_data is not None and texture.animation_data.action is not None)) + + def _is_dyntext(self, texture): + return texture.type == "IMAGE" and texture.image is None + _attrib_colors = { "ptAttribActivator": (0.188, 0.086, 0.349, 1.0), From 6c5dff5d563da704948c47bb567759e7277d65a2 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 8 Jun 2017 12:58:48 -0400 Subject: [PATCH 11/14] Update RT Lights to IDProps --- korman/exporter/rtlight.py | 8 +++----- korman/properties/prop_lamp.py | 15 +++++++++++---- korman/ui/ui_lamp.py | 2 +- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/korman/exporter/rtlight.py b/korman/exporter/rtlight.py index c249218..daa6b76 100644 --- a/korman/exporter/rtlight.py +++ b/korman/exporter/rtlight.py @@ -171,13 +171,11 @@ class LightConverter: sv_mod, sv_key = bo.plasma_modifiers.softvolume, None if sv_mod.enabled: sv_key = sv_mod.get_key(self._exporter()) - elif rtlamp.soft_region: - sv_bo = bpy.data.objects.get(rtlamp.soft_region, None) - if sv_bo is None: - raise ExportError("'{}': Invalid object for Lamp soft volume '{}'".format(bo.name, rtlamp.soft_region)) + elif rtlamp.lamp_region: + sv_bo = rtlamp.lamp_region sv_mod = sv_bo.plasma_modifiers.softvolume if not sv_mod.enabled: - raise ExportError("'{}': '{}' is not a SoftVolume".format(bo.name, rtlamp.soft_region)) + raise ExportError("'{}': '{}' is not a SoftVolume".format(bo.name, sv_bo.name)) sv_key = sv_mod.get_key(self._exporter()) pl_light.softVolume = sv_key diff --git a/korman/properties/prop_lamp.py b/korman/properties/prop_lamp.py index 428fa10..61a5a7a 100644 --- a/korman/properties/prop_lamp.py +++ b/korman/properties/prop_lamp.py @@ -16,7 +16,9 @@ import bpy from bpy.props import * -class PlasmaLamp(bpy.types.PropertyGroup): +from .. import idprops + +class PlasmaLamp(idprops.IDPropObjectMixin, bpy.types.PropertyGroup): affect_characters = BoolProperty(name="Affect Avatars", description="This lamp affects avatars", options=set(), @@ -43,9 +45,10 @@ class PlasmaLamp(bpy.types.PropertyGroup): default=False, options=set()) - soft_region = StringProperty(name="Soft Volume", - description="Soft region this light is active inside", - options=set()) + lamp_region = PointerProperty(name="Soft Volume", + description="Soft region this light is active inside", + type=bpy.types.Object, + poll=idprops.poll_softvolume_objects) # For LimitedDirLights size_height = FloatProperty(name="Height", @@ -55,3 +58,7 @@ class PlasmaLamp(bpy.types.PropertyGroup): def has_light_group(self, bo): return bool(bo.users_group) + + @classmethod + def _idprop_mapping(cls): + return {"lamp_region": "soft_region"} diff --git a/korman/ui/ui_lamp.py b/korman/ui/ui_lamp.py index 754a4d9..bb043c7 100644 --- a/korman/ui/ui_lamp.py +++ b/korman/ui/ui_lamp.py @@ -53,7 +53,7 @@ class PlasmaLampPanel(LampButtonsPanel, bpy.types.Panel): if not context.object.plasma_modifiers.softvolume.enabled: layout.separator() - layout.prop_search(rtlamp, "soft_region", bpy.data, "objects") + layout.prop(rtlamp, "lamp_region") def _draw_area_lamp(self, context): From 9f158c3726842268a966d95ef31b7439d869312d Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 8 Jun 2017 19:07:14 -0400 Subject: [PATCH 12/14] Update Texture props to ID properties --- korman/exporter/material.py | 6 +++--- korman/properties/prop_texture.py | 14 +++++++++++--- korman/ui/ui_texture.py | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/korman/exporter/material.py b/korman/exporter/material.py index f66f52d..eb817b1 100644 --- a/korman/exporter/material.py +++ b/korman/exporter/material.py @@ -476,11 +476,11 @@ class MaterialConverter: # Whoever wrote this PyHSPlasma binding didn't follow the convention. Sigh. visregions = [] for region in texture.plasma_layer.vis_regions: - rgn = bpy.data.objects.get(region.region_name, None) + rgn = region.control_region if rgn is None: - raise ExportError("'{}': VisControl '{}' not found".format(texture.name, region.region_name)) + raise ExportError("'{}': Has an invalid Visibility Control".format(texture.name)) if not rgn.plasma_modifiers.visregion.enabled: - raise ExportError("'{}': '{}' is not a VisControl".format(texture.name, region.region_name)) + raise ExportError("'{}': '{}' is not a VisControl".format(texture.name, rgn.name)) visregions.append(self._mgr.find_create_key(plVisRegion, bl=rgn)) pl_env.visRegions = visregions diff --git a/korman/properties/prop_texture.py b/korman/properties/prop_texture.py index bfd93a7..8cf3c26 100644 --- a/korman/properties/prop_texture.py +++ b/korman/properties/prop_texture.py @@ -16,10 +16,18 @@ import bpy from bpy.props import * -class EnvMapVisRegion(bpy.types.PropertyGroup): +from .. import idprops + +class EnvMapVisRegion(idprops.IDPropObjectMixin, bpy.types.PropertyGroup): enabled = BoolProperty(default=True) - region_name = StringProperty(name="Control", - description="Object defining a Plasma Visibility Control") + control_region = PointerProperty(name="Control", + description="Object defining a Plasma Visibility Control", + type=bpy.types.Object, + poll=idprops.poll_visregion_objects) + + @classmethod + def _idprop_mapping(cls): + return {"control_region": "region_name"} class PlasmaLayer(bpy.types.PropertyGroup): diff --git a/korman/ui/ui_texture.py b/korman/ui/ui_texture.py index 98fbf8a..ba706de 100644 --- a/korman/ui/ui_texture.py +++ b/korman/ui/ui_texture.py @@ -52,7 +52,7 @@ class PlasmaEnvMapPanel(TextureButtonsPanel, bpy.types.Panel): op.index = layer_props.active_region_index rgns = layer_props.vis_regions if layer_props.vis_regions: - layout.prop_search(rgns[layer_props.active_region_index], "region_name", bpy.data, "objects") + layout.prop(rgns[layer_props.active_region_index], "control_region") layout.separator() layout.prop(layer_props, "envmap_color") From 484b16925ab383868967309d112126bcb5cf6a40 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 8 Jun 2017 22:08:23 -0400 Subject: [PATCH 13/14] Convert the rest of the message nodes to ID props --- korman/exporter/material.py | 19 +++--- korman/nodes/node_messages.py | 120 ++++++++++++++++++++++------------ 2 files changed, 87 insertions(+), 52 deletions(-) diff --git a/korman/exporter/material.py b/korman/exporter/material.py index eb817b1..6b64622 100644 --- a/korman/exporter/material.py +++ b/korman/exporter/material.py @@ -357,8 +357,9 @@ class MaterialConverter: return None fcurves = [] + texture = tex_slot.texture mat_action = harvest_fcurves(bm, fcurves, "texture_slots[{}]".format(idx)) - tex_action = harvest_fcurves(tex_slot.texture, fcurves) + tex_action = harvest_fcurves(texture, fcurves) if not fcurves: return base_layer @@ -370,7 +371,7 @@ class MaterialConverter: if ctrl is not None: if layer_animation is None: name = "{}_LayerAnim".format(base_layer.key.name) - layer_animation = self.get_texture_animation_key(bo, bm, tex_slot=tex_slot).object + layer_animation = self.get_texture_animation_key(bo, bm, texture).object setattr(layer_animation, attr, ctrl) # Alrighty, if we exported any controllers, layer_animation is a plLayerAnimation. We need to do @@ -695,19 +696,15 @@ class MaterialConverter: def get_bump_layer(self, bo): return self._bump_mats.get(bo, None) - def get_texture_animation_key(self, bo, bm, tex_name=None, tex_slot=None): + def get_texture_animation_key(self, bo, bm, texture): """Finds or creates the appropriate key for sending messages to an animated Texture""" - assert tex_name or tex_slot - if tex_slot is None: - tex_slot = bm.texture_slots.get(tex_name, None) - if tex_slot is None: - raise ExportError("Material '{}' does not contain Texture '{}'".format(bm.name, tex_name)) - if tex_name is None: - tex_name = tex_slot.name + tex_name = texture.name + if not tex_name in bm.texture_slots: + raise ExportError("Texture '{}' not used in Material '{}'".format(bm.name, tex_name)) name = "{}_{}_LayerAnim".format(bm.name, tex_name) - layer = tex_slot.texture.plasma_layer + layer = texture.plasma_layer pClass = plLayerSDLAnimation if layer.anim_sdl_var else plLayerAnimation return self._mgr.find_create_key(pClass, bl=bo, name=name) diff --git a/korman/nodes/node_messages.py b/korman/nodes/node_messages.py index 1308c1d..af8b19a 100644 --- a/korman/nodes/node_messages.py +++ b/korman/nodes/node_messages.py @@ -44,7 +44,7 @@ class PlasmaMessageNode(PlasmaNodeBase): return False -class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node): +class PlasmaAnimCmdMsgNode(idprops.IDPropMixin, PlasmaMessageNode, bpy.types.Node): bl_category = "MSG" bl_idname = "PlasmaAnimCmdMsgNode" bl_label = "Animation Command" @@ -55,12 +55,32 @@ class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node): items=[("OBJECT", "Object", "Mesh Action"), ("TEXTURE", "Texture", "Texture Action")], default="OBJECT") - object_name = StringProperty(name="Object", - description="Target object name") - material_name = StringProperty(name="Material", - description="Target material name") - texture_name = StringProperty(name="Texture", - description="Target texture slot name") + + def _poll_material_textures(self, value): + if self.target_object is None: + return False + if self.target_material is None: + return False + return value.name in self.target_material.texture_slots + + def _poll_mesh_materials(self, value): + if self.target_object is None: + return False + if self.target_object.type != "MESH": + return False + return value.name in self.target_object.data.materials + + target_object = PointerProperty(name="Object", + description="Target object", + type=bpy.types.Object) + target_material = PointerProperty(name="Material", + description="Target material", + type=bpy.types.Material, + poll=_poll_mesh_materials) + target_texture = PointerProperty(name="Texture", + description="Target texture", + type=bpy.types.Texture, + poll=_poll_material_textures) go_to = EnumProperty(name="Go To", description="Where should the animation start?", @@ -122,19 +142,16 @@ class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node): def draw_buttons(self, context, layout): layout.prop(self, "anim_type") - layout.prop_search(self, "object_name", bpy.data, "objects") + layout.prop(self, "target_object") if self.anim_type != "OBJECT": - bo = bpy.data.objects.get(self.object_name) - if bo is None or not hasattr(bo.data, "materials"): - layout.label("Invalid Object", icon="ERROR") - else: - layout.prop_search(self, "material_name", bo.data, "materials") - material = bpy.data.materials.get(self.material_name, None) - if material is None: - layout.label("Invalid Material", icon="ERROR") - else: - layout.prop_search(self, "texture_name", material, "texture_slots") + col = layout.column() + col.enabled = self.target_object is not None + col.prop(self, "target_material") + + col = layout.column() + col.enabled = self.target_object is not None and self.target_material is not None + col.prop(self, "target_texture") layout.prop(self, "go_to") layout.prop(self, "action") @@ -150,8 +167,7 @@ class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node): if self.anim_type != "OBJECT": loops = None else: - obj = bpy.data.objects.get(self.object_name, None) - loops = None if obj is None else obj.plasma_modifiers.animation_loop + loops = None if self.target_object is None else self.target_object.plasma_modifiers.animation_loop if loops is not None and loops.enabled: layout.prop_search(self, "loop_name", loops, "loops", icon="PMARKER_ACT") else: @@ -171,9 +187,9 @@ class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node): msg = plAnimCmdMsg() # We're either sending this off to an AGMasterMod or a LayerAnim - obj = bpy.data.objects.get(self.object_name, None) + obj = self.target_object if obj is None: - self.raise_error("invalid object: '{}'".format(self.object_name)) + self.raise_error("target object must be specified") if self.anim_type == "OBJECT": if not obj.plasma_object.has_animation_data: self.raise_error("invalid animation") @@ -186,10 +202,13 @@ class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node): else: _agmod_trash, target = exporter.animation.get_anigraph_keys(obj) else: - material = bpy.data.materials.get(self.material_name, None) + material = self.target_material if material is None: - self.raise_error("invalid material: '{}'".format(self.material_name)) - target = exporter.mesh.material.get_texture_animation_key(obj, material, self.texture_name) + self.raise_error("target material must be specified") + texture = self.target_texture + if texture is None: + self.raise_error("target texture must be specified") + target = exporter.mesh.material.get_texture_animation_key(obj, material, texture) if target is None: raise RuntimeError() @@ -225,6 +244,17 @@ class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node): def has_callbacks(self): return self.event != "NONE" + @classmethod + def _idprop_mapping(cls): + return {"target_object": "object_name", + "target_material": "material_name", + "target_texture": "texture_name"} + + def _idprop_sources(self): + return {"object_name": bpy.data.objects, + "material_name": bpy.data.materials, + "texture_name": bpy.data.textures} + class PlasmaEnableMsgNode(PlasmaMessageNode, bpy.types.Node): bl_category = "MSG" @@ -377,14 +407,15 @@ class PlasmaLinkToAgeMsg(PlasmaMessageNode, bpy.types.Node): layout.prop(self, "spawn_point") -class PlasmaOneShotMsgNode(PlasmaMessageNode, bpy.types.Node): +class PlasmaOneShotMsgNode(idprops.IDPropObjectMixin, PlasmaMessageNode, bpy.types.Node): bl_category = "MSG" bl_idname = "PlasmaOneShotMsgNode" bl_label = "One Shot" bl_width_default = 210 - pos = StringProperty(name="Position", - description="Object defining the OneShot position") + pos_object = PointerProperty(name="Position", + description="Object defining the OneShot position", + type=bpy.types.Object) seek = EnumProperty(name="Seek", description="How the avatar should approach the OneShot position", items=[("SMART", "Smart Seek", "Let the engine figure out the best path"), @@ -417,7 +448,7 @@ class PlasmaOneShotMsgNode(PlasmaMessageNode, bpy.types.Node): row = layout.row() row.prop(self, "drivable") row.prop(self, "reversable") - layout.prop_search(self, "pos", bpy.data, "objects", icon="EMPTY_DATA") + layout.prop(self, "pos_object", icon="EMPTY_DATA") layout.prop(self, "seek") def export(self, exporter, bo, so): @@ -431,22 +462,24 @@ class PlasmaOneShotMsgNode(PlasmaMessageNode, bpy.types.Node): def get_key(self, exporter, so): name = self.key_name - if self.pos: - bo = bpy.data.objects.get(self.pos, None) - if bo is None: - raise ExportError("Node '{}' in '{}' specifies an invalid Position Empty".format(self.name, self.id_data.name)) - pos_so = exporter.mgr.find_create_object(plSceneObject, bl=bo) + if self.pos_object is not None: + pos_so = exporter.mgr.find_create_object(plSceneObject, bl=self.pos_object) return exporter.mgr.find_create_key(plOneShotMod, name=name, so=pos_so) else: return exporter.mgr.find_create_key(plOneShotMod, name=name, so=so) def harvest_actors(self): - return (self.pos,) + if self.pos_object: + return (self.pos_object.name,) @property def has_callbacks(self): return bool(self.marker) + @classmethod + def _idprop_mapping(cls): + return {"pos_object": "pos"} + class PlasmaOneShotCallbackSocket(PlasmaMessageSocketBase, bpy.types.NodeSocket): marker = StringProperty(name="Marker", @@ -456,7 +489,7 @@ class PlasmaOneShotCallbackSocket(PlasmaMessageSocketBase, bpy.types.NodeSocket) layout.prop(self, "marker") -class PlasmaSceneObjectMsgRcvrNode(PlasmaNodeBase, bpy.types.Node): +class PlasmaSceneObjectMsgRcvrNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node): bl_category = "MSG" bl_idname = "PlasmaSceneObjectMsgRcvrNode" bl_label = "Send To Object" @@ -471,19 +504,24 @@ class PlasmaSceneObjectMsgRcvrNode(PlasmaNodeBase, bpy.types.Node): }), ]) - object_name = StringProperty(name="Object", - description="Object to send the message to") + target_object = PointerProperty(name="Object", + description="Object to send the message to", + type=bpy.types.Object) def draw_buttons(self, context, layout): - layout.prop_search(self, "object_name", bpy.data, "objects") + layout.prop(self, "target_object") def get_key(self, exporter, so): - bo = bpy.data.objects.get(self.object_name, None) + bo = self.target_object if bo is None: - self.raise_error("invalid object specified: '{}'".format(self.object_name)) + self.raise_error("target object must be specified") ref_so_key = exporter.mgr.find_create_key(plSceneObject, bl=bo) return ref_so_key + @classmethod + def _idprop_mapping(cls): + return {"target_object": "object_name"} + class PlasmaSoundMsgNode(idprops.IDPropObjectMixin, PlasmaMessageNode, bpy.types.Node): bl_category = "MSG" From d8dd3cc8b0bc380ed7134974238fe614b095d996 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sat, 19 Aug 2017 14:40:15 -0400 Subject: [PATCH 14/14] Ensure node trees are upgraded properly --- korman/idprops.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/korman/idprops.py b/korman/idprops.py index f7ea5b2..45a6fab 100644 --- a/korman/idprops.py +++ b/korman/idprops.py @@ -138,3 +138,20 @@ def poll_visregion_objects(self, value): def poll_envmap_textures(self, value): return isinstance(value, bpy.types.EnvironmentMapTexture) + +@bpy.app.handlers.persistent +def _upgrade_node_trees(dummy): + """ + Logic node haxxor incoming! + Logic nodes appear to have issues with silently updating themselves. I expect that Blender is + doing something strange in the UI code that causes our metaprogramming tricks to be bypassed. + Therefore, we will loop through all Plasma node trees and forcibly update them on blend load. + """ + + for tree in bpy.data.node_groups: + if tree.bl_idname != "PlasmaNodeTree": + continue + for node in tree.nodes: + if isinstance(node, IDPropMixin): + assert node._try_upgrade_idprops() +bpy.app.handlers.load_post.append(_upgrade_node_trees)