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. 67
      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. 103
      korman/properties/modifiers/water.py
  21. 13
      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 = {
"name": "Korman",
"author": "Guild of Writers",
"blender": (2, 78, 0),
"blender": (2, 79, 0),
"location": "File > Import-Export",
"description": "Exporter for Cyan Worlds' Plasma Engine",
"warning": "beta",

7
korman/exporter/etlight.py

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

25
korman/exporter/material.py

@ -357,8 +357,9 @@ class MaterialConverter:
return None
fcurves = []
texture = tex_slot.texture
mat_action = harvest_fcurves(bm, fcurves, "texture_slots[{}]".format(idx))
tex_action = harvest_fcurves(tex_slot.texture, fcurves)
tex_action = harvest_fcurves(texture, fcurves)
if not fcurves:
return base_layer
@ -370,7 +371,7 @@ class MaterialConverter:
if ctrl is not None:
if layer_animation is None:
name = "{}_LayerAnim".format(base_layer.key.name)
layer_animation = self.get_texture_animation_key(bo, bm, tex_slot=tex_slot).object
layer_animation = self.get_texture_animation_key(bo, bm, texture).object
setattr(layer_animation, attr, ctrl)
# Alrighty, if we exported any controllers, layer_animation is a plLayerAnimation. We need to do
@ -476,11 +477,11 @@ class MaterialConverter:
# Whoever wrote this PyHSPlasma binding didn't follow the convention. Sigh.
visregions = []
for region in texture.plasma_layer.vis_regions:
rgn = bpy.data.objects.get(region.region_name, None)
rgn = region.control_region
if rgn is None:
raise ExportError("'{}': VisControl '{}' not found".format(texture.name, region.region_name))
raise ExportError("'{}': Has an invalid Visibility Control".format(texture.name))
if not rgn.plasma_modifiers.visregion.enabled:
raise ExportError("'{}': '{}' is not a VisControl".format(texture.name, region.region_name))
raise ExportError("'{}': '{}' is not a VisControl".format(texture.name, rgn.name))
visregions.append(self._mgr.find_create_key(plVisRegion, bl=rgn))
pl_env.visRegions = visregions
@ -695,19 +696,15 @@ class MaterialConverter:
def get_bump_layer(self, bo):
return self._bump_mats.get(bo, None)
def get_texture_animation_key(self, bo, bm, tex_name=None, tex_slot=None):
def get_texture_animation_key(self, bo, bm, texture):
"""Finds or creates the appropriate key for sending messages to an animated Texture"""
assert tex_name or tex_slot
if tex_slot is None:
tex_slot = bm.texture_slots.get(tex_name, None)
if tex_slot is None:
raise ExportError("Material '{}' does not contain Texture '{}'".format(bm.name, tex_name))
if tex_name is None:
tex_name = tex_slot.name
tex_name = texture.name
if not tex_name in bm.texture_slots:
raise ExportError("Texture '{}' not used in Material '{}'".format(bm.name, tex_name))
name = "{}_{}_LayerAnim".format(bm.name, tex_name)
layer = tex_slot.texture.plasma_layer
layer = texture.plasma_layer
pClass = plLayerSDLAnimation if layer.anim_sdl_var else plLayerAnimation
return self._mgr.find_create_key(pClass, bl=bo, name=name)

8
korman/exporter/rtlight.py

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

6
korman/helpers.py

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

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

67
korman/nodes/node_logic.py

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

156
korman/nodes/node_messages.py

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

92
korman/nodes/node_python.py

@ -19,6 +19,7 @@ from pathlib import Path
from PyHSPlasma import *
from .node_core import *
from .. import idprops
_single_user_attribs = {
"ptAttribBoolean", "ptAttribInt", "ptAttribFloat", "ptAttribString", "ptAttribDropDownList",
@ -419,7 +420,7 @@ class PlasmaAttribNumericNode(PlasmaAttribNodeBase, bpy.types.Node):
return self.value_float
class PlasmaAttribObjectNode(PlasmaAttribNodeBase, bpy.types.Node):
class PlasmaAttribObjectNode(idprops.IDPropObjectMixin, PlasmaAttribNodeBase, bpy.types.Node):
bl_category = "PYTHON"
bl_idname = "PlasmaAttribObjectNode"
bl_label = "Object Attribute"
@ -427,8 +428,9 @@ class PlasmaAttribObjectNode(PlasmaAttribNodeBase, bpy.types.Node):
pl_attrib = ("ptAttribSceneobject", "ptAttribSceneobjectList", "ptAttribAnimation",
"ptAttribSwimCurrent", "ptAttribWaveSet")
object_name = StringProperty(name="Object",
description="Object containing the required data")
target_object = PointerProperty(name="Object",
description="Object containing the required data",
type=bpy.types.Object)
def init(self, context):
super().init(context)
@ -436,7 +438,7 @@ class PlasmaAttribObjectNode(PlasmaAttribNodeBase, bpy.types.Node):
self.outputs[0].link_limit = 1
def draw_buttons(self, context, layout):
layout.prop_search(self, "object_name", bpy.data, "objects", text=self.attribute_name)
layout.prop(self, "target_object", text=self.attribute_name)
def get_key(self, exporter, so):
attrib = self.to_socket
@ -444,9 +446,9 @@ class PlasmaAttribObjectNode(PlasmaAttribNodeBase, bpy.types.Node):
self.raise_error("must be connected to a Python File node!")
attrib = attrib.attribute_type
bo = bpy.data.objects.get(self.object_name, None)
bo = self.target_object
if bo is None:
self.raise_error("invalid object specified: '{}'".format(self.object_name))
self.raise_error("Target object must be specified")
ref_so_key = exporter.mgr.find_create_key(plSceneObject, bl=bo)
ref_so = ref_so_key.object
@ -467,6 +469,10 @@ class PlasmaAttribObjectNode(PlasmaAttribNodeBase, bpy.types.Node):
self.raise_error("water modifier not enabled on '{}'".format(self.object_name))
return exporter.mgr.find_create_key(plWaveSet7, so=ref_so, bl=bo)
@classmethod
def _idprop_mapping(cls):
return {"target_object": "object_name"}
class PlasmaAttribStringNode(PlasmaAttribNodeBase, bpy.types.Node):
bl_category = "PYTHON"
@ -486,7 +492,7 @@ class PlasmaAttribStringNode(PlasmaAttribNodeBase, bpy.types.Node):
self.value = attrib.simple_value
class PlasmaAttribTextureNode(PlasmaAttribNodeBase, bpy.types.Node):
class PlasmaAttribTextureNode(idprops.IDPropMixin, PlasmaAttribNodeBase, bpy.types.Node):
bl_category = "PYTHON"
bl_idname = "PlasmaAttribTextureNode"
bl_label = "Texture Attribute"
@ -494,8 +500,31 @@ class PlasmaAttribTextureNode(PlasmaAttribNodeBase, bpy.types.Node):
pl_attrib = ("ptAttribMaterial", "ptAttribMaterialList",
"ptAttribDynamicMap", "ptAttribMaterialAnimation")
material_name = StringProperty(name="Material")
texture_name = StringProperty(name="Texture")
def _poll_texture(self, value):
if self.material is not None:
# is this the type of dealio that we're looking for?
attrib = self.to_socket
if attrib is not None:
attrib = attrib.attribute_type
if attrib == "ptAttribDynamicMap":
if not self._is_dyntext(value):
return False
elif attrib == "ptAttribMaterialAnimation":
if not self._is_animated(self.material, value):
return False
# must be a legal option... but is it a member of this material?
return value.name in self.material.texture_slots
return False
material = PointerProperty(name="Material",
description="Material the texture is attached to",
type=bpy.types.Material)
texture = PointerProperty(name="Texture",
description="Texture to expose to Python",
type=bpy.types.Texture,
poll=_poll_texture)
def init(self, context):
super().init(context)
@ -509,36 +538,47 @@ class PlasmaAttribTextureNode(PlasmaAttribNodeBase, bpy.types.Node):
layout.prop_search(self, "texture_name", material, "texture_slots")
def get_key(self, exporter, so):
material = bpy.data.materials.get(self.material_name, None)
if material is None:
self.raise_error("invalid Material '{}'".format(self.material_name))
tex_slot = material.texture_slots.get(self.texture_name, None)
if tex_slot is None:
self.raise_error("invalid Texture '{}'".format(self.texture_name))
if self.material is None:
self.raise_error("Material must be specified")
if self.texture is None:
self.raise_error("Texture must be specified")
attrib = self.to_socket
if attrib is None:
self.raise_error("must be connected to a Python File node!")
attrib = attrib.attribute_type
# Helpers
texture = tex_slot.texture
is_animated = ((material.animation_data is not None and material.animation_data.action is not None)
or (texture.animation_data is not None and texture.animation_data.action is not None))
is_dyntext = texture.type == "IMAGE" and texture.image is None
material = self.material
texture = self.texture
# Your attribute stuff here...
if attrib == "ptAttribDynamicMap":
if not is_dyntext:
if not self._is_dyntext(material, texture):
self.raise_error("Texture '{}' is not a Dynamic Text Map".format(self.texture_name))
name = "{}_{}_DynText".format(self.material_name, self.texture_name)
name = "{}_{}_DynText".format(material.name, texture.name)
return exporter.mgr.find_create_key(plDynamicTextMap, name=name, so=so)
elif is_animated:
name = "{}_{}_LayerAnim".format(self.material_name, self.texture_name)
elif self._is_animated(material, texture):
name = "{}_{}_LayerAnim".format(material_name, texture.name)
return exporter.mgr.find_create_key(plLayerAnimation, name=name, so=so)
else:
name = "{}_{}".format(self.material_name, self.texture_name)
name = "{}_{}".format(material.name, texture.name)
return exporter.mgr.find_create_key(plLayer, name=name, so=so)
@classmethod
def _idprop_mapping(cls):
return {"material": "material_name",
"texture": "texture_name"}
def _idprop_sources(self):
return {"material_name": bpy.data.materials,
"texture_name": bpy.data.textures}
def _is_animated(self, material, texture):
return ((material.animation_data is not None and material.animation_data.action is not None)
or (texture.animation_data is not None and texture.animation_data.action is not None))
def _is_dyntext(self, texture):
return texture.type == "IMAGE" and texture.image is None
_attrib_colors = {
"ptAttribActivator": (0.188, 0.086, 0.349, 1.0),

22
korman/nodes/node_softvolume.py

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

2
korman/operators/op_lightmap.py

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

5
korman/operators/op_sound.py

@ -36,19 +36,16 @@ class PlasmaSoundOpenOperator(SoundOperator, bpy.types.Operator):
def execute(self, context):
# Check to see if the sound exists... Because the sneakily introduced bpy.data.sounds.load
# check_existing doesn't tell us if it already exists... dammit...
# We don't want to take ownership forcefully if we don't have to.
for i in bpy.data.sounds:
if self.filepath == i.filepath:
sound = i
break
else:
sound = bpy.data.sounds.load(self.filepath)
sound.plasma_owned = True
sound.use_fake_user = True
# Now do the stanky leg^H^H^H^H^H^H^H^H^H^H deed and put the sound on the mod
dest = eval(self.data_path)
setattr(dest, self.sound_property, sound.name)
setattr(dest, self.sound_property, sound)
return {"FINISHED"}
def invoke(self, context, event):

15
korman/properties/modifiers/anim.py

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

33
korman/properties/modifiers/avatar.py

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

29
korman/properties/modifiers/logic.py

@ -19,29 +19,31 @@ from PyHSPlasma import *
from .base import PlasmaModifierProperties
from ...exporter import ExportError
from ... import idprops
game_versions = [("pvPrime", "Ages Beyond Myst (63.11)", "Targets the original Uru (Live) game"),
("pvPots", "Path of the Shell (63.12)", "Targets the most recent offline expansion pack"),
("pvMoul", "Myst Online: Uru Live (70)", "Targets the most recent online game")]
class PlasmaVersionedNodeTree(bpy.types.PropertyGroup):
class PlasmaVersionedNodeTree(idprops.IDPropMixin, bpy.types.PropertyGroup):
name = StringProperty(name="Name")
version = EnumProperty(name="Version",
description="Plasma versions this node tree exports under",
items=game_versions,
options={"ENUM_FLAG"},
default=set(list(zip(*game_versions))[0]))
node_tree_name = StringProperty(name="Node Tree",
description="Node Tree to export")
node_tree = PointerProperty(name="Node Tree",
description="Node Tree to export",
type=bpy.types.NodeTree)
node_name = StringProperty(name="Node Ref",
description="Attach a reference to this node")
@property
def node_tree(self):
try:
return bpy.data.node_groups[self.node_tree_name]
except KeyError:
raise ExportError("Node Tree '{}' does not exist!".format(self.node_tree_name))
@classmethod
def _idprop_mapping(cls):
return {"node_tree": "node_tree_name"}
def _idprop_sources(self):
return {"node_tree_name": bpy.data.node_groups}
class PlasmaAdvancedLogic(PlasmaModifierProperties):
@ -60,19 +62,22 @@ class PlasmaAdvancedLogic(PlasmaModifierProperties):
for i in self.logic_groups:
our_versions = [globals()[j] for j in i.version]
if version in our_versions:
if i.node_tree is None:
raise ExportError("'{}': Advanced Logic is missing a node tree for '{}'".format(bo.name, i.version))
# If node_name is defined, then we're only adding a reference. We will make sure that
# the entire node tree is exported once before the post_export step, however.
if i.node_name:
exporter.want_node_trees[i.node_tree_name] = (bo, so)
exporter.want_node_trees[i.node_tree.name] = (bo, so)
node = i.node_tree.nodes.get(i.node_name, None)
if node is None:
raise ExportError("Node '{}' does not exist in '{}'".format(i.node_name, i.node_tree_name))
raise ExportError("Node '{}' does not exist in '{}'".format(i.node_name, i.node_tree.name))
# We are going to assume get_key will do the adding correctly. Single modifiers
# should fetch the appropriate SceneObject before doing anything, so this will
# be a no-op in that case. Multi modifiers should accept any SceneObject, however
node.get_key(exporter, so)
else:
exporter.node_trees_exported.add(i.node_tree_name)
exporter.node_trees_exported.add(i.node_tree.name)
i.node_tree.export(exporter, bo, so)
def harvest_actors(self):

41
korman/properties/modifiers/region.py

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

94
korman/properties/modifiers/render.py

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

104
korman/properties/modifiers/sound.py

@ -23,6 +23,7 @@ from PyHSPlasma import *
from ... import korlib
from .base import PlasmaModifierProperties
from ...exporter import ExportError
from ... import idprops
class PlasmaSfxFade(bpy.types.PropertyGroup):
fade_type = EnumProperty(name="Type",
@ -38,9 +39,17 @@ class PlasmaSfxFade(bpy.types.PropertyGroup):
options=set(), subtype="TIME", unit="TIME")
class PlasmaSound(bpy.types.PropertyGroup):
def _sound_picked(self, context):
if not self.sound_data:
class PlasmaSound(idprops.IDPropMixin, bpy.types.PropertyGroup):
def _get_name_proxy(self):
if self.sound is not None:
return self.sound.name
return ""
def _set_name_proxy(self, value):
self.sound = bpy.data.sounds.get(value, None)
# This is the actual pointer update callback
if not self.sound:
self.name = "[Empty]"
return
@ -54,24 +63,33 @@ class PlasmaSound(bpy.types.PropertyGroup):
else:
self.is_valid = True
self.is_stereo = header.numChannels == 2
self._update_name(context)
self._update_name()
def _update_name(self, context):
def _update_name(self, context=None):
if self.is_stereo and self.channel != {"L", "R"}:
self.name = "{}:{}".format(self.sound_data, "L" if "L" in self.channel else "R")
self.name = "{}:{}".format(self._sound_name, "L" if "L" in self.channel else "R")
else:
self.name = self.sound_data
self.name = self._sound_name
enabled = BoolProperty(name="Enabled", default=True, options=set())
sound_data = StringProperty(name="Sound", description="Sound Datablock",
options=set(), update=_sound_picked)
sound = PointerProperty(name="Sound",
description="Sound Datablock",
type=bpy.types.Sound)
# This is needed because pointer properties do not seem to allow update CBs... Bug?
sound_data_proxy = StringProperty(name="Sound",
description="Name of sound datablock",
get=_get_name_proxy,
set=_set_name_proxy,
options=set())
is_stereo = BoolProperty(default=True, options={"HIDDEN"})
is_valid = BoolProperty(default=False, options={"HIDDEN"})
soft_region = StringProperty(name="Soft Volume",
sfx_region = PointerProperty(name="Soft Volume",
description="Soft region this sound can be heard in",
options=set())
type=bpy.types.Object,
poll=idprops.poll_softvolume_objects)
sfx_type = EnumProperty(name="Category",
description="Describes the purpose of this sound",
@ -170,9 +188,9 @@ class PlasmaSound(bpy.types.PropertyGroup):
def _convert_sound(self, exporter, so, pClass, wavHeader, dataSize, channel=None):
if channel is None:
name = "Sfx-{}_{}".format(so.key.name, self.sound_data)
name = "Sfx-{}_{}".format(so.key.name, self._sound_name)
else:
name = "Sfx-{}_{}:{}".format(so.key.name, self.sound_data, channel)
name = "Sfx-{}_{}:{}".format(so.key.name, self._sound_name, channel)
exporter.report.msg("[{}] {}", pClass.__name__[2:], name, indent=1)
sound = exporter.mgr.find_create_object(pClass, so=so, name=name)
@ -181,13 +199,10 @@ class PlasmaSound(bpy.types.PropertyGroup):
sv_mod, sv_key = self.id_data.plasma_modifiers.softvolume, None
if sv_mod.enabled:
sv_key = sv_mod.get_key(exporter, so)
elif self.soft_region:
sv_bo = bpy.data.objects.get(self.soft_region, None)
if sv_bo is None:
raise ExportError("'{}': Invalid object '{}' for SoundEmit '{}' soft volume".format(self.id_data.name, self.soft_region, self.sound_data))
sv_mod = sv_bo.plasma_modifiers.softvolume
elif self.sfx_region:
sv_mod = self.sfx_region.plasma_modifiers.softvolume
if not sv_mod.enabled:
raise ExportError("'{}': SoundEmit '{}', '{}' is not a SoftVolume".format(self.id_data.name, self.sound_data, self.soft_region))
raise ExportError("'{}': SoundEmit '{}', '{}' is not a SoftVolume".format(self.id_data.name, self._sound_name, self.sfx_region.name))
sv_key = sv_mod.get_key(exporter)
if sv_key is not None:
sv_key.object.listenState |= plSoftVolume.kListenCheck | plSoftVolume.kListenDirty | plSoftVolume.kListenRegistered
@ -305,21 +320,36 @@ class PlasmaSound(bpy.types.PropertyGroup):
key = sound.key
return key
@classmethod
def _idprop_mapping(cls):
return {"sound": "sound_data",
"sfx_region": "soft_region"}
def _idprop_sources(self):
return {"sound_data": bpy.data.sounds,
"soft_region": bpy.data.objects}
@property
def is_3d_stereo(self):
return self.sfx_type == "kSoundFX" and self.channel == {"L", "R"} and self.is_stereo
def _raise_error(self, msg):
raise ExportError("SoundEmitter '{}': Sound '{}' {}".format(self.id_data.name, self.sound_data, msg))
if self.sound:
raise ExportError("SoundEmitter '{}': Sound '{}' {}".format(self.id_data.name, self.sound.name, msg))
else:
raise ExportError("SoundEmitter '{}': {}".format(self.id_data.name, msg))
@property
def _sound(self):
try:
sound = bpy.data.sounds.get(self.sound_data)
except:
self._raise_error("is not loaded")
else:
return sound
if not self.sound:
self._raise_error("has an invalid sound specified")
return self.sound
@property
def _sound_name(self):
if self.sound:
return self.sound.name
return ""
class PlasmaSoundEmitter(PlasmaModifierProperties):
@ -341,7 +371,7 @@ class PlasmaSoundEmitter(PlasmaModifierProperties):
# Pass this off to each individual sound for conversion
for i in self.sounds:
if i.sound_data and i.enabled:
if i.enabled:
i.convert_sound(exporter, so, winaud)
def get_sound_indices(self, name=None, sound=None):
@ -374,26 +404,6 @@ class PlasmaSoundEmitter(PlasmaModifierProperties):
else:
raise ValueError(name)
@classmethod
def register(cls):
bpy.types.Sound.plasma_owned = BoolProperty(default=False, options={"HIDDEN"})
@property
def requires_actor(self):
return True
@persistent
def _toss_orphaned_sounds(scene):
used_sounds = set()
for i in bpy.data.objects:
soundemit = i.plasma_modifiers.soundemit
used_sounds.update((j.sound_data for j in soundemit.sounds))
dead_sounds = [i for i in bpy.data.sounds if i.plasma_owned and i.name not in used_sounds]
for i in dead_sounds:
i.use_fake_user = False
i.user_clear()
bpy.data.sounds.remove(i)
# collects orphaned Plasma owned sound datablocks
bpy.app.handlers.save_pre.append(_toss_orphaned_sounds)

103
korman/properties/modifiers/water.py

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

13
korman/properties/prop_lamp.py

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

14
korman/properties/prop_texture.py

@ -16,10 +16,18 @@
import bpy
from bpy.props import *
class EnvMapVisRegion(bpy.types.PropertyGroup):
from .. import idprops
class EnvMapVisRegion(idprops.IDPropObjectMixin, bpy.types.PropertyGroup):
enabled = BoolProperty(default=True)
region_name = StringProperty(name="Control",
description="Object defining a Plasma Visibility Control")
control_region = PointerProperty(name="Control",
description="Object defining a Plasma Visibility Control",
type=bpy.types.Object,
poll=idprops.poll_visregion_objects)
@classmethod
def _idprop_mapping(cls):
return {"control_region": "region_name"}
class PlasmaLayer(bpy.types.PropertyGroup):

6
korman/ui/modifiers/anim.py

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

8
korman/ui/modifiers/avatar.py

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

2
korman/ui/modifiers/logic.py

@ -39,7 +39,7 @@ def advanced_logic(modifier, layout, context):
if modifier.logic_groups:
logic = modifier.logic_groups[modifier.active_group_index]
layout.row().prop_menu_enum(logic, "version")
layout.prop_search(logic, "node_tree_name", bpy.data, "node_groups", icon="NODETREE")
layout.prop(logic, "node_tree", icon="NODETREE")
try:
layout.prop_search(logic, "node_name", logic.node_tree, "nodes", icon="NODE")
except:

2
korman/ui/modifiers/region.py

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

18
korman/ui/modifiers/render.py

@ -43,7 +43,7 @@ def followmod(modifier, layout, context):
layout.row().prop(modifier, "follow_mode", expand=True)
layout.prop(modifier, "leader_type")
if modifier.leader_type == "kFollowObject":
layout.prop_search(modifier, "leader_object", bpy.data, "objects", icon="OUTLINER_OB_MESH")
layout.prop(modifier, "leader", icon="OUTLINER_OB_MESH")
def lighting(modifier, layout, context):
split = layout.split()
@ -84,11 +84,10 @@ def lighting(modifier, layout, context):
def lightmap(modifier, layout, context):
layout.row(align=True).prop(modifier, "quality", expand=True)
layout.prop(modifier, "render_layers", text="Active Render Layers")
layout.prop_search(modifier, "light_group", bpy.data, "groups", icon="GROUP")
layout.prop(modifier, "lights")
layout.prop_search(modifier, "uv_map", context.active_object.data, "uv_textures")
operator = layout.operator("object.plasma_lightmap_preview", "Preview Lightmap", icon="RENDER_STILL")
operator.light_group = modifier.light_group
# Kind of clever stuff to show the user a preview...
# We can't show images, so we make a hidden ImageTexture called LIGHTMAPGEN_PREVIEW. We check
@ -117,7 +116,7 @@ def viewfacemod(modifier, layout, context):
if modifier.preset_options == "Custom":
layout.row().prop(modifier, "follow_mode")
if modifier.follow_mode == "kFaceObj":
layout.prop_search(modifier, "target_object", bpy.data, "objects", icon="OUTLINER_OB_MESH")
layout.prop(modifier, "target", icon="OUTLINER_OB_MESH")
layout.separator()
layout.prop(modifier, "pivot_on_y")
@ -136,9 +135,10 @@ def viewfacemod(modifier, layout, context):
class VisRegionListUI(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0):
myIcon = "ERROR" if bpy.data.objects.get(item.region_name, None) is None else "OBJECT_DATA"
label = item.region_name if item.region_name else "[No Object Specified]"
layout.label(label, icon=myIcon)
if item.control_region is None:
layout.label("[No Object Specified]", icon="ERROR")
else:
layout.label(item.control_region.name, icon="OBJECT_DATA")
layout.prop(item, "enabled", text="")
@ -156,7 +156,7 @@ def visibility(modifier, layout, context):
op.index = modifier.active_region_index
if modifier.regions:
layout.prop_search(modifier.regions[modifier.active_region_index], "region_name", bpy.data, "objects")
layout.prop(modifier.regions[modifier.active_region_index], "control_region")
def visregion(modifier, layout, context):
layout.prop(modifier, "mode")
@ -164,7 +164,7 @@ def visregion(modifier, layout, context):
# Only allow SoftVolume spec if this is not an FX and this object is not an SV itself
sv = modifier.id_data.plasma_modifiers.softvolume
if modifier.mode != "fx" and not sv.enabled:
layout.prop_search(modifier, "softvolume", bpy.data, "objects")
layout.prop(modifier, "soft_region")
# Other settings
layout.prop(modifier, "replace_normal")

14
korman/ui/modifiers/sound.py

@ -22,10 +22,8 @@ def _draw_fade_ui(modifier, layout, label):
class SoundListUI(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0):
if item.sound_data:
sound = bpy.data.sounds.get(item.sound_data)
icon = "SOUND" if sound is not None else "ERROR"
layout.prop(item, "name", emboss=False, icon=icon, text="")
if item.sound:
layout.prop(item, "name", emboss=False, icon="SOUND", text="")
layout.prop(item, "enabled", text="")
else:
layout.label("[Empty]")
@ -51,13 +49,13 @@ def soundemit(modifier, layout, context):
else:
# Sound datablock picker
row = layout.row(align=True)
row.prop_search(sound, "sound_data", bpy.data, "sounds", text="")
row.prop_search(sound, "sound_data_proxy", bpy.data, "sounds", text="")
open_op = row.operator("sound.plasma_open", icon="FILESEL", text="")
open_op.data_path = repr(sound)
open_op.sound_property = "sound_data"
# Pack/Unpack
data = bpy.data.sounds.get(sound.sound_data)
data = sound.sound
if data is not None:
if data.packed_file is None:
row.operator("sound.plasma_pack", icon="UGLYPACKAGE", text="")
@ -65,7 +63,7 @@ def soundemit(modifier, layout, context):
row.operator_menu_enum("sound.plasma_unpack", "method", icon="PACKAGE", text="")
# If an invalid sound data block is spec'd, let them know about it.
if sound.sound_data and not sound.is_valid:
if data and not sound.is_valid:
layout.label(text="Invalid sound specified", icon="ERROR")
# Core Props
@ -102,4 +100,4 @@ def soundemit(modifier, layout, context):
if not sv.enabled:
col.separator()
col.label("Soft Region:")
col.prop_search(sound, "soft_region", bpy.data, "objects", text="")
col.prop(sound, "sfx_region", text="")

12
korman/ui/modifiers/water.py

@ -19,9 +19,9 @@ def swimregion(modifier, layout, context):
split = layout.split()
col = split.column()
col.label("Detector Region:")
col.prop_search(modifier, "region_name", bpy.data, "objects", text="")
col.prop(modifier, "region", text="")
region_bo = bpy.data.objects.get(modifier.region_name, None)
region_bo = modifier.region
col = split.column()
col.enabled = region_bo is not None
bounds_src = region_bo if region_bo is not None else modifier.id_data
@ -52,11 +52,11 @@ def swimregion(modifier, layout, context):
col.prop(modifier, "near_velocity", text="Near")
col.prop(modifier, "far_velocity", text="Far")
layout.prop_search(modifier, "current_object", bpy.data, "objects")
layout.prop(modifier, "current")
def water_basic(modifier, layout, context):
layout.prop_search(modifier, "wind_object_name", bpy.data, "objects")
layout.prop_search(modifier, "envmap_name", bpy.data, "textures")
layout.prop(modifier, "wind_object")
layout.prop(modifier, "envmap")
row = layout.row()
row.prop(modifier, "wind_speed")
@ -128,7 +128,7 @@ def water_shore(modifier, layout, context):
# Display the active shore
if modifier.shores:
shore = modifier.shores[modifier.active_shore_index]
layout.prop_search(shore, "object_name", bpy.data, "objects", icon="MESH_DATA")
layout.prop(shore, "shore_object", icon="MESH_DATA")
split = layout.split()
col = split.column()

2
korman/ui/ui_lamp.py

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

2
korman/ui/ui_texture.py

@ -52,7 +52,7 @@ class PlasmaEnvMapPanel(TextureButtonsPanel, bpy.types.Panel):
op.index = layer_props.active_region_index
rgns = layer_props.vis_regions
if layer_props.vis_regions:
layout.prop_search(rgns[layer_props.active_region_index], "region_name", bpy.data, "objects")
layout.prop(rgns[layer_props.active_region_index], "control_region")
layout.separator()
layout.prop(layer_props, "envmap_color")

Loading…
Cancel
Save