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/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/exporter/material.py b/korman/exporter/material.py index f66f52d..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 @@ -476,11 +477,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 @@ -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/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/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/idprops.py b/korman/idprops.py new file mode 100644 index 0000000..45a6fab --- /dev/null +++ b/korman/idprops.py @@ -0,0 +1,157 @@ +# 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() } + + +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" + +def poll_mesh_objects(self, value): + return value.type == "MESH" + +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) + +@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) 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/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): diff --git a/korman/nodes/node_messages.py b/korman/nodes/node_messages.py index c455742..af8b19a 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) @@ -43,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" @@ -54,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?", @@ -121,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") @@ -149,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: @@ -170,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") @@ -185,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() @@ -224,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" @@ -376,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"), @@ -416,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): @@ -430,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", @@ -455,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" @@ -470,28 +504,38 @@ 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(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 +584,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 +629,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 +650,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/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), 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/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/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/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/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/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/properties/modifiers/region.py b/korman/properties/modifiers/region.py index 8101cd9..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 @@ -88,7 +89,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 @@ -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/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/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/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/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/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/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): 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") 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: 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") 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") 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="") 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() 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): 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")