Browse Source

Merge pull request #56 from Hoikas/idprops

ID Datablock Properties
pull/66/head
Joseph Davies 7 years ago committed by GitHub
parent
commit
368c49aca3
  1. 2
      korman/__init__.py
  2. 7
      korman/exporter/etlight.py
  3. 25
      korman/exporter/material.py
  4. 8
      korman/exporter/rtlight.py
  5. 6
      korman/helpers.py
  6. 157
      korman/idprops.py
  7. 68
      korman/nodes/node_conditions.py
  8. 69
      korman/nodes/node_logic.py
  9. 156
      korman/nodes/node_messages.py
  10. 92
      korman/nodes/node_python.py
  11. 22
      korman/nodes/node_softvolume.py
  12. 2
      korman/operators/op_lightmap.py
  13. 5
      korman/operators/op_sound.py
  14. 15
      korman/properties/modifiers/anim.py
  15. 33
      korman/properties/modifiers/avatar.py
  16. 29
      korman/properties/modifiers/logic.py
  17. 41
      korman/properties/modifiers/region.py
  18. 94
      korman/properties/modifiers/render.py
  19. 104
      korman/properties/modifiers/sound.py
  20. 107
      korman/properties/modifiers/water.py
  21. 15
      korman/properties/prop_lamp.py
  22. 14
      korman/properties/prop_texture.py
  23. 6
      korman/ui/modifiers/anim.py
  24. 8
      korman/ui/modifiers/avatar.py
  25. 2
      korman/ui/modifiers/logic.py
  26. 2
      korman/ui/modifiers/region.py
  27. 18
      korman/ui/modifiers/render.py
  28. 14
      korman/ui/modifiers/sound.py
  29. 12
      korman/ui/modifiers/water.py
  30. 2
      korman/ui/ui_lamp.py
  31. 2
      korman/ui/ui_texture.py

2
korman/__init__.py

@ -22,7 +22,7 @@ from . import operators
bl_info = { bl_info = {
"name": "Korman", "name": "Korman",
"author": "Guild of Writers", "author": "Guild of Writers",
"blender": (2, 78, 0), "blender": (2, 79, 0),
"location": "File > Import-Export", "location": "File > Import-Export",
"description": "Exporter for Cyan Worlds' Plasma Engine", "description": "Exporter for Cyan Worlds' Plasma Engine",
"warning": "beta", "warning": "beta",

7
korman/exporter/etlight.py

@ -146,12 +146,9 @@ class LightBaker:
def _generate_lightgroup(self, bo, user_lg=None): def _generate_lightgroup(self, bo, user_lg=None):
"""Makes a new light group for the baking process that excludes all Plasma RT lamps""" """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)) shouldibake = (user_lg is not None and bool(user_lg.objects))
mesh = bo.data mesh = bo.data
for material in mesh.materials: for material in mesh.materials:
if material is None: if material is None:
# material is not assigned to this material... (why is this even a thing?) # material is not assigned to this material... (why is this even a thing?)
@ -245,7 +242,7 @@ class LightBaker:
uv_textures = mesh.uv_textures uv_textures = mesh.uv_textures
# Create a special light group for baking # 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 return False
# We need to ensure that we bake onto the "BlahObject_LIGHTMAPGEN" image # We need to ensure that we bake onto the "BlahObject_LIGHTMAPGEN" image

25
korman/exporter/material.py

@ -357,8 +357,9 @@ class MaterialConverter:
return None return None
fcurves = [] fcurves = []
texture = tex_slot.texture
mat_action = harvest_fcurves(bm, fcurves, "texture_slots[{}]".format(idx)) 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: if not fcurves:
return base_layer return base_layer
@ -370,7 +371,7 @@ class MaterialConverter:
if ctrl is not None: if ctrl is not None:
if layer_animation is None: if layer_animation is None:
name = "{}_LayerAnim".format(base_layer.key.name) 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) setattr(layer_animation, attr, ctrl)
# Alrighty, if we exported any controllers, layer_animation is a plLayerAnimation. We need to do # 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. # Whoever wrote this PyHSPlasma binding didn't follow the convention. Sigh.
visregions = [] visregions = []
for region in texture.plasma_layer.vis_regions: 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: 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: 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)) visregions.append(self._mgr.find_create_key(plVisRegion, bl=rgn))
pl_env.visRegions = visregions pl_env.visRegions = visregions
@ -695,19 +696,15 @@ class MaterialConverter:
def get_bump_layer(self, bo): def get_bump_layer(self, bo):
return self._bump_mats.get(bo, None) 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""" """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_name = texture.name
tex_slot = bm.texture_slots.get(tex_name, None) if not tex_name in bm.texture_slots:
if tex_slot is None: raise ExportError("Texture '{}' not used in Material '{}'".format(bm.name, tex_name))
raise ExportError("Material '{}' does not contain Texture '{}'".format(bm.name, tex_name))
if tex_name is None:
tex_name = tex_slot.name
name = "{}_{}_LayerAnim".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 pClass = plLayerSDLAnimation if layer.anim_sdl_var else plLayerAnimation
return self._mgr.find_create_key(pClass, bl=bo, name=name) return self._mgr.find_create_key(pClass, bl=bo, name=name)

8
korman/exporter/rtlight.py

@ -171,13 +171,11 @@ class LightConverter:
sv_mod, sv_key = bo.plasma_modifiers.softvolume, None sv_mod, sv_key = bo.plasma_modifiers.softvolume, None
if sv_mod.enabled: if sv_mod.enabled:
sv_key = sv_mod.get_key(self._exporter()) sv_key = sv_mod.get_key(self._exporter())
elif rtlamp.soft_region: elif rtlamp.lamp_region:
sv_bo = bpy.data.objects.get(rtlamp.soft_region, None) sv_bo = rtlamp.lamp_region
if sv_bo is None:
raise ExportError("'{}': Invalid object for Lamp soft volume '{}'".format(bo.name, rtlamp.soft_region))
sv_mod = sv_bo.plasma_modifiers.softvolume sv_mod = sv_bo.plasma_modifiers.softvolume
if not sv_mod.enabled: 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()) sv_key = sv_mod.get_key(self._exporter())
pl_light.softVolume = sv_key pl_light.softVolume = sv_key

6
korman/helpers.py

@ -51,9 +51,9 @@ class TemporaryObject:
def ensure_power_of_two(value): def ensure_power_of_two(value):
return pow(2, math.floor(math.log(value, 2))) 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""" def find_modifier(bo, modid):
bo = bpy.data.objects.get(boname, None) """Given a Blender Object, finds a given modifier and returns it or None"""
if bo is not None: if bo is not None:
# if they give us the wrong modid, it is a bug and an AttributeError # if they give us the wrong modid, it is a bug and an AttributeError
return getattr(bo.plasma_modifiers, modid) return getattr(bo.plasma_modifiers, modid)

157
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 <http://www.gnu.org/licenses/>.
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)

68
korman/nodes/node_conditions.py

@ -21,8 +21,9 @@ from PyHSPlasma import *
from .node_core import * from .node_core import *
from ..properties.modifiers.physics import bounds_types 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_category = "CONDITIONS"
bl_idname = "PlasmaClickableNode" bl_idname = "PlasmaClickableNode"
bl_label = "Clickable" bl_label = "Clickable"
@ -31,8 +32,10 @@ class PlasmaClickableNode(PlasmaNodeBase, bpy.types.Node):
# These are the Python attributes we can fill in # These are the Python attributes we can fill in
pl_attrib = {"ptAttribActivator", "ptAttribActivatorList", "ptAttribNamedActivator"} pl_attrib = {"ptAttribActivator", "ptAttribActivatorList", "ptAttribNamedActivator"}
clickable = StringProperty(name="Clickable", clickable_object = PointerProperty(name="Clickable",
description="Mesh that is clickable") description="Mesh object that is clickable",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects)
bounds = EnumProperty(name="Bounds", bounds = EnumProperty(name="Bounds",
description="Clickable's bounds (NOTE: only used if your clickable is not a collider)", description="Clickable's bounds (NOTE: only used if your clickable is not a collider)",
items=bounds_types, items=bounds_types,
@ -63,7 +66,7 @@ class PlasmaClickableNode(PlasmaNodeBase, bpy.types.Node):
]) ])
def draw_buttons(self, context, layout): 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") layout.prop(self, "bounds")
def export(self, exporter, parent_bo, parent_so): 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. # 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. # 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) # Case: sitting modifier (exports from sit position empty)
if self.clickable: if self.clickable_object:
clickable_bo = bpy.data.objects.get(self.clickable, None) clickable_so = exporter.mgr.find_create_object(plSceneObject, bl=self.clickable_object)
if clickable_bo is None: return (self.clickable_object, clickable_so)
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)
else: else:
return (None, parent_so) return (None, parent_so)
def harvest_actors(self): 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_category = "CONDITIONS"
bl_idname = "PlasmaClickableRegionNode" bl_idname = "PlasmaClickableRegionNode"
bl_label = "Clickable Region Settings" bl_label = "Clickable Region Settings"
bl_width_default = 200 bl_width_default = 200
region = StringProperty(name="Region", region_object = PointerProperty(name="Region",
description="Object that defines the region mesh") description="Object that defines the region mesh",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects)
bounds = EnumProperty(name="Bounds", bounds = EnumProperty(name="Bounds",
description="Physical object's bounds (NOTE: only used if your clickable is not a collider)", description="Physical object's bounds (NOTE: only used if your clickable is not a collider)",
items=bounds_types, items=bounds_types,
@ -161,14 +168,14 @@ class PlasmaClickableRegionNode(PlasmaNodeBase, bpy.types.Node):
]) ])
def draw_buttons(self, context, layout): 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, "bounds")
def convert_subcondition(self, exporter, parent_bo, parent_so, logicmod): def convert_subcondition(self, exporter, parent_bo, parent_so, logicmod):
# REMEMBER: parent_so doesn't have to be the actual region scene object... # 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: 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 region_so = exporter.mgr.find_create_key(plSceneObject, bl=region_bo).object
# Try to figure out the appropriate bounds type for the region.... # 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 objinbox_key.object.satisfied = True
logicmod.addCondition(objinbox_key) logicmod.addCondition(objinbox_key)
@classmethod
def _idprop_mapping(cls):
return {"region_object": "region"}
class PlasmaClickableRegionSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): class PlasmaClickableRegionSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
bl_color = (0.412, 0.0, 0.055, 1.0) 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="") row.prop(self, "threshold", text="")
class PlasmaVolumeSensorNode(PlasmaNodeBase, bpy.types.Node): class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node):
bl_category = "CONDITIONS" bl_category = "CONDITIONS"
bl_idname = "PlasmaVolumeSensorNode" bl_idname = "PlasmaVolumeSensorNode"
bl_label = "Region Sensor" bl_label = "Region Sensor"
@ -320,8 +331,10 @@ class PlasmaVolumeSensorNode(PlasmaNodeBase, bpy.types.Node):
pl_attrib = {"ptAttribActivator", "ptAttribActivatorList", "ptAttribNamedActivator"} pl_attrib = {"ptAttribActivator", "ptAttribActivatorList", "ptAttribNamedActivator"}
# Region Mesh # Region Mesh
region = StringProperty(name="Region", region_object = PointerProperty(name="Region",
description="Object that defines the region mesh") description="Object that defines the region mesh",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects)
bounds = EnumProperty(name="Bounds", bounds = EnumProperty(name="Bounds",
description="Physical object's bounds", description="Physical object's bounds",
items=bounds_types) items=bounds_types)
@ -364,11 +377,13 @@ class PlasmaVolumeSensorNode(PlasmaNodeBase, bpy.types.Node):
layout.prop(self, "report_on") layout.prop(self, "report_on")
# Okay, if they changed the name of the ObData, that's THEIR problem... # 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") layout.prop(self, "bounds")
def get_key(self, exporter, parent_so): def get_key(self, exporter, parent_so):
bo = self.region_object bo = self.region_object
if bo is None:
self.raise_error("Region cannot be empty")
so = exporter.mgr.find_create_object(plSceneObject, bl=bo) so = exporter.mgr.find_create_object(plSceneObject, bl=bo)
rgn_enter, rgn_exit = None, None rgn_enter, rgn_exit = None, None
@ -393,6 +408,8 @@ class PlasmaVolumeSensorNode(PlasmaNodeBase, bpy.types.Node):
def export(self, exporter, bo, parent_so): def export(self, exporter, bo, parent_so):
# We need to ensure we export to the correct SO # We need to ensure we export to the correct SO
region_bo = self.region_object 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) 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) 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) logicmod.addCondition(volKey)
return logicKey return logicKey
@property @classmethod
def region_object(self): def _idprop_mapping(cls):
phys_bo = bpy.data.objects.get(self.region, None) return {"region_object": "region"}
if phys_bo is None:
self.raise_error("invalid Region object: '{}'".format(self.region))
return phys_bo
@property @property
def report_enters(self): def report_enters(self):

69
korman/nodes/node_logic.py

@ -20,8 +20,9 @@ from PyHSPlasma import *
from .node_core import * from .node_core import *
from ..properties.modifiers.physics import bounds_types, bounds_type_index 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_category = "LOGIC"
bl_idname = "PlasmaExcludeRegionNode" bl_idname = "PlasmaExcludeRegionNode"
bl_label = "Exclude Region" bl_label = "Exclude Region"
@ -31,17 +32,17 @@ class PlasmaExcludeRegionNode(PlasmaNodeBase, bpy.types.Node):
pl_attribs = {"ptAttribExcludeRegion"} pl_attribs = {"ptAttribExcludeRegion"}
def _get_bounds(self): def _get_bounds(self):
bo = bpy.data.objects.get(self.region, None) if self.region_object is not None:
if bo is not None: return bounds_type_index(self.region_object.plasma_modifiers.collision.bounds)
return bounds_type_index(bo.plasma_modifiers.collision.bounds)
return bounds_type_index("hull") return bounds_type_index("hull")
def _set_bounds(self, value): def _set_bounds(self, value):
bo = bpy.data.objects.get(self.region, None) if self.region_object is not None:
if bo is not None: self.region_object.plasma_modifiers.collision.bounds = value
bo.plasma_modifiers.collision.bounds = value
region = StringProperty(name="Region", region_object = PointerProperty(name="Region",
description="Region object's name") description="Region object's name",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects)
bounds = EnumProperty(name="Bounds", bounds = EnumProperty(name="Bounds",
description="Region bounds", description="Region bounds",
items=bounds_types, items=bounds_types,
@ -74,56 +75,58 @@ class PlasmaExcludeRegionNode(PlasmaNodeBase, bpy.types.Node):
]) ])
def draw_buttons(self, context, layout): 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, "bounds")
layout.prop(self, "block_cameras") layout.prop(self, "block_cameras")
def get_key(self, exporter, parent_so): def get_key(self, exporter, parent_so):
region_bo = bpy.data.objects.get(self.region, None) if self.region_object is None:
if region_bo is None: self.raise_error("Region must be set")
self.raise_error("invalid region object '{}'".format(self.region)) return exporter.mgr.find_create_key(plExcludeRegionModifier, bl=self.region_object, name=self.key_name)
return exporter.mgr.find_create_key(plExcludeRegionModifier, bl=region_bo, name=self.key_name)
def harvest_actors(self): 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): def export(self, exporter, bo, parent_so):
region_bo = bpy.data.objects.get(self.region, None) excludergn = self.get_key(exporter, parent_so).object
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.setFlag(plExcludeRegionModifier.kBlockCameras, self.block_cameras) excludergn.setFlag(plExcludeRegionModifier.kBlockCameras, self.block_cameras)
region_so = exporter.mgr.find_create_object(plSceneObject, bl=self.region_object)
# Safe points # Safe points
for i in self.find_input_sockets("safe_point"): for i in self.find_input_sockets("safe_point"):
if not i.safepoint_name: safept = i.safepoint_object
continue if safept:
safept = bpy.data.objects.get(i.safepoint_name, None) excludergn.addSafePoint(exporter.mgr.find_create_key(plSceneObject, bl=safept))
if safept is None:
self.raise_error("invalid SafePoint '{}'".format(i.safepoint_name))
excludergn.addSafePoint(exporter.mgr.find_create_key(plSceneObject, bl=safept))
# Ensure the region is exported # Ensure the region is exported
phys_name = "{}_XRgn".format(self.region) phys_name = "{}_XRgn".format(self.region_object.name)
simIface, physical = exporter.physics.generate_physical(region_bo, region_so, self.bounds, phys_name) simIface, physical = exporter.physics.generate_physical(self.region_object, region_so, self.bounds, phys_name)
simIface.setProperty(plSimulationInterface.kPinned, True) simIface.setProperty(plSimulationInterface.kPinned, True)
physical.setProperty(plSimulationInterface.kPinned, True) physical.setProperty(plSimulationInterface.kPinned, True)
physical.LOSDBs |= plSimDefs.kLOSDBUIBlockers 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) bl_color = (0.0, 0.0, 0.0, 0.0)
safepoint_name = StringProperty(name="Safe Point", safepoint_object = PointerProperty(name="Safe Point",
description="A point outside of this exclude region to move the avatar to") description="A point outside of this exclude region to move the avatar to",
type=bpy.types.Object)
def draw(self, context, layout, node, text): 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 @property
def is_used(self): 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): class PlasmaExcludeMessageSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):

156
korman/nodes/node_messages.py

@ -21,6 +21,7 @@ from PyHSPlasma import *
from .node_core import * from .node_core import *
from ..properties.modifiers.region import footstep_surfaces, footstep_surface_ids from ..properties.modifiers.region import footstep_surfaces, footstep_surface_ids
from ..exporter import ExportError from ..exporter import ExportError
from .. import idprops
class PlasmaMessageSocketBase(PlasmaNodeSocketBase): class PlasmaMessageSocketBase(PlasmaNodeSocketBase):
bl_color = (0.004, 0.282, 0.349, 1.0) bl_color = (0.004, 0.282, 0.349, 1.0)
@ -43,7 +44,7 @@ class PlasmaMessageNode(PlasmaNodeBase):
return False return False
class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node): class PlasmaAnimCmdMsgNode(idprops.IDPropMixin, PlasmaMessageNode, bpy.types.Node):
bl_category = "MSG" bl_category = "MSG"
bl_idname = "PlasmaAnimCmdMsgNode" bl_idname = "PlasmaAnimCmdMsgNode"
bl_label = "Animation Command" bl_label = "Animation Command"
@ -54,12 +55,32 @@ class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node):
items=[("OBJECT", "Object", "Mesh Action"), items=[("OBJECT", "Object", "Mesh Action"),
("TEXTURE", "Texture", "Texture Action")], ("TEXTURE", "Texture", "Texture Action")],
default="OBJECT") default="OBJECT")
object_name = StringProperty(name="Object",
description="Target object name") def _poll_material_textures(self, value):
material_name = StringProperty(name="Material", if self.target_object is None:
description="Target material name") return False
texture_name = StringProperty(name="Texture", if self.target_material is None:
description="Target texture slot name") 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", go_to = EnumProperty(name="Go To",
description="Where should the animation start?", description="Where should the animation start?",
@ -121,19 +142,16 @@ class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node):
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop(self, "anim_type") layout.prop(self, "anim_type")
layout.prop_search(self, "object_name", bpy.data, "objects") layout.prop(self, "target_object")
if self.anim_type != "OBJECT": if self.anim_type != "OBJECT":
bo = bpy.data.objects.get(self.object_name) col = layout.column()
if bo is None or not hasattr(bo.data, "materials"): col.enabled = self.target_object is not None
layout.label("Invalid Object", icon="ERROR") col.prop(self, "target_material")
else:
layout.prop_search(self, "material_name", bo.data, "materials") col = layout.column()
material = bpy.data.materials.get(self.material_name, None) col.enabled = self.target_object is not None and self.target_material is not None
if material is None: col.prop(self, "target_texture")
layout.label("Invalid Material", icon="ERROR")
else:
layout.prop_search(self, "texture_name", material, "texture_slots")
layout.prop(self, "go_to") layout.prop(self, "go_to")
layout.prop(self, "action") layout.prop(self, "action")
@ -149,8 +167,7 @@ class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node):
if self.anim_type != "OBJECT": if self.anim_type != "OBJECT":
loops = None loops = None
else: else:
obj = bpy.data.objects.get(self.object_name, None) loops = None if self.target_object is None else self.target_object.plasma_modifiers.animation_loop
loops = None if obj is None else obj.plasma_modifiers.animation_loop
if loops is not None and loops.enabled: if loops is not None and loops.enabled:
layout.prop_search(self, "loop_name", loops, "loops", icon="PMARKER_ACT") layout.prop_search(self, "loop_name", loops, "loops", icon="PMARKER_ACT")
else: else:
@ -170,9 +187,9 @@ class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node):
msg = plAnimCmdMsg() msg = plAnimCmdMsg()
# We're either sending this off to an AGMasterMod or a LayerAnim # 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: 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 self.anim_type == "OBJECT":
if not obj.plasma_object.has_animation_data: if not obj.plasma_object.has_animation_data:
self.raise_error("invalid animation") self.raise_error("invalid animation")
@ -185,10 +202,13 @@ class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node):
else: else:
_agmod_trash, target = exporter.animation.get_anigraph_keys(obj) _agmod_trash, target = exporter.animation.get_anigraph_keys(obj)
else: else:
material = bpy.data.materials.get(self.material_name, None) material = self.target_material
if material is None: if material is None:
self.raise_error("invalid material: '{}'".format(self.material_name)) self.raise_error("target material must be specified")
target = exporter.mesh.material.get_texture_animation_key(obj, material, self.texture_name) 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: if target is None:
raise RuntimeError() raise RuntimeError()
@ -224,6 +244,17 @@ class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node):
def has_callbacks(self): def has_callbacks(self):
return self.event != "NONE" 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): class PlasmaEnableMsgNode(PlasmaMessageNode, bpy.types.Node):
bl_category = "MSG" bl_category = "MSG"
@ -376,14 +407,15 @@ class PlasmaLinkToAgeMsg(PlasmaMessageNode, bpy.types.Node):
layout.prop(self, "spawn_point") layout.prop(self, "spawn_point")
class PlasmaOneShotMsgNode(PlasmaMessageNode, bpy.types.Node): class PlasmaOneShotMsgNode(idprops.IDPropObjectMixin, PlasmaMessageNode, bpy.types.Node):
bl_category = "MSG" bl_category = "MSG"
bl_idname = "PlasmaOneShotMsgNode" bl_idname = "PlasmaOneShotMsgNode"
bl_label = "One Shot" bl_label = "One Shot"
bl_width_default = 210 bl_width_default = 210
pos = StringProperty(name="Position", pos_object = PointerProperty(name="Position",
description="Object defining the OneShot position") description="Object defining the OneShot position",
type=bpy.types.Object)
seek = EnumProperty(name="Seek", seek = EnumProperty(name="Seek",
description="How the avatar should approach the OneShot position", description="How the avatar should approach the OneShot position",
items=[("SMART", "Smart Seek", "Let the engine figure out the best path"), 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 = layout.row()
row.prop(self, "drivable") row.prop(self, "drivable")
row.prop(self, "reversable") 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") layout.prop(self, "seek")
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
@ -430,22 +462,24 @@ class PlasmaOneShotMsgNode(PlasmaMessageNode, bpy.types.Node):
def get_key(self, exporter, so): def get_key(self, exporter, so):
name = self.key_name name = self.key_name
if self.pos: if self.pos_object is not None:
bo = bpy.data.objects.get(self.pos, None) pos_so = exporter.mgr.find_create_object(plSceneObject, bl=self.pos_object)
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)
return exporter.mgr.find_create_key(plOneShotMod, name=name, so=pos_so) return exporter.mgr.find_create_key(plOneShotMod, name=name, so=pos_so)
else: else:
return exporter.mgr.find_create_key(plOneShotMod, name=name, so=so) return exporter.mgr.find_create_key(plOneShotMod, name=name, so=so)
def harvest_actors(self): def harvest_actors(self):
return (self.pos,) if self.pos_object:
return (self.pos_object.name,)
@property @property
def has_callbacks(self): def has_callbacks(self):
return bool(self.marker) return bool(self.marker)
@classmethod
def _idprop_mapping(cls):
return {"pos_object": "pos"}
class PlasmaOneShotCallbackSocket(PlasmaMessageSocketBase, bpy.types.NodeSocket): class PlasmaOneShotCallbackSocket(PlasmaMessageSocketBase, bpy.types.NodeSocket):
marker = StringProperty(name="Marker", marker = StringProperty(name="Marker",
@ -455,7 +489,7 @@ class PlasmaOneShotCallbackSocket(PlasmaMessageSocketBase, bpy.types.NodeSocket)
layout.prop(self, "marker") layout.prop(self, "marker")
class PlasmaSceneObjectMsgRcvrNode(PlasmaNodeBase, bpy.types.Node): class PlasmaSceneObjectMsgRcvrNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node):
bl_category = "MSG" bl_category = "MSG"
bl_idname = "PlasmaSceneObjectMsgRcvrNode" bl_idname = "PlasmaSceneObjectMsgRcvrNode"
bl_label = "Send To Object" bl_label = "Send To Object"
@ -470,28 +504,38 @@ class PlasmaSceneObjectMsgRcvrNode(PlasmaNodeBase, bpy.types.Node):
}), }),
]) ])
object_name = StringProperty(name="Object", target_object = PointerProperty(name="Object",
description="Object to send the message to") description="Object to send the message to",
type=bpy.types.Object)
def draw_buttons(self, context, layout): 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): def get_key(self, exporter, so):
bo = bpy.data.objects.get(self.object_name, None) bo = self.target_object
if bo is None: 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_key = exporter.mgr.find_create_key(plSceneObject, bl=bo)
return ref_so_key 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_category = "MSG"
bl_idname = "PlasmaSoundMsgNode" bl_idname = "PlasmaSoundMsgNode"
bl_label = "Sound" bl_label = "Sound"
bl_width_default = 190 bl_width_default = 190
object_name = StringProperty(name="Object", def _poll_sound_emitters(self, value):
description="Sound emitter object") 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", sound_name = StringProperty(name="Sound",
description="Sound datablock") description="Sound datablock")
@ -540,20 +584,19 @@ class PlasmaSoundMsgNode(PlasmaMessageNode, bpy.types.Node):
msg.setCmd(plSoundMsg.kAddCallbacks) msg.setCmd(plSoundMsg.kAddCallbacks)
def convert_message(self, exporter, so): def convert_message(self, exporter, so):
sound_bo = bpy.data.objects.get(self.object_name, None) if self.emitter_object is None:
if sound_bo is None: self.raise_error("Sound emitter must be set")
self.raise_error("'{}' is not a valid object".format(self.object_name)) soundemit = self.emitter_object.plasma_modifiers.soundemit
soundemit = sound_bo.plasma_modifiers.soundemit
if not soundemit.enabled: 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 # Always test the specified audible for validity
if self.sound_name and soundemit.sounds.get(self.sound_name, None) is None: 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... # 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 # 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) indices = (-1,) if not self.sound_name or len(soundemit.sounds) == 1 else soundemit.get_sound_indices(self.sound_name)
for idx in indices: for idx in indices:
msg = plSoundMsg() msg = plSoundMsg()
@ -586,10 +629,9 @@ class PlasmaSoundMsgNode(PlasmaMessageNode, bpy.types.Node):
yield msg yield msg
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop_search(self, "object_name", bpy.data, "objects") layout.prop(self, "emitter_object")
bo = bpy.data.objects.get(self.object_name, None) if self.emitter_object is not None:
if bo is not None: soundemit = self.emitter_object.plasma_modifiers.soundemit
soundemit = bo.plasma_modifiers.soundemit
if soundemit.enabled: if soundemit.enabled:
layout.prop_search(self, "sound_name", soundemit, "sounds", icon="SOUND") layout.prop_search(self, "sound_name", soundemit, "sounds", icon="SOUND")
else: else:
@ -608,6 +650,10 @@ class PlasmaSoundMsgNode(PlasmaMessageNode, bpy.types.Node):
def has_callbacks(self): def has_callbacks(self):
return True return True
@classmethod
def _idprop_mapping(cls):
return {"emitter_object": "object_name"}
class PlasmaTimerCallbackMsgNode(PlasmaMessageNode, bpy.types.Node): class PlasmaTimerCallbackMsgNode(PlasmaMessageNode, bpy.types.Node):
bl_category = "MSG" bl_category = "MSG"

92
korman/nodes/node_python.py

@ -19,6 +19,7 @@ from pathlib import Path
from PyHSPlasma import * from PyHSPlasma import *
from .node_core import * from .node_core import *
from .. import idprops
_single_user_attribs = { _single_user_attribs = {
"ptAttribBoolean", "ptAttribInt", "ptAttribFloat", "ptAttribString", "ptAttribDropDownList", "ptAttribBoolean", "ptAttribInt", "ptAttribFloat", "ptAttribString", "ptAttribDropDownList",
@ -419,7 +420,7 @@ class PlasmaAttribNumericNode(PlasmaAttribNodeBase, bpy.types.Node):
return self.value_float return self.value_float
class PlasmaAttribObjectNode(PlasmaAttribNodeBase, bpy.types.Node): class PlasmaAttribObjectNode(idprops.IDPropObjectMixin, PlasmaAttribNodeBase, bpy.types.Node):
bl_category = "PYTHON" bl_category = "PYTHON"
bl_idname = "PlasmaAttribObjectNode" bl_idname = "PlasmaAttribObjectNode"
bl_label = "Object Attribute" bl_label = "Object Attribute"
@ -427,8 +428,9 @@ class PlasmaAttribObjectNode(PlasmaAttribNodeBase, bpy.types.Node):
pl_attrib = ("ptAttribSceneobject", "ptAttribSceneobjectList", "ptAttribAnimation", pl_attrib = ("ptAttribSceneobject", "ptAttribSceneobjectList", "ptAttribAnimation",
"ptAttribSwimCurrent", "ptAttribWaveSet") "ptAttribSwimCurrent", "ptAttribWaveSet")
object_name = StringProperty(name="Object", target_object = PointerProperty(name="Object",
description="Object containing the required data") description="Object containing the required data",
type=bpy.types.Object)
def init(self, context): def init(self, context):
super().init(context) super().init(context)
@ -436,7 +438,7 @@ class PlasmaAttribObjectNode(PlasmaAttribNodeBase, bpy.types.Node):
self.outputs[0].link_limit = 1 self.outputs[0].link_limit = 1
def draw_buttons(self, context, layout): 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): def get_key(self, exporter, so):
attrib = self.to_socket 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!") self.raise_error("must be connected to a Python File node!")
attrib = attrib.attribute_type attrib = attrib.attribute_type
bo = bpy.data.objects.get(self.object_name, None) bo = self.target_object
if bo is None: 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_key = exporter.mgr.find_create_key(plSceneObject, bl=bo)
ref_so = ref_so_key.object 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)) self.raise_error("water modifier not enabled on '{}'".format(self.object_name))
return exporter.mgr.find_create_key(plWaveSet7, so=ref_so, bl=bo) 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): class PlasmaAttribStringNode(PlasmaAttribNodeBase, bpy.types.Node):
bl_category = "PYTHON" bl_category = "PYTHON"
@ -486,7 +492,7 @@ class PlasmaAttribStringNode(PlasmaAttribNodeBase, bpy.types.Node):
self.value = attrib.simple_value self.value = attrib.simple_value
class PlasmaAttribTextureNode(PlasmaAttribNodeBase, bpy.types.Node): class PlasmaAttribTextureNode(idprops.IDPropMixin, PlasmaAttribNodeBase, bpy.types.Node):
bl_category = "PYTHON" bl_category = "PYTHON"
bl_idname = "PlasmaAttribTextureNode" bl_idname = "PlasmaAttribTextureNode"
bl_label = "Texture Attribute" bl_label = "Texture Attribute"
@ -494,8 +500,31 @@ class PlasmaAttribTextureNode(PlasmaAttribNodeBase, bpy.types.Node):
pl_attrib = ("ptAttribMaterial", "ptAttribMaterialList", pl_attrib = ("ptAttribMaterial", "ptAttribMaterialList",
"ptAttribDynamicMap", "ptAttribMaterialAnimation") "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): def init(self, context):
super().init(context) super().init(context)
@ -509,36 +538,47 @@ class PlasmaAttribTextureNode(PlasmaAttribNodeBase, bpy.types.Node):
layout.prop_search(self, "texture_name", material, "texture_slots") layout.prop_search(self, "texture_name", material, "texture_slots")
def get_key(self, exporter, so): def get_key(self, exporter, so):
material = bpy.data.materials.get(self.material_name, None) if self.material is None:
if material is None: self.raise_error("Material must be specified")
self.raise_error("invalid Material '{}'".format(self.material_name)) if self.texture is None:
tex_slot = material.texture_slots.get(self.texture_name, None) self.raise_error("Texture must be specified")
if tex_slot is None:
self.raise_error("invalid Texture '{}'".format(self.texture_name))
attrib = self.to_socket attrib = self.to_socket
if attrib is None: if attrib is None:
self.raise_error("must be connected to a Python File node!") self.raise_error("must be connected to a Python File node!")
attrib = attrib.attribute_type attrib = attrib.attribute_type
material = self.material
# Helpers texture = self.texture
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
# Your attribute stuff here... # Your attribute stuff here...
if attrib == "ptAttribDynamicMap": 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)) 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) return exporter.mgr.find_create_key(plDynamicTextMap, name=name, so=so)
elif is_animated: elif self._is_animated(material, texture):
name = "{}_{}_LayerAnim".format(self.material_name, self.texture_name) name = "{}_{}_LayerAnim".format(material_name, texture.name)
return exporter.mgr.find_create_key(plLayerAnimation, name=name, so=so) return exporter.mgr.find_create_key(plLayerAnimation, name=name, so=so)
else: 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) 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 = { _attrib_colors = {
"ptAttribActivator": (0.188, 0.086, 0.349, 1.0), "ptAttribActivator": (0.188, 0.086, 0.349, 1.0),

22
korman/nodes/node_softvolume.py

@ -19,6 +19,7 @@ from collections import OrderedDict
from PyHSPlasma import * from PyHSPlasma import *
from .node_core import PlasmaNodeBase, PlasmaNodeSocketBase, PlasmaTreeOutputNodeBase from .node_core import PlasmaNodeBase, PlasmaNodeSocketBase, PlasmaTreeOutputNodeBase
from .. import idprops
class PlasmaSoftVolumeOutputNode(PlasmaTreeOutputNodeBase, bpy.types.Node): class PlasmaSoftVolumeOutputNode(PlasmaTreeOutputNodeBase, bpy.types.Node):
bl_category = "SV" bl_category = "SV"
@ -71,7 +72,7 @@ class PlasmaSoftVolumePropertiesNode(PlasmaNodeBase, bpy.types.Node):
softvolume.outsideStrength = self.outside_strength / 100 softvolume.outsideStrength = self.outside_strength / 100
class PlasmaSoftVolumeReferenceNode(PlasmaNodeBase, bpy.types.Node): class PlasmaSoftVolumeReferenceNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node):
bl_category = "SV" bl_category = "SV"
bl_idname = "PlasmaSoftVolumeReferenceNode" bl_idname = "PlasmaSoftVolumeReferenceNode"
bl_label = "Soft Region" bl_label = "Soft Region"
@ -84,19 +85,24 @@ class PlasmaSoftVolumeReferenceNode(PlasmaNodeBase, bpy.types.Node):
}), }),
]) ])
soft_object = StringProperty(name="Soft Volume", soft_volume = PointerProperty(name="Soft Volume",
description="Object whose Soft Volume modifier we should use") description="Object whose Soft Volume modifier we should use",
type=bpy.types.Object,
poll=idprops.poll_softvolume_objects)
def draw_buttons(self, context, layout): 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): def get_key(self, exporter, so):
softvol = bpy.data.objects.get(self.soft_object, None) if self.soft_volume is None:
if softvol is None: self.raise_error("Invalid SoftVolume object reference")
self.raise_error("Volume Object '{}' not found".format(self.soft_object))
# Don't use SO here because that's the tree owner's SO. This soft region will find or create # Don't use SO here because that's the tree owner's SO. This soft region will find or create
# its own SceneObject. Yay! # 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): class PlasmaSoftVolumeInvertNode(PlasmaNodeBase, bpy.types.Node):

2
korman/operators/op_lightmap.py

@ -32,8 +32,6 @@ class LightmapAutobakePreviewOperator(_LightingOperator, bpy.types.Operator):
bl_label = "Preview Lightmap" bl_label = "Preview Lightmap"
bl_options = {"INTERNAL"} bl_options = {"INTERNAL"}
light_group = StringProperty(name="Light Group")
def __init__(self): def __init__(self):
super().__init__() super().__init__()

5
korman/operators/op_sound.py

@ -36,19 +36,16 @@ class PlasmaSoundOpenOperator(SoundOperator, bpy.types.Operator):
def execute(self, context): def execute(self, context):
# Check to see if the sound exists... Because the sneakily introduced bpy.data.sounds.load # 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... # 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: for i in bpy.data.sounds:
if self.filepath == i.filepath: if self.filepath == i.filepath:
sound = i sound = i
break break
else: else:
sound = bpy.data.sounds.load(self.filepath) 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 # 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) dest = eval(self.data_path)
setattr(dest, self.sound_property, sound.name) setattr(dest, self.sound_property, sound)
return {"FINISHED"} return {"FINISHED"}
def invoke(self, context, event): def invoke(self, context, event):

15
korman/properties/modifiers/anim.py

@ -19,6 +19,7 @@ from PyHSPlasma import *
from .base import PlasmaModifierProperties from .base import PlasmaModifierProperties
from ...exporter import ExportError, utils from ...exporter import ExportError, utils
from ... import idprops
def _convert_frame_time(frame_num): def _convert_frame_time(frame_num):
fps = bpy.context.scene.render.fps fps = bpy.context.scene.render.fps
@ -92,9 +93,15 @@ class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties):
atcanim.loopEnd = atcanim.end atcanim.loopEnd = atcanim.end
class AnimGroupObject(bpy.types.PropertyGroup): class AnimGroupObject(idprops.IDPropObjectMixin, bpy.types.PropertyGroup):
object_name = StringProperty(name="Child Animation", child_anim = PointerProperty(name="Child Animation",
description="Object whose action is a 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): class PlasmaAnimationGroupModifier(ActionModifier, PlasmaModifierProperties):
@ -123,7 +130,7 @@ class PlasmaAnimationGroupModifier(ActionModifier, PlasmaModifierProperties):
agmaster.msgForwarder = msgfwd.key agmaster.msgForwarder = msgfwd.key
agmaster.isGrouped, agmaster.isGroupMaster = True, True agmaster.isGrouped, agmaster.isGroupMaster = True, True
for i in self.children: 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: if child_bo is None:
msg = "Animation Group '{}' specifies an invalid object '{}'. Ignoring..." msg = "Animation Group '{}' specifies an invalid object '{}'. Ignoring..."
exporter.report.warn(msg.format(self.key_name, i.object_name), ident=2) exporter.report.warn(msg.format(self.key_name, i.object_name), ident=2)

33
korman/properties/modifiers/avatar.py

@ -20,13 +20,14 @@ from PyHSPlasma import *
from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz
from ...exporter.explosions import ExportError from ...exporter.explosions import ExportError
from ...helpers import find_modifier from ...helpers import find_modifier
from ... import idprops
sitting_approach_flags = [("kApproachFront", "Front", "Approach from the font"), sitting_approach_flags = [("kApproachFront", "Front", "Approach from the font"),
("kApproachLeft", "Left", "Approach from the left"), ("kApproachLeft", "Left", "Approach from the left"),
("kApproachRight", "Right", "Approach from the right"), ("kApproachRight", "Right", "Approach from the right"),
("kApproachRear", "Rear", "Approach from the rear guard")] ("kApproachRear", "Rear", "Approach from the rear guard")]
class PlasmaSittingBehavior(PlasmaModifierProperties, PlasmaModifierLogicWiz): class PlasmaSittingBehavior(idprops.IDPropObjectMixin, PlasmaModifierProperties, PlasmaModifierLogicWiz):
pl_id = "sittingmod" pl_id = "sittingmod"
bl_category = "Avatar" bl_category = "Avatar"
@ -39,10 +40,14 @@ class PlasmaSittingBehavior(PlasmaModifierProperties, PlasmaModifierLogicWiz):
default={"kApproachFront", "kApproachLeft", "kApproachRight"}, default={"kApproachFront", "kApproachLeft", "kApproachRight"},
options={"ENUM_FLAG"}) options={"ENUM_FLAG"})
clickable_obj = StringProperty(name="Clickable", clickable_object = PointerProperty(name="Clickable",
description="Object that defines the clickable area") description="Object that defines the clickable area",
region_obj = StringProperty(name="Region", type=bpy.types.Object,
description="Object that defines the region mesh") 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", facing_enabled = BoolProperty(name="Avatar Facing",
description="The avatar must be facing the clickable's Y-axis", 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): def export(self, exporter, bo, so):
# The user absolutely MUST specify a clickable or this won't export worth crap. # 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 self.clickable_object is None:
if clickable_obj is None:
raise ExportError("'{}': Sitting Behavior's clickable object is invalid".format(self.key_name)) raise ExportError("'{}': Sitting Behavior's clickable object is invalid".format(self.key_name))
# Generate the logic nodes now # Generate the logic nodes now
@ -65,7 +69,7 @@ class PlasmaSittingBehavior(PlasmaModifierProperties, PlasmaModifierLogicWiz):
def harvest_actors(self): def harvest_actors(self):
if self.facing_enabled: if self.facing_enabled:
return (self.clickable_obj,) return (self.clickable_object.name,)
return () return ()
def logicwiz(self, bo): def logicwiz(self, bo):
@ -81,16 +85,16 @@ class PlasmaSittingBehavior(PlasmaModifierProperties, PlasmaModifierLogicWiz):
# Clickable # Clickable
clickable = nodes.new("PlasmaClickableNode") clickable = nodes.new("PlasmaClickableNode")
clickable.link_output(sittingmod, "satisfies", "condition") clickable.link_output(sittingmod, "satisfies", "condition")
clickable.clickable = self.clickable_obj clickable.clickable_object = self.clickable_object
clickable.bounds = find_modifier(self.clickable_obj, "collision").bounds clickable.bounds = find_modifier(self.clickable_object, "collision").bounds
# Avatar Region (optional) # 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: if region_phys is not None:
region = nodes.new("PlasmaClickableRegionNode") region = nodes.new("PlasmaClickableRegionNode")
region.link_output(clickable, "satisfies", "region") region.link_output(clickable, "satisfies", "region")
region.name = "ClickableAvRegion" region.name = "ClickableAvRegion"
region.region = self.region_obj region.region_object = self.region_object
region.bounds = region_phys.bounds region.bounds = region_phys.bounds
# Facing Target (optional) # Facing Target (optional)
@ -105,6 +109,11 @@ class PlasmaSittingBehavior(PlasmaModifierProperties, PlasmaModifierLogicWiz):
# facing target conditional for us. isn't that nice? # facing target conditional for us. isn't that nice?
clickable.find_input_socket("facing").allow_simple = False clickable.find_input_socket("facing").allow_simple = False
@classmethod
def _idprop_mapping(cls):
return {"clickable_object": "clickable_obj",
"region_object": "region_obj"}
@property @property
def key_name(self): def key_name(self):
return "{}_SitBeh".format(self.id_data.name) return "{}_SitBeh".format(self.id_data.name)

29
korman/properties/modifiers/logic.py

@ -19,29 +19,31 @@ from PyHSPlasma import *
from .base import PlasmaModifierProperties from .base import PlasmaModifierProperties
from ...exporter import ExportError from ...exporter import ExportError
from ... import idprops
game_versions = [("pvPrime", "Ages Beyond Myst (63.11)", "Targets the original Uru (Live) game"), 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"), ("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")] ("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") name = StringProperty(name="Name")
version = EnumProperty(name="Version", version = EnumProperty(name="Version",
description="Plasma versions this node tree exports under", description="Plasma versions this node tree exports under",
items=game_versions, items=game_versions,
options={"ENUM_FLAG"}, options={"ENUM_FLAG"},
default=set(list(zip(*game_versions))[0])) default=set(list(zip(*game_versions))[0]))
node_tree_name = StringProperty(name="Node Tree", node_tree = PointerProperty(name="Node Tree",
description="Node Tree to export") description="Node Tree to export",
type=bpy.types.NodeTree)
node_name = StringProperty(name="Node Ref", node_name = StringProperty(name="Node Ref",
description="Attach a reference to this node") description="Attach a reference to this node")
@property @classmethod
def node_tree(self): def _idprop_mapping(cls):
try: return {"node_tree": "node_tree_name"}
return bpy.data.node_groups[self.node_tree_name]
except KeyError: def _idprop_sources(self):
raise ExportError("Node Tree '{}' does not exist!".format(self.node_tree_name)) return {"node_tree_name": bpy.data.node_groups}
class PlasmaAdvancedLogic(PlasmaModifierProperties): class PlasmaAdvancedLogic(PlasmaModifierProperties):
@ -60,19 +62,22 @@ class PlasmaAdvancedLogic(PlasmaModifierProperties):
for i in self.logic_groups: for i in self.logic_groups:
our_versions = [globals()[j] for j in i.version] our_versions = [globals()[j] for j in i.version]
if version in our_versions: 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 # 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. # the entire node tree is exported once before the post_export step, however.
if i.node_name: 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) node = i.node_tree.nodes.get(i.node_name, None)
if node is 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 # 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 # 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 # be a no-op in that case. Multi modifiers should accept any SceneObject, however
node.get_key(exporter, so) node.get_key(exporter, so)
else: 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) i.node_tree.export(exporter, bo, so)
def harvest_actors(self): def harvest_actors(self):

41
korman/properties/modifiers/region.py

@ -19,6 +19,7 @@ from PyHSPlasma import *
from ...exporter import ExportError from ...exporter import ExportError
from ...helpers import TemporaryObject from ...helpers import TemporaryObject
from ... import idprops
from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz
from .physics import bounds_types from .physics import bounds_types
@ -88,7 +89,7 @@ class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierLogicWiz):
# Region Sensor # Region Sensor
volsens = nodes.new("PlasmaVolumeSensorNode") volsens = nodes.new("PlasmaVolumeSensorNode")
volsens.name = "RegionSensor" volsens.name = "RegionSensor"
volsens.region = bo.name volsens.region_object = bo
volsens.bounds = self.bounds volsens.bounds = self.bounds
volsens.find_input_socket("enter").allow = True volsens.find_input_socket("enter").allow = True
volsens.find_input_socket("exit").allow = True volsens.find_input_socket("exit").allow = True
@ -145,7 +146,7 @@ class PlasmaPanicLinkRegion(PlasmaModifierProperties):
return True return True
class PlasmaSoftVolume(PlasmaModifierProperties): class PlasmaSoftVolume(idprops.IDPropMixin, PlasmaModifierProperties):
pl_id = "softvolume" pl_id = "softvolume"
bl_category = "Region" bl_category = "Region"
@ -156,8 +157,9 @@ class PlasmaSoftVolume(PlasmaModifierProperties):
use_nodes = BoolProperty(name="Use Nodes", use_nodes = BoolProperty(name="Use Nodes",
description="Make this a node-based Soft Volume", description="Make this a node-based Soft Volume",
default=False) default=False)
node_tree_name = StringProperty(name="Node Tree", node_tree = PointerProperty(name="Node Tree",
description="Node Tree detailing soft volume logic") description="Node Tree detailing soft volume logic",
type=bpy.types.NodeTree)
# Basic # Basic
invert = BoolProperty(name="Invert", invert = BoolProperty(name="Invert",
@ -179,9 +181,10 @@ class PlasmaSoftVolume(PlasmaModifierProperties):
so = exporter.mgr.find_create_object(plSceneObject, bl=self.id_data) so = exporter.mgr.find_create_object(plSceneObject, bl=self.id_data)
if self.use_nodes: 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: 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) return output.get_key(exporter, so)
else: else:
pClass = plSoftVolumeInvert if self.invert else plSoftVolumeSimple pClass = plSoftVolumeInvert if self.invert else plSoftVolumeSimple
@ -221,13 +224,19 @@ class PlasmaSoftVolume(PlasmaModifierProperties):
sv.volume = isect sv.volume = isect
def _export_sv_nodes(self, exporter, bo, so): def _export_sv_nodes(self, exporter, bo, so):
if self.node_tree_name not in exporter.node_trees_exported: tree = self.get_node_tree()
exporter.node_trees_exported.add(self.node_tree_name) if tree.name not in exporter.node_trees_exported:
self.node_tree.export(exporter, bo, so) exporter.node_trees_exported.add(tree.name)
tree.export(exporter, bo, so)
@property
def node_tree(self): def get_node_tree(self):
tree = bpy.data.node_groups.get(self.node_tree_name, None) if self.node_tree is None:
if tree is None: raise ExportError("SoftVolume '{}' does not specify a valid Node Tree!".format(self.key_name))
raise ExportError("SoftVolume '{}': Node Tree '{}' does not exist!".format(self.key_name, self.node_tree_name)) return self.node_tree
return tree
@classmethod
def _idprop_mapping(cls):
return {"node_tree": "node_tree_name"}
def _idprop_sources(self):
return {"node_tree_name": bpy.data.node_groups}

94
korman/properties/modifiers/render.py

@ -22,6 +22,7 @@ from .base import PlasmaModifierProperties
from ...exporter.etlight import _NUM_RENDER_LAYERS from ...exporter.etlight import _NUM_RENDER_LAYERS
from ...exporter import utils from ...exporter import utils
from ...exporter.explosions import ExportError from ...exporter.explosions import ExportError
from ... import idprops
class PlasmaFadeMod(PlasmaModifierProperties): class PlasmaFadeMod(PlasmaModifierProperties):
@ -81,7 +82,7 @@ class PlasmaFadeMod(PlasmaModifierProperties):
mod.farTrans = self.far_trans mod.farTrans = self.far_trans
class PlasmaFollowMod(PlasmaModifierProperties): class PlasmaFollowMod(idprops.IDPropObjectMixin, PlasmaModifierProperties):
pl_id = "followmod" pl_id = "followmod"
bl_category = "Render" bl_category = "Render"
@ -108,8 +109,9 @@ class PlasmaFollowMod(PlasmaModifierProperties):
("kFollowObject", "Object", "Follow an object"), ("kFollowObject", "Object", "Follow an object"),
]) ])
leader_object = StringProperty(name="Leader Object", leader = PointerProperty(name="Leader Object",
description="Object to follow") description="Object to follow",
type=bpy.types.Object)
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
fm = exporter.mgr.find_create_object(plFollowMod, so=so, name=self.key_name) 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 self.leader_type == "kFollowObject":
# If this object is following another object, make sure that the # If this object is following another object, make sure that the
# leader has been selected and is a valid SO. # leader has been selected and is a valid SO.
if self.leader_object: if self.leader:
leader_obj = bpy.data.objects.get(self.leader_object, None) fm.leader = exporter.mgr.find_create_key(plSceneObject, bl=self.leader)
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)
else: else:
raise ExportError("'{}': Follow's leader object must be selected".format(self.key_name)) raise ExportError("'{}': Follow's leader object must be selected".format(self.key_name))
@classmethod
def _idprop_mapping(cls):
return {"leader": "leader_object"}
@property @property
def requires_actor(self): def requires_actor(self):
return True return True
class PlasmaLightMapGen(PlasmaModifierProperties): class PlasmaLightMapGen(idprops.IDPropMixin, PlasmaModifierProperties):
pl_id = "lightmap" pl_id = "lightmap"
bl_category = "Render" bl_category = "Render"
@ -159,8 +161,9 @@ class PlasmaLightMapGen(PlasmaModifierProperties):
size=_NUM_RENDER_LAYERS, size=_NUM_RENDER_LAYERS,
default=((True,) * _NUM_RENDER_LAYERS)) default=((True,) * _NUM_RENDER_LAYERS))
light_group = StringProperty(name="Light Group", lights = PointerProperty(name="Light Group",
description="Group that defines the collection of lights to bake") description="Group that defines the collection of lights to bake",
type=bpy.types.Group)
uv_map = StringProperty(name="UV Texture", uv_map = StringProperty(name="UV Texture",
description="UV Texture used as the basis for the lightmap") description="UV Texture used as the basis for the lightmap")
@ -208,6 +211,13 @@ class PlasmaLightMapGen(PlasmaModifierProperties):
# Mmm... cheating # Mmm... cheating
mat_mgr.export_prepared_layer(layer, lightmap_im) 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 @property
def key_name(self): def key_name(self):
return "{}_LIGHTMAPGEN".format(self.id_data.name) return "{}_LIGHTMAPGEN".format(self.id_data.name)
@ -317,7 +327,7 @@ class PlasmaShadowCasterMod(PlasmaModifierProperties):
caster.castFlags |= plShadowCaster.kSelfShadow caster.castFlags |= plShadowCaster.kSelfShadow
class PlasmaViewFaceMod(PlasmaModifierProperties): class PlasmaViewFaceMod(idprops.IDPropObjectMixin, PlasmaModifierProperties):
pl_id = "viewfacemod" pl_id = "viewfacemod"
bl_category = "Render" bl_category = "Render"
@ -340,8 +350,9 @@ class PlasmaViewFaceMod(PlasmaModifierProperties):
("kFacePlay", "Player", "Face the local player"), ("kFacePlay", "Player", "Face the local player"),
("kFaceObj", "Object", "Face an object"), ("kFaceObj", "Object", "Face an object"),
]) ])
target_object = StringProperty(name="Target Object", target = PointerProperty(name="Target Object",
description="Object to face") description="Object to face",
type=bpy.types.Object)
pivot_on_y = BoolProperty(name="Pivot on local Y", pivot_on_y = BoolProperty(name="Pivot on local Y",
description="Swivel only around the local Y axis", description="Swivel only around the local Y axis",
@ -376,12 +387,8 @@ class PlasmaViewFaceMod(PlasmaModifierProperties):
if self.follow_mode == "kFaceObj": if self.follow_mode == "kFaceObj":
# If this swivel is following an object, make sure that the # If this swivel is following an object, make sure that the
# target has been selected and is a valid SO. # target has been selected and is a valid SO.
if self.target_object: if self.target:
target_obj = bpy.data.objects.get(self.target_object, None) vfm.faceObj = exporter.mgr.find_create_key(plSceneObject, bl=self.target)
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)
else: else:
raise ExportError("'{}': Swivel's target object must be selected".format(self.key_name)) raise ExportError("'{}': Swivel's target object must be selected".format(self.key_name))
@ -395,12 +402,16 @@ class PlasmaViewFaceMod(PlasmaModifierProperties):
if self.offset_local: if self.offset_local:
vfm.setFlag(plViewFaceModifier.kOffsetLocal, True) vfm.setFlag(plViewFaceModifier.kOffsetLocal, True)
@classmethod
def _idprop_mapping(cls):
return {"target": "target_object"}
@property @property
def requires_actor(self): def requires_actor(self):
return True return True
class PlasmaVisControl(PlasmaModifierProperties): class PlasmaVisControl(idprops.IDPropObjectMixin, PlasmaModifierProperties):
pl_id = "visregion" pl_id = "visregion"
bl_category = "Render" bl_category = "Render"
@ -412,8 +423,10 @@ class PlasmaVisControl(PlasmaModifierProperties):
items=[("normal", "Normal", "Objects are only visible when the camera is inside this region"), 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"), ("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")]) ("fx", "Special FX", "This is a list of objects used for special effects only")])
softvolume = StringProperty(name="Region", soft_region = PointerProperty(name="Region",
description="Object defining the SoftVolume for this VisRegion") description="Object defining the SoftVolume for this VisRegion",
type=bpy.types.Object,
poll=idprops.poll_softvolume_objects)
replace_normal = BoolProperty(name="Hide Drawables", replace_normal = BoolProperty(name="Hide Drawables",
description="Hides drawables attached to this region", description="Hides drawables attached to this region",
default=True) default=True)
@ -430,21 +443,31 @@ class PlasmaVisControl(PlasmaModifierProperties):
exporter.report.msg("[VisRegion] I'm a SoftVolume myself :)", indent=1) exporter.report.msg("[VisRegion] I'm a SoftVolume myself :)", indent=1)
rgn.region = this_sv.get_key(exporter, so) rgn.region = this_sv.get_key(exporter, so)
else: else:
exporter.report.msg("[VisRegion] SoftVolume '{}'", self.softvolume, indent=1) if not self.soft_region:
sv_bo = bpy.data.objects.get(self.softvolume, None) raise ExportError("'{}': Visibility Control must have a Soft Volume selected".format(self.key_name))
if sv_bo is None: sv_bo = self.soft_region
raise ExportError("'{}': Invalid object '{}' for VisControl soft volume".format(bo.name, self.softvolume))
sv = sv_bo.plasma_modifiers.softvolume sv = sv_bo.plasma_modifiers.softvolume
exporter.report.msg("[VisRegion] SoftVolume '{}'", sv_bo.name, indent=1)
if not sv.enabled: 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.region = sv.get_key(exporter)
rgn.setProperty(plVisRegion.kIsNot, self.mode == "exclude") 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) enabled = BoolProperty(default=True)
region_name = StringProperty(name="Control", control_region = PointerProperty(name="Control",
description="Object defining a Plasma Visibility 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): class PlasmaVisibilitySet(PlasmaModifierProperties):
@ -474,7 +497,6 @@ class PlasmaVisibilitySet(PlasmaModifierProperties):
for region in self.regions: for region in self.regions:
if not region.enabled: if not region.enabled:
continue continue
rgn_bo = bpy.data.objects.get(region.region_name, None) if not region.control_region:
if rgn_bo is None: raise ExportError("{}: Not all Visibility Controls are set up properly in Visibility Set".format(bo.name))
raise ExportError("{}: Invalid VisControl '{}' in VisSet modifier".format(bo.name, region.region_name)) addRegion(exporter.mgr.find_create_key(plVisRegion, bl=region.control_region))
addRegion(exporter.mgr.find_create_key(plVisRegion, bl=rgn_bo))

104
korman/properties/modifiers/sound.py

@ -23,6 +23,7 @@ from PyHSPlasma import *
from ... import korlib from ... import korlib
from .base import PlasmaModifierProperties from .base import PlasmaModifierProperties
from ...exporter import ExportError from ...exporter import ExportError
from ... import idprops
class PlasmaSfxFade(bpy.types.PropertyGroup): class PlasmaSfxFade(bpy.types.PropertyGroup):
fade_type = EnumProperty(name="Type", fade_type = EnumProperty(name="Type",
@ -38,9 +39,17 @@ class PlasmaSfxFade(bpy.types.PropertyGroup):
options=set(), subtype="TIME", unit="TIME") options=set(), subtype="TIME", unit="TIME")
class PlasmaSound(bpy.types.PropertyGroup): class PlasmaSound(idprops.IDPropMixin, bpy.types.PropertyGroup):
def _sound_picked(self, context): def _get_name_proxy(self):
if not self.sound_data: 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]" self.name = "[Empty]"
return return
@ -54,24 +63,33 @@ class PlasmaSound(bpy.types.PropertyGroup):
else: else:
self.is_valid = True self.is_valid = True
self.is_stereo = header.numChannels == 2 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"}: 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: else:
self.name = self.sound_data self.name = self._sound_name
enabled = BoolProperty(name="Enabled", default=True, options=set()) enabled = BoolProperty(name="Enabled", default=True, options=set())
sound_data = StringProperty(name="Sound", description="Sound Datablock", sound = PointerProperty(name="Sound",
options=set(), update=_sound_picked) 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_stereo = BoolProperty(default=True, options={"HIDDEN"})
is_valid = BoolProperty(default=False, 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", 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", sfx_type = EnumProperty(name="Category",
description="Describes the purpose of this sound", 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): def _convert_sound(self, exporter, so, pClass, wavHeader, dataSize, channel=None):
if channel is None: if channel is None:
name = "Sfx-{}_{}".format(so.key.name, self.sound_data) name = "Sfx-{}_{}".format(so.key.name, self._sound_name)
else: 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) exporter.report.msg("[{}] {}", pClass.__name__[2:], name, indent=1)
sound = exporter.mgr.find_create_object(pClass, so=so, name=name) 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 sv_mod, sv_key = self.id_data.plasma_modifiers.softvolume, None
if sv_mod.enabled: if sv_mod.enabled:
sv_key = sv_mod.get_key(exporter, so) sv_key = sv_mod.get_key(exporter, so)
elif self.soft_region: elif self.sfx_region:
sv_bo = bpy.data.objects.get(self.soft_region, None) sv_mod = self.sfx_region.plasma_modifiers.softvolume
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
if not sv_mod.enabled: 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) sv_key = sv_mod.get_key(exporter)
if sv_key is not None: if sv_key is not None:
sv_key.object.listenState |= plSoftVolume.kListenCheck | plSoftVolume.kListenDirty | plSoftVolume.kListenRegistered sv_key.object.listenState |= plSoftVolume.kListenCheck | plSoftVolume.kListenDirty | plSoftVolume.kListenRegistered
@ -305,21 +320,36 @@ class PlasmaSound(bpy.types.PropertyGroup):
key = sound.key key = sound.key
return 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 @property
def is_3d_stereo(self): def is_3d_stereo(self):
return self.sfx_type == "kSoundFX" and self.channel == {"L", "R"} and self.is_stereo return self.sfx_type == "kSoundFX" and self.channel == {"L", "R"} and self.is_stereo
def _raise_error(self, msg): 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 @property
def _sound(self): def _sound(self):
try: if not self.sound:
sound = bpy.data.sounds.get(self.sound_data) self._raise_error("has an invalid sound specified")
except: return self.sound
self._raise_error("is not loaded")
else: @property
return sound def _sound_name(self):
if self.sound:
return self.sound.name
return ""
class PlasmaSoundEmitter(PlasmaModifierProperties): class PlasmaSoundEmitter(PlasmaModifierProperties):
@ -341,7 +371,7 @@ class PlasmaSoundEmitter(PlasmaModifierProperties):
# Pass this off to each individual sound for conversion # Pass this off to each individual sound for conversion
for i in self.sounds: for i in self.sounds:
if i.sound_data and i.enabled: if i.enabled:
i.convert_sound(exporter, so, winaud) i.convert_sound(exporter, so, winaud)
def get_sound_indices(self, name=None, sound=None): def get_sound_indices(self, name=None, sound=None):
@ -374,26 +404,6 @@ class PlasmaSoundEmitter(PlasmaModifierProperties):
else: else:
raise ValueError(name) raise ValueError(name)
@classmethod
def register(cls):
bpy.types.Sound.plasma_owned = BoolProperty(default=False, options={"HIDDEN"})
@property @property
def requires_actor(self): def requires_actor(self):
return True 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)

107
korman/properties/modifiers/water.py

@ -20,8 +20,9 @@ from PyHSPlasma import *
from .base import PlasmaModifierProperties from .base import PlasmaModifierProperties
from ...exporter import ExportError, ExportAssertionError 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" pl_id = "swimregion"
bl_category = "Water" bl_category = "Water"
@ -35,9 +36,10 @@ class PlasmaSwimRegion(PlasmaModifierProperties, bpy.types.PropertyGroup):
"STRAIGHT": plSwimStraightCurrentRegion, "STRAIGHT": plSwimStraightCurrentRegion,
} }
region_name = StringProperty(name="Region", region = PointerProperty(name="Region",
description="Swimming detector region", description="Swimming detector region",
options=set()) type=bpy.types.Object,
poll=idprops.poll_mesh_objects)
down_buoyancy = FloatProperty(name="Downward Buoyancy", down_buoyancy = FloatProperty(name="Downward Buoyancy",
description="Distance the avatar sinks into the water", 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", description="Current velocity far from the region center",
min=-100.0, max=100.0, default=0.0, min=-100.0, max=100.0, default=0.0,
options=set()) options=set())
current_object = StringProperty(name="Current Object", current = PointerProperty(name="Current Object",
description="Object whose Y-axis defines the direction of the current", description="Object whose Y-axis defines the direction of the current",
options=set()) type=bpy.types.Object,
poll=idprops.poll_empty_objects)
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
swimIface = self.get_key(exporter, so).object swimIface = self.get_key(exporter, so).object
@ -99,12 +102,9 @@ class PlasmaSwimRegion(PlasmaModifierProperties, bpy.types.PropertyGroup):
swimIface.nearVel = self.near_velocity swimIface.nearVel = self.near_velocity
swimIface.farVel = self.far_velocity swimIface.farVel = self.far_velocity
if isinstance(swimIface, (plSwimCircularCurrentRegion, plSwimStraightCurrentRegion)): 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)) raise ExportError("Swimming Surface '{}' does not specify a current object".format(bo.name))
current_bo = bpy.data.objects.get(self.current_object, None) swimIface.currentObj = exporter.mgr.find_create_key(plSceneObject, bl=self.current)
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)
# The surface needs bounds for LOS -- this is generally a flat plane, or I would think... # 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 # 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 physical.LOSDBs |= plSimDefs.kLOSDBSwimRegion
# Detector region bounds # Detector region bounds
if self.region_name: if self.region is not None:
region_bo = bpy.data.objects.get(self.region_name, None) region_so = exporter.mgr.find_create_object(plSceneObject, bl=self.region)
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)
# Good news: if this phys has already been exported, this is basically a noop # Good news: if this phys has already been exported, this is basically a noop
det_name = "{}_SwimDetector".format(self.region_name) det_name = "{}_SwimDetector".format(self.region.name)
bounds = region_bo.plasma_modifiers.collision.bounds bounds = self.region.plasma_modifiers.collision.bounds
simIface, physical = exporter.physics.generate_physical(region_bo, region_so, bounds, det_name) simIface, physical = exporter.physics.generate_physical(self.region, region_so, bounds, det_name)
physical.memberGroup = plSimDefs.kGroupDetector physical.memberGroup = plSimDefs.kGroupDetector
physical.reportGroup |= 1 << plSimDefs.kGroupAvatar 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) return exporter.mgr.find_create_key(pClass, bl=self.id_data, so=so)
def harvest_actors(self): def harvest_actors(self):
if self.current_type != "NONE" and self.current_object: if self.current_type != "NONE" and self.current:
return set((self.current_object,)) return set((self.current.name,))
return set() 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" pl_id = "water_basic"
bl_category = "Water" bl_category = "Water"
bl_label = "Basic Water" bl_label = "Basic Water"
bl_description = "Basic water properties" bl_description = "Basic water properties"
wind_object_name = StringProperty(name="Wind Object", wind_object = PointerProperty(name="Wind Object",
description="Object whose Y axis represents the wind direction") description="Object whose Y axis represents the wind direction",
type=bpy.types.Object,
poll=idprops.poll_empty_objects)
wind_speed = FloatProperty(name="Wind Speed", wind_speed = FloatProperty(name="Wind Speed",
description="Magnitude of the wind", description="Magnitude of the wind",
default=1.0) default=1.0)
envmap_name = StringProperty(name="EnvMap", envmap = PointerProperty(name="EnvMap",
description="Texture defining an environment map for this water object") 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", envmap_radius = FloatProperty(name="Environment Sphere Radius",
description="How far away the first object you want to see is", description="How far away the first object you want to see is",
min=5.0, max=10000.0, min=5.0, max=10000.0,
@ -232,12 +238,9 @@ class PlasmaWaterModifier(PlasmaModifierProperties, bpy.types.PropertyGroup):
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
waveset = exporter.mgr.find_create_object(plWaveSet7, name=bo.name, so=so) waveset = exporter.mgr.find_create_object(plWaveSet7, name=bo.name, so=so)
if self.wind_object_name: if self.wind_object:
wind_obj = bpy.data.objects.get(self.wind_object_name, None) if self.wind_object.plasma_object.enabled and self.wind_object.plasma_modifiers.animation.enabled:
if wind_obj is None: waveset.refObj = exporter.mgr.find_create_key(plSceneObject, bl=self.wind_object)
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)
waveset.setFlag(plWaveSet7.kHasRefObject, True) waveset.setFlag(plWaveSet7.kHasRefObject, True)
# This is much like what happened in PyPRP # 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) state.depthFalloff = hsVector3(self.depth_opacity, self.depth_reflection, self.depth_wave)
# Environment Map # Environment Map
if self.envmap_name: if self.envmap:
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))
# maybe, just maybe, we're absuing our privledges? # 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 waveset.envMap = dem.key
state.envCenter = dem.position state.envCenter = dem.position
state.envRefresh = dem.refreshRate state.envRefresh = dem.refreshRate
@ -293,15 +290,30 @@ class PlasmaWaterModifier(PlasmaModifierProperties, bpy.types.PropertyGroup):
if not mods.water_shore.enabled: if not mods.water_shore.enabled:
mods.water_shore.convert_default(state) 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 @property
def key_name(self): def key_name(self):
return "{}_WaveSet7".format(self.id_data.name) 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") display_name = StringProperty(name="Display Name")
object_name = StringProperty(name="Shore Object", shore_object = PointerProperty(name="Shore Object",
description="Object that waves crash upon") 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): class PlasmaWaterShoreModifier(PlasmaModifierProperties):
@ -365,10 +377,9 @@ class PlasmaWaterShoreModifier(PlasmaModifierProperties):
wavestate = waveset.state wavestate = waveset.state
for i in self.shores: for i in self.shores:
shore = bpy.data.objects.get(i.object_name, None) if i.shore_object is None:
if shore is None: raise ExportError("'{}': Shore Object for '{}' is invalid".format(self.key_name, i.display_name))
raise ExportError("'{}': Shore Object '{}' does not exist".format(self.key_name, i.object_name)) waveset.addShore(exporter.mgr.find_create_key(plSceneObject, bl=i.shore_object))
waveset.addShore(exporter.mgr.find_create_key(plSceneObject, bl=shore))
wavestate.wispiness = self.wispiness / 100.0 wavestate.wispiness = self.wispiness / 100.0
wavestate.minColor = hsColorRGBA(*self.shore_tint, alpha=(self.shore_opacity / 100.0)) wavestate.minColor = hsColorRGBA(*self.shore_tint, alpha=(self.shore_opacity / 100.0))

15
korman/properties/prop_lamp.py

@ -16,7 +16,9 @@
import bpy import bpy
from bpy.props import * 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", affect_characters = BoolProperty(name="Affect Avatars",
description="This lamp affects avatars", description="This lamp affects avatars",
options=set(), options=set(),
@ -43,9 +45,10 @@ class PlasmaLamp(bpy.types.PropertyGroup):
default=False, default=False,
options=set()) options=set())
soft_region = StringProperty(name="Soft Volume", lamp_region = PointerProperty(name="Soft Volume",
description="Soft region this light is active inside", description="Soft region this light is active inside",
options=set()) type=bpy.types.Object,
poll=idprops.poll_softvolume_objects)
# For LimitedDirLights # For LimitedDirLights
size_height = FloatProperty(name="Height", size_height = FloatProperty(name="Height",
@ -55,3 +58,7 @@ class PlasmaLamp(bpy.types.PropertyGroup):
def has_light_group(self, bo): def has_light_group(self, bo):
return bool(bo.users_group) return bool(bo.users_group)
@classmethod
def _idprop_mapping(cls):
return {"lamp_region": "soft_region"}

14
korman/properties/prop_texture.py

@ -16,10 +16,18 @@
import bpy import bpy
from bpy.props import * from bpy.props import *
class EnvMapVisRegion(bpy.types.PropertyGroup): from .. import idprops
class EnvMapVisRegion(idprops.IDPropObjectMixin, bpy.types.PropertyGroup):
enabled = BoolProperty(default=True) enabled = BoolProperty(default=True)
region_name = StringProperty(name="Control", control_region = PointerProperty(name="Control",
description="Object defining a Plasma Visibility 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): class PlasmaLayer(bpy.types.PropertyGroup):

6
korman/ui/modifiers/anim.py

@ -45,8 +45,8 @@ def animation(modifier, layout, context):
class GroupListUI(bpy.types.UIList): class GroupListUI(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): 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]" label = item.child_anim.name if item.child_anim is not None else "[No Child Specified]"
icon = "ACTION" if item.object_name else "ERROR" icon = "ACTION" if item.child_anim is not None else "ERROR"
layout.label(text=label, icon=icon) layout.label(text=label, icon=icon)
@ -68,7 +68,7 @@ def animation_group(modifier, layout, context):
op.index = modifier.active_child_index op.index = modifier.active_child_index
if modifier.children: 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): class LoopListUI(bpy.types.UIList):

8
korman/ui/modifiers/avatar.py

@ -21,14 +21,14 @@ def sittingmod(modifier, layout, context):
layout.row().prop(modifier, "approach") layout.row().prop(modifier, "approach")
col = layout.column() col = layout.column()
col.prop_search(modifier, "clickable_obj", bpy.data, "objects", icon="MESH_DATA") col.prop(modifier, "clickable_object", icon="MESH_DATA")
clickable = find_modifier(modifier.clickable_obj, "collision") clickable = find_modifier(modifier.clickable_object, "collision")
if clickable is not None: if clickable is not None:
col.prop(clickable, "bounds") col.prop(clickable, "bounds")
col = layout.column() col = layout.column()
col.prop_search(modifier, "region_obj", bpy.data, "objects", icon="MESH_DATA") col.prop(modifier, "region_object", icon="MESH_DATA")
region = find_modifier(modifier.region_obj, "collision") region = find_modifier(modifier.region_object, "collision")
if region is not None: if region is not None:
col.prop(region, "bounds") col.prop(region, "bounds")

2
korman/ui/modifiers/logic.py

@ -39,7 +39,7 @@ def advanced_logic(modifier, layout, context):
if modifier.logic_groups: if modifier.logic_groups:
logic = modifier.logic_groups[modifier.active_group_index] logic = modifier.logic_groups[modifier.active_group_index]
layout.row().prop_menu_enum(logic, "version") 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: try:
layout.prop_search(logic, "node_name", logic.node_tree, "nodes", icon="NODE") layout.prop_search(logic, "node_name", logic.node_tree, "nodes", icon="NODE")
except: except:

2
korman/ui/modifiers/region.py

@ -28,7 +28,7 @@ def softvolume(modifier, layout, context):
row = layout.row() row = layout.row()
row.prop(modifier, "use_nodes", text="", icon="NODETREE") row.prop(modifier, "use_nodes", text="", icon="NODETREE")
if modifier.use_nodes: if modifier.use_nodes:
row.prop_search(modifier, "node_tree_name", bpy.data, "node_groups") row.prop(modifier, "node_tree")
else: else:
row.label("Simple Soft Volume") row.label("Simple Soft Volume")

18
korman/ui/modifiers/render.py

@ -43,7 +43,7 @@ def followmod(modifier, layout, context):
layout.row().prop(modifier, "follow_mode", expand=True) layout.row().prop(modifier, "follow_mode", expand=True)
layout.prop(modifier, "leader_type") layout.prop(modifier, "leader_type")
if modifier.leader_type == "kFollowObject": 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): def lighting(modifier, layout, context):
split = layout.split() split = layout.split()
@ -84,11 +84,10 @@ def lighting(modifier, layout, context):
def lightmap(modifier, layout, context): def lightmap(modifier, layout, context):
layout.row(align=True).prop(modifier, "quality", expand=True) layout.row(align=True).prop(modifier, "quality", expand=True)
layout.prop(modifier, "render_layers", text="Active Render Layers") 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") 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 = 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... # 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 # 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": if modifier.preset_options == "Custom":
layout.row().prop(modifier, "follow_mode") layout.row().prop(modifier, "follow_mode")
if modifier.follow_mode == "kFaceObj": 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.separator()
layout.prop(modifier, "pivot_on_y") layout.prop(modifier, "pivot_on_y")
@ -136,9 +135,10 @@ def viewfacemod(modifier, layout, context):
class VisRegionListUI(bpy.types.UIList): class VisRegionListUI(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): 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" if item.control_region is None:
label = item.region_name if item.region_name else "[No Object Specified]" layout.label("[No Object Specified]", icon="ERROR")
layout.label(label, icon=myIcon) else:
layout.label(item.control_region.name, icon="OBJECT_DATA")
layout.prop(item, "enabled", text="") layout.prop(item, "enabled", text="")
@ -156,7 +156,7 @@ def visibility(modifier, layout, context):
op.index = modifier.active_region_index op.index = modifier.active_region_index
if modifier.regions: 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): def visregion(modifier, layout, context):
layout.prop(modifier, "mode") 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 # 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 sv = modifier.id_data.plasma_modifiers.softvolume
if modifier.mode != "fx" and not sv.enabled: if modifier.mode != "fx" and not sv.enabled:
layout.prop_search(modifier, "softvolume", bpy.data, "objects") layout.prop(modifier, "soft_region")
# Other settings # Other settings
layout.prop(modifier, "replace_normal") layout.prop(modifier, "replace_normal")

14
korman/ui/modifiers/sound.py

@ -22,10 +22,8 @@ def _draw_fade_ui(modifier, layout, label):
class SoundListUI(bpy.types.UIList): class SoundListUI(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0):
if item.sound_data: if item.sound:
sound = bpy.data.sounds.get(item.sound_data) layout.prop(item, "name", emboss=False, icon="SOUND", text="")
icon = "SOUND" if sound is not None else "ERROR"
layout.prop(item, "name", emboss=False, icon=icon, text="")
layout.prop(item, "enabled", text="") layout.prop(item, "enabled", text="")
else: else:
layout.label("[Empty]") layout.label("[Empty]")
@ -51,13 +49,13 @@ def soundemit(modifier, layout, context):
else: else:
# Sound datablock picker # Sound datablock picker
row = layout.row(align=True) 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 = row.operator("sound.plasma_open", icon="FILESEL", text="")
open_op.data_path = repr(sound) open_op.data_path = repr(sound)
open_op.sound_property = "sound_data" open_op.sound_property = "sound_data"
# Pack/Unpack # Pack/Unpack
data = bpy.data.sounds.get(sound.sound_data) data = sound.sound
if data is not None: if data is not None:
if data.packed_file is None: if data.packed_file is None:
row.operator("sound.plasma_pack", icon="UGLYPACKAGE", text="") 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="") 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 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") layout.label(text="Invalid sound specified", icon="ERROR")
# Core Props # Core Props
@ -102,4 +100,4 @@ def soundemit(modifier, layout, context):
if not sv.enabled: if not sv.enabled:
col.separator() col.separator()
col.label("Soft Region:") col.label("Soft Region:")
col.prop_search(sound, "soft_region", bpy.data, "objects", text="") col.prop(sound, "sfx_region", text="")

12
korman/ui/modifiers/water.py

@ -19,9 +19,9 @@ def swimregion(modifier, layout, context):
split = layout.split() split = layout.split()
col = split.column() col = split.column()
col.label("Detector Region:") 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 = split.column()
col.enabled = region_bo is not None col.enabled = region_bo is not None
bounds_src = region_bo if region_bo is not None else modifier.id_data 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, "near_velocity", text="Near")
col.prop(modifier, "far_velocity", text="Far") 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): def water_basic(modifier, layout, context):
layout.prop_search(modifier, "wind_object_name", bpy.data, "objects") layout.prop(modifier, "wind_object")
layout.prop_search(modifier, "envmap_name", bpy.data, "textures") layout.prop(modifier, "envmap")
row = layout.row() row = layout.row()
row.prop(modifier, "wind_speed") row.prop(modifier, "wind_speed")
@ -128,7 +128,7 @@ def water_shore(modifier, layout, context):
# Display the active shore # Display the active shore
if modifier.shores: if modifier.shores:
shore = modifier.shores[modifier.active_shore_index] 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() split = layout.split()
col = split.column() col = split.column()

2
korman/ui/ui_lamp.py

@ -53,7 +53,7 @@ class PlasmaLampPanel(LampButtonsPanel, bpy.types.Panel):
if not context.object.plasma_modifiers.softvolume.enabled: if not context.object.plasma_modifiers.softvolume.enabled:
layout.separator() layout.separator()
layout.prop_search(rtlamp, "soft_region", bpy.data, "objects") layout.prop(rtlamp, "lamp_region")
def _draw_area_lamp(self, context): def _draw_area_lamp(self, context):

2
korman/ui/ui_texture.py

@ -52,7 +52,7 @@ class PlasmaEnvMapPanel(TextureButtonsPanel, bpy.types.Panel):
op.index = layer_props.active_region_index op.index = layer_props.active_region_index
rgns = layer_props.vis_regions rgns = layer_props.vis_regions
if 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.separator()
layout.prop(layer_props, "envmap_color") layout.prop(layer_props, "envmap_color")

Loading…
Cancel
Save