Browse Source

Files Reformatted and Restructured for Better Visibility and Understanding.

pull/301/head
dhhruv 4 years ago
parent
commit
74278b21ba
  1. 12
      korman/__init__.py
  2. 116
      korman/addon_prefs.py
  3. 679
      korman/exporter/animation.py
  4. 58
      korman/exporter/camera.py
  5. 88
      korman/exporter/convert.py
  6. 55
      korman/exporter/decal.py
  7. 156
      korman/exporter/etlight.py
  8. 28
      korman/exporter/explosions.py
  9. 27
      korman/exporter/image.py
  10. 123
      korman/exporter/locman.py
  11. 73
      korman/exporter/logger.py
  12. 55
      korman/exporter/manager.py
  13. 713
      korman/exporter/material.py
  14. 205
      korman/exporter/mesh.py
  15. 136
      korman/exporter/outfile.py
  16. 116
      korman/exporter/physics.py
  17. 44
      korman/exporter/python.py
  18. 52
      korman/exporter/rtlight.py
  19. 21
      korman/exporter/utils.py
  20. 8
      korman/helpers.py
  21. 42
      korman/idprops.py
  22. 60
      korman/korlib/__init__.py
  23. 51
      korman/korlib/console.py
  24. 52
      korman/korlib/python.py
  25. 82
      korman/korlib/texture.py
  26. 15
      korman/nodes/__init__.py
  27. 577
      korman/nodes/node_avatar.py
  28. 470
      korman/nodes/node_conditions.py
  29. 152
      korman/nodes/node_core.py
  30. 65
      korman/nodes/node_deprecated.py
  31. 153
      korman/nodes/node_logic.py
  32. 880
      korman/nodes/node_messages.py
  33. 381
      korman/nodes/node_python.py
  34. 267
      korman/nodes/node_responder.py
  35. 165
      korman/nodes/node_softvolume.py
  36. 2
      korman/operators/__init__.py
  37. 387
      korman/operators/op_export.py
  38. 83
      korman/operators/op_image.py
  39. 55
      korman/operators/op_lightmap.py
  40. 357
      korman/operators/op_mesh.py
  41. 74
      korman/operators/op_modifier.py
  42. 134
      korman/operators/op_nodes.py
  43. 20
      korman/operators/op_sound.py
  44. 47
      korman/operators/op_toolbox.py
  45. 89
      korman/operators/op_ui.py
  46. 6
      korman/operators/op_world.py
  47. 26
      korman/ordered_set.py
  48. 22
      korman/plasma_attributes.py
  49. 68
      korman/plasma_launcher.py
  50. 20
      korman/properties/modifiers/__init__.py
  51. 155
      korman/properties/modifiers/anim.py
  52. 140
      korman/properties/modifiers/avatar.py
  53. 90
      korman/properties/modifiers/base.py
  54. 630
      korman/properties/modifiers/gui.py
  55. 68
      korman/properties/modifiers/logic.py
  56. 130
      korman/properties/modifiers/physics.py
  57. 234
      korman/properties/modifiers/region.py
  58. 946
      korman/properties/modifiers/render.py
  59. 467
      korman/properties/modifiers/sound.py
  60. 498
      korman/properties/modifiers/water.py
  61. 62
      korman/properties/prop_anim.py
  62. 529
      korman/properties/prop_camera.py
  63. 23
      korman/properties/prop_image.py
  64. 105
      korman/properties/prop_lamp.py
  65. 76
      korman/properties/prop_object.py
  66. 182
      korman/properties/prop_scene.py
  67. 11
      korman/properties/prop_sound.py
  68. 7
      korman/properties/prop_text.py
  69. 179
      korman/properties/prop_texture.py
  70. 222
      korman/properties/prop_world.py
  71. 8
      korman/render.py
  72. 1
      korman/ui/__init__.py
  73. 69
      korman/ui/modifiers/anim.py
  74. 2
      korman/ui/modifiers/avatar.py
  75. 35
      korman/ui/modifiers/gui.py
  76. 27
      korman/ui/modifiers/logic.py
  77. 2
      korman/ui/modifiers/physics.py
  78. 18
      korman/ui/modifiers/region.py
  79. 187
      korman/ui/modifiers/render.py
  80. 36
      korman/ui/modifiers/sound.py
  81. 32
      korman/ui/modifiers/water.py
  82. 38
      korman/ui/ui_anim.py
  83. 94
      korman/ui/ui_camera.py
  84. 1
      korman/ui/ui_image.py
  85. 10
      korman/ui/ui_lamp.py
  86. 49
      korman/ui/ui_list.py
  87. 19
      korman/ui/ui_menus.py
  88. 43
      korman/ui/ui_modifiers.py
  89. 1
      korman/ui/ui_object.py
  90. 29
      korman/ui/ui_render_layer.py
  91. 75
      korman/ui/ui_scene.py
  92. 1
      korman/ui/ui_text.py
  93. 30
      korman/ui/ui_texture.py
  94. 118
      korman/ui/ui_toolbox.py
  95. 105
      korman/ui/ui_world.py

12
korman/__init__.py

@ -21,13 +21,13 @@ from . import nodes
from . import operators from . import operators
bl_info = { bl_info = {
"name": "Korman", "name": "Korman",
"author": "Guild of Writers", "author": "Guild of Writers",
"blender": (2, 79, 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",
"category": "System", "category": "System",
} }

116
korman/addon_prefs.py

@ -17,31 +17,47 @@ import bpy
from bpy.props import * from bpy.props import *
from . import korlib from . import korlib
game_versions = [("pvPrime", "Ages Beyond Myst (63.11)", "Targets the original Uru (Live) game"), game_versions = [
("pvPots", "Path of the Shell (63.12)", "Targets the most recent offline expansion pack"), ("pvPrime", "Ages Beyond Myst (63.11)", "Targets the original Uru (Live) game"),
("pvMoul", "Myst Online: Uru Live (70)", "Targets the most recent online 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 PlasmaGame(bpy.types.PropertyGroup): class PlasmaGame(bpy.types.PropertyGroup):
name = StringProperty(name="Name", name = StringProperty(
description="Name of the Plasma Game", name="Name", description="Name of the Plasma Game", options=set()
options=set()) )
path = StringProperty(name="Path", path = StringProperty(
description="Path to this Plasma Game", name="Path", description="Path to this Plasma Game", options=set()
options=set()) )
version = EnumProperty(name="Version", version = EnumProperty(
description="Plasma version of this game", name="Version",
items=game_versions, description="Plasma version of this game",
options=set()) items=game_versions,
options=set(),
player = StringProperty(name="Player", )
description="Name of the player to use when launching the game",
options=set()) player = StringProperty(
ki = IntProperty(name="KI", name="Player",
description="KI Number of the player to use when launching the game", description="Name of the player to use when launching the game",
options=set(), min=0) options=set(),
serverini = StringProperty(name="Server INI", )
description="Name of the server configuation to use when launching the game", ki = IntProperty(
options=set()) name="KI",
description="KI Number of the player to use when launching the game",
options=set(),
min=0,
)
serverini = StringProperty(
name="Server INI",
description="Name of the server configuation to use when launching the game",
options=set(),
)
@property @property
def can_launch(self): def can_launch(self):
@ -60,28 +76,36 @@ class KormanAddonPreferences(bpy.types.AddonPreferences):
def _check_py22_exe(self, context): def _check_py22_exe(self, context):
if self._ensure_abspath((2, 2)): if self._ensure_abspath((2, 2)):
self._check_python((2, 2)) self._check_python((2, 2))
def _check_py23_exe(self, context): def _check_py23_exe(self, context):
if self._ensure_abspath((2, 3)): if self._ensure_abspath((2, 3)):
self._check_python((2, 3)) self._check_python((2, 3))
def _check_py27_exe(self, context): def _check_py27_exe(self, context):
if self._ensure_abspath((2, 7)): if self._ensure_abspath((2, 7)):
self._check_python((2, 7)) self._check_python((2, 7))
python22_executable = StringProperty(name="Python 2.2", python22_executable = StringProperty(
description="Path to the Python 2.2 executable", name="Python 2.2",
options=set(), description="Path to the Python 2.2 executable",
subtype="FILE_PATH", options=set(),
update=_check_py22_exe) subtype="FILE_PATH",
python23_executable = StringProperty(name="Python 2.3", update=_check_py22_exe,
description="Path to the Python 2.3 executable", )
options=set(), python23_executable = StringProperty(
subtype="FILE_PATH", name="Python 2.3",
update=_check_py23_exe) description="Path to the Python 2.3 executable",
python27_executable = StringProperty(name="Python 2.7", options=set(),
description="Path to the Python 2.7 executable", subtype="FILE_PATH",
options=set(), update=_check_py23_exe,
subtype="FILE_PATH", )
update=_check_py27_exe) python27_executable = StringProperty(
name="Python 2.7",
description="Path to the Python 2.7 executable",
options=set(),
subtype="FILE_PATH",
update=_check_py27_exe,
)
def _validate_py_exes(self): def _validate_py_exes(self):
if not self.is_property_set("python22_valid"): if not self.is_property_set("python22_valid"):
@ -96,7 +120,9 @@ class KormanAddonPreferences(bpy.types.AddonPreferences):
python22_valid = BoolProperty(options={"HIDDEN", "SKIP_SAVE"}) python22_valid = BoolProperty(options={"HIDDEN", "SKIP_SAVE"})
python23_valid = BoolProperty(options={"HIDDEN", "SKIP_SAVE"}) python23_valid = BoolProperty(options={"HIDDEN", "SKIP_SAVE"})
python27_valid = BoolProperty(options={"HIDDEN", "SKIP_SAVE"}) python27_valid = BoolProperty(options={"HIDDEN", "SKIP_SAVE"})
python_validated = BoolProperty(get=_validate_py_exes, options={"HIDDEN", "SKIP_SAVE"}) python_validated = BoolProperty(
get=_validate_py_exes, options={"HIDDEN", "SKIP_SAVE"}
)
def _check_python(self, py_version): def _check_python(self, py_version):
py_exe = getattr(self, "python{}{}_executable".format(*py_version)) py_exe = getattr(self, "python{}{}_executable".format(*py_version))
@ -121,8 +147,15 @@ class KormanAddonPreferences(bpy.types.AddonPreferences):
main_col.label("Plasma Games:") main_col.label("Plasma Games:")
row = main_col.row() row = main_col.row()
row.template_list("PlasmaGameListRW", "games", self, "games", self, row.template_list(
"active_game_index", rows=3) "PlasmaGameListRW",
"games",
self,
"games",
self,
"active_game_index",
rows=3,
)
col = row.column(align=True) col = row.column(align=True)
col.operator("world.plasma_game_add", icon="ZOOMIN", text="") col.operator("world.plasma_game_add", icon="ZOOMIN", text="")
col.operator("world.plasma_game_remove", icon="ZOOMOUT", text="") col.operator("world.plasma_game_remove", icon="ZOOMOUT", text="")
@ -171,4 +204,5 @@ class KormanAddonPreferences(bpy.types.AddonPreferences):
# Register the old-timey per-world Plasma Games for use in the conversion # Register the old-timey per-world Plasma Games for use in the conversion
# operator. What fun. I guess.... # operator. What fun. I guess....
from .properties.prop_world import PlasmaGames from .properties.prop_world import PlasmaGames
PlasmaGames.games = CollectionProperty(type=PlasmaGame) PlasmaGames.games = CollectionProperty(type=PlasmaGame)

679
korman/exporter/animation.py

File diff suppressed because it is too large Load Diff

58
korman/exporter/camera.py

@ -22,6 +22,7 @@ from .explosions import *
from .. import helpers from .. import helpers
from . import utils from . import utils
class CameraConverter: class CameraConverter:
def __init__(self, exporter): def __init__(self, exporter):
self._exporter = weakref.ref(exporter) self._exporter = weakref.ref(exporter)
@ -31,7 +32,9 @@ class CameraConverter:
brain.poaOffset = hsVector3(*camera_props.poa_offset) brain.poaOffset = hsVector3(*camera_props.poa_offset)
if camera_props.poa_type == "object": if camera_props.poa_type == "object":
brain.subject = self._mgr.find_create_key(plSceneObject, bl=camera_props.poa_object) brain.subject = self._mgr.find_create_key(
plSceneObject, bl=camera_props.poa_object
)
brain.xPanLimit = camera_props.x_pan_angle / 2.0 brain.xPanLimit = camera_props.x_pan_angle / 2.0
brain.zPanLimit = camera_props.y_pan_angle / 2.0 brain.zPanLimit = camera_props.y_pan_angle / 2.0
@ -72,7 +75,9 @@ class CameraConverter:
brain.setFlags(plCameraBrain1.kIgnoreSubworldMovement, True) brain.setFlags(plCameraBrain1.kIgnoreSubworldMovement, True)
def export_camera(self, so, bo, camera_type, camera_props, camera_trans=[]): def export_camera(self, so, bo, camera_type, camera_props, camera_trans=[]):
brain = getattr(self, "_export_{}_camera".format(camera_type))(so, bo, camera_props) brain = getattr(self, "_export_{}_camera".format(camera_type))(
so, bo, camera_props
)
mod = self._export_camera_modifier(so, bo, camera_props, camera_trans) mod = self._export_camera_modifier(so, bo, camera_props, camera_trans)
mod.brain = brain.key mod.brain = brain.key
@ -97,7 +102,9 @@ class CameraConverter:
continue continue
cam_trans = plCameraModifier.CamTrans() cam_trans = plCameraModifier.CamTrans()
if manual_trans.camera: if manual_trans.camera:
cam_trans.transTo = self._mgr.find_create_key(plCameraModifier, bl=manual_trans.camera) cam_trans.transTo = self._mgr.find_create_key(
plCameraModifier, bl=manual_trans.camera
)
cam_trans.ignore = manual_trans.mode == "ignore" cam_trans.ignore = manual_trans.mode == "ignore"
trans_info = manual_trans.transition trans_info = manual_trans.transition
@ -121,9 +128,15 @@ class CameraConverter:
if props.poa_type == "avatar": if props.poa_type == "avatar":
brain.circleFlags |= plCameraBrain1_Circle.kCircleLocalAvatar brain.circleFlags |= plCameraBrain1_Circle.kCircleLocalAvatar
elif props.poa_type == "object": elif props.poa_type == "object":
brain.poaObject = self._mgr.find_create_key(plSceneObject, bl=props.poa_object) brain.poaObject = self._mgr.find_create_key(
plSceneObject, bl=props.poa_object
)
else: else:
self._report.warn("Circle Camera '{}' has no Point of Attention. Is this intended?", bo.name, indent=3) self._report.warn(
"Circle Camera '{}' has no Point of Attention. Is this intended?",
bo.name,
indent=3,
)
if props.circle_pos == "farthest": if props.circle_pos == "farthest":
brain.circleFlags |= plCameraBrain1_Circle.kFarthest brain.circleFlags |= plCameraBrain1_Circle.kFarthest
@ -134,7 +147,9 @@ class CameraConverter:
if props.circle_center is None: if props.circle_center is None:
brain.center = hsVector3(*bo.matrix_world.translation) brain.center = hsVector3(*bo.matrix_world.translation)
else: else:
brain.centerObject = self._mgr.find_create_key(plSceneObject, bl=props.circle_center) brain.centerObject = self._mgr.find_create_key(
plSceneObject, bl=props.circle_center
)
# This flag has no effect in CWE, but I'm using it for correctness' sake # This flag has no effect in CWE, but I'm using it for correctness' sake
brain.circleFlags |= plCameraBrain1_Circle.kHasCenterObject brain.circleFlags |= plCameraBrain1_Circle.kHasCenterObject
@ -172,7 +187,11 @@ class CameraConverter:
def _export_fixed_camera(self, so, bo, props): def _export_fixed_camera(self, so, bo, props):
anim_mod = bo.plasma_modifiers.animation anim_mod = bo.plasma_modifiers.animation
if props.anim_enabled and not anim_mod.enabled and bo.plasma_object.has_animation_data: if (
props.anim_enabled
and not anim_mod.enabled
and bo.plasma_object.has_animation_data
):
anim_mod.convert_object_animations(self._exporter(), bo, so) anim_mod.convert_object_animations(self._exporter(), bo, so)
brain = self._mgr.find_create_object(plCameraBrain1_Fixed, so=so) brain = self._mgr.find_create_object(plCameraBrain1_Fixed, so=so)
self._convert_brain(so, bo, props, brain) self._convert_brain(so, bo, props, brain)
@ -203,11 +222,16 @@ class CameraConverter:
# The rail is defined by a position controller in Plasma. Cyan uses a separate # The rail is defined by a position controller in Plasma. Cyan uses a separate
# path object, but it makes more sense to me to just animate the camera with # path object, but it makes more sense to me to just animate the camera with
# the details of the path... # the details of the path...
pos_fcurves = tuple(i for i in helpers.fetch_fcurves(bo, False) if i.data_path == "location") pos_fcurves = tuple(
pos_ctrl = self._exporter().animation.convert_transform_controller(pos_fcurves, bo.rotation_mode, i for i in helpers.fetch_fcurves(bo, False) if i.data_path == "location"
bo.matrix_local, bo.matrix_parent_inverse) )
pos_ctrl = self._exporter().animation.convert_transform_controller(
pos_fcurves, bo.rotation_mode, bo.matrix_local, bo.matrix_parent_inverse
)
if pos_ctrl is None: if pos_ctrl is None:
raise ExportError("'{}': Rail Camera lacks appropriate rail keyframes".format(bo.name)) raise ExportError(
"'{}': Rail Camera lacks appropriate rail keyframes".format(bo.name)
)
path = plAnimPath() path = plAnimPath()
path.controller = pos_ctrl path.controller = pos_ctrl
path.affineParts = utils.affine_parts(bo.matrix_local) path.affineParts = utils.affine_parts(bo.matrix_local)
@ -217,8 +241,16 @@ class CameraConverter:
if abs(f1 - f2) > 0.001: if abs(f1 - f2) > 0.001:
break break
# to avoid single/duplicate keyframe client crash (per Hoikas) # to avoid single/duplicate keyframe client crash (per Hoikas)
if any((len(i.keys) == 1 for i in (pos_ctrl.X, pos_ctrl.Y, pos_ctrl.Z) if i is not None)): if any(
raise ExportError("'{}': Rail Camera must have more than one keyframe", bo.name) (
len(i.keys) == 1
for i in (pos_ctrl.X, pos_ctrl.Y, pos_ctrl.Z)
if i is not None
)
):
raise ExportError(
"'{}': Rail Camera must have more than one keyframe", bo.name
)
else: else:
# The animation is a loop # The animation is a loop
path.flags |= plAnimPath.kWrap path.flags |= plAnimPath.kWrap

88
korman/exporter/convert.py

@ -40,17 +40,24 @@ from . import physics
from . import rtlight from . import rtlight
from . import utils from . import utils
class Exporter: class Exporter:
def __init__(self, op): def __init__(self, op):
self._op = op # Blender export operator self._op = op # Blender export operator
self._objects = [] self._objects = []
self.actors = set() self.actors = set()
self.want_node_trees = defaultdict(set) self.want_node_trees = defaultdict(set)
self.exported_nodes = {} self.exported_nodes = {}
def run(self): def run(self):
log = logger.ExportVerboseLogger if self._op.verbose else logger.ExportProgressLogger log = (
with ConsoleToggler(self._op.show_console), log(self._op.filepath) as self.report, ExitStack() as self.exit_stack: logger.ExportVerboseLogger
if self._op.verbose
else logger.ExportProgressLogger
)
with ConsoleToggler(self._op.show_console), log(
self._op.filepath
) as self.report, ExitStack() as self.exit_stack:
# Step 0: Init export resmgr and stuff # Step 0: Init export resmgr and stuff
self.mgr = manager.ExportManager(self) self.mgr = manager.ExportManager(self)
self.mesh = mesh.MeshConverter(self) self.mesh = mesh.MeshConverter(self)
@ -153,7 +160,13 @@ class Exporter:
# Grab a naive listing of enabled pages # Grab a naive listing of enabled pages
age = scene.world.plasma_age age = scene.world.plasma_age
pages_enabled = frozenset((page.name for page in age.pages if page.enabled and self._op.version in page.version)) pages_enabled = frozenset(
(
page.name
for page in age.pages
if page.enabled and self._op.version in page.version
)
)
all_pages = frozenset((page.name for page in age.pages)) all_pages = frozenset((page.name for page in age.pages))
# Because we can have an unnamed or a named default page, we need to see if that is enabled... # Because we can have an unnamed or a named default page, we need to see if that is enabled...
@ -228,13 +241,18 @@ class Exporter:
parent = bo.parent parent = bo.parent
if parent is not None: if parent is not None:
if parent.plasma_object.enabled: if parent.plasma_object.enabled:
self.report.msg("Attaching to parent SceneObject '{}'", parent.name, indent=1) self.report.msg(
"Attaching to parent SceneObject '{}'", parent.name, indent=1
)
parent_ci = self._export_coordinate_interface(None, parent) parent_ci = self._export_coordinate_interface(None, parent)
parent_ci.addChild(so.key) parent_ci.addChild(so.key)
else: else:
self.report.warn("You have parented Plasma Object '{}' to '{}', which has not been marked for export. \ self.report.warn(
"You have parented Plasma Object '{}' to '{}', which has not been marked for export. \
The object may not appear in the correct location or animate properly.".format( The object may not appear in the correct location or animate properly.".format(
bo.name, parent.name)) bo.name, parent.name
)
)
def _export_coordinate_interface(self, so, bl): def _export_coordinate_interface(self, so, bl):
"""Ensures that the SceneObject has a CoordinateInterface""" """Ensures that the SceneObject has a CoordinateInterface"""
@ -260,7 +278,10 @@ class Exporter:
self.report.msg("\nExporting localization...") self.report.msg("\nExporting localization...")
for bl_obj in self._objects: for bl_obj in self._objects:
for mod in filter(lambda x: hasattr(x, "export_localization"), bl_obj.plasma_modifiers.modifiers): for mod in filter(
lambda x: hasattr(x, "export_localization"),
bl_obj.plasma_modifiers.modifiers,
):
mod.export_localization(self) mod.export_localization(self)
inc_progress() inc_progress()
@ -279,10 +300,17 @@ class Exporter:
try: try:
export_fn = getattr(self, export_fn) export_fn = getattr(self, export_fn)
except AttributeError: except AttributeError:
self.report.warn("""'{}' is a Plasma Object of Blender type '{}' self.report.warn(
... And I have NO IDEA what to do with that! Tossing.""".format(bl_obj.name, bl_obj.type)) """'{}' is a Plasma Object of Blender type '{}'
... And I have NO IDEA what to do with that! Tossing.""".format(
bl_obj.name, bl_obj.type
)
)
continue continue
log_msg("Blender Object '{}' of type '{}'".format(bl_obj.name, bl_obj.type), indent=1) log_msg(
"Blender Object '{}' of type '{}'".format(bl_obj.name, bl_obj.type),
indent=1,
)
# Create a sceneobject if one does not exist. # Create a sceneobject if one does not exist.
# Before we call the export_fn, we need to determine if this object is an actor of any # Before we call the export_fn, we need to determine if this object is an actor of any
@ -300,7 +328,9 @@ class Exporter:
def _export_camera_blobj(self, so, bo): def _export_camera_blobj(self, so, bo):
# Hey, guess what? Blender's camera data is utter crap! # Hey, guess what? Blender's camera data is utter crap!
camera = bo.data.plasma_camera camera = bo.data.plasma_camera
self.camera.export_camera(so, bo, camera.camera_type, camera.settings, camera.transitions) self.camera.export_camera(
so, bo, camera.camera_type, camera.settings, camera.transitions
)
def _export_empty_blobj(self, so, bo): def _export_empty_blobj(self, so, bo):
pass pass
@ -319,7 +349,9 @@ class Exporter:
if bo.data.materials: if bo.data.materials:
self.mesh.export_object(meshObj, so) self.mesh.export_object(meshObj, so)
else: else:
self.report.msg("No material(s) on the ObData, so no drawables", indent=1) self.report.msg(
"No material(s) on the ObData, so no drawables", indent=1
)
def _export_referenced_node_trees(self): def _export_referenced_node_trees(self):
self.report.progress_advance() self.report.progress_advance()
@ -398,7 +430,12 @@ class Exporter:
for mod in bl_obj.plasma_modifiers.modifiers: for mod in bl_obj.plasma_modifiers.modifiers:
proc = getattr(mod, "post_export", None) proc = getattr(mod, "post_export", None)
if proc is not None: if proc is not None:
self.report.msg("Post processing '{}' modifier '{}'", bl_obj.name, mod.bl_label, indent=1) self.report.msg(
"Post processing '{}' modifier '{}'",
bl_obj.name,
mod.bl_label,
indent=1,
)
proc(self, bl_obj, sceneobject) proc(self, bl_obj, sceneobject)
inc_progress() inc_progress()
@ -413,13 +450,24 @@ class Exporter:
@functools.singledispatch @functools.singledispatch
def handle_temporary(temporary, parent): def handle_temporary(temporary, parent):
raise RuntimeError("Temporary object of type '{}' generated by '{}' was unhandled".format(temporary.__class__, parent.name)) raise RuntimeError(
"Temporary object of type '{}' generated by '{}' was unhandled".format(
temporary.__class__, parent.name
)
)
@handle_temporary.register(bpy.types.Object) @handle_temporary.register(bpy.types.Object)
def _(temporary, parent): def _(temporary, parent):
self.exit_stack.enter_context(TemporaryObject(temporary, bpy.data.objects.remove)) self.exit_stack.enter_context(
self.report.msg("'{}': generated Object '{}' (Plasma Object: {})", parent.name, TemporaryObject(temporary, bpy.data.objects.remove)
temporary.name, temporary.plasma_object.enabled, indent=1) )
self.report.msg(
"'{}': generated Object '{}' (Plasma Object: {})",
parent.name,
temporary.name,
temporary.plasma_object.enabled,
indent=1,
)
if temporary.plasma_object.enabled: if temporary.plasma_object.enabled:
new_objects.append(temporary) new_objects.append(temporary)
@ -435,7 +483,9 @@ class Exporter:
@handle_temporary.register(bpy.types.NodeTree) @handle_temporary.register(bpy.types.NodeTree)
def _(temporary, parent): def _(temporary, parent):
self.exit_stack.enter_context(TemporaryObject(temporary, bpy.data.node_groups.remove)) self.exit_stack.enter_context(
TemporaryObject(temporary, bpy.data.node_groups.remove)
)
self.report.msg("'{}' generated NodeTree '{}'", parent.name, temporary.name) self.report.msg("'{}' generated NodeTree '{}'", parent.name, temporary.name)
if temporary.bl_idname == "PlasmaNodeTree": if temporary.bl_idname == "PlasmaNodeTree":
parent_so = self.mgr.find_create_object(plSceneObject, bl=parent) parent_so = self.mgr.find_create_object(plSceneObject, bl=parent)

55
korman/exporter/decal.py

@ -21,18 +21,24 @@ import weakref
from ..exporter.explosions import ExportError from ..exporter.explosions import ExportError
def _get_puddle_class(exporter, name, vs): def _get_puddle_class(exporter, name, vs):
if vs: if vs:
# sigh... thou shalt not... # sigh... thou shalt not...
exporter.report.warn("'{}': Cannot use 'Water Ripple (Shallow) on a waveset--forcing to 'Water Ripple (Deep)", name) exporter.report.warn(
"'{}': Cannot use 'Water Ripple (Shallow) on a waveset--forcing to 'Water Ripple (Deep)",
name,
)
return plDynaRippleVSMgr return plDynaRippleVSMgr
return plDynaPuddleMgr return plDynaPuddleMgr
def _get_footprint_class(exporter, name, vs): def _get_footprint_class(exporter, name, vs):
if vs: if vs:
raise ExportError("'{}': Footprints cannot be attached to wavesets", name) raise ExportError("'{}': Footprints cannot be attached to wavesets", name)
return plDynaFootMgr return plDynaFootMgr
class DecalConverter: class DecalConverter:
_decal_lookup = { _decal_lookup = {
"footprint_dry": _get_footprint_class, "footprint_dry": _get_footprint_class,
@ -53,7 +59,9 @@ class DecalConverter:
# We don't care about: DynaDecalMgrs in another page. # We don't care about: DynaDecalMgrs in another page.
decal_mgrs, so_key = self._decal_managers.get(decal_name), so.key decal_mgrs, so_key = self._decal_managers.get(decal_name), so.key
if decal_mgrs is None: if decal_mgrs is None:
raise ExportError("'{}': Invalid decal manager '{}'", so_key.name, decal_name) raise ExportError(
"'{}': Invalid decal manager '{}'", so_key.name, decal_name
)
# If we are waveset water, then we can only have one target... # If we are waveset water, then we can only have one target...
waveset_id = plFactory.ClassIndex("plWaveSet7") waveset_id = plFactory.ClassIndex("plWaveSet7")
@ -61,12 +69,17 @@ class DecalConverter:
so_loc = so_key.location so_loc = so_key.location
for key, decal_mgr in ((i, i.object) for i in decal_mgrs): for key, decal_mgr in ((i, i.object) for i in decal_mgrs):
if key.location == so_loc and getattr(decal_mgr, "waveSet", None) == waveset: if (
key.location == so_loc
and getattr(decal_mgr, "waveSet", None) == waveset
):
decal_mgr.addTarget(so_key) decal_mgr.addTarget(so_key)
# HACKAGE: Add the wet/dirty notifes now that we know about all the decal managers. # HACKAGE: Add the wet/dirty notifes now that we know about all the decal managers.
notify_names = self._notifies[decal_name] notify_names = self._notifies[decal_name]
notify_keys = itertools.chain.from_iterable((self._decal_managers[i] for i in notify_names)) notify_keys = itertools.chain.from_iterable(
(self._decal_managers[i] for i in notify_names)
)
for notify_key in notify_keys: for notify_key in notify_keys:
for i in (i.object for i in decal_mgrs): for i in (i.object for i in decal_mgrs):
i.addNotify(notify_key) i.addNotify(notify_key)
@ -76,7 +89,9 @@ class DecalConverter:
def export_active_print_shape(self, print_shape, decal_name): def export_active_print_shape(self, print_shape, decal_name):
decal_mgrs = self._decal_managers.get(decal_name) decal_mgrs = self._decal_managers.get(decal_name)
if decal_mgrs is None: if decal_mgrs is None:
raise ExportError("'{}': Invalid decal manager '{}'", print_shape.key.name, decal_name) raise ExportError(
"'{}': Invalid decal manager '{}'", print_shape.key.name, decal_name
)
for i in decal_mgrs: for i in decal_mgrs:
print_shape.addDecalMgr(i) print_shape.addDecalMgr(i)
@ -84,7 +99,9 @@ class DecalConverter:
mat_mgr = self._exporter().mesh.material mat_mgr = self._exporter().mesh.material
mat_keys = mat_mgr.get_materials(bo) mat_keys = mat_mgr.get_materials(bo)
if not mat_keys: if not mat_keys:
raise ExportError("'{}': Cannot print decal onto object with no materials", bo.name) raise ExportError(
"'{}': Cannot print decal onto object with no materials", bo.name
)
zFlags = hsGMatState.kZIncLayer | hsGMatState.kZNoZWrite zFlags = hsGMatState.kZIncLayer | hsGMatState.kZNoZWrite
for material in (i.object for i in mat_keys): for material in (i.object for i in mat_keys):
@ -97,7 +114,14 @@ class DecalConverter:
layer.state.ZFlags |= zFlags layer.state.ZFlags |= zFlags
def generate_dynamic_decal(self, bo, decal_name): def generate_dynamic_decal(self, bo, decal_name):
decal = next((i for i in bpy.context.scene.plasma_scene.decal_managers if i.name == decal_name), None) decal = next(
(
i
for i in bpy.context.scene.plasma_scene.decal_managers
if i.name == decal_name
),
None,
)
if decal is None: if decal is None:
raise ExportError("'{}': Invalid decal manager '{}'", bo.name, decal_name) raise ExportError("'{}': Invalid decal manager '{}'", bo.name, decal_name)
@ -112,7 +136,9 @@ class DecalConverter:
name = "{}_{}".format(decal_name, bo.name) if is_waveset else decal_name name = "{}_{}".format(decal_name, bo.name) if is_waveset else decal_name
decal_mgr = exporter.mgr.find_object(pClass, bl=bo, name=name) decal_mgr = exporter.mgr.find_object(pClass, bl=bo, name=name)
if decal_mgr is None: if decal_mgr is None:
self._report.msg("Exporing decal manager '{}' to '{}'", decal_name, name, indent=2) self._report.msg(
"Exporing decal manager '{}' to '{}'", decal_name, name, indent=2
)
decal_mgr = exporter.mgr.add_object(pClass, bl=bo, name=name) decal_mgr = exporter.mgr.add_object(pClass, bl=bo, name=name)
self._decal_managers[decal_name].append(decal_mgr.key) self._decal_managers[decal_name].append(decal_mgr.key)
@ -126,7 +152,9 @@ class DecalConverter:
image = decal.image image = decal.image
if image is None: if image is None:
raise ExportError("'{}': decal manager '{}' has no image set", bo.name, decal_name) raise ExportError(
"'{}': decal manager '{}' has no image set", bo.name, decal_name
)
blend = getattr(hsGMatState, decal.blend) blend = getattr(hsGMatState, decal.blend)
mats = exporter.mesh.material.export_print_materials(bo, image, name, blend) mats = exporter.mesh.material.export_print_materials(bo, image, name, blend)
@ -159,8 +187,13 @@ class DecalConverter:
decal_mgr.waitOnEnable = decal_type == "footprint_wet" decal_mgr.waitOnEnable = decal_type == "footprint_wet"
if decal_type in {"puddle", "ripple"}: if decal_type in {"puddle", "ripple"}:
decal_mgr.wetLength = decal.wet_time decal_mgr.wetLength = decal.wet_time
self._notifies[decal_name].update((i.name for i in decal.wet_managers self._notifies[decal_name].update(
if i.enabled and i.name != decal_name)) (
i.name
for i in decal.wet_managers
if i.enabled and i.name != decal_name
)
)
# UV Animations are hardcoded in PlasmaMAX. Any reason why we should expose this? # UV Animations are hardcoded in PlasmaMAX. Any reason why we should expose this?
# I can't think of any presently... Note testing the final instance instead of the # I can't think of any presently... Note testing the final instance instead of the

156
korman/exporter/etlight.py

@ -25,6 +25,7 @@ from ..helpers import *
_NUM_RENDER_LAYERS = 20 _NUM_RENDER_LAYERS = 20
class LightBaker: class LightBaker:
"""ExportTime Lighting""" """ExportTime Lighting"""
@ -132,7 +133,9 @@ class LightBaker:
# Lightmap passes are expensive, so we will warn about any passes that seem # Lightmap passes are expensive, so we will warn about any passes that seem
# particularly wasteful. # particularly wasteful.
try: try:
largest_pass = max((len(value) for key, value in bake.items() if key[0] != "vcol")) largest_pass = max(
(len(value) for key, value in bake.items() if key[0] != "vcol")
)
except ValueError: except ValueError:
largest_pass = 0 largest_pass = 0
@ -146,19 +149,25 @@ class LightBaker:
self._report.msg("Preparing to bake...", indent=1) self._report.msg("Preparing to bake...", indent=1)
for key, value in bake.items(): for key, value in bake.items():
if key[0] == "lightmap": if key[0] == "lightmap":
for i in range(len(value)-1, -1, -1): for i in range(len(value) - 1, -1, -1):
obj = value[i] obj = value[i]
if not self._prep_for_lightmap(obj, toggle): if not self._prep_for_lightmap(obj, toggle):
self._report.msg("Lightmap '{}' will not be baked -- no applicable lights", self._report.msg(
obj.name, indent=2) "Lightmap '{}' will not be baked -- no applicable lights",
obj.name,
indent=2,
)
value.pop(i) value.pop(i)
elif key[0] == "vcol": elif key[0] == "vcol":
for i in range(len(value)-1, -1, -1): for i in range(len(value) - 1, -1, -1):
obj = value[i] obj = value[i]
if not self._prep_for_vcols(obj, toggle): if not self._prep_for_vcols(obj, toggle):
if self._has_valid_material(obj): if self._has_valid_material(obj):
self._report.msg("VCols '{}' will not be baked -- no applicable lights", self._report.msg(
obj.name, indent=2) "VCols '{}' will not be baked -- no applicable lights",
obj.name,
indent=2,
)
value.pop(i) value.pop(i)
else: else:
raise RuntimeError(key[0]) raise RuntimeError(key[0])
@ -172,14 +181,28 @@ class LightBaker:
if value: if value:
if key[0] == "lightmap": if key[0] == "lightmap":
num_objs = len(value) num_objs = len(value)
self._report.msg("{} Lightmap(s) [H:{:X}]", num_objs, hash(key[1:]), indent=1) self._report.msg(
"{} Lightmap(s) [H:{:X}]", num_objs, hash(key[1:]), indent=1
)
if largest_pass > 1 and num_objs < round(largest_pass * 0.02): if largest_pass > 1 and num_objs < round(largest_pass * 0.02):
pass_names = set((i.plasma_modifiers.lightmap.bake_pass_name for i in value)) pass_names = set(
(i.plasma_modifiers.lightmap.bake_pass_name for i in value)
)
pass_msg = ", ".join(pass_names) pass_msg = ", ".join(pass_names)
self._report.warn("Small lightmap bake pass! Bake Pass(es): {}".format(pass_msg), indent=2) self._report.warn(
"Small lightmap bake pass! Bake Pass(es): {}".format(
pass_msg
),
indent=2,
)
self._bake_lightmaps(value, key[1:]) self._bake_lightmaps(value, key[1:])
elif key[0] == "vcol": elif key[0] == "vcol":
self._report.msg("{} Vertex Color(s) [H:{:X}]", len(value), hash(key[1:]), indent=1) self._report.msg(
"{} Vertex Color(s) [H:{:X}]",
len(value),
hash(key[1:]),
indent=1,
)
self._bake_vcols(value, key[1:]) self._bake_vcols(value, key[1:])
self._fix_vertex_colors(value) self._fix_vertex_colors(value)
else: else:
@ -232,8 +255,10 @@ class LightBaker:
if len(edge.link_faces) != 2: if len(edge.link_faces) != 2:
# Either a border edge, or an abomination. # Either a border edge, or an abomination.
continue continue
if mesh.use_auto_smooth and (not edge.smooth if mesh.use_auto_smooth and (
or edge.calc_face_angle() > mesh.auto_smooth_angle): not edge.smooth
or edge.calc_face_angle() > mesh.auto_smooth_angle
):
# Normals are split for edges marked as sharp by the user, and edges # Normals are split for edges marked as sharp by the user, and edges
# whose angle is above the theshold. Auto smooth must be on in both cases. # whose angle is above the theshold. Auto smooth must be on in both cases.
continue continue
@ -241,10 +266,16 @@ class LightBaker:
# Alright, this edge is connected to our loop AND our face. # Alright, this edge is connected to our loop AND our face.
# Now for the Fun Stuff(c)... First, actually get ahold of the other # Now for the Fun Stuff(c)... First, actually get ahold of the other
# face (the one we're connected to via this edge). # face (the one we're connected to via this edge).
other_face = next(f for f in edge.link_faces if f != face) other_face = next(
f for f in edge.link_faces if f != face
)
# Now get ahold of the loop sharing our vertex on the OTHER SIDE # Now get ahold of the loop sharing our vertex on the OTHER SIDE
# of that damnable edge... # of that damnable edge...
other_loop = next(loop for loop in other_face.loops if loop.vert == vert) other_loop = next(
loop
for loop in other_face.loops
if loop.vert == vert
)
other_color = other_loop[light_vcol] other_color = other_loop[light_vcol]
# Phew ! Good, now just pick whichever color has the highest average value # Phew ! Good, now just pick whichever color has the highest average value
if sum(max_color) / 3 < sum(other_color) / 3: if sum(max_color) / 3 < sum(other_color) / 3:
@ -256,7 +287,7 @@ 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"""
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:
@ -274,7 +305,9 @@ class LightBaker:
source = [i for i in bpy.context.scene.objects if i.type == "LAMP"] source = [i for i in bpy.context.scene.objects if i.type == "LAMP"]
else: else:
source = lg.objects source = lg.objects
dest = bpy.data.groups.new("_LIGHTMAPGEN_{}_{}".format(bo.name, mat_name)) dest = bpy.data.groups.new(
"_LIGHTMAPGEN_{}_{}".format(bo.name, mat_name)
)
# Rules: # Rules:
# 1) No animated lights, period. # 1) No animated lights, period.
@ -321,9 +354,17 @@ class LightBaker:
if mod.image is not None: if mod.image is not None:
uv_texture_names = frozenset((i.name for i in obj.data.uv_textures)) uv_texture_names = frozenset((i.name for i in obj.data.uv_textures))
if self.lightmap_uvtex_name in uv_texture_names: if self.lightmap_uvtex_name in uv_texture_names:
self._report.msg("'{}': Skipping due to valid lightmap override", obj.name, indent=1) self._report.msg(
"'{}': Skipping due to valid lightmap override",
obj.name,
indent=1,
)
else: else:
self._report.warn("'{}': Have lightmap, but regenerating UVs", obj.name, indent=1) self._report.warn(
"'{}': Have lightmap, but regenerating UVs",
obj.name,
indent=1,
)
self._prep_for_lightmap_uvs(obj, mod.image, toggle) self._prep_for_lightmap_uvs(obj, mod.image, toggle)
return False return False
return True return True
@ -332,15 +373,27 @@ class LightBaker:
def vcol_bake_required(obj) -> bool: def vcol_bake_required(obj) -> bool:
if obj.plasma_modifiers.lightmap.bake_lightmap: if obj.plasma_modifiers.lightmap.bake_lightmap:
return False return False
vcol_layer_names = frozenset((vcol_layer.name.lower() for vcol_layer in obj.data.vertex_colors)) vcol_layer_names = frozenset(
(vcol_layer.name.lower() for vcol_layer in obj.data.vertex_colors)
)
manual_layer_names = _VERTEX_COLOR_LAYERS & vcol_layer_names manual_layer_names = _VERTEX_COLOR_LAYERS & vcol_layer_names
if manual_layer_names: if manual_layer_names:
self._report.msg("'{}': Skipping due to valid manual vertex color layer(s): '{}'", obj.name, manual_layer_names.pop(), indent=1) self._report.msg(
"'{}': Skipping due to valid manual vertex color layer(s): '{}'",
obj.name,
manual_layer_names.pop(),
indent=1,
)
return False return False
if self.force: if self.force:
return True return True
if self.vcol_layer_name.lower() in vcol_layer_names: if self.vcol_layer_name.lower() in vcol_layer_names:
self._report.msg("'{}': Skipping due to valid matching vertex color layer(s): '{}'", obj.name, self.vcol_layer_name, indent=1) self._report.msg(
"'{}': Skipping due to valid matching vertex color layer(s): '{}'",
obj.name,
self.vcol_layer_name,
indent=1,
)
return False return False
return True return True
@ -351,7 +404,11 @@ class LightBaker:
if lightmap_mod.bake_pass_name: if lightmap_mod.bake_pass_name:
bake_pass = bake_passes.get(lightmap_mod.bake_pass_name, None) bake_pass = bake_passes.get(lightmap_mod.bake_pass_name, None)
if bake_pass is None: if bake_pass is None:
raise ExportError("Bake Lighting '{}': Could not find pass '{}'".format(i.name, lightmap_mod.bake_pass_name)) raise ExportError(
"Bake Lighting '{}': Could not find pass '{}'".format(
i.name, lightmap_mod.bake_pass_name
)
)
lm_layers = tuple(bake_pass.render_layers) lm_layers = tuple(bake_pass.render_layers)
else: else:
lm_layers = default_layers lm_layers = default_layers
@ -359,12 +416,23 @@ class LightBaker:
# In order for Blender to be able to bake this properly, at least one of the # In order for Blender to be able to bake this properly, at least one of the
# layers this object is on must be selected. We will sanity check this now. # layers this object is on must be selected. We will sanity check this now.
obj_layers = tuple(i.layers) obj_layers = tuple(i.layers)
lm_active_layers = set((i for i, value in enumerate(lm_layers) if value)) lm_active_layers = set(
obj_active_layers = set((i for i, value in enumerate(obj_layers) if value)) (i for i, value in enumerate(lm_layers) if value)
)
obj_active_layers = set(
(i for i, value in enumerate(obj_layers) if value)
)
if not lm_active_layers & obj_active_layers: if not lm_active_layers & obj_active_layers:
raise ExportError("Bake Lighting '{}': At least one layer the object is on must be selected".format(i.name)) raise ExportError(
"Bake Lighting '{}': At least one layer the object is on must be selected".format(
if lightmap_bake_required(i) is False and vcol_bake_required(i) is False: i.name
)
)
if (
lightmap_bake_required(i) is False
and vcol_bake_required(i) is False
):
continue continue
method = "lightmap" if lightmap_mod.bake_lightmap else "vcol" method = "lightmap" if lightmap_mod.bake_lightmap else "vcol"
@ -407,7 +475,11 @@ class LightBaker:
# Due to our batching, however, materials that are transparent cannot be lightmapped. # Due to our batching, however, materials that are transparent cannot be lightmapped.
for material in (i for i in mesh.materials if i is not None): for material in (i for i in mesh.materials if i is not None):
if material.use_transparency: if material.use_transparency:
raise ExportError("'{}': Cannot lightmap material '{}' because it is transparnt".format(bo.name, material.name)) raise ExportError(
"'{}': Cannot lightmap material '{}' because it is transparnt".format(
bo.name, material.name
)
)
for slot in (j for j in material.texture_slots if j is not None): for slot in (j for j in material.texture_slots if j is not None):
toggle.track(slot, "use", False) toggle.track(slot, "use", False)
@ -485,8 +557,12 @@ class LightBaker:
# from sharing UVs. Sigh. # from sharing UVs. Sigh.
if self._mesh.is_collapsed(bo): if self._mesh.is_collapsed(bo):
# Danger: uv_base.name -> UnicodeDecodeError (wtf? another blender bug?) # Danger: uv_base.name -> UnicodeDecodeError (wtf? another blender bug?)
self._report.warn("'{}': packing islands in UV Texture '{}' due to modifier collapse", self._report.warn(
bo.name, modifier.uv_map, indent=2) "'{}': packing islands in UV Texture '{}' due to modifier collapse",
bo.name,
modifier.uv_map,
indent=2,
)
with self._set_mode("EDIT"): with self._set_mode("EDIT"):
bpy.ops.mesh.select_all(action="SELECT") bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.uv.select_all(action="SELECT") bpy.ops.uv.select_all(action="SELECT")
@ -534,13 +610,19 @@ class LightBaker:
# future exports as an optimization. We won't reach this point if there is already an # future exports as an optimization. We won't reach this point if there is already an
# autocolor layer (gulp). # autocolor layer (gulp).
if not self.force and needs_vcol_layer: if not self.force and needs_vcol_layer:
self._mesh.context_stack.enter_context(TemporaryObject(vcol_layer.name, lambda layer_name: vcols.remove(vcols[layer_name]))) self._mesh.context_stack.enter_context(
TemporaryObject(
vcol_layer.name, lambda layer_name: vcols.remove(vcols[layer_name])
)
)
# Indicate we should bake # Indicate we should bake
return True return True
def _remove_stale_uvtexes(self, bake): def _remove_stale_uvtexes(self, bake):
lightmap_iter = itertools.chain.from_iterable((value for key, value in bake.items() if key[0] == "lightmap")) lightmap_iter = itertools.chain.from_iterable(
(value for key, value in bake.items() if key[0] == "lightmap")
)
for bo in lightmap_iter: for bo in lightmap_iter:
uv_textures = bo.data.uv_textures uv_textures = bo.data.uv_textures
uvtex = uv_textures.get(self.lightmap_uvtex_name, None) uvtex = uv_textures.get(self.lightmap_uvtex_name, None)
@ -571,7 +653,9 @@ class LightBaker:
else: else:
i.select = False i.select = False
if isinstance(i.data, bpy.types.Mesh) and not self._has_valid_material(i): if isinstance(i.data, bpy.types.Mesh) and not self._has_valid_material(
i
):
toggle.track(i, "hide_render", True) toggle.track(i, "hide_render", True)
else: else:
for i in bpy.data.objects: for i in bpy.data.objects:
@ -581,7 +665,9 @@ class LightBaker:
for mat in (j for j in i.data.materials if j is not None): for mat in (j for j in i.data.materials if j is not None):
toggle.track(mat, "use_vertex_color_paint", False) toggle.track(mat, "use_vertex_color_paint", False)
toggle.track(i, "hide_render", False) toggle.track(i, "hide_render", False)
elif isinstance(i.data, bpy.types.Mesh) and not self._has_valid_material(i): elif isinstance(
i.data, bpy.types.Mesh
) and not self._has_valid_material(i):
toggle.track(i, "hide_render", True) toggle.track(i, "hide_render", True)
i.select = value i.select = value

28
korman/exporter/explosions.py

@ -13,6 +13,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Korman. If not, see <http://www.gnu.org/licenses/>. # along with Korman. If not, see <http://www.gnu.org/licenses/>.
class NonfatalExportError(Exception): class NonfatalExportError(Exception):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
assert args assert args
@ -34,12 +35,16 @@ class ExportError(Exception):
class BlendNotSupported(ExportError): class BlendNotSupported(ExportError):
def __init__(self, progression, axis): def __init__(self, progression, axis):
super(ExportError, self).__init__("Alpha Blend not supported: {}, {}", progression, axis) super(ExportError, self).__init__(
"Alpha Blend not supported: {}, {}", progression, axis
)
class BlenderOptionNotSupportedError(ExportError): class BlenderOptionNotSupportedError(ExportError):
def __init__(self, opt): def __init__(self, opt):
super(ExportError, self).__init__("Unsupported Blender Option: '{}'".format(opt)) super(ExportError, self).__init__(
"Unsupported Blender Option: '{}'".format(opt)
)
class ExportAssertionError(ExportError): class ExportAssertionError(ExportError):
@ -60,14 +65,15 @@ class PlasmaLaunchError(ExportError):
class TooManyUVChannelsError(ExportError): class TooManyUVChannelsError(ExportError):
def __init__(self, obj, mat, numUVTexs, maxUVTexCount=8): def __init__(self, obj, mat, numUVTexs, maxUVTexCount=8):
msg = "There are too many UV Textures on the material '{}' associated with object '{}'. You can have at most {} (there are {})".format( msg = "There are too many UV Textures on the material '{}' associated with object '{}'. You can have at most {} (there are {})".format(
mat.name, obj.name, maxUVTexCount, numUVTexs) mat.name, obj.name, maxUVTexCount, numUVTexs
)
super(ExportError, self).__init__(msg) super(ExportError, self).__init__(msg)
class TooManyVerticesError(ExportError): class TooManyVerticesError(ExportError):
def __init__(self, mesh, matname, vertcount): def __init__(self, mesh, matname, vertcount):
msg = "There are too many vertices ({}) on the mesh data '{}' associated with material '{}'".format( msg = "There are too many vertices ({}) on the mesh data '{}' associated with material '{}'".format(
vertcount, mesh, matname vertcount, mesh, matname
) )
super(ExportError, self).__init__(msg) super(ExportError, self).__init__(msg)
@ -76,11 +82,15 @@ class UndefinedPageError(ExportError):
mistakes = {} mistakes = {}
def __init__(self): def __init__(self):
super(ExportError, self).__init__("You have objects in pages that do not exist!") super(ExportError, self).__init__(
"You have objects in pages that do not exist!"
)
def add(self, page, obj): def add(self, page, obj):
if page not in self.mistakes: if page not in self.mistakes:
self.mistakes[page] = [obj,] self.mistakes[page] = [
obj,
]
else: else:
self.mistakes[page].append(obj) self.mistakes[page].append(obj)
@ -93,4 +103,8 @@ class UndefinedPageError(ExportError):
class UnsupportedTextureError(ExportError): class UnsupportedTextureError(ExportError):
def __init__(self, texture, material): def __init__(self, texture, material):
super(ExportError, self).__init__("Cannot export texture '{}' on material '{}' -- unsupported type '{}'".format(texture.name, material.name, texture.type)) super(ExportError, self).__init__(
"Cannot export texture '{}' on material '{}' -- unsupported type '{}'".format(
texture.name, material.name, texture.type
)
)

27
korman/exporter/image.py

@ -26,6 +26,7 @@ _ENTRY_MAGICK = b"KTE\x00"
_IMAGE_MAGICK = b"KTT\x00" _IMAGE_MAGICK = b"KTT\x00"
_MIP_MAGICK = b"KTM\x00" _MIP_MAGICK = b"KTM\x00"
@enum.unique @enum.unique
class _HeaderBits(enum.IntEnum): class _HeaderBits(enum.IntEnum):
last_export = 0 last_export = 0
@ -79,7 +80,10 @@ class ImageCache:
image, tag = texture.image, texture.tag image, tag = texture.image, texture.tag
image_name = str(texture) image_name = str(texture)
key = (image_name, tag, compression) key = (image_name, tag, compression)
ex_method, im_method = self._exporter().texcache_method, image.plasma_image.texcache_method ex_method, im_method = (
self._exporter().texcache_method,
image.plasma_image.texcache_method,
)
method = set((ex_method, im_method)) method = set((ex_method, im_method))
if texture.ephemeral or "skip" in method: if texture.ephemeral or "skip" in method:
self._images.pop(key, None) self._images.pop(key, None)
@ -122,7 +126,10 @@ class ImageCache:
# If the texture is ephemeral (eg a lightmap) or has been marked "rebuild" or "skip" # If the texture is ephemeral (eg a lightmap) or has been marked "rebuild" or "skip"
# in the UI, we don't want anything from the cache. In the first two cases, we never # in the UI, we don't want anything from the cache. In the first two cases, we never
# want to cache that crap. In the latter case, we just want to signal a recache is needed. # want to cache that crap. In the latter case, we just want to signal a recache is needed.
ex_method, im_method = self._exporter().texcache_method, texture.image.plasma_image.texcache_method ex_method, im_method = (
self._exporter().texcache_method,
texture.image.plasma_image.texcache_method,
)
method = set((ex_method, im_method)) method = set((ex_method, im_method))
if method != {"use"} or texture.ephemeral: if method != {"use"} or texture.ephemeral:
return None return None
@ -150,7 +157,10 @@ class ImageCache:
finally: finally:
if exists: if exists:
cached_image.modify_time = path.stat().st_mtime cached_image.modify_time = path.stat().st_mtime
if cached_image.export_time and cached_image.export_time < cached_image.modify_time: if (
cached_image.export_time
and cached_image.export_time < cached_image.modify_time
):
return None return None
else: else:
cached_image.modify_time = 0 cached_image.modify_time = 0
@ -158,9 +168,15 @@ class ImageCache:
# ensure the data has been loaded from the cache # ensure the data has been loaded from the cache
if cached_image.image_data is None: if cached_image.image_data is None:
try: try:
cached_image.image_data = tuple(self._read_image_data(cached_image, self._read_stream)) cached_image.image_data = tuple(
self._read_image_data(cached_image, self._read_stream)
)
except AssertionError: except AssertionError:
self._report.warn("Cached copy of '{}' is corrupt and will be discarded", cached_image.name, indent=2) self._report.warn(
"Cached copy of '{}' is corrupt and will be discarded",
cached_image.name,
indent=2,
)
self._images.pop(key) self._images.pop(key)
return None return None
return cached_image return cached_image
@ -229,7 +245,6 @@ class ImageCache:
stream.seek(pos) stream.seek(pos)
yield tuple(_read_image_mips()) yield tuple(_read_image_mips())
def _read_index(self, index_pos, stream): def _read_index(self, index_pos, stream):
stream.seek(index_pos) stream.seek(index_pos)
assert stream.read(4) == _INDEX_MAGICK assert stream.read(4) == _INDEX_MAGICK

123
korman/exporter/locman.py

@ -34,6 +34,7 @@ _SP_LANGUAGES = {"English", "French", "German", "Italian", "Spanish"}
# as CDATA instead of XML encoding the entry. # as CDATA instead of XML encoding the entry.
_ESHTML_REGEX = re.compile("<.+>") _ESHTML_REGEX = re.compile("<.+>")
class LocalizationConverter: class LocalizationConverter:
def __init__(self, exporter=None, **kwargs): def __init__(self, exporter=None, **kwargs):
if exporter is not None: if exporter is not None:
@ -49,18 +50,26 @@ class LocalizationConverter:
self._strings = defaultdict(lambda: defaultdict(dict)) self._strings = defaultdict(lambda: defaultdict(dict))
def add_string(self, set_name, element_name, language, value, indent=0): def add_string(self, set_name, element_name, language, value, indent=0):
self._report.msg("Accepted '{}' translation for '{}'.", element_name, language, indent=indent) self._report.msg(
"Accepted '{}' translation for '{}'.", element_name, language, indent=indent
)
if isinstance(value, bpy.types.Text): if isinstance(value, bpy.types.Text):
if value.is_modified: if value.is_modified:
self._report.warn("'{}' translation for '{}' is modified on the disk but not reloaded in Blender.", self._report.warn(
element_name, language, indent=indent) "'{}' translation for '{}' is modified on the disk but not reloaded in Blender.",
element_name,
language,
indent=indent,
)
value = value.as_string() value = value.as_string()
self._strings[set_name][element_name][language] = value self._strings[set_name][element_name][language] = value
@contextmanager @contextmanager
def _generate_file(self, filename, **kwargs): def _generate_file(self, filename, **kwargs):
if self._exporter is not None: if self._exporter is not None:
with self._exporter().output.generate_dat_file(filename, **kwargs) as handle: with self._exporter().output.generate_dat_file(
filename, **kwargs
) as handle:
yield handle yield handle
else: else:
dirname = kwargs.get("dirname", "dat") dirname = kwargs.get("dirname", "dat")
@ -77,42 +86,68 @@ class LocalizationConverter:
age_name = self._age_name age_name = self._age_name
def write_text_file(language, file_name, contents): def write_text_file(language, file_name, contents):
with self._generate_file(dirname="ageresources", filename=file_name) as stream: with self._generate_file(
dirname="ageresources", filename=file_name
) as stream:
try: try:
stream.write(contents.encode("windows-1252")) stream.write(contents.encode("windows-1252"))
except UnicodeEncodeError: except UnicodeEncodeError:
self._report.warn("Translation '{}': Contents contains characters that cannot be used in this version of Plasma. They will appear as a '?' in game.", self._report.warn(
language, indent=2) "Translation '{}': Contents contains characters that cannot be used in this version of Plasma. They will appear as a '?' in game.",
language,
indent=2,
)
# Yes, there are illegal characters... As a stopgap, we will export the file with # Yes, there are illegal characters... As a stopgap, we will export the file with
# replacement characters ("?") just so it'll work dammit. # replacement characters ("?") just so it'll work dammit.
stream.write(contents.encode("windows-1252", "replace")) stream.write(contents.encode("windows-1252", "replace"))
return True return True
locs = itertools.chain(self._strings["Journals"].items(), self._strings["DynaTexts"].items()) locs = itertools.chain(
self._strings["Journals"].items(), self._strings["DynaTexts"].items()
)
for journal_name, translations in locs: for journal_name, translations in locs:
self._report.msg("Copying localization '{}'", journal_name, indent=1) self._report.msg("Copying localization '{}'", journal_name, indent=1)
for language_name, value in translations.items(): for language_name, value in translations.items():
if language_name not in _SP_LANGUAGES: if language_name not in _SP_LANGUAGES:
self._report.warn("Translation '{}' will not be used because it is not supported in this version of Plasma.", self._report.warn(
language_name, indent=2) "Translation '{}' will not be used because it is not supported in this version of Plasma.",
language_name,
indent=2,
)
continue continue
suffix = "_{}".format(language_name.lower()) if language_name != "English" else "" suffix = (
"_{}".format(language_name.lower())
if language_name != "English"
else ""
)
file_name = "{}--{}{}.txt".format(age_name, journal_name, suffix) file_name = "{}--{}{}.txt".format(age_name, journal_name, suffix)
write_text_file(language_name, file_name, value) write_text_file(language_name, file_name, value)
# Ensure that default (read: "English") journal is available # Ensure that default (read: "English") journal is available
if "English" not in translations: if "English" not in translations:
language_name, value = next(((language_name, value) for language_name, value in translations.items() language_name, value = next(
if language_name in _SP_LANGUAGES), (None, None)) (
(language_name, value)
for language_name, value in translations.items()
if language_name in _SP_LANGUAGES
),
(None, None),
)
if language_name is not None: if language_name is not None:
file_name = "{}--{}.txt".format(age_name, journal_name) file_name = "{}--{}.txt".format(age_name, journal_name)
# If you manage to screw up this badly... Well, I am very sorry. # If you manage to screw up this badly... Well, I am very sorry.
if write_text_file(language_name, file_name, value): if write_text_file(language_name, file_name, value):
self._report.warn("No 'English' translation available, so '{}' will be used as the default", self._report.warn(
language_name, indent=2) "No 'English' translation available, so '{}' will be used as the default",
language_name,
indent=2,
)
else: else:
self._report.port("No 'English' nor any other suitable default translation available", indent=2) self._report.port(
"No 'English' nor any other suitable default translation available",
indent=2,
)
def _generate_loc_files(self): def _generate_loc_files(self):
if not self._strings: if not self._strings:
@ -131,7 +166,11 @@ class LocalizationConverter:
database[language_name][set_name][element_name] = value database[language_name][set_name][element_name] = value
for language_name, sets in database.items(): for language_name, sets in database.items():
self._generate_loc_file("{}{}.loc".format(self._age_name, language_name), sets, language_name) self._generate_loc_file(
"{}{}.loc".format(self._age_name, language_name),
sets,
language_name,
)
# Generate an empty localization file to defeat any old ones from Korman 0.11 (and lower) # Generate an empty localization file to defeat any old ones from Korman 0.11 (and lower)
if method == "database_back_compat": if method == "database_back_compat":
@ -156,21 +195,25 @@ class LocalizationConverter:
enc = plEncryptedStream.kEncAes if self._version == pvEoa else None enc = plEncryptedStream.kEncAes if self._version == pvEoa else None
with self._generate_file(filename, enc=enc) as stream: with self._generate_file(filename, enc=enc) as stream:
write_line("<?xml version=\"1.0\" encoding=\"utf-8\"?>") write_line('<?xml version="1.0" encoding="utf-8"?>')
write_line("<localizations>") write_line("<localizations>")
write_line("<age name=\"{}\">", self._age_name, indent=1) write_line('<age name="{}">', self._age_name, indent=1)
for set_name, elements in sets.items(): for set_name, elements in sets.items():
write_line("<set name=\"{}\">", set_name, indent=2) write_line('<set name="{}">', set_name, indent=2)
for element_name, value in elements.items(): for element_name, value in elements.items():
write_line("<element name=\"{}\">", element_name, indent=3) write_line('<element name="{}">', element_name, indent=3)
for translation_language, translation_value in iter_element(value): for translation_language, translation_value in iter_element(value):
if _ESHTML_REGEX.search(translation_value): if _ESHTML_REGEX.search(translation_value):
encoded_value = "<![CDATA[{}]]>".format(translation_value) encoded_value = "<![CDATA[{}]]>".format(translation_value)
else: else:
encoded_value = xml_escape(translation_value) encoded_value = xml_escape(translation_value)
write_line("<translation language=\"{language}\">{translation}</translation>", write_line(
language=translation_language, translation=encoded_value, indent=4) '<translation language="{language}">{translation}</translation>',
language=translation_language,
translation=encoded_value,
indent=4,
)
write_line("</element>", indent=3) write_line("</element>", indent=3)
write_line("</set>", indent=2) write_line("</set>", indent=2)
@ -182,8 +225,14 @@ class LocalizationConverter:
def run(self): def run(self):
age_props = bpy.context.scene.world.plasma_age age_props = bpy.context.scene.world.plasma_age
loc_path = str(Path(self._path) / "dat" / "{}.loc".format(self._age_name)) loc_path = str(Path(self._path) / "dat" / "{}.loc".format(self._age_name))
log = logger.ExportVerboseLogger if age_props.verbose else logger.ExportProgressLogger log = (
with korlib.ConsoleToggler(age_props.show_console), log(loc_path) as self._report: logger.ExportVerboseLogger
if age_props.verbose
else logger.ExportProgressLogger
)
with korlib.ConsoleToggler(age_props.show_console), log(
loc_path
) as self._report:
self._report.progress_add_step("Harvesting Translations") self._report.progress_add_step("Harvesting Translations")
self._report.progress_add_step("Generating Localization") self._report.progress_add_step("Generating Localization")
self._report.progress_start("Exporting Localization Data") self._report.progress_start("Exporting Localization Data")
@ -204,15 +253,29 @@ class LocalizationConverter:
inc_progress = self._report.progress_increment inc_progress = self._report.progress_increment
for i in objects: for i in objects:
for mod_type in filter(None, (getattr(j, "pl_id", None) for j in TranslationMixin.__subclasses__())): for mod_type in filter(
None,
(getattr(j, "pl_id", None) for j in TranslationMixin.__subclasses__()),
):
modifier = getattr(i.plasma_modifiers, mod_type) modifier = getattr(i.plasma_modifiers, mod_type)
if modifier.enabled: if modifier.enabled:
translations = [j for j in modifier.translations if j.text_id is not None] translations = [
j for j in modifier.translations if j.text_id is not None
]
if not translations: if not translations:
self._report.error("'{}': No content translations available. The localization will not be exported.", self._report.error(
i.name, indent=2) "'{}': No content translations available. The localization will not be exported.",
i.name,
indent=2,
)
for j in translations: for j in translations:
self.add_string(modifier.localization_set, modifier.key_name, j.language, j.text_id, indent=1) self.add_string(
modifier.localization_set,
modifier.key_name,
j.language,
j.text_id,
indent=1,
)
inc_progress() inc_progress()
def _run_generate(self): def _run_generate(self):

73
korman/exporter/logger.py

@ -23,6 +23,7 @@ _HEADING_SIZE = 60
_MAX_ELIPSES = 3 _MAX_ELIPSES = 3
_MAX_TIME_UNTIL_ELIPSES = 2.0 _MAX_TIME_UNTIL_ELIPSES = 2.0
class _ExportLogger: class _ExportLogger:
def __init__(self, print_logs, age_path=None): def __init__(self, print_logs, age_path=None):
self._errors = [] self._errors = []
@ -37,7 +38,9 @@ class _ExportLogger:
if self._age_path is not None: if self._age_path is not None:
# Make the log file name from the age file path -- this ensures we're not trying to write # Make the log file name from the age file path -- this ensures we're not trying to write
# the log file to the same directory Blender.exe is in, which might be a permission error # the log file to the same directory Blender.exe is in, which might be a permission error
my_path = self._age_path.with_name("{}_export".format(self._age_path.stem)).with_suffix(".log") my_path = self._age_path.with_name(
"{}_export".format(self._age_path.stem)
).with_suffix(".log")
self._file = open(str(my_path), "w") self._file = open(str(my_path), "w")
return self return self
@ -85,7 +88,6 @@ class _ExportLogger:
cache = args[0] if len(args) == 1 else args[0].format(*args[1:]) cache = args[0] if len(args) == 1 else args[0].format(*args[1:])
self._porting.append(cache) self._porting.append(cache)
def progress_add_step(self, name): def progress_add_step(self, name):
pass pass
@ -113,8 +115,12 @@ class _ExportLogger:
if num_errors == 1: if num_errors == 1:
raise NonfatalExportError(self._errors[0]) raise NonfatalExportError(self._errors[0])
elif num_errors: elif num_errors:
raise NonfatalExportError("""{} errors were encountered during export. Check the export log for more details: raise NonfatalExportError(
{}""", num_errors, self._file.name) """{} errors were encountered during export. Check the export log for more details:
{}""",
num_errors,
self._file.name,
)
def save(self): def save(self):
# TODO # TODO
@ -160,7 +166,9 @@ class ExportProgressLogger(_ExportLogger):
if value is not None: if value is not None:
export_time = time.perf_counter() - self._time_start_overall export_time = time.perf_counter() - self._time_start_overall
with self._print_condition: with self._print_condition:
self._progress_print_step(done=(self._step_progress == self._step_max), error=True) self._progress_print_step(
done=(self._step_progress == self._step_max), error=True
)
self._progress_print_line("\nABORTED AFTER {:.2f}s".format(export_time)) self._progress_print_line("\nABORTED AFTER {:.2f}s".format(export_time))
self._progress_print_heading("ERROR") self._progress_print_heading("ERROR")
self._progress_print_line(str(value)) self._progress_print_line(str(value))
@ -191,13 +199,17 @@ class ExportProgressLogger(_ExportLogger):
def progress_end(self): def progress_end(self):
self._progress_print_step(done=True) self._progress_print_step(done=True)
assert self._step_id+1 == len(self._progress_steps) assert self._step_id + 1 == len(self._progress_steps)
export_time = time.perf_counter() - self._time_start_overall export_time = time.perf_counter() - self._time_start_overall
with self._print_condition: with self._print_condition:
if self._age_path is not None: if self._age_path is not None:
self.msg("\nExported '{}' in {:.2f}s", self._age_path.name, export_time) self.msg("\nExported '{}' in {:.2f}s", self._age_path.name, export_time)
self._progress_print_line("\nEXPORTED '{}' IN {:.2f}s".format(self._age_path.name, export_time)) self._progress_print_line(
"\nEXPORTED '{}' IN {:.2f}s".format(
self._age_path.name, export_time
)
)
else: else:
self._progress_print_line("\nCOMPLETED IN {:.2f}s".format(export_time)) self._progress_print_line("\nCOMPLETED IN {:.2f}s".format(export_time))
self._progress_print_heading() self._progress_print_heading()
@ -230,7 +242,9 @@ class ExportProgressLogger(_ExportLogger):
num_chars = len(text) num_chars = len(text)
border = "-" * int((_HEADING_SIZE - (num_chars + 2)) / 2) border = "-" * int((_HEADING_SIZE - (num_chars + 2)) / 2)
pad = " " if num_chars % 2 == 1 else "" pad = " " if num_chars % 2 == 1 else ""
line = "{border} {pad}{text} {border}".format(border=border, pad=pad, text=text) line = "{border} {pad}{text} {border}".format(
border=border, pad=pad, text=text
)
self._progress_print_line(line) self._progress_print_line(line)
else: else:
self._progress_print_line("-" * _HEADING_SIZE) self._progress_print_line("-" * _HEADING_SIZE)
@ -238,7 +252,9 @@ class ExportProgressLogger(_ExportLogger):
def _progress_print_step(self, done=False, error=False): def _progress_print_step(self, done=False, error=False):
with self._print_condition: with self._print_condition:
if done: if done:
stage = "DONE IN {:.2f}s".format(time.perf_counter() - self._time_start_step) stage = "DONE IN {:.2f}s".format(
time.perf_counter() - self._time_start_step
)
print_func = self._progress_print_line print_func = self._progress_print_line
self._progress_print_volatile("") self._progress_print_volatile("")
else: else:
@ -246,28 +262,39 @@ class ExportProgressLogger(_ExportLogger):
stage = "{} of {}".format(self._step_progress, self._step_max) stage = "{} of {}".format(self._step_progress, self._step_max)
else: else:
stage = "" stage = ""
print_func = self._progress_print_line if error else self._progress_print_volatile print_func = (
self._progress_print_line
if error
else self._progress_print_volatile
)
# ALLLLL ABOARD!!!!! HAHAHAHA # ALLLLL ABOARD!!!!! HAHAHAHA
step_name = self._progress_steps[self._step_id] step_name = self._progress_steps[self._step_id]
whitespace = ' ' * (self._step_spacing - len(step_name)) whitespace = " " * (self._step_spacing - len(step_name))
num_steps = len(self._progress_steps) num_steps = len(self._progress_steps)
step_id = self._step_id + 1 step_id = self._step_id + 1
stage_max_whitespace = len(str(num_steps)) * 2 stage_max_whitespace = len(str(num_steps)) * 2
stage_space_used = len(str(step_id)) + len(str(num_steps)) stage_space_used = len(str(step_id)) + len(str(num_steps))
stage_whitespace = ' ' * (stage_max_whitespace - stage_space_used + 1) stage_whitespace = " " * (stage_max_whitespace - stage_space_used + 1)
# f-strings would be nice here... # f-strings would be nice here...
line = "{step_name}{step_whitespace}(step {step_id}/{num_steps}):{stage_whitespace}{stage}".format( line = "{step_name}{step_whitespace}(step {step_id}/{num_steps}):{stage_whitespace}{stage}".format(
step_name=step_name, step_whitespace=whitespace, step_id=step_id, num_steps=num_steps, step_name=step_name,
stage_whitespace=stage_whitespace, stage=stage) step_whitespace=whitespace,
step_id=step_id,
num_steps=num_steps,
stage_whitespace=stage_whitespace,
stage=stage,
)
print_func(line) print_func(line)
def _progress_get_max(self): def _progress_get_max(self):
return self._step_max return self._step_max
def _progress_set_max(self, value): def _progress_set_max(self, value):
assert self._step_id != -1 assert self._step_id != -1
self._step_max = value self._step_max = value
self._progress_print_step() self._progress_print_step()
progress_range = property(_progress_get_max, _progress_set_max) progress_range = property(_progress_get_max, _progress_set_max)
def progress_start(self, action): def progress_start(self, action):
@ -289,13 +316,13 @@ class ExportProgressLogger(_ExportLogger):
while self._progress_alive: while self._progress_alive:
with self._print_condition: with self._print_condition:
signalled = self._print_condition.wait(timeout=1.0) signalled = self._print_condition.wait(timeout=1.0)
print(end='\r') print(end="\r")
# First, we need to print out any queued whole lines. # First, we need to print out any queued whole lines.
# NOTE: no need to lock anything here as Blender uses CPython (GIL) # NOTE: no need to lock anything here as Blender uses CPython (GIL)
with self._cursor: with self._cursor:
if self._queued_lines: if self._queued_lines:
print(*self._queued_lines, sep='\n') print(*self._queued_lines, sep="\n")
self._queued_lines.clear() self._queued_lines.clear()
# Now, we need to print out the current volatile line, if any. # Now, we need to print out the current volatile line, if any.
@ -307,20 +334,28 @@ class ExportProgressLogger(_ExportLogger):
# If the proc is long running, let us display some elipses so as to not alarm the user # If the proc is long running, let us display some elipses so as to not alarm the user
if self._time_start_step != 0: if self._time_start_step != 0:
if (time.perf_counter() - self._time_start_step) > _MAX_TIME_UNTIL_ELIPSES: if (
num_dots = 0 if signalled or num_dots == _MAX_ELIPSES else num_dots + 1 time.perf_counter() - self._time_start_step
) > _MAX_TIME_UNTIL_ELIPSES:
num_dots = (
0
if signalled or num_dots == _MAX_ELIPSES
else num_dots + 1
)
else: else:
num_dots = 0 num_dots = 0
print('.' * num_dots, end=" " * (_MAX_ELIPSES - num_dots)) print("." * num_dots, end=" " * (_MAX_ELIPSES - num_dots))
self._cursor.update() self._cursor.update()
def _progress_get_current(self): def _progress_get_current(self):
return self._step_progress return self._step_progress
def _progress_set_current(self, value): def _progress_set_current(self, value):
assert self._step_id != -1 assert self._step_id != -1
self._step_progress = value self._step_progress = value
if self._step_max != 0: if self._step_max != 0:
self._progress_print_step() self._progress_print_step()
progress_value = property(_progress_get_current, _progress_set_current) progress_value = property(_progress_get_current, _progress_set_current)

55
korman/exporter/manager.py

@ -75,7 +75,7 @@ class ExportManager:
def add_object(self, pl, name=None, bl=None, loc=None, so=None): def add_object(self, pl, name=None, bl=None, loc=None, so=None):
"""Automates adding a converted Blender object to our Plasma Resource Manager""" """Automates adding a converted Blender object to our Plasma Resource Manager"""
assert (bl or loc or so) assert bl or loc or so
if loc is not None: if loc is not None:
location = loc location = loc
elif so is not None: elif so is not None:
@ -98,7 +98,7 @@ class ExportManager:
self.AddObject(location, pl) self.AddObject(location, pl)
node = self._nodes[location] node = self._nodes[location]
if node: # All objects must be in the scene node if node: # All objects must be in the scene node
if isinstance(pl, plSceneObject): if isinstance(pl, plSceneObject):
node.addSceneObject(pl.key) node.addSceneObject(pl.key)
pl.sceneNode = node.key pl.sceneNode = node.key
@ -145,7 +145,9 @@ class ExportManager:
if want_pysdl: if want_pysdl:
self._pack_agesdl_hook(age) self._pack_agesdl_hook(age)
sdl = self.add_object(plSceneObject, name="AgeSDLHook", loc=builtin) sdl = self.add_object(plSceneObject, name="AgeSDLHook", loc=builtin)
pfm = self.add_object(plPythonFileMod, name="VeryVerySpecialPythonFileMod", so=sdl) pfm = self.add_object(
plPythonFileMod, name="VeryVerySpecialPythonFileMod", so=sdl
)
pfm.filename = replace_python2_identifier(age) pfm.filename = replace_python2_identifier(age)
# Textures.prp # Textures.prp
@ -191,7 +193,7 @@ class ExportManager:
else: else:
return plEncryptedStream.kEncXtea return plEncryptedStream.kEncXtea
def find_interfaces(self, pClass, so : plSceneObject) -> Iterable[plObjInterface]: def find_interfaces(self, pClass, so: plSceneObject) -> Iterable[plObjInterface]:
assert issubclass(pClass, plObjInterface) assert issubclass(pClass, plObjInterface)
for i in (i.object for i in so.interfaces): for i in (i.object for i in so.interfaces):
@ -231,8 +233,12 @@ class ExportManager:
# potentially cause URU to crash. I'm uncertain though, so we'll just warn # potentially cause URU to crash. I'm uncertain though, so we'll just warn
# for now. # for now.
if issubclass(pClass, plSingleModifier): if issubclass(pClass, plSingleModifier):
self._exporter().report.warn("Adding SingleModifier '{}' (type: '{}'') to another SceneObject '{}'", self._exporter().report.warn(
key.name, pClass.__name__[2:], so.key.name) "Adding SingleModifier '{}' (type: '{}'') to another SceneObject '{}'",
key.name,
pClass.__name__[2:],
so.key.name,
)
so.addModifier(key) so.addModifier(key)
return key return key
@ -281,7 +287,10 @@ class ExportManager:
generator = (i for i in bpy.data.texts if i.name.lower() == namei) generator = (i for i in bpy.data.texts if i.name.lower() == namei)
result, collision = next(generator, None), next(generator, None) result, collision = next(generator, None), next(generator, None)
if collision is not None: if collision is not None:
raise explosions.ExportError("There are multiple copies of case insensitive text block '{}'.", name) raise explosions.ExportError(
"There are multiple copies of case insensitive text block '{}'.",
name,
)
return result return result
# AgeSDL Hook Python # AgeSDL Hook Python
@ -321,24 +330,44 @@ class ExportManager:
with output.generate_dat_file(f, enc=self._encryption) as stream: with output.generate_dat_file(f, enc=self._encryption) as stream:
fni = bpy.context.scene.world.plasma_fni fni = bpy.context.scene.world.plasma_fni
stream.writeLine("Graphics.Renderer.SetClearColor {:.2f} {:.2f} {:.2f}".format(*fni.clear_color)) stream.writeLine(
"Graphics.Renderer.SetClearColor {:.2f} {:.2f} {:.2f}".format(
*fni.clear_color
)
)
stream.writeLine("Graphics.Renderer.SetYon {:.1f}".format(fni.yon)) stream.writeLine("Graphics.Renderer.SetYon {:.1f}".format(fni.yon))
if fni.fog_method == "none": if fni.fog_method == "none":
stream.writeLine("Graphics.Renderer.Fog.SetDefLinear 0 0 0") stream.writeLine("Graphics.Renderer.Fog.SetDefLinear 0 0 0")
else: else:
stream.writeLine("Graphics.Renderer.Fog.SetDefColor {:.2f} {:.2f} {:.2f}".format(*fni.fog_color)) stream.writeLine(
"Graphics.Renderer.Fog.SetDefColor {:.2f} {:.2f} {:.2f}".format(
*fni.fog_color
)
)
if fni.fog_method == "linear": if fni.fog_method == "linear":
stream.writeLine("Graphics.Renderer.Fog.SetDefLinear {:.2f} {:.2f} {:.2f}".format(fni.fog_start, fni.fog_end, fni.fog_density)) stream.writeLine(
"Graphics.Renderer.Fog.SetDefLinear {:.2f} {:.2f} {:.2f}".format(
fni.fog_start, fni.fog_end, fni.fog_density
)
)
elif fni.fog_method == "exp": elif fni.fog_method == "exp":
stream.writeLine("Graphics.Renderer.Fog.SetDefExp {:.2f} {:.2f}".format(fni.fog_end, fni.fog_density)) stream.writeLine(
"Graphics.Renderer.Fog.SetDefExp {:.2f} {:.2f}".format(
fni.fog_end, fni.fog_density
)
)
elif fni.fog_method == "exp2": elif fni.fog_method == "exp2":
stream.writeLine("Graphics.Renderer.Fog.SetDefExp2 {:.2f} {:.2f}".format(fni.fog_end, fni.fog_density)) stream.writeLine(
"Graphics.Renderer.Fog.SetDefExp2 {:.2f} {:.2f}".format(
fni.fog_end, fni.fog_density
)
)
def _write_pages(self): def _write_pages(self):
age_name = self._age_info.name age_name = self._age_info.name
output = self._exporter().output output = self._exporter().output
for loc in self._pages.values(): for loc in self._pages.values():
page = self.mgr.FindPage(loc) # not cached because it's C++ owned page = self.mgr.FindPage(loc) # not cached because it's C++ owned
chapter = "_District_" if self.mgr.getVer() <= pvMoul else "_" chapter = "_District_" if self.mgr.getVer() <= pvMoul else "_"
f = "{}{}{}.prp".format(age_name, chapter, page.page) f = "{}{}{}.prp".format(age_name, chapter, page.page)

713
korman/exporter/material.py

File diff suppressed because it is too large Load Diff

205
korman/exporter/mesh.py

@ -32,6 +32,7 @@ _WARN_VERTS_PER_SPAN = 0x8000
_VERTEX_COLOR_LAYERS = {"col", "color", "colour"} _VERTEX_COLOR_LAYERS = {"col", "color", "colour"}
class _GeoSpan: class _GeoSpan:
def __init__(self, bo, bm, geospan, pass_index=None): def __init__(self, bo, bm, geospan, pass_index=None):
self.geospan = geospan self.geospan = geospan
@ -42,7 +43,9 @@ class _GeoSpan:
"""Determines the color all vertex colors should be multipled by in this span.""" """Determines the color all vertex colors should be multipled by in this span."""
if self.geospan.props & plGeometrySpan.kDiffuseFoldedIn: if self.geospan.props & plGeometrySpan.kDiffuseFoldedIn:
color = bm.diffuse_color color = bm.diffuse_color
base_layer = self.geospan.material.object.layers[0].object.bottomOfStack.object base_layer = self.geospan.material.object.layers[
0
].object.bottomOfStack.object
return (color.r, color.b, color.g, base_layer.opacity) return (color.r, color.b, color.g, base_layer.opacity)
if not bo.plasma_modifiers.lighting.preshade: if not bo.plasma_modifiers.lighting.preshade:
return (0.0, 0.0, 0.0, 0.0) return (0.0, 0.0, 0.0, 0.0)
@ -57,7 +60,7 @@ class _RenderLevel:
MAJOR_LATE = 8 MAJOR_LATE = 8
_MAJOR_SHIFT = 28 _MAJOR_SHIFT = 28
_MINOR_MASK = ((1 << _MAJOR_SHIFT) - 1) _MINOR_MASK = (1 << _MAJOR_SHIFT) - 1
def __init__(self, bo, pass_index, blend_span=False): def __init__(self, bo, pass_index, blend_span=False):
if blend_span: if blend_span:
@ -75,20 +78,24 @@ class _RenderLevel:
def _get_major(self): def _get_major(self):
return self.level >> self._MAJOR_SHIFT return self.level >> self._MAJOR_SHIFT
def _set_major(self, value): def _set_major(self, value):
self.level = self._calc_level(value, self.minor) self.level = self._calc_level(value, self.minor)
major = property(_get_major, _set_major) major = property(_get_major, _set_major)
def _get_minor(self): def _get_minor(self):
return self.level & self._MINOR_MASK return self.level & self._MINOR_MASK
def _set_minor(self, value): def _set_minor(self, value):
self.level = self._calc_level(self.major, value) self.level = self._calc_level(self.major, value)
minor = property(_get_minor, _set_minor) minor = property(_get_minor, _set_minor)
def _calc_level(self, major : int, minor : int=0) -> int: def _calc_level(self, major: int, minor: int = 0) -> int:
return ((major << self._MAJOR_SHIFT) & 0xFFFFFFFF) | minor return ((major << self._MAJOR_SHIFT) & 0xFFFFFFFF) | minor
def _determine_level(self, bo : bpy.types.Object, blend_span : bool) -> int: def _determine_level(self, bo: bpy.types.Object, blend_span: bool) -> int:
mods = bo.plasma_modifiers mods = bo.plasma_modifiers
if mods.test_property("draw_framebuf"): if mods.test_property("draw_framebuf"):
return self._calc_level(self.MAJOR_FRAMEBUF) return self._calc_level(self.MAJOR_FRAMEBUF)
@ -175,7 +182,11 @@ class _MeshManager:
ident = i.identifier ident = i.identifier
if ident == "rna_type": if ident == "rna_type":
continue continue
props[ident] = getattr(bstruct, ident) if getattr(i, "array_length", 0) == 0 else tuple(getattr(bstruct, ident)) props[ident] = (
getattr(bstruct, ident)
if getattr(i, "array_length", 0) == 0
else tuple(getattr(bstruct, ident))
)
return props return props
def __enter__(self): def __enter__(self):
@ -197,7 +208,7 @@ class _MeshManager:
if isinstance(i.data, mesh_type) and i.is_modified(scene, "RENDER"): if isinstance(i.data, mesh_type) and i.is_modified(scene, "RENDER"):
# Remember, storing actual pointers to the Blender objects can cause bad things to # Remember, storing actual pointers to the Blender objects can cause bad things to
# happen because Blender's memory management SUCKS! # happen because Blender's memory management SUCKS!
self._overrides[i.name] = { "mesh": i.data.name, "modifiers": [] } self._overrides[i.name] = {"mesh": i.data.name, "modifiers": []}
i.data = i.to_mesh(scene, True, "RENDER", calc_tessface=False) i.data = i.to_mesh(scene, True, "RENDER", calc_tessface=False)
# If the modifiers are left on the object, the lightmap bake can break under some # If the modifiers are left on the object, the lightmap bake can break under some
@ -223,11 +234,16 @@ class _MeshManager:
data_meshes.remove(trash_mesh) data_meshes.remove(trash_mesh)
# If modifiers were removed, reapply them now unless they're read-only. # If modifiers were removed, reapply them now unless they're read-only.
readonly_attributes = {("DECIMATE", "face_count"),} readonly_attributes = {
("DECIMATE", "face_count"),
}
for cached_mod in override["modifiers"]: for cached_mod in override["modifiers"]:
mod = bo.modifiers.new(cached_mod["name"], cached_mod["type"]) mod = bo.modifiers.new(cached_mod["name"], cached_mod["type"])
for key, value in cached_mod.items(): for key, value in cached_mod.items():
if key in {"name", "type"} or (cached_mod["type"], key) in readonly_attributes: if (
key in {"name", "type"}
or (cached_mod["type"], key) in readonly_attributes
):
continue continue
setattr(mod, key, value) setattr(mod, key, value)
self._entered = False self._entered = False
@ -275,8 +291,13 @@ class MeshConverter(_MeshManager):
alpha_layer = self._find_vtx_alpha_layer(mesh.vertex_colors) alpha_layer = self._find_vtx_alpha_layer(mesh.vertex_colors)
if alpha_layer is None: if alpha_layer is None:
return False return False
alpha_loops = (alpha_layer[i.loop_start:i.loop_start+i.loop_total] for i in polygons) alpha_loops = (
opaque = (sum(i.color) == len(i.color) for i in itertools.chain.from_iterable(alpha_loops)) alpha_layer[i.loop_start : i.loop_start + i.loop_total] for i in polygons
)
opaque = (
sum(i.color) == len(i.color)
for i in itertools.chain.from_iterable(alpha_loops)
)
has_alpha = not all(opaque) has_alpha = not all(opaque)
return has_alpha return has_alpha
@ -341,7 +362,9 @@ class MeshConverter(_MeshManager):
geospan.props |= plGeometrySpan.kPropNoShadow geospan.props |= plGeometrySpan.kPropNoShadow
# Harvest lights # Harvest lights
permaLights, permaProjs = self._exporter().light.find_material_light_keys(bo, bm) permaLights, permaProjs = self._exporter().light.find_material_light_keys(
bo, bm
)
for i in permaLights: for i in permaLights:
geospan.addPermaLight(i) geospan.addPermaLight(i)
for i in permaProjs: for i in permaProjs:
@ -375,12 +398,16 @@ class MeshConverter(_MeshManager):
# Recall that materials is a mapping of exported materials to blender material indices. # Recall that materials is a mapping of exported materials to blender material indices.
# Therefore, geodata maps blender material indices to working geometry data. # Therefore, geodata maps blender material indices to working geometry data.
# Maybe the logic is a bit inverted, but it keeps the inner loop simple. # Maybe the logic is a bit inverted, but it keeps the inner loop simple.
geodata = { idx: _GeoData(len(mesh.vertices)) for idx, _ in materials } geodata = {idx: _GeoData(len(mesh.vertices)) for idx, _ in materials}
bumpmap = self.material.get_bump_layer(bo) bumpmap = self.material.get_bump_layer(bo)
# Locate relevant vertex color layers now... # Locate relevant vertex color layers now...
lm = bo.plasma_modifiers.lightmap lm = bo.plasma_modifiers.lightmap
color = None if lm.bake_lightmap else self._find_vtx_color_layer(mesh.tessface_vertex_colors) color = (
None
if lm.bake_lightmap
else self._find_vtx_color_layer(mesh.tessface_vertex_colors)
)
alpha = self._find_vtx_alpha_layer(mesh.tessface_vertex_colors) alpha = self._find_vtx_alpha_layer(mesh.tessface_vertex_colors)
# Convert Blender faces into things we can stuff into libHSPlasma # Convert Blender faces into things we can stuff into libHSPlasma
@ -400,7 +427,12 @@ class MeshConverter(_MeshManager):
# Unpack colors # Unpack colors
if color is None: if color is None:
tessface_colors = ((1.0, 1.0, 1.0), (1.0, 1.0, 1.0), (1.0, 1.0, 1.0), (1.0, 1.0, 1.0)) tessface_colors = (
(1.0, 1.0, 1.0),
(1.0, 1.0, 1.0),
(1.0, 1.0, 1.0),
(1.0, 1.0, 1.0),
)
else: else:
src = color[i] src = color[i]
tessface_colors = (src.color1, src.color2, src.color3, src.color4) tessface_colors = (src.color1, src.color2, src.color3, src.color4)
@ -411,31 +443,63 @@ class MeshConverter(_MeshManager):
else: else:
src = alpha[i] src = alpha[i]
# average color becomes the alpha value # average color becomes the alpha value
tessface_alphas = ((sum(src.color1) / 3), (sum(src.color2) / 3), tessface_alphas = (
(sum(src.color3) / 3), (sum(src.color4) / 3)) (sum(src.color1) / 3),
(sum(src.color2) / 3),
(sum(src.color3) / 3),
(sum(src.color4) / 3),
)
if bumpmap is not None: if bumpmap is not None:
gradPass = [] gradPass = []
gradUVWs = [] gradUVWs = []
if len(tessface.vertices) != 3: if len(tessface.vertices) != 3:
gradPass.append([tessface.vertices[0], tessface.vertices[1], tessface.vertices[2]]) gradPass.append(
gradPass.append([tessface.vertices[0], tessface.vertices[2], tessface.vertices[3]]) [
gradUVWs.append((tuple((uvw[0] for uvw in tessface_uvws)), tessface.vertices[0],
tuple((uvw[1] for uvw in tessface_uvws)), tessface.vertices[1],
tuple((uvw[2] for uvw in tessface_uvws)))) tessface.vertices[2],
gradUVWs.append((tuple((uvw[0] for uvw in tessface_uvws)), ]
tuple((uvw[2] for uvw in tessface_uvws)), )
tuple((uvw[3] for uvw in tessface_uvws)))) gradPass.append(
[
tessface.vertices[0],
tessface.vertices[2],
tessface.vertices[3],
]
)
gradUVWs.append(
(
tuple((uvw[0] for uvw in tessface_uvws)),
tuple((uvw[1] for uvw in tessface_uvws)),
tuple((uvw[2] for uvw in tessface_uvws)),
)
)
gradUVWs.append(
(
tuple((uvw[0] for uvw in tessface_uvws)),
tuple((uvw[2] for uvw in tessface_uvws)),
tuple((uvw[3] for uvw in tessface_uvws)),
)
)
else: else:
gradPass.append(tessface.vertices) gradPass.append(tessface.vertices)
gradUVWs.append((tuple((uvw[0] for uvw in tessface_uvws)), gradUVWs.append(
tuple((uvw[1] for uvw in tessface_uvws)), (
tuple((uvw[2] for uvw in tessface_uvws)))) tuple((uvw[0] for uvw in tessface_uvws)),
tuple((uvw[1] for uvw in tessface_uvws)),
tuple((uvw[2] for uvw in tessface_uvws)),
)
)
for p, vids in enumerate(gradPass): for p, vids in enumerate(gradPass):
dPosDu += self._get_bump_gradient(bumpmap[1], gradUVWs[p], mesh, vids, bumpmap[0], 0) dPosDu += self._get_bump_gradient(
dPosDv += self._get_bump_gradient(bumpmap[1], gradUVWs[p], mesh, vids, bumpmap[0], 1) bumpmap[1], gradUVWs[p], mesh, vids, bumpmap[0], 0
)
dPosDv += self._get_bump_gradient(
bumpmap[1], gradUVWs[p], mesh, vids, bumpmap[0], 1
)
dPosDv = -dPosDv dPosDv = -dPosDv
# Convert to per-material indices # Convert to per-material indices
@ -444,14 +508,18 @@ class MeshConverter(_MeshManager):
# Calculate vertex colors. # Calculate vertex colors.
if mat2span_LUT: if mat2span_LUT:
mult_color = geospans[mat2span_LUT[tessface.material_index]].mult_color mult_color = geospans[
mat2span_LUT[tessface.material_index]
].mult_color
else: else:
mult_color = (1.0, 1.0, 1.0, 1.0) mult_color = (1.0, 1.0, 1.0, 1.0)
tessface_color, tessface_alpha = tessface_colors[j], tessface_alphas[j] tessface_color, tessface_alpha = tessface_colors[j], tessface_alphas[j]
vertex_color = (int(tessface_color[0] * mult_color[0] * 255), vertex_color = (
int(tessface_color[1] * mult_color[1] * 255), int(tessface_color[0] * mult_color[0] * 255),
int(tessface_color[2] * mult_color[2] * 255), int(tessface_color[1] * mult_color[1] * 255),
int(tessface_alpha * mult_color[0] * 255)) int(tessface_color[2] * mult_color[2] * 255),
int(tessface_alpha * mult_color[0] * 255),
)
# Now, we'll index into the vertex dict using the per-face elements :( # Now, we'll index into the vertex dict using the per-face elements :(
# We're using tuples because lists are not hashable. The many mathutils and PyHSPlasma # We're using tuples because lists are not hashable. The many mathutils and PyHSPlasma
@ -467,7 +535,9 @@ class MeshConverter(_MeshManager):
normal = source.normal if use_smooth else tessface.normal normal = source.normal if use_smooth else tessface.normal
# MOUL/DX9 craps its pants if any element of the normal is exactly 0.0 # MOUL/DX9 craps its pants if any element of the normal is exactly 0.0
normal = map(lambda x: max(x, 0.01) if x >= 0.0 else min(x, -0.01), normal) normal = map(
lambda x: max(x, 0.01) if x >= 0.0 else min(x, -0.01), normal
)
normal = hsVector3(*normal) normal = hsVector3(*normal)
normal.normalize() normal.normalize()
geoVertex.normal = normal geoVertex.normal = normal
@ -495,7 +565,7 @@ class MeshConverter(_MeshManager):
# PyHSPlasma now returns tuples to indicate this. # PyHSPlasma now returns tuples to indicate this.
geoUVs = list(geoVertex.uvs) geoUVs = list(geoVertex.uvs)
geoUVs[num_user_uvs] += dPosDu geoUVs[num_user_uvs] += dPosDu
geoUVs[num_user_uvs+1] += dPosDv geoUVs[num_user_uvs + 1] += dPosDv
geoVertex.uvs = geoUVs geoVertex.uvs = geoUVs
face_verts.append(data.blender2gs[vertex][coluv]) face_verts.append(data.blender2gs[vertex][coluv])
@ -520,7 +590,9 @@ class MeshConverter(_MeshManager):
# TODO: consider busting up the mesh into multiple geospans? # TODO: consider busting up the mesh into multiple geospans?
# or hack plDrawableSpans::composeGeometry to do it for us? # or hack plDrawableSpans::composeGeometry to do it for us?
if numVerts > _WARN_VERTS_PER_SPAN: if numVerts > _WARN_VERTS_PER_SPAN:
raise explosions.TooManyVerticesError(bo.data.name, geospan.material.name, numVerts) raise explosions.TooManyVerticesError(
bo.data.name, geospan.material.name, numVerts
)
# If we're bump mapping, we need to normalize our magic UVW channels # If we're bump mapping, we need to normalize our magic UVW channels
if bumpmap is not None: if bumpmap is not None:
@ -534,7 +606,6 @@ class MeshConverter(_MeshManager):
geospan.indices = data.triangles geospan.indices = data.triangles
geospan.vertices = data.vertices geospan.vertices = data.vertices
def _get_bump_gradient(self, xform, uvws, mesh, vIds, uvIdx, iUV): def _get_bump_gradient(self, xform, uvws, mesh, vIds, uvIdx, iUV):
v0 = hsVector3(*mesh.vertices[vIds[0]].co) v0 = hsVector3(*mesh.vertices[vIds[0]].co)
v1 = hsVector3(*mesh.vertices[vIds[1]].co) v1 = hsVector3(*mesh.vertices[vIds[1]].co)
@ -576,11 +647,19 @@ class MeshConverter(_MeshManager):
def _enumerate_materials(self, bo, mesh): def _enumerate_materials(self, bo, mesh):
material_source = mesh.materials material_source = mesh.materials
valid_materials = set((tf.material_index for tf in mesh.tessfaces if material_source[tf.material_index] is not None)) valid_materials = set(
(
tf.material_index
for tf in mesh.tessfaces
if material_source[tf.material_index] is not None
)
)
# Sequence of tuples (material_index, material) # Sequence of tuples (material_index, material)
return sorted(((i, material_source[i]) for i in valid_materials), key=lambda x: x[0]) return sorted(
((i, material_source[i]) for i in valid_materials), key=lambda x: x[0]
)
def export_object(self, bo, so : plSceneObject): def export_object(self, bo, so: plSceneObject):
# If this object has modifiers, then it's a unique mesh, and we don't need to try caching it # If this object has modifiers, then it's a unique mesh, and we don't need to try caching it
# Otherwise, let's *try* to share meshes as best we can... # Otherwise, let's *try* to share meshes as best we can...
if bo.modifiers: if bo.modifiers:
@ -628,8 +707,12 @@ class MeshConverter(_MeshManager):
_diindices = {} _diindices = {}
for i in geospans: for i in geospans:
dspan = self._find_create_dspan(bo, i.geospan, i.pass_index) dspan = self._find_create_dspan(bo, i.geospan, i.pass_index)
self._report.msg("Exported hsGMaterial '{}' geometry into '{}'", self._report.msg(
i.geospan.material.name, dspan.key.name, indent=1) "Exported hsGMaterial '{}' geometry into '{}'",
i.geospan.material.name,
dspan.key.name,
indent=1,
)
idx = dspan.addSourceSpan(i.geospan) idx = dspan.addSourceSpan(i.geospan)
diidx = _diindices.setdefault(dspan, []) diidx = _diindices.setdefault(dspan, [])
diidx.append(idx) diidx.append(idx)
@ -648,7 +731,9 @@ class MeshConverter(_MeshManager):
waveset_mod = bo.plasma_modifiers.water_basic waveset_mod = bo.plasma_modifiers.water_basic
if waveset_mod.enabled: if waveset_mod.enabled:
if len(materials) > 1: if len(materials) > 1:
msg = "'{}' is a WaveSet -- only one material is supported".format(bo.name) msg = "'{}' is a WaveSet -- only one material is supported".format(
bo.name
)
self._exporter().report.warn(msg, indent=1) self._exporter().report.warn(msg, indent=1)
blmat = materials[0][1] blmat = materials[0][1]
self._check_vtx_nonpreshaded(bo, mesh, 0, blmat) self._check_vtx_nonpreshaded(bo, mesh, 0, blmat)
@ -656,8 +741,12 @@ class MeshConverter(_MeshManager):
geospan = self._create_geospan(bo, mesh, None, blmat, matKey) geospan = self._create_geospan(bo, mesh, None, blmat, matKey)
# FIXME: Can some of this be generalized? # FIXME: Can some of this be generalized?
geospan.props |= (plGeometrySpan.kWaterHeight | plGeometrySpan.kLiteVtxNonPreshaded | geospan.props |= (
plGeometrySpan.kPropReverseSort | plGeometrySpan.kPropNoShadow) plGeometrySpan.kWaterHeight
| plGeometrySpan.kLiteVtxNonPreshaded
| plGeometrySpan.kPropReverseSort
| plGeometrySpan.kPropNoShadow
)
geospan.waterHeight = bo.matrix_world.translation[2] geospan.waterHeight = bo.matrix_world.translation[2]
return [_GeoSpan(bo, blmat, geospan)], None return [_GeoSpan(bo, blmat, geospan)], None
else: else:
@ -666,9 +755,12 @@ class MeshConverter(_MeshManager):
for i, (blmat_idx, blmat) in enumerate(materials): for i, (blmat_idx, blmat) in enumerate(materials):
self._check_vtx_nonpreshaded(bo, mesh, blmat_idx, blmat) self._check_vtx_nonpreshaded(bo, mesh, blmat_idx, blmat)
matKey = self.material.export_material(bo, blmat) matKey = self.material.export_material(bo, blmat)
geospans[i] = _GeoSpan(bo, blmat, geospans[i] = _GeoSpan(
self._create_geospan(bo, mesh, blmat_idx, blmat, matKey), bo,
blmat.pass_index) blmat,
self._create_geospan(bo, mesh, blmat_idx, blmat, matKey),
blmat.pass_index,
)
mat2span_LUT[blmat_idx] = i mat2span_LUT[blmat_idx] = i
return geospans, mat2span_LUT return geospans, mat2span_LUT
@ -689,7 +781,9 @@ class MeshConverter(_MeshManager):
# AgeName_[District_]_Page_RenderLevel_Crit[Blend]Spans # AgeName_[District_]_Page_RenderLevel_Crit[Blend]Spans
# Just because it's nice to be consistent # Just because it's nice to be consistent
node = self._mgr.get_scene_node(location=location) node = self._mgr.get_scene_node(location=location)
name = "{}_{:08X}_{:X}{}".format(node.name, crit.render_level.level, crit.criteria, crit.span_type) name = "{}_{:08X}_{:X}{}".format(
node.name, crit.render_level.level, crit.criteria, crit.span_type
)
dspan = self._mgr.add_object(pl=plDrawableSpans, name=name, loc=location) dspan = self._mgr.add_object(pl=plDrawableSpans, name=name, loc=location)
criteria = crit.criteria criteria = crit.criteria
@ -699,7 +793,7 @@ class MeshConverter(_MeshManager):
if criteria & plDrawable.kCritSortSpans: if criteria & plDrawable.kCritSortSpans:
dspan.props |= plDrawable.kPropSortSpans dspan.props |= plDrawable.kPropSortSpans
dspan.renderLevel = crit.render_level.level dspan.renderLevel = crit.render_level.level
dspan.sceneNode = node # AddViaNotify dspan.sceneNode = node # AddViaNotify
self._dspans[location][crit] = dspan self._dspans[location][crit] = dspan
return dspan return dspan
@ -707,13 +801,18 @@ class MeshConverter(_MeshManager):
return self._dspans[location][crit] return self._dspans[location][crit]
def _find_vtx_alpha_layer(self, color_collection): def _find_vtx_alpha_layer(self, color_collection):
alpha_layer = next((i for i in color_collection if i.name.lower() == "alpha"), None) alpha_layer = next(
(i for i in color_collection if i.name.lower() == "alpha"), None
)
if alpha_layer is not None: if alpha_layer is not None:
return alpha_layer.data return alpha_layer.data
return None return None
def _find_vtx_color_layer(self, color_collection): def _find_vtx_color_layer(self, color_collection):
manual_layer = next((i for i in color_collection if i.name.lower() in _VERTEX_COLOR_LAYERS), None) manual_layer = next(
(i for i in color_collection if i.name.lower() in _VERTEX_COLOR_LAYERS),
None,
)
if manual_layer is not None: if manual_layer is not None:
return manual_layer.data return manual_layer.data
baked_layer = color_collection.get("autocolor") baked_layer = color_collection.get("autocolor")

136
korman/exporter/outfile.py

@ -31,6 +31,7 @@ import zipfile
_CHUNK_SIZE = 0xA00000 _CHUNK_SIZE = 0xA00000
_encoding = locale.getpreferredencoding(False) _encoding = locale.getpreferredencoding(False)
def _hashfile(filename, hasher, block=0xFFFF): def _hashfile(filename, hasher, block=0xFFFF):
with open(str(filename), "rb") as handle: with open(str(filename), "rb") as handle:
h = hasher() h = hasher()
@ -40,6 +41,7 @@ def _hashfile(filename, hasher, block=0xFFFF):
data = handle.read(block) data = handle.read(block)
return h.digest() return h.digest()
@enum.unique @enum.unique
class _FileType(enum.Enum): class _FileType(enum.Enum):
generated_dat = 0 generated_dat = 0
@ -58,6 +60,7 @@ _GATHER_BUILD = {
_FileType.video: "avi", _FileType.video: "avi",
} }
class _OutputFile: class _OutputFile:
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.file_type = kwargs.get("file_type") self.file_type = kwargs.get("file_type")
@ -71,7 +74,9 @@ class _OutputFile:
if self.file_type in (_FileType.generated_dat, _FileType.generated_ancillary): if self.file_type in (_FileType.generated_dat, _FileType.generated_ancillary):
self.file_data = kwargs.get("file_data", None) self.file_data = kwargs.get("file_data", None)
self.file_path = kwargs.get("file_path", None) self.file_path = kwargs.get("file_path", None)
self.mod_time = Path(self.file_path).stat().st_mtime if self.file_path else None self.mod_time = (
Path(self.file_path).stat().st_mtime if self.file_path else None
)
# need either a data buffer OR a file path # need either a data buffer OR a file path
assert bool(self.file_data) ^ bool(self.file_path) assert bool(self.file_data) ^ bool(self.file_path)
@ -121,7 +126,9 @@ class _OutputFile:
with plEncryptedStream().open(backing_stream, fmCreate, enc) as enc_stream: with plEncryptedStream().open(backing_stream, fmCreate, enc) as enc_stream:
if self.file_path: if self.file_path:
if plEncryptedStream.IsFileEncrypted(self.file_path): if plEncryptedStream.IsFileEncrypted(self.file_path):
with plEncryptedStream().open(self.file_path, fmRead, plEncryptedStream.kEncAuto) as dec_stream: with plEncryptedStream().open(
self.file_path, fmRead, plEncryptedStream.kEncAuto
) as dec_stream:
self._enc_spin_wash(enc_stream, dec_stream) self._enc_spin_wash(enc_stream, dec_stream)
else: else:
with hsFileStream().open(self.file_path, fmRead) as dec_stream: with hsFileStream().open(self.file_path, fmRead) as dec_stream:
@ -188,45 +195,63 @@ class OutputFiles:
self._time = time.time() self._time = time.time()
def add_ancillary(self, filename, dirname="", text_id=None, str_data=None): def add_ancillary(self, filename, dirname="", text_id=None, str_data=None):
of = _OutputFile(file_type=_FileType.generated_ancillary, of = _OutputFile(
dirname=dirname, filename=filename, file_type=_FileType.generated_ancillary,
id_data=text_id, file_data=str_data) dirname=dirname,
filename=filename,
id_data=text_id,
file_data=str_data,
)
self._files.add(of) self._files.add(of)
def add_python_code(self, filename, text_id=None, str_data=None): def add_python_code(self, filename, text_id=None, str_data=None):
assert filename not in self._py_files assert filename not in self._py_files
of = _OutputFile(file_type=_FileType.python_code, of = _OutputFile(
dirname="Python", filename=filename, file_type=_FileType.python_code,
id_data=text_id, file_data=str_data, dirname="Python",
skip_hash=True, filename=filename,
internal=(self._version != pvMoul), id_data=text_id,
needs_glue=False) file_data=str_data,
skip_hash=True,
internal=(self._version != pvMoul),
needs_glue=False,
)
self._files.add(of) self._files.add(of)
self._py_files.add(filename) self._py_files.add(filename)
def add_python_mod(self, filename, text_id=None, str_data=None): def add_python_mod(self, filename, text_id=None, str_data=None):
assert filename not in self._py_files assert filename not in self._py_files
of = _OutputFile(file_type=_FileType.python_code, of = _OutputFile(
dirname="Python", filename=filename, file_type=_FileType.python_code,
id_data=text_id, file_data=str_data, dirname="Python",
skip_hash=True, filename=filename,
internal=(self._version != pvMoul), id_data=text_id,
needs_glue=True) file_data=str_data,
skip_hash=True,
internal=(self._version != pvMoul),
needs_glue=True,
)
self._files.add(of) self._files.add(of)
self._py_files.add(filename) self._py_files.add(filename)
def add_sdl(self, filename, text_id=None, str_data=None): def add_sdl(self, filename, text_id=None, str_data=None):
of = _OutputFile(file_type=_FileType.sdl, of = _OutputFile(
dirname="SDL", filename=filename, file_type=_FileType.sdl,
id_data=text_id, file_data=str_data, dirname="SDL",
enc=self.super_secure_encryption) filename=filename,
id_data=text_id,
file_data=str_data,
enc=self.super_secure_encryption,
)
self._files.add(of) self._files.add(of)
def add_sfx(self, sound_id): def add_sfx(self, sound_id):
of = _OutputFile(file_type=_FileType.sfx, of = _OutputFile(
dirname="sfx", filename=sound_id.name, file_type=_FileType.sfx,
id_data=sound_id) dirname="sfx",
filename=sound_id.name,
id_data=sound_id,
)
self._files.add(of) self._files.add(of)
@contextmanager @contextmanager
@ -243,7 +268,7 @@ class OutputFiles:
else: else:
file_path = self._export_path.joinpath(dirname, filename) file_path = self._export_path.joinpath(dirname, filename)
file_path.parent.mkdir(parents=True, exist_ok=True) file_path.parent.mkdir(parents=True, exist_ok=True)
file_path = str(file_path) # FIXME when we bump to Python 3.6+ file_path = str(file_path) # FIXME when we bump to Python 3.6+
stream = hsFileStream(self._version) stream = hsFileStream(self._version)
stream.open(file_path, fmCreate) stream.open(file_path, fmCreate)
backing_stream = stream backing_stream = stream
@ -274,8 +299,9 @@ class OutputFiles:
# instead of doing lots of buffer copying to encrypt as a post step. # instead of doing lots of buffer copying to encrypt as a post step.
if not bogus: if not bogus:
kwargs = { kwargs = {
"file_type": _FileType.generated_dat if dirname == "dat" else "file_type": _FileType.generated_dat
_FileType.generated_ancillary, if dirname == "dat"
else _FileType.generated_ancillary,
"dirname": dirname, "dirname": dirname,
"filename": filename, "filename": filename,
"skip_hash": kwargs.get("skip_hash", False), "skip_hash": kwargs.get("skip_hash", False),
@ -318,15 +344,24 @@ class OutputFiles:
py_code = "{}\n\n{}\n".format(i.file_data, plasma_python_glue) py_code = "{}\n\n{}\n".format(i.file_data, plasma_python_glue)
else: else:
py_code = i.file_data py_code = i.file_data
result, pyc = korlib.compyle(i.filename, py_code, py_version, report, indent=1) result, pyc = korlib.compyle(
i.filename, py_code, py_version, report, indent=1
)
if result: if result:
pyc_objects.append((i.filename, pyc)) pyc_objects.append((i.filename, pyc))
except korlib.PythonNotAvailableError as error: except korlib.PythonNotAvailableError as error:
report.warn("Python {} is not available. Your Age scripts were not packaged.", error, indent=1) report.warn(
"Python {} is not available. Your Age scripts were not packaged.",
error,
indent=1,
)
else: else:
if pyc_objects: if pyc_objects:
with self.generate_dat_file("{}.pak".format(self._exporter().age_name), with self.generate_dat_file(
dirname="Python", enc=self.super_secure_encryption) as stream: "{}.pak".format(self._exporter().age_name),
dirname="Python",
enc=self.super_secure_encryption,
) as stream:
korlib.package_python(stream, pyc_objects) korlib.package_python(stream, pyc_objects)
def save(self): def save(self):
@ -376,7 +411,10 @@ class OutputFiles:
def _write_deps(self): def _write_deps(self):
times = (self._time, self._time) times = (self._time, self._time)
func = lambda x: not x.internal and x.file_type not in (_FileType.generated_ancillary, _FileType.generated_dat) func = lambda x: not x.internal and x.file_type not in (
_FileType.generated_ancillary,
_FileType.generated_dat,
)
report = self._exporter().report report = self._exporter().report
for i in self._generate_files(func): for i in self._generate_files(func):
@ -391,8 +429,11 @@ class OutputFiles:
if i.file_path != dst_path: if i.file_path != dst_path:
shutil.copy2(i.file_path, dst_path) shutil.copy2(i.file_path, dst_path)
else: else:
report.warn("No data found for dependency file '{}'. It will not be copied into the export directory.", report.warn(
str(i.dirname / i.filename), indent=1) "No data found for dependency file '{}'. It will not be copied into the export directory.",
str(i.dirname / i.filename),
indent=1,
)
def _write_gather_build(self): def _write_gather_build(self):
report = self._exporter().report report = self._exporter().report
@ -400,17 +441,26 @@ class OutputFiles:
for i in self._generate_files(): for i in self._generate_files():
key = _GATHER_BUILD.get(i.file_type) key = _GATHER_BUILD.get(i.file_type)
if key is None: if key is None:
report.warn("Output file '{}' of type '{}' is not supported by MOULa's GatherBuild format.", report.warn(
i.file_type, i.filename) "Output file '{}' of type '{}' is not supported by MOULa's GatherBuild format.",
i.file_type,
i.filename,
)
else: else:
path_str = str(PureWindowsPath(i.dirname, i.filename)) path_str = str(PureWindowsPath(i.dirname, i.filename))
files.setdefault(key, []).append(path_str) files.setdefault(key, []).append(path_str)
self.add_ancillary("contents.json", str_data=json.dumps(files, ensure_ascii=False, indent=2)) self.add_ancillary(
"contents.json", str_data=json.dumps(files, ensure_ascii=False, indent=2)
)
def _write_sumfile(self): def _write_sumfile(self):
version = self._version version = self._version
dat_only = self._exporter().dat_only dat_only = self._exporter().dat_only
enc = plEncryptedStream.kEncAes if version >= pvEoa else plEncryptedStream.kEncXtea enc = (
plEncryptedStream.kEncAes
if version >= pvEoa
else plEncryptedStream.kEncXtea
)
filename = "{}.sum".format(self._exporter().age_name) filename = "{}.sum".format(self._exporter().age_name)
if dat_only: if dat_only:
func = lambda x: (not x.skip_hash and not x.internal) and x.dirname == "dat" func = lambda x: (not x.skip_hash and not x.internal) and x.dirname == "dat"
@ -445,7 +495,7 @@ class OutputFiles:
func = lambda x: not x.internal func = lambda x: not x.internal
report = self._exporter().report report = self._exporter().report
with zipfile.ZipFile(str(self._export_file), 'w', zipfile.ZIP_DEFLATED) as zf: with zipfile.ZipFile(str(self._export_file), "w", zipfile.ZIP_DEFLATED) as zf:
for i in self._generate_files(func): for i in self._generate_files(func):
arcpath = i.filename if dat_only else str(Path(i.dirname, i.filename)) arcpath = i.filename if dat_only else str(Path(i.dirname, i.filename))
if i.file_data: if i.file_data:
@ -458,7 +508,11 @@ class OutputFiles:
elif i.file_path: elif i.file_path:
zf.write(i.file_path, arcpath) zf.write(i.file_path, arcpath)
else: else:
report.warn("No data found for dependency file '{}'. It will not be archived.", arcpath, indent=1) report.warn(
"No data found for dependency file '{}'. It will not be archived.",
arcpath,
indent=1,
)
@property @property
def _version(self): def _version(self):

116
korman/exporter/physics.py

@ -24,11 +24,13 @@ from .explosions import ExportError, ExportAssertionError
from ..helpers import bmesh_from_object, TemporaryObject from ..helpers import bmesh_from_object, TemporaryObject
from . import utils from . import utils
def _set_phys_prop(prop, sim, phys, value=True): def _set_phys_prop(prop, sim, phys, value=True):
"""Sets properties on plGenericPhysical and plSimulationInterface (seeing as how they are duped)""" """Sets properties on plGenericPhysical and plSimulationInterface (seeing as how they are duped)"""
sim.setProperty(prop, value) sim.setProperty(prop, value)
phys.setProperty(prop, value) phys.setProperty(prop, value)
class PhysicsConverter: class PhysicsConverter:
def __init__(self, exporter): def __init__(self, exporter):
self._exporter = weakref.ref(exporter) self._exporter = weakref.ref(exporter)
@ -56,8 +58,16 @@ class PhysicsConverter:
if len(v) == 3: if len(v) == 3:
indices += v indices += v
elif len(v) == 4: elif len(v) == 4:
indices += (v[0], v[1], v[2],) indices += (
indices += (v[0], v[2], v[3],) v[0],
v[1],
v[2],
)
indices += (
v[0],
v[2],
v[3],
)
return indices return indices
def _convert_mesh_data(self, bo, physical, local_space, mat, indices=True): def _convert_mesh_data(self, bo, physical, local_space, mat, indices=True):
@ -75,7 +85,10 @@ class PhysicsConverter:
vertices = [hsVector3(*i.co) for i in mesh.vertices] vertices = [hsVector3(*i.co) for i in mesh.vertices]
else: else:
# Dagnabbit... # Dagnabbit...
vertices = [hsVector3(i.co.x * scale.x, i.co.y * scale.y, i.co.z * scale.z) for i in mesh.vertices] vertices = [
hsVector3(i.co.x * scale.x, i.co.y * scale.y, i.co.z * scale.z)
for i in mesh.vertices
]
else: else:
# apply the transform to the physical itself # apply the transform to the physical itself
utils.transform_mesh(mesh, mat) utils.transform_mesh(mesh, mat)
@ -114,7 +127,9 @@ class PhysicsConverter:
vertices = [hsVector3(*i.co) for i in mesh.vertices] vertices = [hsVector3(*i.co) for i in mesh.vertices]
else: else:
# Flatten out all points to the given Z-coordinate # Flatten out all points to the given Z-coordinate
vertices = [hsVector3(i.co.x, i.co.y, z_coord) for i in mesh.vertices] vertices = [
hsVector3(i.co.x, i.co.y, z_coord) for i in mesh.vertices
]
physical.verts = vertices physical.verts = vertices
physical.indices = self._convert_indices(mesh) physical.indices = self._convert_indices(mesh)
physical.boundsType = plSimDefs.kProxyBounds physical.boundsType = plSimDefs.kProxyBounds
@ -126,23 +141,28 @@ class PhysicsConverter:
simIface = so.sim.object simIface = so.sim.object
physical = simIface.physical.object physical = simIface.physical.object
member_group = getattr(plSimDefs, kwargs.get("member_group", "kGroupLOSOnly")) member_group = getattr(
if physical.memberGroup != member_group and member_group != plSimDefs.kGroupLOSOnly: plSimDefs, kwargs.get("member_group", "kGroupLOSOnly")
)
if (
physical.memberGroup != member_group
and member_group != plSimDefs.kGroupLOSOnly
):
self._report.warn("{}: Physical memberGroup overwritten!", bo.name) self._report.warn("{}: Physical memberGroup overwritten!", bo.name)
physical.memberGroup = member_group physical.memberGroup = member_group
self._apply_props(simIface, physical, kwargs) self._apply_props(simIface, physical, kwargs)
def generate_physical(self, bo, so, **kwargs): def generate_physical(self, bo, so, **kwargs):
"""Generates a physical object for the given object pair. """Generates a physical object for the given object pair.
The following optional arguments are allowed: The following optional arguments are allowed:
- bounds: (defaults to collision modifier setting) - bounds: (defaults to collision modifier setting)
- member_group: str attribute of plSimDefs, defaults to kGroupStatic - member_group: str attribute of plSimDefs, defaults to kGroupStatic
NOTE that kGroupLOSOnly generation will only succeed if no one else NOTE that kGroupLOSOnly generation will only succeed if no one else
has generated this physical in another group has generated this physical in another group
- properties: sequence of str bit names from plSimulationInterface - properties: sequence of str bit names from plSimulationInterface
- losdbs: sequence of str bit names from plSimDefs - losdbs: sequence of str bit names from plSimDefs
- report_groups: sequence of str bit names from plSimDefs - report_groups: sequence of str bit names from plSimDefs
- collide_groups: sequence of str bit names from plSimDefs - collide_groups: sequence of str bit names from plSimDefs
""" """
if so.sim is None: if so.sim is None:
simIface = self._mgr.add_object(pl=plSimulationInterface, bl=bo) simIface = self._mgr.add_object(pl=plSimulationInterface, bl=bo)
@ -167,12 +187,17 @@ class PhysicsConverter:
if mod.dynamic: if mod.dynamic:
if ver <= pvPots: if ver <= pvPots:
physical.collideGroup = (1 << plSimDefs.kGroupDynamic) | \ physical.collideGroup = (1 << plSimDefs.kGroupDynamic) | (
(1 << plSimDefs.kGroupStatic) 1 << plSimDefs.kGroupStatic
)
physical.memberGroup = plSimDefs.kGroupDynamic physical.memberGroup = plSimDefs.kGroupDynamic
physical.mass = mod.mass physical.mass = mod.mass
_set_phys_prop(plSimulationInterface.kStartInactive, simIface, physical, _set_phys_prop(
value=mod.start_asleep) plSimulationInterface.kStartInactive,
simIface,
physical,
value=mod.start_asleep,
)
elif not mod.avatar_blocker: elif not mod.avatar_blocker:
physical.memberGroup = plSimDefs.kGroupLOSOnly physical.memberGroup = plSimDefs.kGroupLOSOnly
else: else:
@ -181,7 +206,9 @@ class PhysicsConverter:
# Line of Sight DB # Line of Sight DB
if mod.camera_blocker: if mod.camera_blocker:
physical.LOSDBs |= plSimDefs.kLOSDBCameraBlockers physical.LOSDBs |= plSimDefs.kLOSDBCameraBlockers
_set_phys_prop(plSimulationInterface.kCameraAvoidObject, simIface, physical) _set_phys_prop(
plSimulationInterface.kCameraAvoidObject, simIface, physical
)
if mod.terrain: if mod.terrain:
physical.LOSDBs |= plSimDefs.kLOSDBAvatarWalkable physical.LOSDBs |= plSimDefs.kLOSDBAvatarWalkable
@ -189,7 +216,11 @@ class PhysicsConverter:
# This could result in a few orphaned PhysicalSndGroups, but I think that's preferable # This could result in a few orphaned PhysicalSndGroups, but I think that's preferable
# to having a bunch of empty objects...? # to having a bunch of empty objects...?
if mod.surface != "kNone": if mod.surface != "kNone":
sndgroup = self._mgr.find_create_object(plPhysicalSndGroup, so=so, name="SURFACEGEN_{}".format(mod.surface)) sndgroup = self._mgr.find_create_object(
plPhysicalSndGroup,
so=so,
name="SURFACEGEN_{}".format(mod.surface),
)
sndgroup.group = getattr(plPhysicalSndGroup, mod.surface) sndgroup.group = getattr(plPhysicalSndGroup, mod.surface)
physical.soundGroup = sndgroup.key physical.soundGroup = sndgroup.key
else: else:
@ -202,7 +233,9 @@ class PhysicsConverter:
# would miss cases where we have animated detectors (subworlds!!!) # would miss cases where we have animated detectors (subworlds!!!)
def _iter_object_tree(bo, stop_at_subworld): def _iter_object_tree(bo, stop_at_subworld):
while bo is not None: while bo is not None:
if stop_at_subworld and self.is_dedicated_subworld(bo, sanity_check=False): if stop_at_subworld and self.is_dedicated_subworld(
bo, sanity_check=False
):
return return
yield bo yield bo
bo = bo.parent bo = bo.parent
@ -229,7 +262,9 @@ class PhysicsConverter:
# Any physical that is parented by not kickable (dynamic) is passive - # Any physical that is parented by not kickable (dynamic) is passive -
# meaning we don't need to report back any changes from physics. Same for # meaning we don't need to report back any changes from physics. Same for
# plFilterCoordInterface, which filters out some axes. # plFilterCoordInterface, which filters out some axes.
if (bo.parent is not None and not mod.dynamic) or bo.plasma_object.ci_type == plFilterCoordInterface: if (
bo.parent is not None and not mod.dynamic
) or bo.plasma_object.ci_type == plFilterCoordInterface:
_set_phys_prop(plSimulationInterface.kPassive, simIface, physical) _set_phys_prop(plSimulationInterface.kPassive, simIface, physical)
# If the mass is zero, then we will fail to animate. Fix that. # If the mass is zero, then we will fail to animate. Fix that.
@ -246,7 +281,11 @@ class PhysicsConverter:
elif ver == pvMoul: elif ver == pvMoul:
if self._exporter().has_coordiface(bo): if self._exporter().has_coordiface(bo):
local_space = True local_space = True
mat = subworld.matrix_world.inverted() * bo.matrix_world if subworld else bo.matrix_world mat = (
subworld.matrix_world.inverted() * bo.matrix_world
if subworld
else bo.matrix_world
)
else: else:
local_space, mat = False, bo.matrix_world local_space, mat = False, bo.matrix_world
else: else:
@ -256,9 +295,16 @@ class PhysicsConverter:
simIface = so.sim.object simIface = so.sim.object
physical = simIface.physical.object physical = simIface.physical.object
member_group = getattr(plSimDefs, kwargs.get("member_group", "kGroupLOSOnly")) member_group = getattr(
if physical.memberGroup != member_group and member_group != plSimDefs.kGroupLOSOnly: plSimDefs, kwargs.get("member_group", "kGroupLOSOnly")
self._report.warn("{}: Physical memberGroup overwritten!", bo.name, indent=2) )
if (
physical.memberGroup != member_group
and member_group != plSimDefs.kGroupLOSOnly
):
self._report.warn(
"{}: Physical memberGroup overwritten!", bo.name, indent=2
)
physical.memberGroup = member_group physical.memberGroup = member_group
self._apply_props(simIface, physical, kwargs) self._apply_props(simIface, physical, kwargs)
@ -267,7 +313,9 @@ class PhysicsConverter:
"""Exports box bounds based on the object""" """Exports box bounds based on the object"""
physical.boundsType = plSimDefs.kBoxBounds physical.boundsType = plSimDefs.kBoxBounds
vertices = self._convert_mesh_data(bo, physical, local_space, mat, indices=False) vertices = self._convert_mesh_data(
bo, physical, local_space, mat, indices=False
)
physical.calcBoxBounds(vertices) physical.calcBoxBounds(vertices)
def _export_hull(self, bo, physical, local_space, mat): def _export_hull(self, bo, physical, local_space, mat):
@ -285,7 +333,9 @@ class PhysicsConverter:
else: else:
mesh.transform(mat) mesh.transform(mat)
result = bmesh.ops.convex_hull(mesh, input=mesh.verts, use_existing_faces=False) result = bmesh.ops.convex_hull(
mesh, input=mesh.verts, use_existing_faces=False
)
BMVert = bmesh.types.BMVert BMVert = bmesh.types.BMVert
verts = itertools.takewhile(lambda x: isinstance(x, BMVert), result["geom"]) verts = itertools.takewhile(lambda x: isinstance(x, BMVert), result["geom"])
physical.verts = [hsVector3(*i.co) for i in verts] physical.verts = [hsVector3(*i.co) for i in verts]
@ -294,7 +344,9 @@ class PhysicsConverter:
"""Exports sphere bounds based on the object""" """Exports sphere bounds based on the object"""
physical.boundsType = plSimDefs.kSphereBounds physical.boundsType = plSimDefs.kSphereBounds
vertices = self._convert_mesh_data(bo, physical, local_space, mat, indices=False) vertices = self._convert_mesh_data(
bo, physical, local_space, mat, indices=False
)
physical.calcSphereBounds(vertices) physical.calcSphereBounds(vertices)
def _export_trimesh(self, bo, physical, local_space, mat): def _export_trimesh(self, bo, physical, local_space, mat):
@ -304,7 +356,9 @@ class PhysicsConverter:
mod = bo.plasma_modifiers.collision mod = bo.plasma_modifiers.collision
if mod.enabled and mod.proxy_object is not None: if mod.enabled and mod.proxy_object is not None:
physical.boundsType = plSimDefs.kProxyBounds physical.boundsType = plSimDefs.kProxyBounds
vertices, indices = self._convert_mesh_data(mod.proxy_object, physical, local_space, mat) vertices, indices = self._convert_mesh_data(
mod.proxy_object, physical, local_space, mat
)
else: else:
physical.boundsType = plSimDefs.kExplicitBounds physical.boundsType = plSimDefs.kExplicitBounds
vertices, indices = self._convert_mesh_data(bo, physical, local_space, mat) vertices, indices = self._convert_mesh_data(bo, physical, local_space, mat)

44
korman/exporter/python.py

@ -22,6 +22,7 @@ from . import logger
from .. import korlib from .. import korlib
from ..plasma_magic import plasma_python_glue, very_very_special_python from ..plasma_magic import plasma_python_glue, very_very_special_python
class PythonPackageExporter: class PythonPackageExporter:
def __init__(self, filepath, version): def __init__(self, filepath, version):
self._filepath = filepath self._filepath = filepath
@ -52,9 +53,13 @@ class PythonPackageExporter:
code = source code = source
code = "{}\n\n{}\n".format(code, plasma_python_glue) code = "{}\n\n{}\n".format(code, plasma_python_glue)
success, result = korlib.compyle(filename, code, py_version, report, indent=1) success, result = korlib.compyle(
filename, code, py_version, report, indent=1
)
if not success: if not success:
raise ExportError("Failed to compyle '{}':\n{}".format(filename, result)) raise ExportError(
"Failed to compyle '{}':\n{}".format(filename, result)
)
py_code.append((filename, result)) py_code.append((filename, result))
inc_progress() inc_progress()
@ -68,9 +73,13 @@ class PythonPackageExporter:
code = source code = source
# no glue needed here, ma! # no glue needed here, ma!
success, result = korlib.compyle(filename, code, py_version, report, indent=1) success, result = korlib.compyle(
filename, code, py_version, report, indent=1
)
if not success: if not success:
raise ExportError("Failed to compyle '{}':\n{}".format(filename, result)) raise ExportError(
"Failed to compyle '{}':\n{}".format(filename, result)
)
py_code.append((filename, result)) py_code.append((filename, result))
inc_progress() inc_progress()
@ -88,12 +97,19 @@ class PythonPackageExporter:
if age_py.plasma_text.package or age.python_method == "all": if age_py.plasma_text.package or age.python_method == "all":
self._pfms[py_filename] = age_py self._pfms[py_filename] = age_py
else: else:
report.warn("AgeSDL Python Script provided, but not requested for packing... Using default Python.", indent=1) report.warn(
self._pfms[py_filename] = very_very_special_python.format(age_name=fixed_agename) "AgeSDL Python Script provided, but not requested for packing... Using default Python.",
indent=1,
)
self._pfms[py_filename] = very_very_special_python.format(
age_name=fixed_agename
)
else: else:
report.msg("Packing default AgeSDL Python", indent=1) report.msg("Packing default AgeSDL Python", indent=1)
very_very_special_python.format(age_name=age_props.age_name) very_very_special_python.format(age_name=age_props.age_name)
self._pfms[py_filename] = very_very_special_python.format(age_name=fixed_agename) self._pfms[py_filename] = very_very_special_python.format(
age_name=fixed_agename
)
def _harvest_pfms(self, report): def _harvest_pfms(self, report):
objects = bpy.context.scene.objects objects = bpy.context.scene.objects
@ -131,8 +147,14 @@ class PythonPackageExporter:
def run(self): def run(self):
"""Runs a stripped-down version of the Exporter that only handles Python files""" """Runs a stripped-down version of the Exporter that only handles Python files"""
age_props = bpy.context.scene.world.plasma_age age_props = bpy.context.scene.world.plasma_age
log = logger.ExportVerboseLogger if age_props.verbose else logger.ExportProgressLogger log = (
with korlib.ConsoleToggler(age_props.show_console), log(self._filepath) as report: logger.ExportVerboseLogger
if age_props.verbose
else logger.ExportProgressLogger
)
with korlib.ConsoleToggler(age_props.show_console), log(
self._filepath
) as report:
report.progress_add_step("Harvesting Plasma PythonFileMods") report.progress_add_step("Harvesting Plasma PythonFileMods")
report.progress_add_step("Harvesting Helper Python Modules") report.progress_add_step("Harvesting Helper Python Modules")
report.progress_add_step("Compyling Python Code") report.progress_add_step("Compyling Python Code")
@ -170,7 +192,9 @@ class PythonPackageExporter:
if enc is None: if enc is None:
stream = hsFileStream(self._version).open(self._filepath, fmCreate) stream = hsFileStream(self._version).open(self._filepath, fmCreate)
else: else:
stream = plEncryptedStream(self._version).open(self._filepath, fmCreate, enc) stream = plEncryptedStream(self._version).open(
self._filepath, fmCreate, enc
)
try: try:
korlib.package_python(stream, py_code) korlib.package_python(stream, py_code)
finally: finally:

52
korman/exporter/rtlight.py

@ -36,6 +36,7 @@ SHADOW_RESOLUTION = {
"HIGH": 512, "HIGH": 512,
} }
class LightConverter: class LightConverter:
def __init__(self, exporter): def __init__(self, exporter):
self._exporter = weakref.ref(exporter) self._exporter = weakref.ref(exporter)
@ -101,7 +102,7 @@ class LightConverter:
pl.spotOuter = spot_size pl.spotOuter = spot_size
blend = max(0.001, bl.spot_blend) blend = max(0.001, bl.spot_blend)
pl.spotInner = spot_size - (blend*spot_size) pl.spotInner = spot_size - (blend * spot_size)
if bl.use_halo: if bl.use_halo:
pl.falloff = bl.halo_intensity pl.falloff = bl.halo_intensity
@ -182,7 +183,9 @@ class LightConverter:
sv_bo = rtlamp.lamp_region sv_bo = rtlamp.lamp_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, sv_bo.name)) 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
@ -207,7 +210,12 @@ class LightConverter:
# projection Lamp with our own faux Material. Unfortunately, Plasma only supports projecting # projection Lamp with our own faux Material. Unfortunately, Plasma only supports projecting
# one layer. We could exploit the fUnderLay and fOverLay system to export everything, but meh. # one layer. We could exploit the fUnderLay and fOverLay system to export everything, but meh.
if len(tex_slots) > 1: if len(tex_slots) > 1:
self._report.warn("Only one texture slot can be exported per Lamp. Picking the first one: '{}'".format(slot.name), indent=3) self._report.warn(
"Only one texture slot can be exported per Lamp. Picking the first one: '{}'".format(
slot.name
),
indent=3,
)
layer = mat.export_texture_slot(bo, None, None, slot, 0, blend_flags=False) layer = mat.export_texture_slot(bo, None, None, slot, 0, blend_flags=False)
state = layer.state state = layer.state
@ -230,13 +238,21 @@ class LightConverter:
pl_light.setProperty(plLightInfo.kLPOverAll, True) pl_light.setProperty(plLightInfo.kLPOverAll, True)
elif slot.blend_type == "MULTIPLY": elif slot.blend_type == "MULTIPLY":
# From PlasmaMAX # From PlasmaMAX
state.blendFlags |= hsGMatState.kBlendMult | hsGMatState.kBlendInvertColor | hsGMatState.kBlendInvertFinalColor state.blendFlags |= (
hsGMatState.kBlendMult
| hsGMatState.kBlendInvertColor
| hsGMatState.kBlendInvertFinalColor
)
pl_light.setProperty(plLightInfo.kLPOverAll, True) pl_light.setProperty(plLightInfo.kLPOverAll, True)
pl_light.projection = layer.key pl_light.projection = layer.key
def _export_shadow_master(self, bo, rtlamp, pl_light): def _export_shadow_master(self, bo, rtlamp, pl_light):
pClass = plDirectShadowMaster if isinstance(pl_light, plDirectionalLightInfo) else plPointShadowMaster pClass = (
plDirectShadowMaster
if isinstance(pl_light, plDirectionalLightInfo)
else plPointShadowMaster
)
shadow = self.mgr.find_create_object(pClass, bl=bo) shadow = self.mgr.find_create_object(pClass, bl=bo)
shadow.attenDist = rtlamp.shadow_falloff shadow.attenDist = rtlamp.shadow_falloff
@ -249,7 +265,7 @@ class LightConverter:
def find_material_light_keys(self, bo, bm): def find_material_light_keys(self, bo, bm):
"""Given a blender material, we find the keys of all matching Plasma RT Lights. """Given a blender material, we find the keys of all matching Plasma RT Lights.
NOTE: We return a tuple of lists: ([permaLights], [permaProjs])""" NOTE: We return a tuple of lists: ([permaLights], [permaProjs])"""
self._report.msg("Searching for runtime lights...", indent=1) self._report.msg("Searching for runtime lights...", indent=1)
permaLights = [] permaLights = []
permaProjs = [] permaProjs = []
@ -279,21 +295,31 @@ class LightConverter:
break break
else: else:
# didn't find a layer where both lamp and object were, skip it. # didn't find a layer where both lamp and object were, skip it.
self._report.msg("[{}] '{}': not in same layer, skipping...", self._report.msg(
lamp.type, obj.name, indent=2) "[{}] '{}': not in same layer, skipping...",
lamp.type,
obj.name,
indent=2,
)
continue continue
# This is probably where PermaLight vs PermaProj should be sorted out... # This is probably where PermaLight vs PermaProj should be sorted out...
pl_light = self.get_light_key(obj, lamp, None) pl_light = self.get_light_key(obj, lamp, None)
if self._is_projection_lamp(lamp): if self._is_projection_lamp(lamp):
self._report.msg("[{}] PermaProj '{}'", lamp.type, obj.name, indent=2) self._report.msg(
"[{}] PermaProj '{}'", lamp.type, obj.name, indent=2
)
permaProjs.append(pl_light) permaProjs.append(pl_light)
else: else:
self._report.msg("[{}] PermaLight '{}'", lamp.type, obj.name, indent=2) self._report.msg(
"[{}] PermaLight '{}'", lamp.type, obj.name, indent=2
)
permaLights.append(pl_light) permaLights.append(pl_light)
if len(permaLights) > 8: if len(permaLights) > 8:
self._report.warn("More than 8 RT lamps on material: '{}'", bm.name, indent=1) self._report.warn(
"More than 8 RT lamps on material: '{}'", bm.name, indent=1
)
return (permaLights, permaProjs) return (permaLights, permaProjs)
@ -302,7 +328,9 @@ class LightConverter:
xlate = _BL2PL[bl_light.type] xlate = _BL2PL[bl_light.type]
return self.mgr.find_create_key(xlate, bl=bo, so=so) return self.mgr.find_create_key(xlate, bl=bo, so=so)
except LookupError: except LookupError:
raise BlenderOptionNotSupportedError("Object ('{}') lamp type '{}'".format(bo.name, bl_light.type)) raise BlenderOptionNotSupportedError(
"Object ('{}') lamp type '{}'".format(bo.name, bl_light.type)
)
def get_projectors(self, bl_light): def get_projectors(self, bl_light):
for tex in bl_light.texture_slots: for tex in bl_light.texture_slots:

21
korman/exporter/utils.py

@ -22,6 +22,7 @@ from contextlib import contextmanager
from PyHSPlasma import * from PyHSPlasma import *
def affine_parts(xform): def affine_parts(xform):
# Decompose the matrix into the 90s-era 3ds max affine parts sillyness # Decompose the matrix into the 90s-era 3ds max affine parts sillyness
# All that's missing now is something like "(c) 1998 HeadSpin" oh wait... # All that's missing now is something like "(c) 1998 HeadSpin" oh wait...
@ -35,10 +36,12 @@ def affine_parts(xform):
affine.U = quaternion(rot) affine.U = quaternion(rot)
return affine return affine
def color(blcolor, alpha=1.0): def color(blcolor, alpha=1.0):
"""Converts a Blender Color into an hsColorRGBA""" """Converts a Blender Color into an hsColorRGBA"""
return hsColorRGBA(blcolor.r, blcolor.g, blcolor.b, alpha) return hsColorRGBA(blcolor.r, blcolor.g, blcolor.b, alpha)
def matrix44(blmat): def matrix44(blmat):
"""Converts a mathutils.Matrix to an hsMatrix44""" """Converts a mathutils.Matrix to an hsMatrix44"""
hsmat = hsMatrix44() hsmat = hsMatrix44()
@ -49,15 +52,16 @@ def matrix44(blmat):
hsmat[i, 3] = blmat[i][3] hsmat[i, 3] = blmat[i][3]
return hsmat return hsmat
def quaternion(blquat): def quaternion(blquat):
"""Converts a mathutils.Quaternion to an hsQuat""" """Converts a mathutils.Quaternion to an hsQuat"""
return hsQuat(blquat.x, blquat.y, blquat.z, blquat.w) return hsQuat(blquat.x, blquat.y, blquat.z, blquat.w)
@contextmanager @contextmanager
def bmesh_temporary_object(name : str, factory : Callable, page_name : str=None): def bmesh_temporary_object(name: str, factory: Callable, page_name: str = None):
"""Creates a temporary object and mesh that exists only for the duration of """Creates a temporary object and mesh that exists only for the duration of
the context""" the context"""
mesh = bpy.data.meshes.new(name) mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh) obj = bpy.data.objects.new(name, mesh)
obj.draw_type = "WIRE" obj.draw_type = "WIRE"
@ -74,10 +78,11 @@ def bmesh_temporary_object(name : str, factory : Callable, page_name : str=None)
bm.free() bm.free()
bpy.context.scene.objects.unlink(obj) bpy.context.scene.objects.unlink(obj)
@contextmanager @contextmanager
def bmesh_object(name: str) -> Iterator[Tuple[bpy.types.Object, bmesh.types.BMesh]]: def bmesh_object(name: str) -> Iterator[Tuple[bpy.types.Object, bmesh.types.BMesh]]:
"""Creates an object and mesh that will be removed if the context is exited """Creates an object and mesh that will be removed if the context is exited
due to an error""" due to an error"""
mesh = bpy.data.meshes.new(name) mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh) obj = bpy.data.objects.new(name, mesh)
obj.draw_type = "WIRE" obj.draw_type = "WIRE"
@ -95,13 +100,16 @@ def bmesh_object(name: str) -> Iterator[Tuple[bpy.types.Object, bmesh.types.BMes
finally: finally:
bm.free() bm.free()
@contextmanager @contextmanager
def temporary_mesh_object(source : bpy.types.Object) -> bpy.types.Object: def temporary_mesh_object(source: bpy.types.Object) -> bpy.types.Object:
"""Creates a temporary mesh object from a nonmesh object that will only exist for the duration """Creates a temporary mesh object from a nonmesh object that will only exist for the duration
of the context.""" of the context."""
assert source.type != "MESH" assert source.type != "MESH"
obj = bpy.data.objects.new(source.name, source.to_mesh(bpy.context.scene, True, "RENDER")) obj = bpy.data.objects.new(
source.name, source.to_mesh(bpy.context.scene, True, "RENDER")
)
obj.draw_type = "WIRE" obj.draw_type = "WIRE"
obj.parent = source.parent obj.parent = source.parent
obj.matrix_local, obj.matrix_world = source.matrix_local, source.matrix_world obj.matrix_local, obj.matrix_world = source.matrix_local, source.matrix_world
@ -112,6 +120,7 @@ def temporary_mesh_object(source : bpy.types.Object) -> bpy.types.Object:
finally: finally:
bpy.data.objects.remove(obj) bpy.data.objects.remove(obj)
def transform_mesh(mesh: bpy.types.Mesh, matrix: mathutils.Matrix): def transform_mesh(mesh: bpy.types.Mesh, matrix: mathutils.Matrix):
# There is a disparity in terms of how negative scaling is displayed in Blender versus how it is # There is a disparity in terms of how negative scaling is displayed in Blender versus how it is
# applied (Ctrl+A) in that the normals are different. Even though negative scaling is evil, we # applied (Ctrl+A) in that the normals are different. Even though negative scaling is evil, we

8
korman/helpers.py

@ -18,6 +18,7 @@ import bpy
from contextlib import contextmanager from contextlib import contextmanager
import math import math
@contextmanager @contextmanager
def bmesh_from_object(bl): def bmesh_from_object(bl):
"""Converts a Blender Object to a BMesh with modifiers applied.""" """Converts a Blender Object to a BMesh with modifiers applied."""
@ -29,6 +30,7 @@ def bmesh_from_object(bl):
finally: finally:
mesh.free() mesh.free()
class GoodNeighbor: class GoodNeighbor:
"""Leave Things the Way You Found Them! (TM)""" """Leave Things the Way You Found Them! (TM)"""
@ -63,6 +65,7 @@ class TemporaryObject:
class UiHelper: class UiHelper:
"""This fun little helper makes sure that we don't wreck the UI""" """This fun little helper makes sure that we don't wreck the UI"""
def __init__(self, context): def __init__(self, context):
self.active_object = context.active_object self.active_object = context.active_object
self.selected_objects = context.selected_objects self.selected_objects = context.selected_objects
@ -82,7 +85,7 @@ class UiHelper:
def __exit__(self, type, value, traceback): def __exit__(self, type, value, traceback):
for i in bpy.data.objects: for i in bpy.data.objects:
i.select = (i in self.selected_objects) i.select = i in self.selected_objects
scene = bpy.context.scene scene = bpy.context.scene
scene.objects.active = self.active_object scene.objects.active = self.active_object
@ -94,8 +97,10 @@ class UiHelper:
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 fetch_fcurves(id_data, data_fcurves=True): def fetch_fcurves(id_data, data_fcurves=True):
"""Given a Blender ID, yields its FCurves""" """Given a Blender ID, yields its FCurves"""
def _fetch(source): def _fetch(source):
if source is not None and source.action is not None: if source is not None and source.action is not None:
for i in source.action.fcurves: for i in source.action.fcurves:
@ -108,6 +113,7 @@ def fetch_fcurves(id_data, data_fcurves=True):
for i in _fetch(id_data.data.animation_data): for i in _fetch(id_data.data.animation_data):
yield i yield i
def find_modifier(bo, modid): def find_modifier(bo, modid):
"""Given a Blender Object, finds a given modifier and returns it or None""" """Given a Blender Object, finds a given modifier and returns it or None"""
if bo is not None: if bo is not None:

42
korman/idprops.py

@ -16,6 +16,7 @@
import bpy import bpy
from bpy.props import * from bpy.props import *
class IDPropMixin: class IDPropMixin:
""" """
So, here's the rub. So, here's the rub.
@ -43,7 +44,9 @@ class IDPropMixin:
# Let's make sure no one is trying to access an old version... # Let's make sure no one is trying to access an old version...
if attr in _getattribute("_idprop_mapping")().values(): if attr in _getattribute("_idprop_mapping")().values():
raise AttributeError("'{}' has been deprecated... Please use the ID Property".format(attr)) 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 # 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. # during Blender's draw() context. Blender locks all properties during the draw loop.
@ -64,7 +67,9 @@ class IDPropMixin:
# Disallow any attempts to set the old string property # Disallow any attempts to set the old string property
if attr in idprops.values(): if attr in idprops.values():
raise AttributeError("'{}' has been deprecated... Please use the ID Property".format(attr)) raise AttributeError(
"'{}' has been deprecated... Please use the ID Property".format(attr)
)
# Inappropriate touching? # Inappropriate touching?
super().__getattribute__("_try_upgrade_idprops")() super().__getattribute__("_try_upgrade_idprops")()
@ -77,14 +82,18 @@ class IDPropMixin:
if hasattr(super(), "register"): if hasattr(super(), "register"):
super().register() super().register()
cls.idprops_upgraded = BoolProperty(name="INTERNAL: ID Property Upgrader HACK", cls.idprops_upgraded = BoolProperty(
description="HAAAX *throws CRT monitor*", name="INTERNAL: ID Property Upgrader HACK",
get=cls._try_upgrade_idprops, description="HAAAX *throws CRT monitor*",
options={"HIDDEN"}) get=cls._try_upgrade_idprops,
cls.idprops_upgraded_value = BoolProperty(name="INTERNAL: ID Property Upgrade Status", options={"HIDDEN"},
description="Have old StringProperties been upgraded to ID Datablock Properties?", )
default=False, cls.idprops_upgraded_value = BoolProperty(
options={"HIDDEN"}) 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(): for str_prop in cls._idprop_mapping().values():
setattr(cls, str_prop, StringProperty(description="deprecated")) setattr(cls, str_prop, StringProperty(description="deprecated"))
@ -115,36 +124,45 @@ class IDPropObjectMixin(IDPropMixin):
# NOTE: bad problems result when using super() here, so we'll manually reference object # NOTE: bad problems result when using super() here, so we'll manually reference object
cls = object.__getattribute__(self, "__class__") cls = object.__getattribute__(self, "__class__")
idprops = cls._idprop_mapping() idprops = cls._idprop_mapping()
return { i: bpy.data.objects for i in idprops.values() } return {i: bpy.data.objects for i in idprops.values()}
def poll_animated_objects(self, value): def poll_animated_objects(self, value):
return value.plasma_object.has_animation_data return value.plasma_object.has_animation_data
def poll_camera_objects(self, value): def poll_camera_objects(self, value):
return value.type == "CAMERA" return value.type == "CAMERA"
def poll_drawable_objects(self, value): def poll_drawable_objects(self, value):
return value.type == "MESH" and any(value.data.materials) return value.type == "MESH" and any(value.data.materials)
def poll_empty_objects(self, value): def poll_empty_objects(self, value):
return value.type == "EMPTY" return value.type == "EMPTY"
def poll_mesh_objects(self, value): def poll_mesh_objects(self, value):
return value.type == "MESH" return value.type == "MESH"
def poll_softvolume_objects(self, value): def poll_softvolume_objects(self, value):
return value.plasma_modifiers.softvolume.enabled return value.plasma_modifiers.softvolume.enabled
def poll_subworld_objects(self, value): def poll_subworld_objects(self, value):
return value.plasma_modifiers.subworld_def.enabled return value.plasma_modifiers.subworld_def.enabled
def poll_visregion_objects(self, value): def poll_visregion_objects(self, value):
return value.plasma_modifiers.visregion.enabled return value.plasma_modifiers.visregion.enabled
def poll_envmap_textures(self, value): def poll_envmap_textures(self, value):
return isinstance(value, bpy.types.EnvironmentMapTexture) return isinstance(value, bpy.types.EnvironmentMapTexture)
@bpy.app.handlers.persistent @bpy.app.handlers.persistent
def _upgrade_node_trees(dummy): def _upgrade_node_trees(dummy):
""" """
@ -160,4 +178,6 @@ def _upgrade_node_trees(dummy):
for node in tree.nodes: for node in tree.nodes:
if isinstance(node, IDPropMixin): if isinstance(node, IDPropMixin):
assert node._try_upgrade_idprops() assert node._try_upgrade_idprops()
bpy.app.handlers.load_post.append(_upgrade_node_trees) bpy.app.handlers.load_post.append(_upgrade_node_trees)

60
korman/korlib/__init__.py

@ -17,6 +17,7 @@ _KORLIB_API_VERSION = 2
try: try:
from _korlib import _KORLIB_API_VERSION as _C_API_VERSION from _korlib import _KORLIB_API_VERSION as _C_API_VERSION
if _KORLIB_API_VERSION != _C_API_VERSION: if _KORLIB_API_VERSION != _C_API_VERSION:
raise ImportError() raise ImportError()
@ -24,10 +25,12 @@ except ImportError as ex:
from .texture import * from .texture import *
if "_C_API_VERSION" in locals(): if "_C_API_VERSION" in locals():
msg = "Korlib C Module Version mismatch (expected {}, got {}).".format(_KORLIB_API_VERSION, _C_API_VERSION) msg = "Korlib C Module Version mismatch (expected {}, got {}).".format(
_KORLIB_API_VERSION, _C_API_VERSION
)
else: else:
msg = "Korlib C Module did not load correctly." msg = "Korlib C Module did not load correctly."
print(msg, "Using PyKorlib :(", sep=' ') print(msg, "Using PyKorlib :(", sep=" ")
def create_bump_LUT(mipmap): def create_bump_LUT(mipmap):
kLUTHeight = 16 kLUTHeight = 16
@ -41,33 +44,52 @@ except ImportError as ex:
doneH = 0 doneH = 0
doneH = startH * kLUTWidth * 4 doneH = startH * kLUTWidth * 4
buf[0:doneH] = [b for x in range(kLUTWidth) for b in (0, 0, int((x / denom) * 255.9), 255)] * startH buf[0:doneH] = [
b for x in range(kLUTWidth) for b in (0, 0, int((x / denom) * 255.9), 255)
] * startH
startH = doneH startH = doneH
doneH += delH * kLUTWidth * 4 doneH += delH * kLUTWidth * 4
buf[startH:doneH] = [b for x in range(kLUTWidth) for b in (127, 127, int((x / denom) * 255.9), 255)] * delH buf[startH:doneH] = [
b
for x in range(kLUTWidth)
for b in (127, 127, int((x / denom) * 255.9), 255)
] * delH
startH = doneH startH = doneH
doneH += delH * kLUTWidth * 4 doneH += delH * kLUTWidth * 4
buf[startH:doneH] = [b for x in range(kLUTWidth) for b in (0, int((x / denom) * 255.9), 0, 255)] * delH buf[startH:doneH] = [
b for x in range(kLUTWidth) for b in (0, int((x / denom) * 255.9), 0, 255)
] * delH
startH = doneH startH = doneH
doneH += delH * kLUTWidth * 4 doneH += delH * kLUTWidth * 4
buf[startH:doneH] = [b for x in range(kLUTWidth) for b in (127, int((x / denom) * 255.9), 127, 255)] * delH buf[startH:doneH] = [
b
for x in range(kLUTWidth)
for b in (127, int((x / denom) * 255.9), 127, 255)
] * delH
startH = doneH startH = doneH
doneH += delH * kLUTWidth * 4 doneH += delH * kLUTWidth * 4
buf[startH:doneH] = [b for x in range(kLUTWidth) for b in (int((x / denom) * 255.9), 0, 0, 255)] * delH buf[startH:doneH] = [
b for x in range(kLUTWidth) for b in (int((x / denom) * 255.9), 0, 0, 255)
] * delH
startH = doneH startH = doneH
doneH += delH * kLUTWidth * 4 doneH += delH * kLUTWidth * 4
buf[startH:doneH] = [b for x in range(kLUTWidth) for b in (int((x / denom) * 255.9), 127, 127, 255)] * startH buf[startH:doneH] = [
b
for x in range(kLUTWidth)
for b in (int((x / denom) * 255.9), 127, 127, 255)
] * startH
mipmap.setRawImage(bytes(buf)) mipmap.setRawImage(bytes(buf))
def inspect_voribsfile(stream, header): def inspect_voribsfile(stream, header):
raise NotImplementedError("Ogg Vorbis not supported unless _korlib is compiled") raise NotImplementedError("Ogg Vorbis not supported unless _korlib is compiled")
else: else:
from _korlib import * from _korlib import *
from .texture import TextureAlpha from .texture import TextureAlpha
@ -77,8 +99,13 @@ finally:
from .python import * from .python import *
from .texture import TEX_DETAIL_ALPHA, TEX_DETAIL_ADD, TEX_DETAIL_MULTIPLY from .texture import TEX_DETAIL_ALPHA, TEX_DETAIL_ADD, TEX_DETAIL_MULTIPLY
_IDENTIFIER_RANGES = ((ord('0'), ord('9')), (ord('A'), ord('Z')), (ord('a'), ord('z'))) _IDENTIFIER_RANGES = (
(ord("0"), ord("9")),
(ord("A"), ord("Z")),
(ord("a"), ord("z")),
)
from keyword import kwlist as _kwlist from keyword import kwlist as _kwlist
_KEYWORDS = set(_kwlist) _KEYWORDS = set(_kwlist)
# Python 2.x keywords # Python 2.x keywords
_KEYWORDS.add("exec") _KEYWORDS.add("exec")
@ -128,10 +155,19 @@ finally:
def process(identifier): def process(identifier):
# No leading digits in identifiers, so skip the first range element (0...9) # No leading digits in identifiers, so skip the first range element (0...9)
yield next((identifier[0] for low, high in _IDENTIFIER_RANGES[1:] yield next(
if low <= ord(identifier[0]) <= high), '_') (
identifier[0]
for low, high in _IDENTIFIER_RANGES[1:]
if low <= ord(identifier[0]) <= high
),
"_",
)
for i in identifier[1:]: for i in identifier[1:]:
yield next((i for low, high in _IDENTIFIER_RANGES if low <= ord(i) <= high), '_') yield next(
(i for low, high in _IDENTIFIER_RANGES if low <= ord(i) <= high),
"_",
)
if identifier: if identifier:
return "".join(process(identifier)) return "".join(process(identifier))

51
korman/korlib/console.py

@ -19,20 +19,26 @@ import math
import sys import sys
if sys.platform == "win32": if sys.platform == "win32":
class _Coord(ctypes.Structure): class _Coord(ctypes.Structure):
_fields_ = [("x", ctypes.c_short), _fields_ = [("x", ctypes.c_short), ("y", ctypes.c_short)]
("y", ctypes.c_short)]
class _SmallRect(ctypes.Structure): class _SmallRect(ctypes.Structure):
_fields_ = [("Left", ctypes.c_short), _fields_ = [
("Top", ctypes.c_short), ("Left", ctypes.c_short),
("Right", ctypes.c_short), ("Top", ctypes.c_short),
("Bottom", ctypes.c_short),] ("Right", ctypes.c_short),
("Bottom", ctypes.c_short),
]
class _ConsoleScreenBufferInfo(ctypes.Structure): class _ConsoleScreenBufferInfo(ctypes.Structure):
_fields_ = [("dwSize", _Coord), _fields_ = [
("dwCursorPosition", _Coord), ("dwSize", _Coord),
("wAttributes", ctypes.c_ushort), ("dwCursorPosition", _Coord),
("srWindow", _SmallRect), ("wAttributes", ctypes.c_ushort),
("dwMaximumWindowSize", _Coord)] ("srWindow", _SmallRect),
("dwMaximumWindowSize", _Coord),
]
class _ConsoleCursor: class _ConsoleCursor:
def __init__(self): def __init__(self):
@ -42,7 +48,9 @@ if sys.platform == "win32":
@property @property
def _screen_buffer_info(self): def _screen_buffer_info(self):
info = _ConsoleScreenBufferInfo() info = _ConsoleScreenBufferInfo()
ctypes.windll.kernel32.GetConsoleScreenBufferInfo(self._handle, ctypes.pointer(info)) ctypes.windll.kernel32.GetConsoleScreenBufferInfo(
self._handle, ctypes.pointer(info)
)
return info return info
def clear(self): def clear(self):
@ -53,19 +61,28 @@ if sys.platform == "win32":
num_chars = (info.dwSize.x * num_cols) + num_rows num_chars = (info.dwSize.x * num_cols) + num_rows
if num_chars: if num_chars:
nWrite = ctypes.c_ulong() nWrite = ctypes.c_ulong()
empty_char = ctypes.c_char(b' ') empty_char = ctypes.c_char(b" ")
ctypes.windll.kernel32.FillConsoleOutputCharacterA(self._handle, empty_char, ctypes.windll.kernel32.FillConsoleOutputCharacterA(
num_chars, self.position, self._handle,
ctypes.pointer(nWrite)) empty_char,
num_chars,
self.position,
ctypes.pointer(nWrite),
)
def reset(self): def reset(self):
ctypes.windll.kernel32.SetConsoleCursorPosition(self._handle, self.position) ctypes.windll.kernel32.SetConsoleCursorPosition(self._handle, self.position)
def update(self): def update(self):
info = _ConsoleScreenBufferInfo() info = _ConsoleScreenBufferInfo()
ctypes.windll.kernel32.GetConsoleScreenBufferInfo(self._handle, ctypes.pointer(info)) ctypes.windll.kernel32.GetConsoleScreenBufferInfo(
self._handle, ctypes.pointer(info)
)
self.position = info.dwCursorPosition self.position = info.dwCursorPosition
else: else:
class _ConsoleCursor: class _ConsoleCursor:
def clear(self): def clear(self):
# Only clears the current line, unfortunately. # Only clears the current line, unfortunately.

52
korman/korlib/python.py

@ -13,13 +13,14 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Korman. If not, see <http://www.gnu.org/licenses/>. # along with Korman. If not, see <http://www.gnu.org/licenses/>.
from __future__ import generators # Python 2.2 from __future__ import generators # Python 2.2
import marshal import marshal
import os.path import os.path
import sys import sys
_python_executables = {} _python_executables = {}
class PythonNotAvailableError(Exception): class PythonNotAvailableError(Exception):
pass pass
@ -30,7 +31,7 @@ def compyle(file_name, py_code, py_version, report=None, indent=0):
assert my_version == (2, 7) or my_version[0] > 2 assert my_version == (2, 7) or my_version[0] > 2
# Remember: Python 2.2 file, so no single line if statements... # Remember: Python 2.2 file, so no single line if statements...
idx = file_name.find('.') idx = file_name.find(".")
if idx == -1: if idx == -1:
module_name = file_name module_name = file_name
else: else:
@ -48,26 +49,31 @@ def compyle(file_name, py_code, py_version, report=None, indent=0):
py_code = py_code.encode("utf-8") py_code = py_code.encode("utf-8")
except UnicodeError: except UnicodeError:
if report is not None: if report is not None:
report.error("Could not encode '{}'", file_name, indent=indent+1) report.error("Could not encode '{}'", file_name, indent=indent + 1)
return (False, "Could not encode file") return (False, "Could not encode file")
result = subprocess.run(args, input=py_code, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) result = subprocess.run(
args, input=py_code, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
if result.returncode != 0: if result.returncode != 0:
try: try:
error = result.stdout.decode("utf-8").replace('\r\n', '\n') error = result.stdout.decode("utf-8").replace("\r\n", "\n")
except UnicodeError: except UnicodeError:
error = result.stdout error = result.stdout
if report is not None: if report is not None:
report.error("Compylation Error in '{}'\n{}", file_name, error, indent=indent+1) report.error(
"Compylation Error in '{}'\n{}", file_name, error, indent=indent + 1
)
return (result.returncode == 0, result.stdout) return (result.returncode == 0, result.stdout)
else: else:
raise NotImplementedError() raise NotImplementedError()
def _compyle(module_name, py_code): def _compyle(module_name, py_code):
# Old python versions have major issues with Windows style newlines. # Old python versions have major issues with Windows style newlines.
# Also, bad things happen if there is no newline at the end. # Also, bad things happen if there is no newline at the end.
py_code += '\n' # sigh, this is slow on old Python... py_code += "\n" # sigh, this is slow on old Python...
py_code = py_code.replace('\r\n', '\n') py_code = py_code.replace("\r\n", "\n")
py_code = py_code.replace('\r', '\n') py_code = py_code.replace("\r", "\n")
code_object = compile(py_code, module_name, "exec") code_object = compile(py_code, module_name, "exec")
# The difference between us and the py_compile module is twofold: # The difference between us and the py_compile module is twofold:
@ -79,6 +85,7 @@ def _compyle(module_name, py_code):
# Therefore, we simply return the marshalled data as a string. # Therefore, we simply return the marshalled data as a string.
return marshal.dumps(code_object) return marshal.dumps(code_object)
def _find_python(py_version): def _find_python(py_version):
def find_executable(py_version): def find_executable(py_version):
# First, try to use Blender to find the Python executable # First, try to use Blender to find the Python executable
@ -88,7 +95,9 @@ def _find_python(py_version):
pass pass
else: else:
userprefs = bpy.context.user_preferences.addons["korman"].preferences userprefs = bpy.context.user_preferences.addons["korman"].preferences
py_executable = getattr(userprefs, "python{}{}_executable".format(*py_version), None) py_executable = getattr(
userprefs, "python{}{}_executable".format(*py_version), None
)
if verify_python(py_version, py_executable): if verify_python(py_version, py_executable):
return py_executable return py_executable
@ -108,14 +117,18 @@ def _find_python(py_version):
# I give up, you win. # I give up, you win.
return None return None
py_executable = _python_executables.setdefault(py_version, find_executable(py_version)) py_executable = _python_executables.setdefault(
py_version, find_executable(py_version)
)
if py_executable: if py_executable:
return py_executable return py_executable
else: else:
raise PythonNotAvailableError("{}.{}".format(*py_version)) raise PythonNotAvailableError("{}.{}".format(*py_version))
def _find_python_reg(reg_key, py_version): def _find_python_reg(reg_key, py_version):
import winreg import winreg
subkey_name = "Software\\Python\\PythonCore\\{}.{}\\InstallPath".format(*py_version) subkey_name = "Software\\Python\\PythonCore\\{}.{}\\InstallPath".format(*py_version)
try: try:
python_dir = winreg.QueryValue(reg_key, subkey_name) python_dir = winreg.QueryValue(reg_key, subkey_name)
@ -124,6 +137,7 @@ def _find_python_reg(reg_key, py_version):
else: else:
return os.path.join(python_dir, "python.exe") return os.path.join(python_dir, "python.exe")
def package_python(stream, pyc_objects): def package_python(stream, pyc_objects):
# Python.pak format: # Python.pak format:
# uint32_t numFiles # uint32_t numFiles
@ -139,17 +153,17 @@ def package_python(stream, pyc_objects):
# `stream` might be a plEncryptedStream, which doesn't seek very well at all. # `stream` might be a plEncryptedStream, which doesn't seek very well at all.
# Therefore, we will go ahead and calculate the size of the index block so # Therefore, we will go ahead and calculate the size of the index block so
# there is no need to seek around to write offset values # there is no need to seek around to write offset values
base_offset = 4 # uint32_t numFiles base_offset = 4 # uint32_t numFiles
data_offset = 0 data_offset = 0
pyc_info = [] # sad, but makes life easier... pyc_info = [] # sad, but makes life easier...
for module_name, compyled_code in pyc_objects: for module_name, compyled_code in pyc_objects:
pyc_info.append((module_name, data_offset, compyled_code)) pyc_info.append((module_name, data_offset, compyled_code))
# index offset overall # index offset overall
base_offset += 2 # writeSafeStr length base_offset += 2 # writeSafeStr length
# NOTE: This assumes that libHSPlasma's hsStream::writeSafeStr converts # NOTE: This assumes that libHSPlasma's hsStream::writeSafeStr converts
# the Python unicode/string object to UTF-8. Currently, this is true. # the Python unicode/string object to UTF-8. Currently, this is true.
base_offset += len(module_name.encode("utf-8")) # writeSafeStr base_offset += len(module_name.encode("utf-8")) # writeSafeStr
base_offset += 4 base_offset += 4
# current file data offset # current file data offset
@ -165,14 +179,18 @@ def package_python(stream, pyc_objects):
stream.writeInt(len(compyled_code)) stream.writeInt(len(compyled_code))
stream.write(compyled_code) stream.write(compyled_code)
def verify_python(py_version, py_exe): def verify_python(py_version, py_exe):
if not py_exe: if not py_exe:
return False return False
import subprocess import subprocess
try: try:
args = (py_exe, "-V") args = (py_exe, "-V")
result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=5) result = subprocess.run(
args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=5
)
except OSError: except OSError:
return False return False
else: else:
@ -186,11 +204,13 @@ def verify_python(py_version, py_exe):
return False return False
return "{}.{}".format(*py_version) == py_check return "{}.{}".format(*py_version) == py_check
if __name__ == "__main__": if __name__ == "__main__":
# Python tries to be "helpful" on Windows by converting \n to \r\n. # Python tries to be "helpful" on Windows by converting \n to \r\n.
# Therefore we must change the mode of stdout. # Therefore we must change the mode of stdout.
if sys.platform == "win32": if sys.platform == "win32":
import os, msvcrt import os, msvcrt
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
try: try:

82
korman/korlib/texture.py

@ -28,6 +28,7 @@ TEX_DETAIL_ALPHA = 0
TEX_DETAIL_ADD = 1 TEX_DETAIL_ADD = 1
TEX_DETAIL_MULTIPLY = 2 TEX_DETAIL_MULTIPLY = 2
def scale_image(buf, srcW, srcH, dstW, dstH): def scale_image(buf, srcW, srcH, dstW, dstH):
"""Scales an RGBA image using the algorithm from CWE's plMipmap::ScaleNicely""" """Scales an RGBA image using the algorithm from CWE's plMipmap::ScaleNicely"""
dst, dst_idx = bytearray(dstW * dstH * 4), 0 dst, dst_idx = bytearray(dstW * dstH * 4), 0
@ -44,7 +45,7 @@ def scale_image(buf, srcW, srcH, dstW, dstH):
srcY_start = int(max(srcY - filterH, 0)) srcY_start = int(max(srcY - filterH, 0))
srcY_end = int(min(srcY + filterH, srcH - 1)) srcY_end = int(min(srcY + filterH, srcH - 1))
#weightsY = { i - srcY_start: 1.0 - abs(i - srcY) / scaleY \ # weightsY = { i - srcY_start: 1.0 - abs(i - srcY) / scaleY \
# for i in range(srcY_start, srcY_end+1, 1) if i - srcY_start < 16 } # for i in range(srcY_start, srcY_end+1, 1) if i - srcY_start < 16 }
for i in range(16): for i in range(16):
idx = i + srcY_start idx = i + srcY_start
@ -57,7 +58,7 @@ def scale_image(buf, srcW, srcH, dstW, dstH):
srcX_start = int(max(srcX - filterW, 0)) srcX_start = int(max(srcX - filterW, 0))
srcX_end = int(min(srcX + filterW, srcW - 1)) srcX_end = int(min(srcX + filterW, srcW - 1))
#weightsX = { i - srcX_start: 1.0 - abs(i - srcX) / scaleX \ # weightsX = { i - srcX_start: 1.0 - abs(i - srcX) / scaleX \
# for i in range(srcX_start, srcX_end+1, 1) if i - srcX_start < 16 } # for i in range(srcX_start, srcX_end+1, 1) if i - srcX_start < 16 }
for i in range(16): for i in range(16):
idx = i + srcX_start idx = i + srcX_start
@ -67,15 +68,23 @@ def scale_image(buf, srcW, srcH, dstW, dstH):
accum_color = [0.0, 0.0, 0.0, 0.0] accum_color = [0.0, 0.0, 0.0, 0.0]
weight_total = 0.0 weight_total = 0.0
for i in range(srcY_start, srcY_end+1, 1): for i in range(srcY_start, srcY_end + 1, 1):
weightY_idx = i - srcY_start weightY_idx = i - srcY_start
weightY = weightsY[weightY_idx] if weightY_idx < 16 else 1.0 - abs(i - srcY) / filterH weightY = (
weightsY[weightY_idx]
if weightY_idx < 16
else 1.0 - abs(i - srcY) / filterH
)
weightY = 1.0 - abs(i - srcY) / filterH weightY = 1.0 - abs(i - srcY) / filterH
src_idx = (i * src_rowspan) + (srcX_start * 4) src_idx = (i * src_rowspan) + (srcX_start * 4)
for j in range(srcX_start, srcX_end+1, 1): for j in range(srcX_start, srcX_end + 1, 1):
weightX_idx = j - srcX_start weightX_idx = j - srcX_start
weightX = weightsX[weightX_idx] if weightX_idx < 16 else 1.0 - abs(j - srcX) / filterW weightX = (
weightsX[weightX_idx]
if weightX_idx < 16
else 1.0 - abs(j - srcX) / filterW
)
weight = weightY * weightX weight = weightY * weightX
if weight > 0.0: if weight > 0.0:
@ -83,14 +92,14 @@ def scale_image(buf, srcW, srcH, dstW, dstH):
# function. I know this function is supposed to be slow, but dayum... I've unrolled it # function. I know this function is supposed to be slow, but dayum... I've unrolled it
# to avoid all the extra allocations. # to avoid all the extra allocations.
for k in range(4): for k in range(4):
accum_color[k] = accum_color[k] + buf[src_idx+k] * weight accum_color[k] = accum_color[k] + buf[src_idx + k] * weight
weight_total += weight weight_total += weight
src_idx += 4 src_idx += 4
weight_total = max(weight_total, 0.0001) weight_total = max(weight_total, 0.0001)
for i in range(4): for i in range(4):
accum_color[i] = int(accum_color[i] * (1.0 / weight_total)) accum_color[i] = int(accum_color[i] * (1.0 / weight_total))
dst[dst_idx:dst_idx+4] = accum_color dst[dst_idx : dst_idx + 4] = accum_color
dst_idx += 4 dst_idx += 4
return bytes(dst) return bytes(dst)
@ -123,7 +132,7 @@ class GLTexture:
if self._blimg.gl_load() != 0: if self._blimg.gl_load() != 0:
raise RuntimeError("failed to load image") raise RuntimeError("failed to load image")
previous_texture = self._get_integer(bgl.GL_TEXTURE_BINDING_2D) previous_texture = self._get_integer(bgl.GL_TEXTURE_BINDING_2D)
changed_state = (previous_texture != self._blimg.bindcode[0]) changed_state = previous_texture != self._blimg.bindcode[0]
if changed_state: if changed_state:
bgl.glBindTexture(bgl.GL_TEXTURE_2D, self._blimg.bindcode[0]) bgl.glBindTexture(bgl.GL_TEXTURE_2D, self._blimg.bindcode[0])
@ -155,14 +164,18 @@ class GLTexture:
@property @property
def _detail_falloff(self): def _detail_falloff(self):
num_levels = self.num_levels num_levels = self.num_levels
return ((self._texkey.detail_fade_start / 100.0) * num_levels, return (
(self._texkey.detail_fade_stop / 100.0) * num_levels, (self._texkey.detail_fade_start / 100.0) * num_levels,
self._texkey.detail_opacity_start / 100.0, (self._texkey.detail_fade_stop / 100.0) * num_levels,
self._texkey.detail_opacity_stop / 100.0) self._texkey.detail_opacity_start / 100.0,
self._texkey.detail_opacity_stop / 100.0,
def get_level_data(self, level=0, calc_alpha=False, report=None, indent=2, fast=False): )
def get_level_data(
self, level=0, calc_alpha=False, report=None, indent=2, fast=False
):
"""Gets the uncompressed pixel data for a requested mip level, optionally calculating the alpha """Gets the uncompressed pixel data for a requested mip level, optionally calculating the alpha
channel from the image color data channel from the image color data
""" """
# Previously, we would leave the texture bound in OpenGL and use it to do the mipmapping, using # Previously, we would leave the texture bound in OpenGL and use it to do the mipmapping, using
@ -190,7 +203,6 @@ class GLTexture:
else: else:
buf = bytearray(self._image_data) buf = bytearray(self._image_data)
if self._image_inverted: if self._image_inverted:
buf = self._invert_image(eWidth, eHeight, buf) buf = self._invert_image(eWidth, eHeight, buf)
@ -207,11 +219,15 @@ class GLTexture:
# Do we need to calculate the alpha component? # Do we need to calculate the alpha component?
if calc_alpha: if calc_alpha:
for i in range(0, size, 4): for i in range(0, size, 4):
buf[i+3] = int(sum(buf[i:i+3]) / 3) buf[i + 3] = int(sum(buf[i : i + 3]) / 3)
return bytes(buf) return bytes(buf)
def _get_detail_alpha(self, level, dropoff_start, dropoff_stop, detail_max, detail_min): def _get_detail_alpha(
alpha = (level - dropoff_start) * (detail_min - detail_max) / (dropoff_stop - dropoff_start) + detail_max self, level, dropoff_start, dropoff_stop, detail_max, detail_min
):
alpha = (level - dropoff_start) * (detail_min - detail_max) / (
dropoff_stop - dropoff_start
) + detail_max
if detail_min < detail_max: if detail_min < detail_max:
return min(detail_max, max(detail_min, alpha)) return min(detail_max, max(detail_min, alpha))
else: else:
@ -242,8 +258,10 @@ class GLTexture:
def _get_image_data(self): def _get_image_data(self):
return (self._width, self._height, self._image_data) return (self._width, self._height, self._image_data)
def _set_image_data(self, value): def _set_image_data(self, value):
self._width, self._height, self._image_data = value self._width, self._height, self._image_data = value
image_data = property(_get_image_data, _set_image_data) image_data = property(_get_image_data, _set_image_data)
def _invert_image(self, width, height, buf): def _invert_image(self, width, height, buf):
@ -251,30 +269,36 @@ class GLTexture:
finalBuf = bytearray(size) finalBuf = bytearray(size)
row_stride = width * 4 row_stride = width * 4
for i in range(height): for i in range(height):
src, dst = i * row_stride, (height - (i+1)) * row_stride src, dst = i * row_stride, (height - (i + 1)) * row_stride
finalBuf[dst:dst+row_stride] = buf[src:src+row_stride] finalBuf[dst : dst + row_stride] = buf[src : src + row_stride]
return bytes(finalBuf) return bytes(finalBuf)
def _make_detail_map_add(self, data, level): def _make_detail_map_add(self, data, level):
dropoff_start, dropoff_stop, detail_max, detail_min = self._detail_falloff dropoff_start, dropoff_stop, detail_max, detail_min = self._detail_falloff
alpha = self._get_detail_alpha(level, dropoff_start, dropoff_stop, detail_max, detail_min) alpha = self._get_detail_alpha(
level, dropoff_start, dropoff_stop, detail_max, detail_min
)
for i in range(0, len(data), 4): for i in range(0, len(data), 4):
data[i] = int(data[i] * alpha) data[i] = int(data[i] * alpha)
data[i+1] = int(data[i+1] * alpha) data[i + 1] = int(data[i + 1] * alpha)
data[i+2] = int(data[i+2] * alpha) data[i + 2] = int(data[i + 2] * alpha)
def _make_detail_map_alpha(self, data, level): def _make_detail_map_alpha(self, data, level):
dropoff_start, dropoff_end, detail_max, detail_min = self._detail_falloff dropoff_start, dropoff_end, detail_max, detail_min = self._detail_falloff
alpha = self._get_detail_alpha(level, dropoff_start, dropoff_end, detail_max, detail_min) alpha = self._get_detail_alpha(
level, dropoff_start, dropoff_end, detail_max, detail_min
)
for i in range(0, len(data), 4): for i in range(0, len(data), 4):
data[i+3] = int(data[i+3] * alpha) data[i + 3] = int(data[i + 3] * alpha)
def _make_detail_map_mult(self, data, level): def _make_detail_map_mult(self, data, level):
dropoff_start, dropoff_end, detail_max, detail_min = self._detail_falloff dropoff_start, dropoff_end, detail_max, detail_min = self._detail_falloff
alpha = self._get_detail_alpha(level, dropoff_start, dropoff_end, detail_max, detail_min) alpha = self._get_detail_alpha(
level, dropoff_start, dropoff_end, detail_max, detail_min
)
invert_alpha = (1.0 - alpha) * 255.0 invert_alpha = (1.0 - alpha) * 255.0
for i in range(0, len(data), 4): for i in range(0, len(data), 4):
data[i+3] = int(invert_alpha + data[i+3] * alpha) data[i + 3] = int(invert_alpha + data[i + 3] * alpha)
@property @property
def num_levels(self): def num_levels(self):

15
korman/nodes/__init__.py

@ -29,12 +29,14 @@ from .node_python import *
from .node_responder import * from .node_responder import *
from .node_softvolume import * from .node_softvolume import *
class PlasmaNodeCategory(NodeCategory): class PlasmaNodeCategory(NodeCategory):
"""Plasma Node Category""" """Plasma Node Category"""
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return (context.space_data.tree_type == "PlasmaNodeTree") return context.space_data.tree_type == "PlasmaNodeTree"
# Here's what you need to know about this... # Here's what you need to know about this...
# If you add a new category, put the pretty name here! # If you add a new category, put the pretty name here!
@ -50,6 +52,7 @@ _kategory_names = {
"SV": "Soft Volume", "SV": "Soft Volume",
} }
class PlasmaNodeItem(NodeItem): class PlasmaNodeItem(NodeItem):
def __init__(self, **kwargs): def __init__(self, **kwargs):
self._poll_add = kwargs.pop("poll_add", None) self._poll_add = kwargs.pop("poll_add", None)
@ -91,11 +94,17 @@ for cls in dict(globals()).values():
_actual_kategories = [] _actual_kategories = []
for i in sorted(_kategories.keys(), key=lambda x: _kategory_names[x]): for i in sorted(_kategories.keys(), key=lambda x: _kategory_names[x]):
# Note that even though we're sorting the category names, Blender appears to not care... # Note that even though we're sorting the category names, Blender appears to not care...
_kat_items = [PlasmaNodeItem(**j) for j in sorted(_kategories[i], key=lambda x: x["label"])] _kat_items = [
_actual_kategories.append(PlasmaNodeCategory(i, _kategory_names[i], items=_kat_items)) PlasmaNodeItem(**j) for j in sorted(_kategories[i], key=lambda x: x["label"])
]
_actual_kategories.append(
PlasmaNodeCategory(i, _kategory_names[i], items=_kat_items)
)
def register(): def register():
nodeitems_utils.register_node_categories("PLASMA_NODES", _actual_kategories) nodeitems_utils.register_node_categories("PLASMA_NODES", _actual_kategories)
def unregister(): def unregister():
nodeitems_utils.unregister_node_categories("PLASMA_NODES") nodeitems_utils.unregister_node_categories("PLASMA_NODES")

577
korman/nodes/node_avatar.py

@ -22,6 +22,7 @@ from .node_core import PlasmaNodeBase, PlasmaNodeSocketBase
from ..properties.modifiers.avatar import sitting_approach_flags from ..properties.modifiers.avatar import sitting_approach_flags
from ..exporter.explosions import ExportError from ..exporter.explosions import ExportError
class PlasmaSittingBehaviorNode(PlasmaNodeBase, bpy.types.Node): class PlasmaSittingBehaviorNode(PlasmaNodeBase, bpy.types.Node):
bl_category = "AVATAR" bl_category = "AVATAR"
bl_idname = "PlasmaSittingBehaviorNode" bl_idname = "PlasmaSittingBehaviorNode"
@ -30,26 +31,41 @@ class PlasmaSittingBehaviorNode(PlasmaNodeBase, bpy.types.Node):
pl_attrib = {"ptAttribActivator", "ptAttribActivatorList", "ptAttribNamedActivator"} pl_attrib = {"ptAttribActivator", "ptAttribActivatorList", "ptAttribNamedActivator"}
approach = EnumProperty(name="Approach", approach = EnumProperty(
description="Directions an avatar can approach the seat from", name="Approach",
items=sitting_approach_flags, description="Directions an avatar can approach the seat from",
default={"kApproachFront", "kApproachLeft", "kApproachRight"}, items=sitting_approach_flags,
options={"ENUM_FLAG"}) default={"kApproachFront", "kApproachLeft", "kApproachRight"},
options={"ENUM_FLAG"},
input_sockets = OrderedDict([ )
("condition", {
"text": "Condition", input_sockets = OrderedDict(
"type": "PlasmaConditionSocket", [
}), (
]) "condition",
{
output_sockets = OrderedDict([ "text": "Condition",
("satisfies", { "type": "PlasmaConditionSocket",
"text": "Satisfies", },
"type": "PlasmaConditionSocket", ),
"valid_link_sockets": {"PlasmaConditionSocket", "PlasmaPythonFileNodeSocket"}, ]
}), )
])
output_sockets = OrderedDict(
[
(
"satisfies",
{
"text": "Satisfies",
"type": "PlasmaConditionSocket",
"valid_link_sockets": {
"PlasmaConditionSocket",
"PlasmaPythonFileNodeSocket",
},
},
),
]
)
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
col = layout.column() col = layout.column()
@ -70,7 +86,12 @@ class PlasmaSittingBehaviorNode(PlasmaNodeBase, bpy.types.Node):
if i is not None: if i is not None:
sitmod.addNotifyKey(i.get_key(exporter, so)) sitmod.addNotifyKey(i.get_key(exporter, so))
else: else:
exporter.report.warn("'{}' Node '{}' doesn't expose a key. It won't be triggered by '{}'!".format(i.bl_idname, i.name, self.name), indent=3) exporter.report.warn(
"'{}' Node '{}' doesn't expose a key. It won't be triggered by '{}'!".format(
i.bl_idname, i.name, self.name
),
indent=3,
)
@property @property
def requires_actor(self): def requires_actor(self):
@ -80,9 +101,11 @@ class PlasmaSittingBehaviorNode(PlasmaNodeBase, bpy.types.Node):
class PlasmaAnimStageAdvanceSocketIn(PlasmaNodeSocketBase, bpy.types.NodeSocket): class PlasmaAnimStageAdvanceSocketIn(PlasmaNodeSocketBase, bpy.types.NodeSocket):
bl_color = (0.412, 0.2, 0.055, 1.0) bl_color = (0.412, 0.2, 0.055, 1.0)
auto_advance = BoolProperty(name="Advance to Next Stage", auto_advance = BoolProperty(
description="Automatically advance to the next stage when the animation completes instead of halting", name="Advance to Next Stage",
default=True) description="Automatically advance to the next stage when the animation completes instead of halting",
default=True,
)
def draw_content(self, context, layout, node, text): def draw_content(self, context, layout, node, text):
if not self.is_linked: if not self.is_linked:
@ -97,9 +120,11 @@ class PlasmaAnimStageAdvanceSocketIn(PlasmaNodeSocketBase, bpy.types.NodeSocket)
class PlasmaAnimStageRegressSocketIn(PlasmaNodeSocketBase, bpy.types.NodeSocket): class PlasmaAnimStageRegressSocketIn(PlasmaNodeSocketBase, bpy.types.NodeSocket):
bl_color = (0.412, 0.2, 0.055, 1.0) bl_color = (0.412, 0.2, 0.055, 1.0)
auto_regress = BoolProperty(name="Regress to Previous Stage", auto_regress = BoolProperty(
description="Automatically regress to the previous stage when the animation completes instead of halting", name="Regress to Previous Stage",
default=True) description="Automatically regress to the previous stage when the animation completes instead of halting",
default=True,
)
def draw_content(self, context, layout, node, text): def draw_content(self, context, layout, node, text):
if not self.is_linked: if not self.is_linked:
@ -115,17 +140,59 @@ class PlasmaAnimStageOrderSocketOut(PlasmaNodeSocketBase, bpy.types.NodeSocket):
bl_color = (0.412, 0.2, 0.055, 1.0) bl_color = (0.412, 0.2, 0.055, 1.0)
anim_play_flags = [("kPlayNone", "None", "Play stage only when directed by a message"), anim_play_flags = [
("kPlayKey", "Keyboard", "Play stage when the user presses the forward/backward key"), ("kPlayNone", "None", "Play stage only when directed by a message"),
("kPlayAuto", "Automatic", "Play stage automatically")] (
anim_stage_adv_flags = [("kAdvanceNone", "None", "Advance to the next stage only when directed by a message"), "kPlayKey",
("kAdvanceOnMove", "Movement", "Advance to the next stage when the user presses a movement key"), "Keyboard",
("kAdvanceAuto", "Automatic", "Advance to the next stage automatically when this one completes"), "Play stage when the user presses the forward/backward key",
("kAdvanceOnAnyKey", "Any Keypress", "Advance to the next stage when the user presses any key")] ),
anim_stage_rgr_flags = [("kAdvanceNone", "None", "Regress to the previous stage only when directed by a message"), ("kPlayAuto", "Automatic", "Play stage automatically"),
("kAdvanceOnMove", "Movement", "Regress to the previous stage when the user presses a movement key"), ]
("kAdvanceAuto", "Automatic", "Regress to the previous stage automatically when this one completes"), anim_stage_adv_flags = [
("kAdvanceOnAnyKey", "Any Keypress", "Regress to the previous stage when the user presses any key")] (
"kAdvanceNone",
"None",
"Advance to the next stage only when directed by a message",
),
(
"kAdvanceOnMove",
"Movement",
"Advance to the next stage when the user presses a movement key",
),
(
"kAdvanceAuto",
"Automatic",
"Advance to the next stage automatically when this one completes",
),
(
"kAdvanceOnAnyKey",
"Any Keypress",
"Advance to the next stage when the user presses any key",
),
]
anim_stage_rgr_flags = [
(
"kAdvanceNone",
"None",
"Regress to the previous stage only when directed by a message",
),
(
"kAdvanceOnMove",
"Movement",
"Regress to the previous stage when the user presses a movement key",
),
(
"kAdvanceAuto",
"Automatic",
"Regress to the previous stage automatically when this one completes",
),
(
"kAdvanceOnAnyKey",
"Any Keypress",
"Regress to the previous stage when the user presses any key",
),
]
class PlasmaAnimStageSettingsSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): class PlasmaAnimStageSettingsSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
@ -138,63 +205,94 @@ class PlasmaAnimStageSettingsNode(PlasmaNodeBase, bpy.types.Node):
bl_label = "Animation Stage Settings" bl_label = "Animation Stage Settings"
bl_width_default = 325 bl_width_default = 325
forward = EnumProperty(name="Forward", forward = EnumProperty(
description="Selects which events cause this stage to play forward", name="Forward",
items=anim_play_flags, description="Selects which events cause this stage to play forward",
default="kPlayNone") items=anim_play_flags,
backward = EnumProperty(name="Backward", default="kPlayNone",
description="Selects which events cause this stage to play backward", )
items=anim_play_flags, backward = EnumProperty(
default="kPlayNone") name="Backward",
stage_advance = EnumProperty(name="Stage Advance", description="Selects which events cause this stage to play backward",
description="Selects which events cause this stage to advance to the next stage", items=anim_play_flags,
items=anim_stage_adv_flags, default="kPlayNone",
default="kAdvanceNone") )
stage_regress = EnumProperty(name="Stage Regress", stage_advance = EnumProperty(
description="Selects which events cause this stage to regress to the previous stage", name="Stage Advance",
items=anim_stage_rgr_flags, description="Selects which events cause this stage to advance to the next stage",
default="kAdvanceNone") items=anim_stage_adv_flags,
default="kAdvanceNone",
notify_on = EnumProperty(name="Notify", )
description="Which events should send notifications", stage_regress = EnumProperty(
items=[ name="Stage Regress",
("kNotifyEnter", "Enter", description="Selects which events cause this stage to regress to the previous stage",
"Send notification when animation first begins to play"), items=anim_stage_rgr_flags,
("kNotifyLoop", "Loop", default="kAdvanceNone",
"Send notification when animation starts a loop"), )
("kNotifyAdvance", "Advance",
"Send notification when animation is advanced"), notify_on = EnumProperty(
("kNotifyRegress", "Regress", name="Notify",
"Send notification when animation is regressed") description="Which events should send notifications",
], items=[
default={"kNotifyEnter"}, (
options={"ENUM_FLAG"}) "kNotifyEnter",
"Enter",
input_sockets = OrderedDict([ "Send notification when animation first begins to play",
("advance_to", { ),
"text": "Advance to Stage", ("kNotifyLoop", "Loop", "Send notification when animation starts a loop"),
"type": "PlasmaAnimStageAdvanceSocketIn", (
"valid_link_nodes": "PlasmaAnimStageNode", "kNotifyAdvance",
"valid_link_sockets": "PlasmaAnimStageOrderSocketOut", "Advance",
"link_limit": 1, "Send notification when animation is advanced",
}), ),
("regress_to", { (
"text": "Regress to Stage", "kNotifyRegress",
"type": "PlasmaAnimStageRegressSocketIn", "Regress",
"valid_link_nodes": "PlasmaAnimStageNode", "Send notification when animation is regressed",
"valid_link_sockets": "PlasmaAnimStageOrderSocketOut", ),
"link_limit": 1, ],
}), default={"kNotifyEnter"},
]) options={"ENUM_FLAG"},
)
output_sockets = OrderedDict([
("stage", { input_sockets = OrderedDict(
"text": "Stage", [
"type": "PlasmaAnimStageSettingsSocket", (
"valid_link_nodes": "PlasmaAnimStageNode", "advance_to",
"valid_link_sockets": "PlasmaAnimStageSettingsSocket", {
}), "text": "Advance to Stage",
]) "type": "PlasmaAnimStageAdvanceSocketIn",
"valid_link_nodes": "PlasmaAnimStageNode",
"valid_link_sockets": "PlasmaAnimStageOrderSocketOut",
"link_limit": 1,
},
),
(
"regress_to",
{
"text": "Regress to Stage",
"type": "PlasmaAnimStageRegressSocketIn",
"valid_link_nodes": "PlasmaAnimStageNode",
"valid_link_sockets": "PlasmaAnimStageOrderSocketOut",
"link_limit": 1,
},
),
]
)
output_sockets = OrderedDict(
[
(
"stage",
{
"text": "Stage",
"type": "PlasmaAnimStageSettingsSocket",
"valid_link_nodes": "PlasmaAnimStageNode",
"valid_link_sockets": "PlasmaAnimStageSettingsSocket",
},
),
]
)
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop(self, "forward") layout.prop(self, "forward")
@ -215,45 +313,66 @@ class PlasmaAnimStageNode(PlasmaNodeBase, bpy.types.Node):
bl_label = "Animation Stage" bl_label = "Animation Stage"
bl_width_default = 325 bl_width_default = 325
pl_attrib = ("ptAttribAnimation") pl_attrib = "ptAttribAnimation"
anim_name = StringProperty(name="Animation Name", anim_name = StringProperty(
description="Name of animation to play") name="Animation Name", description="Name of animation to play"
)
loop_option = EnumProperty(name="Looping",
description="Loop options for animation playback", loop_option = EnumProperty(
items=[("kDontLoop", "Don't Loop", "Don't loop the animation"), name="Looping",
("kLoop", "Loop", "Loop the animation a finite number of times"), description="Loop options for animation playback",
("kLoopForever", "Loop Forever", "Continue playing animation indefinitely")], items=[
default="kDontLoop") ("kDontLoop", "Don't Loop", "Don't loop the animation"),
num_loops = IntProperty(name="Num Loops", ("kLoop", "Loop", "Loop the animation a finite number of times"),
description="Number of times to loop animation", ("kLoopForever", "Loop Forever", "Continue playing animation indefinitely"),
default=0) ],
default="kDontLoop",
input_sockets = OrderedDict([ )
("stage_settings", { num_loops = IntProperty(
"text": "Stage Settings", name="Num Loops", description="Number of times to loop animation", default=0
"type": "PlasmaAnimStageSettingsSocket", )
"valid_link_nodes": "PlasmaAnimStageSettingsNode",
"valid_link_sockets": "PlasmaAnimStageSettingsSocket", input_sockets = OrderedDict(
"link_limit": 1, [
}), (
]) "stage_settings",
{
output_sockets = OrderedDict([ "text": "Stage Settings",
("stage", { "type": "PlasmaAnimStageSettingsSocket",
"text": "Behavior", "valid_link_nodes": "PlasmaAnimStageSettingsNode",
"type": "PlasmaAnimStageRefSocket", "valid_link_sockets": "PlasmaAnimStageSettingsSocket",
"valid_link_nodes": "PlasmaMultiStageBehaviorNode", "link_limit": 1,
"valid_link_sockets": "PlasmaAnimStageRefSocket", },
}), ),
("stage_reference", { ]
"text": "Stage Progression", )
"type": "PlasmaAnimStageOrderSocketOut",
"valid_link_nodes": "PlasmaAnimStageSettingsNode", output_sockets = OrderedDict(
"valid_link_sockets": {"PlasmaAnimStageAdvanceSocketIn", "PlasmaAnimStageRegressSocketIn"} , [
}), (
]) "stage",
{
"text": "Behavior",
"type": "PlasmaAnimStageRefSocket",
"valid_link_nodes": "PlasmaMultiStageBehaviorNode",
"valid_link_sockets": "PlasmaAnimStageRefSocket",
},
),
(
"stage_reference",
{
"text": "Stage Progression",
"type": "PlasmaAnimStageOrderSocketOut",
"valid_link_nodes": "PlasmaAnimStageSettingsNode",
"valid_link_sockets": {
"PlasmaAnimStageAdvanceSocketIn",
"PlasmaAnimStageRegressSocketIn",
},
},
),
]
)
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop(self, "anim_name") layout.prop(self, "anim_name")
@ -270,7 +389,15 @@ class PlasmaAnimStageNode(PlasmaNodeBase, bpy.types.Node):
stage_socket = self.find_output_socket("stage") stage_socket = self.find_output_socket("stage")
if stage_socket.is_linked: if stage_socket.is_linked:
msbmod = stage_socket.links[0].to_node msbmod = stage_socket.links[0].to_node
idx = next((idx for idx, socket in enumerate(msbmod.find_input_sockets("stage_refs")) if socket.is_linked and socket.links[0].from_node == self)) idx = next(
(
idx
for idx, socket in enumerate(
msbmod.find_input_sockets("stage_refs")
)
if socket.is_linked and socket.links[0].from_node == self
)
)
return idx return idx
@ -284,48 +411,69 @@ class PlasmaMultiStageBehaviorNode(PlasmaNodeBase, bpy.types.Node):
bl_label = "Multistage Behavior" bl_label = "Multistage Behavior"
bl_width_default = 200 bl_width_default = 200
pl_attrib = ("ptAttribBehavior") pl_attrib = "ptAttribBehavior"
freeze_phys = BoolProperty(name="Freeze Physical", freeze_phys = BoolProperty(
description="Freeze physical at end", name="Freeze Physical", description="Freeze physical at end", default=False
default=False) )
reverse_control = BoolProperty(name="Reverse Controls", reverse_control = BoolProperty(
description="Reverse forward/back controls at end", name="Reverse Controls",
default=False) description="Reverse forward/back controls at end",
default=False,
input_sockets = OrderedDict([ )
("seek_target", {
"text": "Seek Target", input_sockets = OrderedDict(
"type": "PlasmaSeekTargetSocketIn", [
"valid_link_sockets": "PlasmaSeekTargetSocketOut", (
}), "seek_target",
("stage_refs", { {
"text": "Stage", "text": "Seek Target",
"type": "PlasmaAnimStageRefSocket", "type": "PlasmaSeekTargetSocketIn",
"valid_link_nodes": "PlasmaAnimStageNode", "valid_link_sockets": "PlasmaSeekTargetSocketOut",
"valid_link_sockets": "PlasmaAnimStageRefSocket", },
"link_limit": 1, ),
"spawn_empty": True, (
}), "stage_refs",
("condition", { {
"text": "Triggered By", "text": "Stage",
"type": "PlasmaConditionSocket", "type": "PlasmaAnimStageRefSocket",
"spawn_empty": True, "valid_link_nodes": "PlasmaAnimStageNode",
}), "valid_link_sockets": "PlasmaAnimStageRefSocket",
]) "link_limit": 1,
"spawn_empty": True,
output_sockets = OrderedDict([ },
("hosts", { ),
"text": "Host Script", (
"type": "PlasmaBehaviorSocket", "condition",
"valid_link_nodes": {"PlasmaPythonFileNode"}, {
"spawn_empty": True, "text": "Triggered By",
}), "type": "PlasmaConditionSocket",
("satisfies", { "spawn_empty": True,
"text": "Trigger", },
"type": "PlasmaConditionSocket", ),
}) ]
]) )
output_sockets = OrderedDict(
[
(
"hosts",
{
"text": "Host Script",
"type": "PlasmaBehaviorSocket",
"valid_link_nodes": {"PlasmaPythonFileNode"},
"spawn_empty": True,
},
),
(
"satisfies",
{
"text": "Trigger",
"type": "PlasmaConditionSocket",
},
),
]
)
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop(self, "freeze_phys") layout.prop(self, "freeze_phys")
@ -337,7 +485,9 @@ class PlasmaMultiStageBehaviorNode(PlasmaNodeBase, bpy.types.Node):
if seek_socket.is_linked: if seek_socket.is_linked:
seek_target = seek_socket.links[0].from_node.target seek_target = seek_socket.links[0].from_node.target
if seek_target is not None: if seek_target is not None:
seek_object = exporter.mgr.find_create_object(plSceneObject, bl=seek_target) seek_object = exporter.mgr.find_create_object(
plSceneObject, bl=seek_target
)
else: else:
self.raise_error("MultiStage Behavior's seek point object is invalid") self.raise_error("MultiStage Behavior's seek point object is invalid")
else: else:
@ -349,7 +499,9 @@ class PlasmaMultiStageBehaviorNode(PlasmaNodeBase, bpy.types.Node):
seek_socket = self.find_input_socket("seek_target") seek_socket = self.find_input_socket("seek_target")
msbmod = self.get_key(exporter, so).object msbmod = self.get_key(exporter, so).object
msbmod.smartSeek = True if seek_socket.is_linked or seek_socket.auto_target else False msbmod.smartSeek = (
True if seek_socket.is_linked or seek_socket.auto_target else False
)
msbmod.freezePhys = self.freeze_phys msbmod.freezePhys = self.freeze_phys
msbmod.reverseFBControlsOnRelease = self.reverse_control msbmod.reverseFBControlsOnRelease = self.reverse_control
@ -365,7 +517,7 @@ class PlasmaMultiStageBehaviorNode(PlasmaNodeBase, bpy.types.Node):
settings = stage.find_input("stage_settings") settings = stage.find_input("stage_settings")
if settings: if settings:
animstage.forwardType = getattr(plAnimStage, settings.forward) animstage.forwardType = getattr(plAnimStage, settings.forward)
animstage.backType =getattr(plAnimStage, settings.backward) animstage.backType = getattr(plAnimStage, settings.backward)
animstage.advanceType = getattr(plAnimStage, settings.stage_advance) animstage.advanceType = getattr(plAnimStage, settings.stage_advance)
animstage.regressType = getattr(plAnimStage, settings.stage_regress) animstage.regressType = getattr(plAnimStage, settings.stage_regress)
for flag in settings.notify_on: for flag in settings.notify_on:
@ -395,13 +547,20 @@ class PlasmaMultiStageBehaviorNode(PlasmaNodeBase, bpy.types.Node):
msbmod.addStage(animstage) msbmod.addStage(animstage)
receivers = ((i, i.get_key(exporter, so)) for i in self.find_outputs("satisfies")) receivers = (
(i, i.get_key(exporter, so)) for i in self.find_outputs("satisfies")
)
for node, key in receivers: for node, key in receivers:
if key is not None: if key is not None:
msbmod.addReceiver(key) msbmod.addReceiver(key)
else: else:
exporter.report.warn("'{}' Node '{}' doesn't expose a key. It won't be triggered by '{}'!", exporter.report.warn(
node.bl_idname, node.name, self.name, indent=3) "'{}' Node '{}' doesn't expose a key. It won't be triggered by '{}'!",
node.bl_idname,
node.name,
self.name,
indent=3,
)
@property @property
def requires_actor(self): def requires_actor(self):
@ -423,7 +582,15 @@ class PlasmaAnimStageRefSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
def draw_content(self, context, layout, node, text): def draw_content(self, context, layout, node, text):
if isinstance(node, PlasmaMultiStageBehaviorNode): if isinstance(node, PlasmaMultiStageBehaviorNode):
try: try:
idx = next((idx for idx, socket in enumerate(node.find_input_sockets("stage_refs")) if socket == self)) idx = next(
(
idx
for idx, socket in enumerate(
node.find_input_sockets("stage_refs")
)
if socket == self
)
)
except StopIteration: except StopIteration:
layout.label(text) layout.label(text)
else: else:
@ -434,9 +601,11 @@ class PlasmaAnimStageRefSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
class PlasmaSeekTargetSocketIn(PlasmaNodeSocketBase, bpy.types.NodeSocket): class PlasmaSeekTargetSocketIn(PlasmaNodeSocketBase, bpy.types.NodeSocket):
bl_color = (0.180, 0.350, 0.180, 1.0) bl_color = (0.180, 0.350, 0.180, 1.0)
auto_target = BoolProperty(name="Auto Smart Seek", auto_target = BoolProperty(
description="Smart Seek causes the avatar to seek to the provided position before starting the behavior ('auto' will use the current object as the seek point)", name="Auto Smart Seek",
default=False) description="Smart Seek causes the avatar to seek to the provided position before starting the behavior ('auto' will use the current object as the seek point)",
default=False,
)
def draw_content(self, context, layout, node, text): def draw_content(self, context, layout, node, text):
if not self.is_linked: if not self.is_linked:
@ -459,18 +628,28 @@ class PlasmaSeekTargetNode(PlasmaNodeBase, bpy.types.Node):
bl_label = "Seek Target" bl_label = "Seek Target"
bl_width_default = 200 bl_width_default = 200
target = PointerProperty(name="Position", target = PointerProperty(
description="Object defining the Seek Point's position", name="Position",
type=bpy.types.Object) description="Object defining the Seek Point's position",
type=bpy.types.Object,
output_sockets = OrderedDict([ )
("seekers", {
"text": "Seekers", output_sockets = OrderedDict(
"type": "PlasmaSeekTargetSocketOut", [
"valid_link_nodes": {"PlasmaMultiStageBehaviorNode", "PlasmaOneShotMsgNode"}, (
"valid_link_sockets": {"PlasmaSeekTargetSocketIn"}, "seekers",
}) {
]) "text": "Seekers",
"type": "PlasmaSeekTargetSocketOut",
"valid_link_nodes": {
"PlasmaMultiStageBehaviorNode",
"PlasmaOneShotMsgNode",
},
"valid_link_sockets": {"PlasmaSeekTargetSocketIn"},
},
)
]
)
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
col = layout.column() col = layout.column()

470
korman/nodes/node_conditions.py

@ -23,6 +23,7 @@ from .node_core import *
from ..properties.modifiers.physics import bounds_types from ..properties.modifiers.physics import bounds_types
from .. import idprops from .. import idprops
class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node): class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node):
bl_category = "CONDITIONS" bl_category = "CONDITIONS"
bl_idname = "PlasmaClickableNode" bl_idname = "PlasmaClickableNode"
@ -32,38 +33,61 @@ class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.N
# 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_object = PointerProperty(name="Clickable", clickable_object = PointerProperty(
description="Mesh object that is clickable", name="Clickable",
type=bpy.types.Object, description="Mesh object that is clickable",
poll=idprops.poll_mesh_objects) type=bpy.types.Object,
bounds = EnumProperty(name="Bounds", poll=idprops.poll_mesh_objects,
description="Clickable's bounds (NOTE: only used if your clickable is not a collider)", )
items=bounds_types, bounds = EnumProperty(
default="hull") name="Bounds",
description="Clickable's bounds (NOTE: only used if your clickable is not a collider)",
input_sockets = OrderedDict([ items=bounds_types,
("region", { default="hull",
"text": "Avatar Inside Region", )
"type": "PlasmaClickableRegionSocket",
}), input_sockets = OrderedDict(
("facing", { [
"text": "Avatar Facing Target", (
"type": "PlasmaFacingTargetSocket", "region",
}), {
("message", { "text": "Avatar Inside Region",
"text": "Message", "type": "PlasmaClickableRegionSocket",
"type": "PlasmaEnableMessageSocket", },
"spawn_empty": True, ),
}), (
]) "facing",
{
output_sockets = OrderedDict([ "text": "Avatar Facing Target",
("satisfies", { "type": "PlasmaFacingTargetSocket",
"text": "Satisfies", },
"type": "PlasmaConditionSocket", ),
"valid_link_sockets": {"PlasmaConditionSocket", "PlasmaPythonFileNodeSocket"}, (
}), "message",
]) {
"text": "Message",
"type": "PlasmaEnableMessageSocket",
"spawn_empty": True,
},
),
]
)
output_sockets = OrderedDict(
[
(
"satisfies",
{
"text": "Satisfies",
"type": "PlasmaConditionSocket",
"valid_link_sockets": {
"PlasmaConditionSocket",
"PlasmaPythonFileNodeSocket",
},
},
),
]
)
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop(self, "clickable_object", icon="MESH_DATA") layout.prop(self, "clickable_object", icon="MESH_DATA")
@ -74,8 +98,12 @@ class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.N
if clickable_bo is None: if clickable_bo is None:
clickable_bo = parent_bo clickable_bo = parent_bo
interface = self._find_create_object(plInterfaceInfoModifier, exporter, bl=clickable_bo, so=clickable_so) interface = self._find_create_object(
logicmod = self._find_create_key(plLogicModifier, exporter, bl=clickable_bo, so=clickable_so) plInterfaceInfoModifier, exporter, bl=clickable_bo, so=clickable_so
)
logicmod = self._find_create_key(
plLogicModifier, exporter, bl=clickable_bo, so=clickable_so
)
interface.addIntfKey(logicmod) interface.addIntfKey(logicmod)
# Matches data seen in Cyan's PRPs... # Matches data seen in Cyan's PRPs...
interface.addIntfKey(logicmod) interface.addIntfKey(logicmod)
@ -91,17 +119,25 @@ class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.N
bounds = phys_mod.bounds if phys_mod.enabled else self.bounds bounds = phys_mod.bounds if phys_mod.enabled else self.bounds
# The actual physical object that does the cursor LOS # The actual physical object that does the cursor LOS
exporter.physics.generate_physical(clickable_bo, clickable_so, bounds=bounds, exporter.physics.generate_physical(
member_group="kGroupLOSOnly", clickable_bo,
properties=["kPinned"], clickable_so,
losdbs=["kLOSDBUIItems"]) bounds=bounds,
member_group="kGroupLOSOnly",
properties=["kPinned"],
losdbs=["kLOSDBUIItems"],
)
# Picking Detector -- detect when the physical is clicked # Picking Detector -- detect when the physical is clicked
detector = self._find_create_object(plPickingDetector, exporter, bl=clickable_bo, so=clickable_so) detector = self._find_create_object(
plPickingDetector, exporter, bl=clickable_bo, so=clickable_so
)
detector.addReceiver(logicmod.key) detector.addReceiver(logicmod.key)
# Clickable # Clickable
activator = self._find_create_object(plActivatorConditionalObject, exporter, bl=clickable_bo, so=clickable_so) activator = self._find_create_object(
plActivatorConditionalObject, exporter, bl=clickable_bo, so=clickable_so
)
activator.addActivator(detector.key) activator.addActivator(detector.key)
logicmod.addCondition(activator.key) logicmod.addCondition(activator.key)
logicmod.setLogicFlag(plLogicModifier.kLocalElement, True) logicmod.setLogicFlag(plLogicModifier.kLocalElement, True)
@ -124,7 +160,9 @@ class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.N
def get_key(self, exporter, parent_so): def get_key(self, exporter, parent_so):
# careful... we really make lots of keys... # careful... we really make lots of keys...
clickable_bo, clickable_so = self._get_objects(exporter, parent_so) clickable_bo, clickable_so = self._get_objects(exporter, parent_so)
key = self._find_create_key(plLogicModifier, exporter, bl=clickable_bo, so=clickable_so) key = self._find_create_key(
plLogicModifier, exporter, bl=clickable_bo, so=clickable_so
)
return key return key
def _get_objects(self, exporter, parent_so): def _get_objects(self, exporter, parent_so):
@ -132,7 +170,9 @@ class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.N
# 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_object: if self.clickable_object:
clickable_so = exporter.mgr.find_create_object(plSceneObject, bl=self.clickable_object) clickable_so = exporter.mgr.find_create_object(
plSceneObject, bl=self.clickable_object
)
return (self.clickable_object, clickable_so) return (self.clickable_object, clickable_so)
else: else:
return (None, parent_so) return (None, parent_so)
@ -150,27 +190,38 @@ class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.N
return {"clickable_object": "clickable"} return {"clickable_object": "clickable"}
class PlasmaClickableRegionNode(idprops.IDPropObjectMixin, 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_object = PointerProperty(name="Region", region_object = PointerProperty(
description="Object that defines the region mesh", name="Region",
type=bpy.types.Object, description="Object that defines the region mesh",
poll=idprops.poll_mesh_objects) type=bpy.types.Object,
bounds = EnumProperty(name="Bounds", poll=idprops.poll_mesh_objects,
description="Physical object's bounds (NOTE: only used if your clickable is not a collider)", )
items=bounds_types, bounds = EnumProperty(
default="hull") name="Bounds",
description="Physical object's bounds (NOTE: only used if your clickable is not a collider)",
output_sockets = OrderedDict([ items=bounds_types,
("satisfies", { default="hull",
"text": "Satisfies", )
"type": "PlasmaClickableRegionSocket",
}), output_sockets = OrderedDict(
]) [
(
"satisfies",
{
"text": "Satisfies",
"type": "PlasmaClickableRegionSocket",
},
),
]
)
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop(self, "region_object", icon="MESH_DATA") layout.prop(self, "region_object", icon="MESH_DATA")
@ -188,22 +239,30 @@ class PlasmaClickableRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.t
bounds = phys_mod.bounds if phys_mod.enabled else self.bounds bounds = phys_mod.bounds if phys_mod.enabled else self.bounds
# Our physical is a detector and it only detects avatars... # Our physical is a detector and it only detects avatars...
exporter.physics.generate_physical(region_bo, region_so, bounds=bounds, exporter.physics.generate_physical(
member_group="kGroupDetector", region_bo,
report_groups=["kGroupAvatar"]) region_so,
bounds=bounds,
member_group="kGroupDetector",
report_groups=["kGroupAvatar"],
)
# I'm glad this crazy mess made sense to someone at Cyan... # I'm glad this crazy mess made sense to someone at Cyan...
# ObjectInVolumeDetector can notify multiple logic mods. This implies we could share this # ObjectInVolumeDetector can notify multiple logic mods. This implies we could share this
# one detector for many unrelated logic mods. However, LogicMods and Conditions appear to # one detector for many unrelated logic mods. However, LogicMods and Conditions appear to
# assume they pwn each other... so we need a unique detector. This detector must be attached # assume they pwn each other... so we need a unique detector. This detector must be attached
# as a modifier to the region's SO however. # as a modifier to the region's SO however.
detector = self._find_create_object(plObjectInVolumeDetector, exporter, bl=region_bo, so=region_so) detector = self._find_create_object(
plObjectInVolumeDetector, exporter, bl=region_bo, so=region_so
)
detector.addReceiver(logicmod.key) detector.addReceiver(logicmod.key)
detector.type = plObjectInVolumeDetector.kTypeAny detector.type = plObjectInVolumeDetector.kTypeAny
# Now, the conditional object. At this point, these seem very silly. At least it's not a plModifier. # Now, the conditional object. At this point, these seem very silly. At least it's not a plModifier.
# All they really do is hold a satisfied boolean... # All they really do is hold a satisfied boolean...
objinbox_key = self._find_create_key(plObjectInBoxConditionalObject, exporter, bl=region_bo, so=parent_so) objinbox_key = self._find_create_key(
plObjectInBoxConditionalObject, exporter, bl=region_bo, so=parent_so
)
objinbox_key.object.satisfied = True objinbox_key.object.satisfied = True
logicmod.addCondition(objinbox_key) logicmod.addCondition(objinbox_key)
@ -225,19 +284,26 @@ class PlasmaFacingTargetNode(PlasmaNodeBase, bpy.types.Node):
bl_idname = "PlasmaFacingTargetNode" bl_idname = "PlasmaFacingTargetNode"
bl_label = "Facing Target" bl_label = "Facing Target"
directional = BoolProperty(name="Directional", directional = BoolProperty(name="Directional", description="TODO", default=True)
description="TODO", tolerance = IntProperty(
default=True) name="Degrees",
tolerance = IntProperty(name="Degrees", description="How far away from the target the avatar can turn (in degrees)",
description="How far away from the target the avatar can turn (in degrees)", min=-180,
min=-180, max=180, default=45) max=180,
default=45,
output_sockets = OrderedDict([ )
("satisfies", {
"text": "Satisfies", output_sockets = OrderedDict(
"type": "PlasmaFacingTargetSocket", [
}), (
]) "satisfies",
{
"text": "Satisfies",
"type": "PlasmaFacingTargetSocket",
},
),
]
)
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop(self, "directional") layout.prop(self, "directional")
@ -247,9 +313,11 @@ class PlasmaFacingTargetNode(PlasmaNodeBase, bpy.types.Node):
class PlasmaFacingTargetSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): class PlasmaFacingTargetSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
bl_color = (0.0, 0.267, 0.247, 1.0) bl_color = (0.0, 0.267, 0.247, 1.0)
allow_simple = BoolProperty(name="Facing Target", allow_simple = BoolProperty(
description="Avatar must be facing the target object", name="Facing Target",
default=True) description="Avatar must be facing the target object",
default=True,
)
def draw_content(self, context, layout, node, text): def draw_content(self, context, layout, node, text):
if self.simple_mode: if self.simple_mode:
@ -274,7 +342,9 @@ class PlasmaFacingTargetSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
# This is a programmer failure, so we need a traceback. # This is a programmer failure, so we need a traceback.
raise RuntimeError("Tried to export an unused PlasmaFacingTargetSocket") raise RuntimeError("Tried to export an unused PlasmaFacingTargetSocket")
facing_key = node._find_create_key(plFacingConditionalObject, exporter, bl=bo, so=so) facing_key = node._find_create_key(
plFacingConditionalObject, exporter, bl=bo, so=so
)
facing = facing_key.object facing = facing_key.object
facing.directional = directional facing.directional = directional
facing.satisfied = True facing.satisfied = True
@ -283,13 +353,13 @@ class PlasmaFacingTargetSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
@property @property
def enable_condition(self): def enable_condition(self):
return ((self.simple_mode and self.allow_simple) or self.is_linked) return (self.simple_mode and self.allow_simple) or self.is_linked
@property @property
def simple_mode(self): def simple_mode(self):
"""Simple mode allows a user to click a button on input sockets to automatically generate a """Simple mode allows a user to click a button on input sockets to automatically generate a
Facing Target condition""" Facing Target condition"""
return (not self.is_linked and not self.is_output) return not self.is_linked and not self.is_output
class PlasmaVolumeReportNode(PlasmaNodeBase, bpy.types.Node): class PlasmaVolumeReportNode(PlasmaNodeBase, bpy.types.Node):
@ -297,22 +367,37 @@ class PlasmaVolumeReportNode(PlasmaNodeBase, bpy.types.Node):
bl_idname = "PlasmaVolumeReportNode" bl_idname = "PlasmaVolumeReportNode"
bl_label = "Region Trigger Settings" bl_label = "Region Trigger Settings"
report_when = EnumProperty(name="When", report_when = EnumProperty(
description="When the region should trigger", name="When",
items=[("each", "Each Event", "The region will trigger on every enter/exit"), description="When the region should trigger",
("first", "First Event", "The region will trigger on the first event only"), items=[
("count", "Population", "When the region has a certain number of objects inside it")]) ("each", "Each Event", "The region will trigger on every enter/exit"),
threshold = IntProperty(name="Threshold", ("first", "First Event", "The region will trigger on the first event only"),
description="How many objects should be in the region for it to trigger", (
min=0) "count",
"Population",
output_sockets = OrderedDict([ "When the region has a certain number of objects inside it",
("settings", { ),
"text": "Trigger Settings", ],
"type": "PlasmaVolumeSettingsSocketOut", )
"valid_link_sockets": {"PlasmaVolumeSettingsSocketIn"}, threshold = IntProperty(
}), name="Threshold",
]) description="How many objects should be in the region for it to trigger",
min=0,
)
output_sockets = OrderedDict(
[
(
"settings",
{
"text": "Trigger Settings",
"type": "PlasmaVolumeSettingsSocketOut",
"valid_link_sockets": {"PlasmaVolumeSettingsSocketIn"},
},
),
]
)
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop(self, "report_when") layout.prop(self, "report_when")
@ -332,47 +417,76 @@ class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.type
pl_attrib = {"ptAttribActivator", "ptAttribActivatorList", "ptAttribNamedActivator"} pl_attrib = {"ptAttribActivator", "ptAttribActivatorList", "ptAttribNamedActivator"}
# Region Mesh # Region Mesh
region_object = PointerProperty(name="Region", region_object = PointerProperty(
description="Object that defines the region mesh", name="Region",
type=bpy.types.Object, description="Object that defines the region mesh",
poll=idprops.poll_mesh_objects) type=bpy.types.Object,
bounds = EnumProperty(name="Bounds", poll=idprops.poll_mesh_objects,
description="Physical object's bounds", )
items=bounds_types) bounds = EnumProperty(
name="Bounds", description="Physical object's bounds", items=bounds_types
)
# Detector Properties # Detector Properties
report_on = EnumProperty(name="Triggerers", report_on = EnumProperty(
description="What triggers this region?", name="Triggerers",
options={"ANIMATABLE", "ENUM_FLAG"}, description="What triggers this region?",
items=[("kGroupAvatar", "Avatars", "Avatars trigger this region"), options={"ANIMATABLE", "ENUM_FLAG"},
("kGroupDynamic", "Dynamics", "Any non-avatar dynamic physical object (eg kickables)")], items=[
default={"kGroupAvatar"}) ("kGroupAvatar", "Avatars", "Avatars trigger this region"),
(
input_sockets = OrderedDict([ "kGroupDynamic",
("enter", { "Dynamics",
"text": "Trigger on Enter", "Any non-avatar dynamic physical object (eg kickables)",
"type": "PlasmaVolumeSettingsSocketIn", ),
"valid_link_sockets": {"PlasmaVolumeSettingsSocketOut"}, ],
}), default={"kGroupAvatar"},
("exit", { )
"text": "Trigger on Exit",
"type": "PlasmaVolumeSettingsSocketIn", input_sockets = OrderedDict(
"valid_link_sockets": {"PlasmaVolumeSettingsSocketOut"}, [
}), (
("message", { "enter",
"text": "Message", {
"type": "PlasmaEnableMessageSocket", "text": "Trigger on Enter",
"spawn_empty": True, "type": "PlasmaVolumeSettingsSocketIn",
}), "valid_link_sockets": {"PlasmaVolumeSettingsSocketOut"},
]) },
),
output_sockets = OrderedDict([ (
("satisfies", { "exit",
"text": "Satisfies", {
"type": "PlasmaConditionSocket", "text": "Trigger on Exit",
"valid_link_sockets": {"PlasmaConditionSocket", "PlasmaPythonFileNodeSocket"}, "type": "PlasmaVolumeSettingsSocketIn",
}), "valid_link_sockets": {"PlasmaVolumeSettingsSocketOut"},
]) },
),
(
"message",
{
"text": "Message",
"type": "PlasmaEnableMessageSocket",
"spawn_empty": True,
},
),
]
)
output_sockets = OrderedDict(
[
(
"satisfies",
{
"text": "Satisfies",
"type": "PlasmaConditionSocket",
"valid_link_sockets": {
"PlasmaConditionSocket",
"PlasmaPythonFileNodeSocket",
},
},
),
]
)
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop(self, "report_on") layout.prop(self, "report_on")
@ -390,9 +504,13 @@ class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.type
parent_key = parent_so.key parent_key = parent_so.key
if self.report_enters: if self.report_enters:
rgn_enter = self._find_create_key(plLogicModifier, exporter, suffix="Enter", bl=bo, so=so) rgn_enter = self._find_create_key(
plLogicModifier, exporter, suffix="Enter", bl=bo, so=so
)
if self.report_exits: if self.report_exits:
rgn_exit = self._find_create_key(plLogicModifier, exporter, suffix="Exit", bl=bo, so=so) rgn_exit = self._find_create_key(
plLogicModifier, exporter, suffix="Exit", bl=bo, so=so
)
if rgn_enter is None: if rgn_enter is None:
return rgn_exit return rgn_exit
@ -411,42 +529,78 @@ class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.type
self.raise_error("Region cannot be empty") 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 = self._find_create_object(plInterfaceInfoModifier, exporter, bl=region_bo, so=region_so) interface = self._find_create_object(
plInterfaceInfoModifier, exporter, bl=region_bo, so=region_so
)
# Region Enters # Region Enters
enter_simple = self.find_input_socket("enter").allow enter_simple = self.find_input_socket("enter").allow
enter_settings = self.find_input("enter", "PlasmaVolumeReportNode") enter_settings = self.find_input("enter", "PlasmaVolumeReportNode")
if enter_simple or enter_settings is not None: if enter_simple or enter_settings is not None:
key = self._export_volume_event(exporter, region_bo, region_so, parent_so, plVolumeSensorConditionalObject.kTypeEnter, enter_settings) key = self._export_volume_event(
exporter,
region_bo,
region_so,
parent_so,
plVolumeSensorConditionalObject.kTypeEnter,
enter_settings,
)
interface.addIntfKey(key) interface.addIntfKey(key)
# Region Exits # Region Exits
exit_simple = self.find_input_socket("exit").allow exit_simple = self.find_input_socket("exit").allow
exit_settings = self.find_input("exit", "PlasmaVolumeReportNode") exit_settings = self.find_input("exit", "PlasmaVolumeReportNode")
if exit_simple or exit_settings is not None: if exit_simple or exit_settings is not None:
key = self._export_volume_event(exporter, region_bo, region_so, parent_so, plVolumeSensorConditionalObject.kTypeExit, exit_settings) key = self._export_volume_event(
exporter,
region_bo,
region_so,
parent_so,
plVolumeSensorConditionalObject.kTypeExit,
exit_settings,
)
interface.addIntfKey(key) interface.addIntfKey(key)
# Don't forget to export the physical object itself! # Don't forget to export the physical object itself!
exporter.physics.generate_physical(region_bo, region_so, bounds=self.bounds, exporter.physics.generate_physical(
member_group="kGroupDetector", region_bo,
report_groups=self.report_on) region_so,
bounds=self.bounds,
def _export_volume_event(self, exporter, region_bo, region_so, parent_so, event, settings): member_group="kGroupDetector",
report_groups=self.report_on,
)
def _export_volume_event(
self, exporter, region_bo, region_so, parent_so, event, settings
):
if event == plVolumeSensorConditionalObject.kTypeEnter: if event == plVolumeSensorConditionalObject.kTypeEnter:
suffix = "Enter" suffix = "Enter"
else: else:
suffix = "Exit" suffix = "Exit"
logicKey = self._find_create_key(plLogicModifier, exporter, suffix=suffix, bl=region_bo, so=region_so) logicKey = self._find_create_key(
plLogicModifier, exporter, suffix=suffix, bl=region_bo, so=region_so
)
logicmod = logicKey.object logicmod = logicKey.object
logicmod.setLogicFlag(plLogicModifier.kMultiTrigger, True) logicmod.setLogicFlag(plLogicModifier.kMultiTrigger, True)
logicmod.notify = self.generate_notify_msg(exporter, parent_so, "satisfies") logicmod.notify = self.generate_notify_msg(exporter, parent_so, "satisfies")
# Now, the detector objects # Now, the detector objects
det = self._find_create_object(plObjectInVolumeDetector, exporter, suffix=suffix, bl=region_bo, so=region_so) det = self._find_create_object(
plObjectInVolumeDetector,
volKey = self._find_create_key(plVolumeSensorConditionalObject, exporter, suffix=suffix, bl=region_bo, so=region_so) exporter,
suffix=suffix,
bl=region_bo,
so=region_so,
)
volKey = self._find_create_key(
plVolumeSensorConditionalObject,
exporter,
suffix=suffix,
bl=region_bo,
so=region_so,
)
volsens = volKey.object volsens = volKey.object
volsens.type = event volsens.type = event
@ -474,13 +628,17 @@ class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.type
@property @property
def report_enters(self): def report_enters(self):
return (self.find_input_socket("enter").allow or return (
self.find_input("enter", "PlasmaVolumeReportNode") is not None) self.find_input_socket("enter").allow
or self.find_input("enter", "PlasmaVolumeReportNode") is not None
)
@property @property
def report_exits(self): def report_exits(self):
return (self.find_input_socket("exit").allow or return (
self.find_input("exit", "PlasmaVolumeReportNode") is not None) self.find_input_socket("exit").allow
or self.find_input("exit", "PlasmaVolumeReportNode") is not None
)
class PlasmaVolumeSettingsSocket(PlasmaNodeSocketBase): class PlasmaVolumeSettingsSocket(PlasmaNodeSocketBase):

152
korman/nodes/node_core.py

@ -21,14 +21,20 @@ import time
from ..exporter import ExportError from ..exporter import ExportError
class PlasmaNodeBase: class PlasmaNodeBase:
def generate_notify_msg(self, exporter, so, socket_id, idname=None): def generate_notify_msg(self, exporter, so, socket_id, idname=None):
notify = plNotifyMsg() notify = plNotifyMsg()
notify.BCastFlags = (plMessage.kNetPropagate | plMessage.kLocalPropagate) notify.BCastFlags = plMessage.kNetPropagate | plMessage.kLocalPropagate
for i in self.find_outputs(socket_id, idname): for i in self.find_outputs(socket_id, idname):
key = i.get_key(exporter, so) key = i.get_key(exporter, so)
if key is None: if key is None:
exporter.report.warn(" '{}' Node '{}' doesn't expose a key. It won't be triggered by '{}'!".format(i.bl_idname, i.name, self.name), indent=3) exporter.report.warn(
" '{}' Node '{}' doesn't expose a key. It won't be triggered by '{}'!".format(
i.bl_idname, i.name, self.name
),
indent=3,
)
elif isinstance(key, tuple): elif isinstance(key, tuple):
for i in key: for i in key:
notify.addReceiver(key) notify.addReceiver(key)
@ -44,7 +50,9 @@ class PlasmaNodeBase:
if single: if single:
name = bl.name if bl is not None else so.key.name name = bl.name if bl is not None else so.key.name
if suffix: if suffix:
working_name = "{}_{}_{}_{}".format(name, self.id_data.name, self.name, suffix) working_name = "{}_{}_{}_{}".format(
name, self.id_data.name, self.name, suffix
)
else: else:
working_name = "{}_{}_{}".format(name, self.id_data.name, self.name) working_name = "{}_{}_{}".format(name, self.id_data.name, self.name)
else: else:
@ -70,17 +78,23 @@ class PlasmaNodeBase:
def _find_create_object(self, pClass, exporter, **kwargs): def _find_create_object(self, pClass, exporter, **kwargs):
"""Finds or creates an hsKeyedObject specific to this node.""" """Finds or creates an hsKeyedObject specific to this node."""
assert "name" not in kwargs assert "name" not in kwargs
kwargs["name"] = self.get_key_name(issubclass(pClass, (plObjInterface, plSingleModifier)), kwargs["name"] = self.get_key_name(
kwargs.pop("suffix", ""), kwargs.get("bl"), issubclass(pClass, (plObjInterface, plSingleModifier)),
kwargs.get("so")) kwargs.pop("suffix", ""),
kwargs.get("bl"),
kwargs.get("so"),
)
return exporter.mgr.find_create_object(pClass, **kwargs) return exporter.mgr.find_create_object(pClass, **kwargs)
def _find_create_key(self, pClass, exporter, **kwargs): def _find_create_key(self, pClass, exporter, **kwargs):
"""Finds or creates a plKey specific to this node.""" """Finds or creates a plKey specific to this node."""
assert "name" not in kwargs assert "name" not in kwargs
kwargs["name"] = self.get_key_name(issubclass(pClass, (plObjInterface, plSingleModifier)), kwargs["name"] = self.get_key_name(
kwargs.pop("suffix", ""), kwargs.get("bl"), issubclass(pClass, (plObjInterface, plSingleModifier)),
kwargs.get("so")) kwargs.pop("suffix", ""),
kwargs.get("bl"),
kwargs.get("so"),
)
return exporter.mgr.find_create_key(pClass, **kwargs) return exporter.mgr.find_create_key(pClass, **kwargs)
def find_input(self, key, idname=None): def find_input(self, key, idname=None):
@ -181,19 +195,27 @@ class PlasmaNodeBase:
"""Generates valid node sockets that can be linked to a specific socket on this node.""" """Generates valid node sockets that can be linked to a specific socket on this node."""
from .node_deprecated import PlasmaDeprecatedNode from .node_deprecated import PlasmaDeprecatedNode
source_socket_props = getattr(self.__class__, "output_sockets", {}) if is_output else \ source_socket_props = (
getattr(self.__class__, "input_sockets", {}) getattr(self.__class__, "output_sockets", {})
if is_output
else getattr(self.__class__, "input_sockets", {})
)
source_socket_def = source_socket_props.get(socket.alias, {}) source_socket_def = source_socket_props.get(socket.alias, {})
valid_dest_sockets = source_socket_def.get("valid_link_sockets") valid_dest_sockets = source_socket_def.get("valid_link_sockets")
valid_dest_nodes = source_socket_def.get("valid_link_nodes") valid_dest_nodes = source_socket_def.get("valid_link_nodes")
for dest_node_cls in bpy.types.Node.__subclasses__(): for dest_node_cls in bpy.types.Node.__subclasses__():
if not issubclass(dest_node_cls, PlasmaNodeBase) or issubclass(dest_node_cls, PlasmaDeprecatedNode): if not issubclass(dest_node_cls, PlasmaNodeBase) or issubclass(
dest_node_cls, PlasmaDeprecatedNode
):
continue continue
# Korman standard node socket definitions # Korman standard node socket definitions
socket_defs = getattr(dest_node_cls, "input_sockets", {}) if is_output else \ socket_defs = (
getattr(dest_node_cls, "output_sockets", {}) getattr(dest_node_cls, "input_sockets", {})
if is_output
else getattr(dest_node_cls, "output_sockets", {})
)
for socket_name, socket_def in socket_defs.items(): for socket_name, socket_def in socket_defs.items():
if socket_def.get("can_link") is False: if socket_def.get("can_link") is False:
continue continue
@ -201,17 +223,29 @@ class PlasmaNodeBase:
continue continue
# Can this socket link to the socket_def on the destination node? # Can this socket link to the socket_def on the destination node?
if valid_dest_nodes is not None and dest_node_cls.bl_idname not in valid_dest_nodes: if (
valid_dest_nodes is not None
and dest_node_cls.bl_idname not in valid_dest_nodes
):
continue continue
if valid_dest_sockets is not None and socket_def["type"] not in valid_dest_sockets: if (
valid_dest_sockets is not None
and socket_def["type"] not in valid_dest_sockets
):
continue continue
# Can the socket_def on the destination node link to this socket? # Can the socket_def on the destination node link to this socket?
valid_source_nodes = socket_def.get("valid_link_nodes") valid_source_nodes = socket_def.get("valid_link_nodes")
valid_source_sockets = socket_def.get("valid_link_sockets") valid_source_sockets = socket_def.get("valid_link_sockets")
if valid_source_nodes is not None and self.bl_idname not in valid_source_nodes: if (
valid_source_nodes is not None
and self.bl_idname not in valid_source_nodes
):
continue continue
if valid_source_sockets is not None and socket.bl_idname not in valid_source_sockets: if (
valid_source_sockets is not None
and socket.bl_idname not in valid_source_sockets
):
continue continue
if valid_source_sockets is None and valid_source_nodes is None: if valid_source_sockets is None and valid_source_nodes is None:
if socket.bl_idname != socket_def["type"]: if socket.bl_idname != socket_def["type"]:
@ -222,10 +256,12 @@ class PlasmaNodeBase:
if poll_add is not None and not poll_add(context): if poll_add is not None and not poll_add(context):
continue continue
yield { "node_idname": dest_node_cls.bl_idname, yield {
"node_text": dest_node_cls.bl_label, "node_idname": dest_node_cls.bl_idname,
"socket_name": socket_name, "node_text": dest_node_cls.bl_label,
"socket_text": socket_def["text"] } "socket_name": socket_name,
"socket_text": socket_def["text"],
}
# Some node types (eg Python) may auto-generate their own sockets, so we ask them now. # Some node types (eg Python) may auto-generate their own sockets, so we ask them now.
for i in dest_node_cls.generate_valid_links_to(context, socket, is_output): for i in dest_node_cls.generate_valid_links_to(context, socket, is_output):
@ -273,10 +309,12 @@ class PlasmaNodeBase:
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return (context.bl_idname == "PlasmaNodeTree") return context.bl_idname == "PlasmaNodeTree"
def raise_error(self, message): def raise_error(self, message):
final = "Plasma Node Tree '{}' Node '{}': {}".format(self.id_data.name, self.name, message) final = "Plasma Node Tree '{}' Node '{}': {}".format(
self.id_data.name, self.name, message
)
raise ExportError(final) raise ExportError(final)
@property @property
@ -285,8 +323,10 @@ class PlasmaNodeBase:
@property @property
def _socket_defs(self): def _socket_defs(self):
return (getattr(self.__class__, "input_sockets", {}), return (
getattr(self.__class__, "output_sockets", {})) getattr(self.__class__, "input_sockets", {}),
getattr(self.__class__, "output_sockets", {}),
)
def _spawn_socket(self, key, options, sockets): def _spawn_socket(self, key, options, sockets):
socket = sockets.new(options["type"], options["text"], key) socket = sockets.new(options["type"], options["text"], key)
@ -299,7 +339,11 @@ class PlasmaNodeBase:
def _tattle(self, socket, link, reason): def _tattle(self, socket, link, reason):
direction = "->" if socket.is_output else "<-" direction = "->" if socket.is_output else "<-"
print("Removing {} {} {} {}".format(link.from_node.name, direction, link.to_node.name, reason)) print(
"Removing {} {} {} {}".format(
link.from_node.name, direction, link.to_node.name, reason
)
)
def unlink_outputs(self, alias, reason=None): def unlink_outputs(self, alias, reason=None):
links = self.id_data.links links = self.id_data.links
@ -320,7 +364,9 @@ class PlasmaNodeBase:
def _update_init_sockets(self, defs, sockets): def _update_init_sockets(self, defs, sockets):
# Create any missing sockets and spawn any required empties. # Create any missing sockets and spawn any required empties.
for alias, options in defs.items(): for alias, options in defs.items():
working_sockets = [(i, socket) for i, socket in enumerate(sockets) if socket.alias == alias] working_sockets = [
(i, socket) for i, socket in enumerate(sockets) if socket.alias == alias
]
if not working_sockets: if not working_sockets:
self._spawn_socket(alias, options, sockets) self._spawn_socket(alias, options, sockets)
elif options.get("spawn_empty", False): elif options.get("spawn_empty", False):
@ -382,7 +428,9 @@ class PlasmaNodeBase:
if allowed_sockets or allowed_nodes: if allowed_sockets or allowed_nodes:
for link in socket.links: for link in socket.links:
if allowed_nodes: if allowed_nodes:
to_from_node = link.to_node if socket.is_output else link.from_node to_from_node = (
link.to_node if socket.is_output else link.from_node
)
if to_from_node.bl_idname not in allowed_nodes: if to_from_node.bl_idname not in allowed_nodes:
try: try:
self._tattle(socket, link, "(bad node)") self._tattle(socket, link, "(bad node)")
@ -392,8 +440,13 @@ class PlasmaNodeBase:
pass pass
continue continue
if allowed_sockets: if allowed_sockets:
to_from_socket = link.to_socket if socket.is_output else link.from_socket to_from_socket = (
if to_from_socket is None or to_from_socket.bl_idname not in allowed_sockets: link.to_socket if socket.is_output else link.from_socket
)
if (
to_from_socket is None
or to_from_socket.bl_idname not in allowed_sockets
):
try: try:
self._tattle(socket, link, "(bad socket)") self._tattle(socket, link, "(bad socket)")
self.id_data.links.remove(link) self.id_data.links.remove(link)
@ -407,16 +460,21 @@ class PlasmaNodeBase:
def _whine(self, msg, *args): def _whine(self, msg, *args):
if args: if args:
msg = msg.format(*args) msg = msg.format(*args)
print("'{}' Node '{}': Whinging about {}".format(self.bl_idname, self.name, msg)) print(
"'{}' Node '{}': Whinging about {}".format(self.bl_idname, self.name, msg)
)
class PlasmaTreeOutputNodeBase(PlasmaNodeBase): class PlasmaTreeOutputNodeBase(PlasmaNodeBase):
"""Represents the final output of a node tree""" """Represents the final output of a node tree"""
@classmethod @classmethod
def poll_add(cls, context): def poll_add(cls, context):
# There can only be one of these nodes per tree, so we will only allow this to be # There can only be one of these nodes per tree, so we will only allow this to be
# added if no other output nodes are found. # added if no other output nodes are found.
return not any((isinstance(node, cls) for node in context.space_data.node_tree.nodes)) return not any(
(isinstance(node, cls) for node in context.space_data.node_tree.nodes)
)
class PlasmaNodeSocketBase: class PlasmaNodeSocketBase:
@ -424,9 +482,9 @@ class PlasmaNodeSocketBase:
def alias(self): def alias(self):
"""Blender appends .000 stuff if it's a dupe. We don't care about dupe identifiers...""" """Blender appends .000 stuff if it's a dupe. We don't care about dupe identifiers..."""
ident = self.identifier ident = self.identifier
if ident.find('.') == -1: if ident.find(".") == -1:
return ident return ident
return ident.rsplit('.', 1)[0] return ident.rsplit(".", 1)[0]
def draw(self, context, layout, node, text): def draw(self, context, layout, node, text):
if not self.is_output: if not self.is_output:
@ -462,9 +520,11 @@ class PlasmaNodeSocketBase:
# loaded. So, only check in that case. # loaded. So, only check in that case.
hval = str(hash((i for i in bpy.data.texts))) hval = str(hash((i for i in bpy.data.texts)))
if hval != self.possible_links_texts_hash: if hval != self.possible_links_texts_hash:
self.has_possible_links_value = any(self.node.generate_valid_links_for(bpy.context, self.has_possible_links_value = any(
self, self.node.generate_valid_links_for(
self.is_output)) bpy.context, self, self.is_output
)
)
self.possible_links_texts_hash = hval self.possible_links_texts_hash = hval
self.possible_links_update_time = tval self.possible_links_update_time = tval
return self.has_possible_links_value return self.has_possible_links_value
@ -475,8 +535,9 @@ class PlasmaNodeSocketBase:
@classmethod @classmethod
def register(cls): def register(cls):
cls.has_possible_links = BoolProperty(options={"HIDDEN", "SKIP_SAVE"}, cls.has_possible_links = BoolProperty(
get=cls._has_possible_links) options={"HIDDEN", "SKIP_SAVE"}, get=cls._has_possible_links
)
cls.has_possible_links_value = BoolProperty(options={"HIDDEN", "SKIP_SAVE"}) cls.has_possible_links_value = BoolProperty(options={"HIDDEN", "SKIP_SAVE"})
cls.possible_links_update_time = FloatProperty(options={"HIDDEN", "SKIP_SAVE"}) cls.possible_links_update_time = FloatProperty(options={"HIDDEN", "SKIP_SAVE"})
cls.possible_links_texts_hash = StringProperty(options={"HIDDEN", "SKIP_SAVE"}) cls.possible_links_texts_hash = StringProperty(options={"HIDDEN", "SKIP_SAVE"})
@ -484,6 +545,7 @@ class PlasmaNodeSocketBase:
class PlasmaNodeSocketInputGeneral(PlasmaNodeSocketBase, bpy.types.NodeSocket): class PlasmaNodeSocketInputGeneral(PlasmaNodeSocketBase, bpy.types.NodeSocket):
"""A general input socket that will steal the output's color""" """A general input socket that will steal the output's color"""
def draw_color(self, context, node): def draw_color(self, context, node):
if self.is_linked: if self.is_linked:
return self.links[0].from_socket.draw_color(context, node) return self.links[0].from_socket.draw_color(context, node)
@ -516,12 +578,16 @@ class PlasmaNodeTree(bpy.types.NodeTree):
if harvest_method is not None: if harvest_method is not None:
actors.update(harvest_method()) actors.update(harvest_method())
elif not isinstance(node, PlasmaNodeBase): elif not isinstance(node, PlasmaNodeBase):
raise ExportError("Plasma Node Tree '{}' Node '{}': is not a valid node for this tree".format(self.id_data.name, node.name)) raise ExportError(
"Plasma Node Tree '{}' Node '{}': is not a valid node for this tree".format(
self.id_data.name, node.name
)
)
return actors return actors
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return (context.scene.render.engine == "PLASMA_GAME") return context.scene.render.engine == "PLASMA_GAME"
@property @property
def requires_actor(self): def requires_actor(self):
@ -539,4 +605,6 @@ def _nuke_plasma_nodes(dummy):
for i in bpy.data.node_groups: for i in bpy.data.node_groups:
if isinstance(i, PlasmaNodeTree): if isinstance(i, PlasmaNodeTree):
i.nodes.clear() i.nodes.clear()
bpy.app.handlers.load_pre.append(_nuke_plasma_nodes) bpy.app.handlers.load_pre.append(_nuke_plasma_nodes)

65
korman/nodes/node_deprecated.py

@ -20,6 +20,7 @@ from collections import OrderedDict
from .node_core import * from .node_core import *
class PlasmaDeprecatedNode(PlasmaNodeBase): class PlasmaDeprecatedNode(PlasmaNodeBase):
@abc.abstractmethod @abc.abstractmethod
def upgrade(self): def upgrade(self):
@ -53,28 +54,44 @@ class PlasmaResponderCommandNode(PlasmaDeprecatedNode, bpy.types.Node):
bl_idname = "PlasmaResponderCommandNode" bl_idname = "PlasmaResponderCommandNode"
bl_label = "Responder Command" bl_label = "Responder Command"
input_sockets = OrderedDict([ input_sockets = OrderedDict(
("whodoneit", { [
"text": "Condition", (
"type": "PlasmaRespCommandSocket", "whodoneit",
}), {
]) "text": "Condition",
"type": "PlasmaRespCommandSocket",
output_sockets = OrderedDict([ },
("msg", { ),
"link_limit": 1, ]
"text": "Message", )
"type": "PlasmaMessageSocket",
}), output_sockets = OrderedDict(
("trigger", { [
"text": "Trigger", (
"type": "PlasmaRespCommandSocket", "msg",
}), {
("reenable", { "link_limit": 1,
"text": "Local Reenable", "text": "Message",
"type": "PlasmaEnableMessageSocket", "type": "PlasmaMessageSocket",
}), },
]) ),
(
"trigger",
{
"text": "Trigger",
"type": "PlasmaRespCommandSocket",
},
),
(
"reenable",
{
"text": "Local Reenable",
"type": "PlasmaEnableMessageSocket",
},
),
]
)
def _find_message_sender_node(self, parentCmdNode=None): def _find_message_sender_node(self, parentCmdNode=None):
if parentCmdNode is None: if parentCmdNode is None:
@ -103,7 +120,6 @@ class PlasmaResponderCommandNode(PlasmaDeprecatedNode, bpy.types.Node):
self._whine("unexpected command node type '{}'", parentCmdNode.bl_idname) self._whine("unexpected command node type '{}'", parentCmdNode.bl_idname)
return None return None
def upgrade(self): def upgrade(self):
senderNode = self._find_message_sender_node() senderNode = self._find_message_sender_node()
if senderNode is None: if senderNode is None:
@ -130,6 +146,7 @@ class PlasmaResponderCommandNode(PlasmaDeprecatedNode, bpy.types.Node):
continue continue
tree.links.new(link.to_socket, fromSocket) tree.links.new(link.to_socket, fromSocket)
@bpy.app.handlers.persistent @bpy.app.handlers.persistent
def _upgrade_node_trees(dummy): def _upgrade_node_trees(dummy):
for tree in bpy.data.node_groups: for tree in bpy.data.node_groups:
@ -150,4 +167,6 @@ def _upgrade_node_trees(dummy):
# toss deprecated nodes # toss deprecated nodes
for node in nuke: for node in nuke:
tree.nodes.remove(node) tree.nodes.remove(node)
bpy.app.handlers.load_post.append(_upgrade_node_trees) bpy.app.handlers.load_post.append(_upgrade_node_trees)

153
korman/nodes/node_logic.py

@ -19,10 +19,17 @@ from collections import OrderedDict
from PyHSPlasma import * from PyHSPlasma import *
from .node_core import * from .node_core import *
from ..properties.modifiers.physics import bounds_types, bounds_type_index, bounds_type_str from ..properties.modifiers.physics import (
bounds_types,
bounds_type_index,
bounds_type_str,
)
from .. import idprops from .. import idprops
class PlasmaExcludeRegionNode(idprops.IDPropObjectMixin, 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"
@ -33,46 +40,70 @@ class PlasmaExcludeRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.typ
def _get_bounds(self): def _get_bounds(self):
if self.region_object is not None: if self.region_object is not None:
return bounds_type_index(self.region_object.plasma_modifiers.collision.bounds) return bounds_type_index(
self.region_object.plasma_modifiers.collision.bounds
)
return bounds_type_index("hull") return bounds_type_index("hull")
def _set_bounds(self, value): def _set_bounds(self, value):
if self.region_object is not None: if self.region_object is not None:
self.region_object.plasma_modifiers.collision.bounds = bounds_type_str(value) self.region_object.plasma_modifiers.collision.bounds = bounds_type_str(
value
region_object = PointerProperty(name="Region", )
description="Region object's name",
type=bpy.types.Object, region_object = PointerProperty(
poll=idprops.poll_mesh_objects) name="Region",
bounds = EnumProperty(name="Bounds", description="Region object's name",
description="Region bounds", type=bpy.types.Object,
items=bounds_types, poll=idprops.poll_mesh_objects,
get=_get_bounds, )
set=_set_bounds) bounds = EnumProperty(
block_cameras = BoolProperty(name="Block Cameras", name="Bounds",
description="The region blocks cameras when it has been cleared") description="Region bounds",
items=bounds_types,
input_sockets = OrderedDict([ get=_get_bounds,
("safe_point", { set=_set_bounds,
"type": "PlasmaExcludeSafePointSocket", )
"text": "Safe Point", block_cameras = BoolProperty(
"spawn_empty": True, name="Block Cameras",
# This never links to anything... description="The region blocks cameras when it has been cleared",
"valid_link_sockets": frozenset(), )
}),
("msg", { input_sockets = OrderedDict(
"type": "PlasmaExcludeMessageSocket", [
"text": "Message", (
"spawn_empty": True, "safe_point",
}), {
]) "type": "PlasmaExcludeSafePointSocket",
"text": "Safe Point",
output_sockets = OrderedDict([ "spawn_empty": True,
("keyref", { # This never links to anything...
"text": "References", "valid_link_sockets": frozenset(),
"type": "PlasmaPythonReferenceNodeSocket", },
"valid_link_nodes": {"PlasmaPythonFileNode"}, ),
}), (
]) "msg",
{
"type": "PlasmaExcludeMessageSocket",
"text": "Message",
"spawn_empty": True,
},
),
]
)
output_sockets = OrderedDict(
[
(
"keyref",
{
"text": "References",
"type": "PlasmaPythonReferenceNodeSocket",
"valid_link_nodes": {"PlasmaPythonFileNode"},
},
),
]
)
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop(self, "region_object", icon="MESH_DATA") layout.prop(self, "region_object", icon="MESH_DATA")
@ -82,21 +113,31 @@ class PlasmaExcludeRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.typ
def get_key(self, exporter, parent_so): def get_key(self, exporter, parent_so):
if self.region_object is None: if self.region_object is None:
self.raise_error("Region must be set") self.raise_error("Region must be set")
return self._find_create_key(plExcludeRegionModifier, exporter, bl=self.region_object) return self._find_create_key(
plExcludeRegionModifier, exporter, bl=self.region_object
)
def harvest_actors(self): def harvest_actors(self):
return (i.safepoint.name for i in self.find_input_sockets("safe_points") if i.safepoint is not None) 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):
excludergn = self.get_key(exporter, parent_so).object excludergn = self.get_key(exporter, parent_so).object
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) 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"):
safept = i.safepoint_object safept = i.safepoint_object
if safept: if safept:
excludergn.addSafePoint(exporter.mgr.find_create_key(plSceneObject, bl=safept)) excludergn.addSafePoint(
exporter.mgr.find_create_key(plSceneObject, bl=safept)
)
# Ensure the region is exported # Ensure the region is exported
if exporter.mgr.getVer() <= pvPots: if exporter.mgr.getVer() <= pvPots:
@ -105,11 +146,15 @@ class PlasmaExcludeRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.typ
else: else:
member_group = "kGroupStatic" member_group = "kGroupStatic"
collide_groups = [] collide_groups = []
exporter.physics.generate_physical(self.region_object, region_so, bounds=self.bounds, exporter.physics.generate_physical(
properties=["kPinned"], self.region_object,
losdbs=["kLOSDBUIBlockers"], region_so,
member_group=member_group, bounds=self.bounds,
collide_groups=collide_groups) properties=["kPinned"],
losdbs=["kLOSDBUIBlockers"],
member_group=member_group,
collide_groups=collide_groups,
)
@property @property
def export_once(self): def export_once(self):
@ -120,12 +165,16 @@ class PlasmaExcludeRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.typ
return {"region_object": "region"} return {"region_object": "region"}
class PlasmaExcludeSafePointSocket(idprops.IDPropObjectMixin, 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_object = PointerProperty(name="Safe Point", safepoint_object = PointerProperty(
description="A point outside of this exclude region to move the avatar to", name="Safe Point",
type=bpy.types.Object) 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(self, "safepoint_object", icon="EMPTY_DATA") layout.prop(self, "safepoint_object", icon="EMPTY_DATA")

880
korman/nodes/node_messages.py

File diff suppressed because it is too large Load Diff

381
korman/nodes/node_python.py

@ -27,10 +27,23 @@ from .. import idprops
from ..plasma_attributes import get_attributes_from_str from ..plasma_attributes import get_attributes_from_str
_single_user_attribs = { _single_user_attribs = {
"ptAttribBoolean", "ptAttribInt", "ptAttribFloat", "ptAttribString", "ptAttribDropDownList", "ptAttribBoolean",
"ptAttribSceneobject", "ptAttribDynamicMap", "ptAttribGUIDialog", "ptAttribExcludeRegion", "ptAttribInt",
"ptAttribWaveSet", "ptAttribSwimCurrent", "ptAttribAnimation", "ptAttribBehavior", "ptAttribFloat",
"ptAttribMaterial", "ptAttribMaterialAnimation", "ptAttribGUIPopUpMenu", "ptAttribGUISkin", "ptAttribString",
"ptAttribDropDownList",
"ptAttribSceneobject",
"ptAttribDynamicMap",
"ptAttribGUIDialog",
"ptAttribExcludeRegion",
"ptAttribWaveSet",
"ptAttribSwimCurrent",
"ptAttribAnimation",
"ptAttribBehavior",
"ptAttribMaterial",
"ptAttribMaterialAnimation",
"ptAttribGUIPopUpMenu",
"ptAttribGUISkin",
"ptAttribGrassShader", "ptAttribGrassShader",
} }
@ -83,9 +96,11 @@ _attrib_key_types = {
"ptAttribGUIPopUpMenu": plFactory.ClassIndex("pfGUIPopUpMenu"), "ptAttribGUIPopUpMenu": plFactory.ClassIndex("pfGUIPopUpMenu"),
"ptAttribGUISkin": plFactory.ClassIndex("pfGUISkin"), "ptAttribGUISkin": plFactory.ClassIndex("pfGUISkin"),
"ptAttribWaveSet": plFactory.ClassIndex("plWaveSet7"), "ptAttribWaveSet": plFactory.ClassIndex("plWaveSet7"),
"ptAttribSwimCurrent": (plFactory.ClassIndex("plSwimRegionInterface"), "ptAttribSwimCurrent": (
plFactory.ClassIndex("plSwimCircularCurrentRegion"), plFactory.ClassIndex("plSwimRegionInterface"),
plFactory.ClassIndex("plSwimStraightCurrentRegion")), plFactory.ClassIndex("plSwimCircularCurrentRegion"),
plFactory.ClassIndex("plSwimStraightCurrentRegion"),
),
"ptAttribClusterList": plFactory.ClassIndex("plClusterGroup"), "ptAttribClusterList": plFactory.ClassIndex("plClusterGroup"),
"ptAttribMaterialAnimation": plFactory.ClassIndex("plLayerAnimation"), "ptAttribMaterialAnimation": plFactory.ClassIndex("plLayerAnimation"),
"ptAttribGrassShader": plFactory.ClassIndex("plGrassShaderMod"), "ptAttribGrassShader": plFactory.ClassIndex("plGrassShaderMod"),
@ -174,8 +189,10 @@ class PlasmaAttribute(bpy.types.PropertyGroup):
def _get_simple_value(self): def _get_simple_value(self):
return getattr(self, self._simple_attrs[self.attribute_type]) return getattr(self, self._simple_attrs[self.attribute_type])
def _set_simple_value(self, value): def _set_simple_value(self, value):
setattr(self, self._simple_attrs[self.attribute_type], value) setattr(self, self._simple_attrs[self.attribute_type], value)
simple_value = property(_get_simple_value, _set_simple_value) simple_value = property(_get_simple_value, _set_simple_value)
@ -203,17 +220,21 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
self.attributes.clear() self.attributes.clear()
self.inputs.clear() self.inputs.clear()
if self.text_id is not None: if self.text_id is not None:
bpy.ops.node.plasma_attributes_to_node(node_path=self.node_path, text_path=self.text_id.name) bpy.ops.node.plasma_attributes_to_node(
node_path=self.node_path, text_path=self.text_id.name
)
filename = StringProperty(name="File Name", filename = StringProperty(
description="Python Filename", name="File Name", description="Python Filename", update=_update_pyfile
update=_update_pyfile) )
filepath = StringProperty(options={"HIDDEN"}) filepath = StringProperty(options={"HIDDEN"})
text_id = PointerProperty(name="Script File", text_id = PointerProperty(
description="Script file datablock", name="Script File",
type=bpy.types.Text, description="Script file datablock",
poll=_poll_pytext, type=bpy.types.Text,
update=_update_pytext) poll=_poll_pytext,
update=_update_pytext,
)
# This property exists for UI purposes ONLY # This property exists for UI purposes ONLY
package = BoolProperty(options={"HIDDEN", "SKIP_SAVE"}) package = BoolProperty(options={"HIDDEN", "SKIP_SAVE"})
@ -223,7 +244,7 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
@property @property
def attribute_map(self): def attribute_map(self):
return { i.attribute_id: i for i in self.attributes } return {i.attribute_id: i for i in self.attributes}
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
main_row = layout.row(align=True) main_row = layout.row(align=True)
@ -233,7 +254,9 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
# open operator # open operator
sel_text = "Load Script" if self.text_id is None else "" sel_text = "Load Script" if self.text_id is None else ""
operator = main_row.operator("file.plasma_file_picker", icon="FILESEL", text=sel_text) operator = main_row.operator(
"file.plasma_file_picker", icon="FILESEL", text=sel_text
)
operator.filter_glob = "*.py" operator.filter_glob = "*.py"
operator.data_path = self.node_path operator.data_path = self.node_path
operator.filename_property = "filename" operator.filename_property = "filename"
@ -251,14 +274,19 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
# rescan operator # rescan operator
row = main_row.row(align=True) row = main_row.row(align=True)
row.enabled = self.text_id is not None row.enabled = self.text_id is not None
operator = row.operator("node.plasma_attributes_to_node", icon="FILE_REFRESH", text="") operator = row.operator(
"node.plasma_attributes_to_node", icon="FILE_REFRESH", text=""
)
if self.text_id is not None: if self.text_id is not None:
operator.text_path = self.text_id.name operator.text_path = self.text_id.name
operator.node_path = self.node_path operator.node_path = self.node_path
# This could happen on an upgrade # This could happen on an upgrade
if self.text_id is None and self.filename: if self.text_id is None and self.filename:
layout.label(text="Script '{}' is not loaded in Blender".format(self.filename), icon="ERROR") layout.label(
text="Script '{}' is not loaded in Blender".format(self.filename),
icon="ERROR",
)
def get_key(self, exporter, so): def get_key(self, exporter, so):
return self._find_create_key(plPythonFileMod, exporter, so=so) return self._find_create_key(plPythonFileMod, exporter, so=so)
@ -279,12 +307,16 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
# Check to see if we should pack this file # Check to see if we should pack this file
if exporter.output.want_py_text(self.text_id): if exporter.output.want_py_text(self.text_id):
exporter.report.msg("Including Python '{}' for package", self.filename, indent=3) exporter.report.msg(
"Including Python '{}' for package", self.filename, indent=3
)
exporter.output.add_python_mod(self.filename, text_id=self.text_id) exporter.output.add_python_mod(self.filename, text_id=self.text_id)
# PFMs can have their own SDL... # PFMs can have their own SDL...
sdl_text = bpy.data.texts.get("{}.sdl".format(py_name), None) sdl_text = bpy.data.texts.get("{}.sdl".format(py_name), None)
if sdl_text is not None: if sdl_text is not None:
exporter.report.msg("Including corresponding SDL '{}'", sdl_text.name, indent=3) exporter.report.msg(
"Including corresponding SDL '{}'", sdl_text.name, indent=3
)
exporter.output.add_sdl(sdl_text.name, text_id=sdl_text) exporter.output.add_sdl(sdl_text.name, text_id=sdl_text)
# Handle exporting the Python Parameters # Handle exporting the Python Parameters
@ -292,7 +324,11 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
for socket in attrib_sockets: for socket in attrib_sockets:
from_node = socket.links[0].from_node from_node = socket.links[0].from_node
value = from_node.value if socket.is_simple_value else from_node.get_key(exporter, so) value = (
from_node.value
if socket.is_simple_value
else from_node.get_key(exporter, so)
)
if isinstance(value, str) or not isinstance(value, Iterable): if isinstance(value, str) or not isinstance(value, Iterable):
value = (value,) value = (value,)
for i in value: for i in value:
@ -312,14 +348,23 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
# an animated lamp. # an animated lamp.
if not bool(bo.users_group): if not bool(bo.users_group):
for light in exporter.mgr.find_interfaces(plLightInfo, so): for light in exporter.mgr.find_interfaces(plLightInfo, so):
exporter.report.msg("Marking RT light '{}' as animated due to usage in a Python File node", exporter.report.msg(
so.key.name, indent=3) "Marking RT light '{}' as animated due to usage in a Python File node",
so.key.name,
indent=3,
)
light.setProperty(plLightInfo.kLPMovable, True) light.setProperty(plLightInfo.kLPMovable, True)
def _export_key_attrib(self, exporter, bo, so : plSceneObject, key : plKey, socket) -> None: def _export_key_attrib(
self, exporter, bo, so: plSceneObject, key: plKey, socket
) -> None:
if key is None: if key is None:
exporter.report.warn("Attribute '{}' didn't return a key and therefore will be unavailable to Python", exporter.report.warn(
self.id_data.name, socket.links[0].name, indent=3) "Attribute '{}' didn't return a key and therefore will be unavailable to Python",
self.id_data.name,
socket.links[0].name,
indent=3,
)
return return
key_type = _attrib_key_types[socket.attribute_type] key_type = _attrib_key_types[socket.attribute_type]
@ -328,9 +373,13 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
else: else:
good_key = key.type == key_type good_key = key.type == key_type
if not good_key: if not good_key:
exporter.report.warn("'{}' Node '{}' returned an unexpected key type '{}'", exporter.report.warn(
self.id_data.name, socket.links[0].from_node.name, "'{}' Node '{}' returned an unexpected key type '{}'",
plFactory.ClassName(key.type), indent=3) self.id_data.name,
socket.links[0].from_node.name,
plFactory.ClassName(key.type),
indent=3,
)
if isinstance(key.object, plSceneObject): if isinstance(key.object, plSceneObject):
self._export_ancillary_sceneobject(exporter, bo, key.object) self._export_ancillary_sceneobject(exporter, bo, key.object)
@ -352,10 +401,12 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
if attrib_type in node_attrib_types: if attrib_type in node_attrib_types:
if issubclass(i, PlasmaAttribNodeBase): if issubclass(i, PlasmaAttribNodeBase):
yield { "node_idname": i.bl_idname, yield {
"node_text": i.bl_label, "node_idname": i.bl_idname,
"socket_name": "pfm", "node_text": i.bl_label,
"socket_text": "Python File" } "socket_name": "pfm",
"socket_text": "Python File",
}
else: else:
for socket_name, socket_def in i.output_sockets.items(): for socket_name, socket_def in i.output_sockets.items():
if socket_def.get("hidden") is True: if socket_def.get("hidden") is True:
@ -365,15 +416,23 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
valid_link_nodes = socket_def.get("valid_link_nodes") valid_link_nodes = socket_def.get("valid_link_nodes")
valid_link_sockets = socket_def.get("valid_link_sockets") valid_link_sockets = socket_def.get("valid_link_sockets")
if valid_link_nodes is not None and self.bl_idname not in valid_link_nodes: if (
valid_link_nodes is not None
and self.bl_idname not in valid_link_nodes
):
continue continue
if valid_link_sockets is not None and "PlasmaPythonFileNodeSocket" not in valid_link_sockets: if (
valid_link_sockets is not None
and "PlasmaPythonFileNodeSocket" not in valid_link_sockets
):
continue continue
yield { "node_idname": i.bl_idname, yield {
"node_text": i.bl_label, "node_idname": i.bl_idname,
"socket_name": socket_name, "node_text": i.bl_label,
"socket_text": socket_def["text"] } "socket_name": socket_name,
"socket_text": socket_def["text"],
}
@classmethod @classmethod
def generate_valid_links_to(cls, context, socket, is_output): def generate_valid_links_to(cls, context, socket, is_output):
@ -394,9 +453,15 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
return return
valid_link_sockets = socket_def.get("valid_link_sockets") valid_link_sockets = socket_def.get("valid_link_sockets")
valid_link_nodes = socket_def.get("valid_link_nodes") valid_link_nodes = socket_def.get("valid_link_nodes")
if valid_link_sockets is not None and "PlasmaPythonFileNodeSocket" not in valid_link_sockets: if (
valid_link_sockets is not None
and "PlasmaPythonFileNodeSocket" not in valid_link_sockets
):
return return
if valid_link_nodes is not None and "PlasmaPythonFileNode" not in valid_link_nodes: if (
valid_link_nodes is not None
and "PlasmaPythonFileNode" not in valid_link_nodes
):
return return
# Ok, apparently this thing can connect as a ptAttribute. The only problem with that is # Ok, apparently this thing can connect as a ptAttribute. The only problem with that is
@ -414,15 +479,20 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
continue continue
# *gulp* # *gulp*
yield { "node_idname": "PlasmaPythonFileNode", yield {
"node_text": text_id.name, "node_idname": "PlasmaPythonFileNode",
"node_settings": { "filename": text_id.name }, "node_text": text_id.name,
"socket_name": attrib["name"], "node_settings": {"filename": text_id.name},
"socket_text": attrib["name"] } "socket_name": attrib["name"],
"socket_text": attrib["name"],
}
def harvest_actors(self): def harvest_actors(self):
for i in self.inputs: for i in self.inputs:
if not i.is_linked or i.attribute_type not in {"ptAttribSceneobject", "ptAttribSceneobjectList"}: if not i.is_linked or i.attribute_type not in {
"ptAttribSceneobject",
"ptAttribSceneobjectList",
}:
continue continue
node = i.links[0].from_node node = i.links[0].from_node
if node.target_object is not None: if node.target_object is not None:
@ -486,7 +556,11 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
if not inputs: if not inputs:
self._make_attrib_socket(attrib, empty) self._make_attrib_socket(attrib, empty)
elif attrib.attribute_type not in _single_user_attribs: elif attrib.attribute_type not in _single_user_attribs:
unconnected = [socket for socket in inputs if not socket.is_linked or socket in toasty_sockets] unconnected = [
socket
for socket in inputs
if not socket.is_linked or socket in toasty_sockets
]
if not unconnected: if not unconnected:
self._make_attrib_socket(attrib, empty) self._make_attrib_socket(attrib, empty)
while len(unconnected) > 1: while len(unconnected) > 1:
@ -643,9 +717,13 @@ class PlasmaAttribDropDownListNode(PlasmaAttribNodeBase, bpy.types.Node):
def _list_items(self, context): def _list_items(self, context):
attrib = self.to_socket attrib = self.to_socket
if attrib is not None: if attrib is not None:
return [(option.value, option.value, "") for option in attrib.attribute_arguments.options] return [
(option.value, option.value, "")
for option in attrib.attribute_arguments.options
]
else: else:
return [] return []
value = EnumProperty(items=_list_items) value = EnumProperty(items=_list_items)
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
@ -665,8 +743,10 @@ class PlasmaAttribIntNode(PlasmaAttribNodeBase, bpy.types.Node):
def _get_int(self): def _get_int(self):
return round(self.value_float) return round(self.value_float)
def _set_int(self, value): def _set_int(self, value):
self.value_float = float(value) self.value_float = float(value)
def _on_update_float(self, context): def _on_update_float(self, context):
self.inited = True self.inited = True
@ -710,35 +790,59 @@ class PlasmaAttribIntNode(PlasmaAttribNodeBase, bpy.types.Node):
return self.value_int return self.value_int
else: else:
return self.value_float return self.value_float
def _set_value(self, value): def _set_value(self, value):
self.value_float = value self.value_float = value
value = property(_get_value, _set_value) value = property(_get_value, _set_value)
def _range_label(self, layout): def _range_label(self, layout):
attrib = self.to_socket attrib = self.to_socket
layout.label(text="Range: [{}, {}]".format(attrib.attribute_arguments.range_values[0], attrib.attribute_arguments.range_values[1])) layout.label(
text="Range: [{}, {}]".format(
attrib.attribute_arguments.range_values[0],
attrib.attribute_arguments.range_values[1],
)
)
def _out_of_range(self, value): def _out_of_range(self, value):
attrib = self.to_socket attrib = self.to_socket
if attrib.attribute_arguments.range_values[0] == attrib.attribute_arguments.range_values[1]: if (
attrib.attribute_arguments.range_values[0]
== attrib.attribute_arguments.range_values[1]
):
# Ignore degenerate intervals # Ignore degenerate intervals
return False return False
if attrib.attribute_arguments.range_values[0] <= value <= attrib.attribute_arguments.range_values[1]: if (
attrib.attribute_arguments.range_values[0]
<= value
<= attrib.attribute_arguments.range_values[1]
):
return False return False
return True return True
class PlasmaAttribObjectNode(idprops.IDPropObjectMixin, 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"
pl_attrib = ("ptAttribSceneobject", "ptAttribSceneobjectList", "ptAttribAnimation", pl_attrib = (
"ptAttribSwimCurrent", "ptAttribWaveSet", "ptAttribGrassShader") "ptAttribSceneobject",
"ptAttribSceneobjectList",
target_object = PointerProperty(name="Object", "ptAttribAnimation",
description="Object containing the required data", "ptAttribSwimCurrent",
type=bpy.types.Object) "ptAttribWaveSet",
"ptAttribGrassShader",
)
target_object = PointerProperty(
name="Object",
description="Object containing the required data",
type=bpy.types.Object,
)
def init(self, context): def init(self, context):
super().init(context) super().init(context)
@ -765,8 +869,12 @@ class PlasmaAttribObjectNode(idprops.IDPropObjectMixin, PlasmaAttribNodeBase, bp
return ref_so_key return ref_so_key
elif attrib == "ptAttribAnimation": elif attrib == "ptAttribAnimation":
anim = bo.plasma_modifiers.animation anim = bo.plasma_modifiers.animation
agmod = exporter.mgr.find_create_key(plAGModifier, so=ref_so, name=anim.key_name) agmod = exporter.mgr.find_create_key(
agmaster = exporter.mgr.find_create_key(plAGMasterModifier, so=ref_so, name=anim.key_name) plAGModifier, so=ref_so, name=anim.key_name
)
agmaster = exporter.mgr.find_create_key(
plAGMasterModifier, so=ref_so, name=anim.key_name
)
return agmaster return agmaster
elif attrib == "ptAttribSwimCurrent": elif attrib == "ptAttribSwimCurrent":
swimregion = bo.plasma_modifiers.swimregion swimregion = bo.plasma_modifiers.swimregion
@ -774,17 +882,22 @@ class PlasmaAttribObjectNode(idprops.IDPropObjectMixin, PlasmaAttribNodeBase, bp
elif attrib == "ptAttribWaveSet": elif attrib == "ptAttribWaveSet":
waveset = bo.plasma_modifiers.water_basic waveset = bo.plasma_modifiers.water_basic
if not waveset.enabled: if not waveset.enabled:
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)
elif attrib == "ptAttribGrassShader": elif attrib == "ptAttribGrassShader":
grass_shader = bo.plasma_modifiers.grass_shader grass_shader = bo.plasma_modifiers.grass_shader
if not grass_shader.enabled: if not grass_shader.enabled:
self.raise_error("grass shader modifier not enabled on '{}'".format(self.object_name)) self.raise_error(
"grass shader modifier not enabled on '{}'".format(self.object_name)
)
if exporter.mgr.getVer() <= pvPots: if exporter.mgr.getVer() <= pvPots:
return None return None
return [exporter.mgr.find_create_key(plGrassShaderMod, so=ref_so, name=i.name) return [
for i in exporter.mesh.material.get_materials(bo)] exporter.mgr.find_create_key(plGrassShaderMod, so=ref_so, name=i.name)
for i in exporter.mesh.material.get_materials(bo)
]
@classmethod @classmethod
def _idprop_mapping(cls): def _idprop_mapping(cls):
@ -810,21 +923,31 @@ class PlasmaAttribStringNode(PlasmaAttribNodeBase, bpy.types.Node):
self.value = attrib.simple_value self.value = attrib.simple_value
class PlasmaAttribTextureNode(idprops.IDPropMixin, 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"
bl_width_default = 175 bl_width_default = 175
pl_attrib = ("ptAttribMaterial", "ptAttribMaterialList", pl_attrib = (
"ptAttribDynamicMap", "ptAttribMaterialAnimation") "ptAttribMaterial",
"ptAttribMaterialList",
"ptAttribDynamicMap",
"ptAttribMaterialAnimation",
)
def _poll_material(self, value: bpy.types.Material) -> bool: def _poll_material(self, value: bpy.types.Material) -> bool:
# Don't filter materials by texture - this would (potentially) result in surprising UX # Don't filter materials by texture - this would (potentially) result in surprising UX
# in that you would have to clear the texture selection before being able to select # in that you would have to clear the texture selection before being able to select
# certain materials. # certain materials.
if self.target_object is not None: if self.target_object is not None:
object_materials = (slot.material for slot in self.target_object.material_slots if slot and slot.material) object_materials = (
slot.material
for slot in self.target_object.material_slots
if slot and slot.material
)
return value in object_materials return value in object_materials
return True return True
@ -845,25 +968,37 @@ class PlasmaAttribTextureNode(idprops.IDPropMixin, PlasmaAttribNodeBase, bpy.typ
if self.material is not None: if self.material is not None:
return value.name in self.material.texture_slots return value.name in self.material.texture_slots
elif self.target_object is not None: elif self.target_object is not None:
for i in (slot.material for slot in self.target_object.material_slots if slot and slot.material): for i in (
if value in (slot.texture for slot in i.texture_slots if slot and slot.texture): slot.material
for slot in self.target_object.material_slots
if slot and slot.material
):
if value in (
slot.texture for slot in i.texture_slots if slot and slot.texture
):
return True return True
return False return False
else: else:
return True return True
target_object = PointerProperty(name="Object", target_object = PointerProperty(
description="", name="Object",
type=bpy.types.Object, description="",
poll=idprops.poll_drawable_objects) type=bpy.types.Object,
material = PointerProperty(name="Material", poll=idprops.poll_drawable_objects,
description="Material the texture is attached to", )
type=bpy.types.Material, material = PointerProperty(
poll=_poll_material) name="Material",
texture = PointerProperty(name="Texture", description="Material the texture is attached to",
description="Texture to expose to Python", type=bpy.types.Material,
type=bpy.types.Texture, poll=_poll_material,
poll=_poll_texture) )
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)
@ -872,14 +1007,26 @@ class PlasmaAttribTextureNode(idprops.IDPropMixin, PlasmaAttribNodeBase, bpy.typ
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
if self.target_object is not None: if self.target_object is not None:
iter_materials = lambda: (i.material for i in self.target_object.material_slots if i and i.material) iter_materials = lambda: (
i.material
for i in self.target_object.material_slots
if i and i.material
)
if self.material is not None: if self.material is not None:
if self.material not in iter_materials(): if self.material not in iter_materials():
layout.label("The selected material is not linked to the target object.", icon="ERROR") layout.label(
"The selected material is not linked to the target object.",
icon="ERROR",
)
layout.alert = True layout.alert = True
if self.texture is not None: if self.texture is not None:
if not frozenset(self.texture.users_material) & frozenset(iter_materials()): if not frozenset(self.texture.users_material) & frozenset(
layout.label("The selected texture is not on a material linked to the target object.", icon="ERROR") iter_materials()
):
layout.label(
"The selected texture is not on a material linked to the target object.",
icon="ERROR",
)
layout.alert = True layout.alert = True
layout.prop(self, "target_object") layout.prop(self, "target_object")
@ -888,31 +1035,51 @@ class PlasmaAttribTextureNode(idprops.IDPropMixin, PlasmaAttribNodeBase, bpy.typ
def get_key(self, exporter, so): def get_key(self, exporter, so):
if not any((self.target_object, self.material, self.texture)): if not any((self.target_object, self.material, self.texture)):
self.raise_error("At least one of: target object, material, or texture must be specified.") self.raise_error(
"At least one of: target object, material, or texture must be specified."
)
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
layer_generator = exporter.mesh.material.get_layers(self.target_object, self.material, self.texture) layer_generator = exporter.mesh.material.get_layers(
self.target_object, self.material, self.texture
)
bottom_layers = (i.object.bottomOfStack for i in layer_generator) bottom_layers = (i.object.bottomOfStack for i in layer_generator)
if attrib == "ptAttribDynamicMap": if attrib == "ptAttribDynamicMap":
yield from filter(lambda x: x and isinstance(x.object, plDynamicTextMap), yield from filter(
(i.object.texture for i in layer_generator)) lambda x: x and isinstance(x.object, plDynamicTextMap),
(i.object.texture for i in layer_generator),
)
elif attrib == "ptAttribMaterialAnimation": elif attrib == "ptAttribMaterialAnimation":
yield from filter(lambda x: x and isinstance(x.object, plLayerAnimationBase), layer_generator) yield from filter(
lambda x: x and isinstance(x.object, plLayerAnimationBase),
layer_generator,
)
elif attrib == "ptAttribMaterialList": elif attrib == "ptAttribMaterialList":
yield from filter(lambda x: x and not isinstance(x.object, plLayerAnimationBase), bottom_layers) yield from filter(
lambda x: x and not isinstance(x.object, plLayerAnimationBase),
bottom_layers,
)
elif attrib == "ptAttribMaterial": elif attrib == "ptAttribMaterial":
# Only return the first key; warn about others. # Only return the first key; warn about others.
result_gen = filter(lambda x: x and not isinstance(x.object, plLayerAnimationBase), bottom_layers) result_gen = filter(
lambda x: x and not isinstance(x.object, plLayerAnimationBase),
bottom_layers,
)
result = next(result_gen, None) result = next(result_gen, None)
remainder = sum((1 for i in result)) remainder = sum((1 for i in result))
if remainder > 1: if remainder > 1:
exporter.report.warn("'{}.{}': Expected a single layer, but mapped to {}. Make the settings more specific.", exporter.report.warn(
self.id_data.name, self.path_from_id(), remainder + 1, indent=2) "'{}.{}': Expected a single layer, but mapped to {}. Make the settings more specific.",
self.id_data.name,
self.path_from_id(),
remainder + 1,
indent=2,
)
if result is not None: if result is not None:
yield result yield result
else: else:
@ -920,16 +1087,19 @@ class PlasmaAttribTextureNode(idprops.IDPropMixin, PlasmaAttribNodeBase, bpy.typ
@classmethod @classmethod
def _idprop_mapping(cls): def _idprop_mapping(cls):
return {"material": "material_name", return {"material": "material_name", "texture": "texture_name"}
"texture": "texture_name"}
def _idprop_sources(self): def _idprop_sources(self):
return {"material_name": bpy.data.materials, return {"material_name": bpy.data.materials, "texture_name": bpy.data.textures}
"texture_name": bpy.data.textures}
def _is_animated(self, material, texture): def _is_animated(self, material, texture):
return ((material.animation_data is not None and material.animation_data.action is not None) return (
or (texture.animation_data is not None and texture.animation_data.action is not None)) 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): def _is_dyntext(self, texture):
return texture.type == "IMAGE" and texture.image is None return texture.type == "IMAGE" and texture.image is None
@ -947,7 +1117,6 @@ _attrib_colors = {
"ptAttribResponder": (0.031, 0.110, 0.290, 1.0), "ptAttribResponder": (0.031, 0.110, 0.290, 1.0),
"ptAttribResponderList": (0.031, 0.110, 0.290, 1.0), "ptAttribResponderList": (0.031, 0.110, 0.290, 1.0),
"ptAttribString": (0.675, 0.659, 0.494, 1.0), "ptAttribString": (0.675, 0.659, 0.494, 1.0),
PlasmaAttribIntNode.pl_attrib: (0.443, 0.439, 0.392, 1.0), PlasmaAttribIntNode.pl_attrib: (0.443, 0.439, 0.392, 1.0),
PlasmaAttribObjectNode.pl_attrib: (0.565, 0.267, 0.0, 1.0), PlasmaAttribObjectNode.pl_attrib: (0.565, 0.267, 0.0, 1.0),
PlasmaAttribTextureNode.pl_attrib: (0.035, 0.353, 0.0, 1.0), PlasmaAttribTextureNode.pl_attrib: (0.035, 0.353, 0.0, 1.0),

267
korman/nodes/node_responder.py

@ -23,6 +23,7 @@ import uuid
from .node_core import * from .node_core import *
from .node_deprecated import PlasmaVersionedNode from .node_deprecated import PlasmaVersionedNode
class PlasmaResponderNode(PlasmaVersionedNode, bpy.types.Node): class PlasmaResponderNode(PlasmaVersionedNode, bpy.types.Node):
bl_category = "LOGIC" bl_category = "LOGIC"
bl_idname = "PlasmaResponderNode" bl_idname = "PlasmaResponderNode"
@ -32,50 +33,70 @@ class PlasmaResponderNode(PlasmaVersionedNode, bpy.types.Node):
# These are the Python attributes we can fill in # These are the Python attributes we can fill in
pl_attrib = {"ptAttribResponder", "ptAttribResponderList", "ptAttribNamedResponder"} pl_attrib = {"ptAttribResponder", "ptAttribResponderList", "ptAttribNamedResponder"}
detect_trigger = BoolProperty(name="Detect Trigger", detect_trigger = BoolProperty(
description="When notified, trigger the Responder", name="Detect Trigger",
default=True) description="When notified, trigger the Responder",
detect_untrigger = BoolProperty(name="Detect UnTrigger", default=True,
description="When notified, untrigger the Responder", )
default=False) detect_untrigger = BoolProperty(
no_ff_sounds = BoolProperty(name="Don't F-Fwd Sounds", name="Detect UnTrigger",
description="When fast-forwarding, play sound effects", description="When notified, untrigger the Responder",
default=False) default=False,
default_state = IntProperty(name="Default State Index", )
options=set()) no_ff_sounds = BoolProperty(
name="Don't F-Fwd Sounds",
input_sockets = OrderedDict([ description="When fast-forwarding, play sound effects",
("condition", { default=False,
"text": "Condition", )
"type": "PlasmaConditionSocket", default_state = IntProperty(name="Default State Index", options=set())
"spawn_empty": True,
}), input_sockets = OrderedDict(
]) [
(
output_sockets = OrderedDict([ "condition",
("keyref", { {
"text": "References", "text": "Condition",
"type": "PlasmaPythonReferenceNodeSocket", "type": "PlasmaConditionSocket",
"valid_link_nodes": {"PlasmaPythonFileNode"}, "spawn_empty": True,
}), },
("state_refs", { ),
"text": "State", ]
"type": "PlasmaRespStateRefSocket", )
"valid_link_nodes": "PlasmaResponderStateNode",
"valid_link_sockets": "PlasmaRespStateRefSocket", output_sockets = OrderedDict(
"link_limit": 1, [
"spawn_empty": True, (
}), "keyref",
{
# This version of the states socket has been deprecated. "text": "References",
# We need to be able to track 1 socket -> 1 state to manage "type": "PlasmaPythonReferenceNodeSocket",
# responder state IDs "valid_link_nodes": {"PlasmaPythonFileNode"},
("states", { },
"text": "States", ),
"type": "PlasmaRespStateSocket", (
"hidden": True, "state_refs",
}), {
]) "text": "State",
"type": "PlasmaRespStateRefSocket",
"valid_link_nodes": "PlasmaResponderStateNode",
"valid_link_sockets": "PlasmaRespStateRefSocket",
"link_limit": 1,
"spawn_empty": True,
},
),
# This version of the states socket has been deprecated.
# We need to be able to track 1 socket -> 1 state to manage
# responder state IDs
(
"states",
{
"text": "States",
"type": "PlasmaRespStateSocket",
"hidden": True,
},
),
]
)
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop(self, "detect_trigger") layout.prop(self, "detect_trigger")
@ -154,6 +175,7 @@ class PlasmaResponderNode(PlasmaVersionedNode, bpy.types.Node):
# linked to other states will be converted at the end of the list. # linked to other states will be converted at the end of the list.
if self.version == 1: if self.version == 1:
states = set() states = set()
def _link_states(state): def _link_states(state):
if state in states: if state in states:
return return
@ -162,6 +184,7 @@ class PlasmaResponderNode(PlasmaVersionedNode, bpy.types.Node):
goto = state.find_output("gotostate") goto = state.find_output("gotostate")
if goto is not None: if goto is not None:
_link_states(goto) _link_states(goto)
for i in self.find_outputs("states"): for i in self.find_outputs("states"):
_link_states(i) _link_states(i)
self.unlink_outputs("states", "socket deprecated (upgrade complete)") self.unlink_outputs("states", "socket deprecated (upgrade complete)")
@ -177,63 +200,98 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node):
resp_node = self.find_input("resp") resp_node = self.find_input("resp")
if resp_node is not None: if resp_node is not None:
try: try:
state_idx = next((idx for idx, node in enumerate(resp_node.find_outputs("state_refs")) if node == self)) state_idx = next(
(
idx
for idx, node in enumerate(resp_node.find_outputs("state_refs"))
if node == self
)
)
except StopIteration: except StopIteration:
return False return False
else: else:
return resp_node.default_state == state_idx return resp_node.default_state == state_idx
return False return False
def _set_default_state(self, value): def _set_default_state(self, value):
if value: if value:
resp_node = self.find_input("resp") resp_node = self.find_input("resp")
if resp_node is not None: if resp_node is not None:
try: try:
state_idx = next((idx for idx, node in enumerate(resp_node.find_outputs("state_refs")) if node == self)) state_idx = next(
(
idx
for idx, node in enumerate(
resp_node.find_outputs("state_refs")
)
if node == self
)
)
except StopIteration: except StopIteration:
self._whine("unable to set default state on responder") self._whine("unable to set default state on responder")
else: else:
resp_node.default_state = state_idx resp_node.default_state = state_idx
default_state = BoolProperty(name="Default State", default_state = BoolProperty(
description="This state is the responder's default", name="Default State",
get=_get_default_state, description="This state is the responder's default",
set=_set_default_state, get=_get_default_state,
options=set()) set=_set_default_state,
options=set(),
input_sockets = OrderedDict([ )
("condition", {
"text": "Triggers State", input_sockets = OrderedDict(
"type": "PlasmaRespStateSocket", [
"spawn_empty": True, (
}), "condition",
("resp", { {
"text": "Responder", "text": "Triggers State",
"type": "PlasmaRespStateRefSocket", "type": "PlasmaRespStateSocket",
"valid_link_nodes": "PlasmaResponderNode", "spawn_empty": True,
"valid_link_sockets": "PlasmaRespStateRefSocket", },
}), ),
]) (
"resp",
output_sockets = OrderedDict([ {
# This socket has been deprecated. "text": "Responder",
("cmds", { "type": "PlasmaRespStateRefSocket",
"text": "Commands", "valid_link_nodes": "PlasmaResponderNode",
"type": "PlasmaRespCommandSocket", "valid_link_sockets": "PlasmaRespStateRefSocket",
"hidden": True, },
}), ),
]
# These sockets are valid. )
("msgs", {
"text": "Send Message", output_sockets = OrderedDict(
"type": "PlasmaMessageSocket", [
"valid_link_sockets": "PlasmaMessageSocket", # This socket has been deprecated.
}), (
("gotostate", { "cmds",
"link_limit": 1, {
"text": "Triggers State", "text": "Commands",
"type": "PlasmaRespStateSocket", "type": "PlasmaRespCommandSocket",
}), "hidden": True,
]) },
),
# These sockets are valid.
(
"msgs",
{
"text": "Send Message",
"type": "PlasmaMessageSocket",
"valid_link_sockets": "PlasmaMessageSocket",
},
),
(
"gotostate",
{
"link_limit": 1,
"text": "Triggers State",
"type": "PlasmaRespStateSocket",
},
),
]
)
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.active = self.find_input("resp") is not None layout.active = self.find_input("resp") is not None
@ -274,11 +332,17 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node):
return -1 return -1
def find_create_wait(self, exporter, so, node): def find_create_wait(self, exporter, so, node):
i, cmd = next(((i, cmd) for i, cmd in enumerate(self.commands) if cmd[0] == node)) i, cmd = next(
wait = next((key for key, value in self.waits.items() if value == i), None) ((i, cmd) for i, cmd in enumerate(self.commands) if cmd[0] == node)
)
wait = next(
(key for key, value in self.waits.items() if value == i), None
)
if wait is None: if wait is None:
wait = self.add_wait(i) wait = self.add_wait(i)
node.convert_callback_message(exporter, so, cmd[1].msg, self.responder.key, wait) node.convert_callback_message(
exporter, so, cmd[1].msg, self.responder.key, wait
)
return wait return wait
def save(self, state): def save(self, state):
@ -317,7 +381,9 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node):
pfmNotify.addEvent(proCallbackEventData()) pfmNotify.addEvent(proCallbackEventData())
state.addCommand(pfmNotify, lastWait) state.addCommand(pfmNotify, lastWait)
def _generate_command(self, exporter, so, responder, commandMgr, msgNode, waitOn=-1): def _generate_command(
self, exporter, so, responder, commandMgr, msgNode, waitOn=-1
):
def prepare_message(exporter, so, responder, commandMgr, waitOn, msg): def prepare_message(exporter, so, responder, commandMgr, waitOn, msg):
idx, command = commandMgr.add_command(msgNode, waitOn) idx, command = commandMgr.add_command(msgNode, waitOn)
if msg.sender is None: if msg.sender is None:
@ -342,7 +408,9 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node):
commandMgr.add_waitable_node(msgNode) commandMgr.add_waitable_node(msgNode)
if msgNode.has_linked_callbacks: if msgNode.has_linked_callbacks:
childWaitOn = commandMgr.add_wait(idx) childWaitOn = commandMgr.add_wait(idx)
msgNode.convert_callback_message(exporter, so, msg, responder.key, childWaitOn) msgNode.convert_callback_message(
exporter, so, msg, responder.key, childWaitOn
)
else: else:
childWaitOn = waitOn childWaitOn = waitOn
@ -352,11 +420,14 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node):
def _get_child_messages(self, node=None): def _get_child_messages(self, node=None):
"""Returns a list of the message nodes sent by `node`. The list is sorted such that any """Returns a list of the message nodes sent by `node`. The list is sorted such that any
messages with callbacks are last in the list, allowing proper wait generation. messages with callbacks are last in the list, allowing proper wait generation.
""" """
if node is None: if node is None:
node = self node = self
return sorted(node.find_outputs("msgs"), key=lambda x: x.has_callbacks and x.has_linked_callbacks) return sorted(
node.find_outputs("msgs"),
key=lambda x: x.has_callbacks and x.has_linked_callbacks,
)
class PlasmaRespStateSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): class PlasmaRespStateSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
@ -369,7 +440,15 @@ class PlasmaRespStateRefSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
def draw_content(self, context, layout, node, text): def draw_content(self, context, layout, node, text):
if isinstance(node, PlasmaResponderNode): if isinstance(node, PlasmaResponderNode):
try: try:
idx = next((idx for idx, socket in enumerate(node.find_output_sockets("state_refs")) if socket == self)) idx = next(
(
idx
for idx, socket in enumerate(
node.find_output_sockets("state_refs")
)
if socket == self
)
)
except StopIteration: except StopIteration:
layout.label(text) layout.label(text)
else: else:

165
korman/nodes/node_softvolume.py

@ -21,17 +21,23 @@ from PyHSPlasma import *
from .node_core import PlasmaNodeBase, PlasmaNodeSocketBase, PlasmaTreeOutputNodeBase from .node_core import PlasmaNodeBase, PlasmaNodeSocketBase, PlasmaTreeOutputNodeBase
from .. import idprops from .. import idprops
class PlasmaSoftVolumeOutputNode(PlasmaTreeOutputNodeBase, bpy.types.Node): class PlasmaSoftVolumeOutputNode(PlasmaTreeOutputNodeBase, bpy.types.Node):
bl_category = "SV" bl_category = "SV"
bl_idname = "PlasmaSoftVolumeOutputNode" bl_idname = "PlasmaSoftVolumeOutputNode"
bl_label = "Soft Volume Output" bl_label = "Soft Volume Output"
input_sockets = OrderedDict([ input_sockets = OrderedDict(
("input", { [
"text": "Final Volume", (
"type": "PlasmaSoftVolumeNodeSocket", "input",
}), {
]) "text": "Final Volume",
"type": "PlasmaSoftVolumeNodeSocket",
},
),
]
)
def get_key(self, exporter, so): def get_key(self, exporter, so):
svNode = self.find_input("input") svNode = self.find_input("input")
@ -42,6 +48,8 @@ class PlasmaSoftVolumeOutputNode(PlasmaTreeOutputNodeBase, bpy.types.Node):
class PlasmaSoftVolumeNodeSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): class PlasmaSoftVolumeNodeSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
bl_color = (0.133, 0.094, 0.345, 1.0) bl_color = (0.133, 0.094, 0.345, 1.0)
class PlasmaSoftVolumePropertiesNodeSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): class PlasmaSoftVolumePropertiesNodeSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
bl_color = (0.067, 0.40, 0.067, 1.0) bl_color = (0.067, 0.40, 0.067, 1.0)
@ -51,17 +59,31 @@ class PlasmaSoftVolumePropertiesNode(PlasmaNodeBase, bpy.types.Node):
bl_idname = "PlasmaSoftVolumePropertiesNode" bl_idname = "PlasmaSoftVolumePropertiesNode"
bl_label = "Soft Volume Properties" bl_label = "Soft Volume Properties"
output_sockets = OrderedDict([ output_sockets = OrderedDict(
("target", { [
"text": "Volume", (
"type": "PlasmaSoftVolumePropertiesNodeSocket" "target",
}), {"text": "Volume", "type": "PlasmaSoftVolumePropertiesNodeSocket"},
]) ),
]
inside_strength = IntProperty(name="Inside", description="Strength inside the region", )
subtype="PERCENTAGE", default=100, min=0, max=100)
outside_strength = IntProperty(name="Outside", description="Strength outside the region", inside_strength = IntProperty(
subtype="PERCENTAGE", default=0, min=0, max=100) name="Inside",
description="Strength inside the region",
subtype="PERCENTAGE",
default=100,
min=0,
max=100,
)
outside_strength = IntProperty(
name="Outside",
description="Strength outside the region",
subtype="PERCENTAGE",
default=0,
min=0,
max=100,
)
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop(self, "inside_strength") layout.prop(self, "inside_strength")
@ -72,23 +94,26 @@ class PlasmaSoftVolumePropertiesNode(PlasmaNodeBase, bpy.types.Node):
softvolume.outsideStrength = self.outside_strength / 100 softvolume.outsideStrength = self.outside_strength / 100
class PlasmaSoftVolumeReferenceNode(idprops.IDPropObjectMixin, 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"
bl_width_default = 150 bl_width_default = 150
output_sockets = OrderedDict([ output_sockets = OrderedDict(
("output", { [
"text": "Volume", ("output", {"text": "Volume", "type": "PlasmaSoftVolumeNodeSocket"}),
"type": "PlasmaSoftVolumeNodeSocket" ]
}), )
])
soft_volume = PointerProperty(name="Soft Volume", soft_volume = PointerProperty(
description="Object whose Soft Volume modifier we should use", name="Soft Volume",
type=bpy.types.Object, description="Object whose Soft Volume modifier we should use",
poll=idprops.poll_softvolume_objects) type=bpy.types.Object,
poll=idprops.poll_softvolume_objects,
)
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop(self, "soft_volume", text="") layout.prop(self, "soft_volume", text="")
@ -111,23 +136,30 @@ class PlasmaSoftVolumeInvertNode(PlasmaNodeBase, bpy.types.Node):
bl_label = "Soft Volume Invert" bl_label = "Soft Volume Invert"
# The only difference between this and PlasmaSoftVolumeLinkNode is this can only have ONE input # The only difference between this and PlasmaSoftVolumeLinkNode is this can only have ONE input
input_sockets = OrderedDict([ input_sockets = OrderedDict(
("properties", { [
"text": "Properties", (
"type": "PlasmaSoftVolumePropertiesNodeSocket", "properties",
}), {
("input", { "text": "Properties",
"text": "Input Volume", "type": "PlasmaSoftVolumePropertiesNodeSocket",
"type": "PlasmaSoftVolumeNodeSocket", },
}), ),
]) (
"input",
output_sockets = OrderedDict([ {
("output", { "text": "Input Volume",
"text": "Output Volume", "type": "PlasmaSoftVolumeNodeSocket",
"type": "PlasmaSoftVolumeNodeSocket" },
}), ),
]) ]
)
output_sockets = OrderedDict(
[
("output", {"text": "Output Volume", "type": "PlasmaSoftVolumeNodeSocket"}),
]
)
def get_key(self, exporter, so): def get_key(self, exporter, so):
return self._find_create_key(plSoftVolumeInvert, exporter, so=so) return self._find_create_key(plSoftVolumeInvert, exporter, so=so)
@ -153,24 +185,31 @@ class PlasmaSoftVolumeInvertNode(PlasmaNodeBase, bpy.types.Node):
class PlasmaSoftVolumeLinkNode(PlasmaNodeBase): class PlasmaSoftVolumeLinkNode(PlasmaNodeBase):
input_sockets = OrderedDict([ input_sockets = OrderedDict(
("properties", { [
"text": "Properties", (
"type": "PlasmaSoftVolumePropertiesNodeSocket", "properties",
}), {
("input", { "text": "Properties",
"text": "Input Volume", "type": "PlasmaSoftVolumePropertiesNodeSocket",
"type": "PlasmaSoftVolumeNodeSocket", },
"spawn_empty": True, ),
}), (
]) "input",
{
output_sockets = OrderedDict([ "text": "Input Volume",
("output", { "type": "PlasmaSoftVolumeNodeSocket",
"text": "Output Volume", "spawn_empty": True,
"type": "PlasmaSoftVolumeNodeSocket" },
}), ),
]) ]
)
output_sockets = OrderedDict(
[
("output", {"text": "Output Volume", "type": "PlasmaSoftVolumeNodeSocket"}),
]
)
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
sv = self.get_key(exporter, so).object sv = self.get_key(exporter, so).object

2
korman/operators/__init__.py

@ -24,8 +24,10 @@ from . import op_toolbox as toolbox
from . import op_ui as ui from . import op_ui as ui
from . import op_world as world from . import op_world as world
def register(): def register():
exporter.register() exporter.register()
def unregister(): def unregister():
exporter.unregister() exporter.unregister()

387
korman/operators/op_export.py

@ -27,6 +27,7 @@ from ..helpers import UiHelper
from .. import korlib, plasma_launcher from .. import korlib, plasma_launcher
from ..properties.prop_world import PlasmaAge from ..properties.prop_world import PlasmaAge
class ExportOperator: class ExportOperator:
def _get_default_path(self, context): def _get_default_path(self, context):
blend_filepath = context.blend_data.filepath blend_filepath = context.blend_data.filepath
@ -55,94 +56,212 @@ class PlasmaAgeExportOperator(ExportOperator, bpy.types.Operator):
# over on the PlasmaAge world properties. We've got a helper so we can access them like they're actually on us... # over on the PlasmaAge world properties. We've got a helper so we can access them like they're actually on us...
# If you want a volatile property, register it directly on this operator! # If you want a volatile property, register it directly on this operator!
_properties = { _properties = {
"verbose": (BoolProperty, {"name": "Display Verbose Log", "verbose": (
"description": "Shows the verbose export log in the console", BoolProperty,
"default": False}), {
"name": "Display Verbose Log",
"show_console": (BoolProperty, {"name": "Display Log Console", "description": "Shows the verbose export log in the console",
"description": "Forces the Blender System Console open during the export", "default": False,
"default": True}), },
),
"texcache_path": (StringProperty, {"name": "Texture Cache Path", "show_console": (
"description": "Texture Cache Filepath"}), BoolProperty,
{
"texcache_method": (EnumProperty, {"name": "Texture Cache", "name": "Display Log Console",
"description": "Texture Cache Settings", "description": "Forces the Blender System Console open during the export",
"items": [("skip", "Don't Use Texture Cache", "The texture cache is neither used nor updated."), "default": True,
("use", "Use Texture Cache", "Use (and update, if needed) cached textures."), },
("rebuild", "Rebuild Texture Cache", "Rebuilds the texture cache from scratch.")], ),
"default": "use"}), "texcache_path": (
StringProperty,
"lighting_method": (EnumProperty, {"name": "Static Lighting", {"name": "Texture Cache Path", "description": "Texture Cache Filepath"},
"description": "Static Lighting Settings", ),
"items": [("skip", "Don't Bake Lighting", "Static lighting is not baked during this export (fastest export)"), "texcache_method": (
("bake", "Bake Lighting", "Static lighting is baked according to your specifications"), EnumProperty,
("force_vcol", "Force Vertex Color Bake", "All static lighting is baked as vertex colors (faster export)"), {
("force_lightmap", "Force Lightmap Bake", "All static lighting is baked as lightmaps (slower export)")], "name": "Texture Cache",
"default": "bake"}), "description": "Texture Cache Settings",
"items": [
"envmap_method": (EnumProperty, {"name": "Environment Maps", (
"description": "Environment Map Settings", "skip",
"items": [("skip", "Don't Export EnvMaps", "Environment Maps are not exported"), "Don't Use Texture Cache",
("dcm2dem", "Downgrade Planar EnvMaps", "When the engine doesn't support them, Planar Environment Maps are downgraded to Cube Maps"), "The texture cache is neither used nor updated.",
("perengine", "Export Supported EnvMaps", "Only environment maps supported by the selected game engine are exported")], ),
"default": "dcm2dem"}), (
"use",
"python_method": (EnumProperty, {"name": "Python", "Use Texture Cache",
"description": "Specifies how Python should be packed", "Use (and update, if needed) cached textures.",
"items": [("none", "Pack Nothing", "Don't pack any Python files."), ),
("as_requested", "Pack Requested Scripts", "Packs any script both linked as a Text file and requested for packaging."), (
("all", "Pack All Scripts", "Packs all Python files linked as a Text file.")], "rebuild",
"default": "as_requested", "Rebuild Texture Cache",
"options": set()}), "Rebuilds the texture cache from scratch.",
),
"localization_method": (EnumProperty, {"name": "Localization", ],
"description": "Specifies how localization data should be exported", "default": "use",
"items": [("database", "Localization Database", "A per-language database compatible with pfLocalizationEditor"), },
("database_back_compat", "Localization Database (Compat Mode)", "A per-language database compatible with pfLocalizationEditor and Korman <=0.11"), ),
("single_file", "Single File", "A single file database, as in Korman <=0.11")], "lighting_method": (
"default": "database", EnumProperty,
"options": set()}), {
"name": "Static Lighting",
"export_active": (BoolProperty, {"name": "INTERNAL: Export currently running", "description": "Static Lighting Settings",
"default": False, "items": [
"options": {"SKIP_SAVE"}}), (
"skip",
"Don't Bake Lighting",
"Static lighting is not baked during this export (fastest export)",
),
(
"bake",
"Bake Lighting",
"Static lighting is baked according to your specifications",
),
(
"force_vcol",
"Force Vertex Color Bake",
"All static lighting is baked as vertex colors (faster export)",
),
(
"force_lightmap",
"Force Lightmap Bake",
"All static lighting is baked as lightmaps (slower export)",
),
],
"default": "bake",
},
),
"envmap_method": (
EnumProperty,
{
"name": "Environment Maps",
"description": "Environment Map Settings",
"items": [
(
"skip",
"Don't Export EnvMaps",
"Environment Maps are not exported",
),
(
"dcm2dem",
"Downgrade Planar EnvMaps",
"When the engine doesn't support them, Planar Environment Maps are downgraded to Cube Maps",
),
(
"perengine",
"Export Supported EnvMaps",
"Only environment maps supported by the selected game engine are exported",
),
],
"default": "dcm2dem",
},
),
"python_method": (
EnumProperty,
{
"name": "Python",
"description": "Specifies how Python should be packed",
"items": [
("none", "Pack Nothing", "Don't pack any Python files."),
(
"as_requested",
"Pack Requested Scripts",
"Packs any script both linked as a Text file and requested for packaging.",
),
(
"all",
"Pack All Scripts",
"Packs all Python files linked as a Text file.",
),
],
"default": "as_requested",
"options": set(),
},
),
"localization_method": (
EnumProperty,
{
"name": "Localization",
"description": "Specifies how localization data should be exported",
"items": [
(
"database",
"Localization Database",
"A per-language database compatible with pfLocalizationEditor",
),
(
"database_back_compat",
"Localization Database (Compat Mode)",
"A per-language database compatible with pfLocalizationEditor and Korman <=0.11",
),
(
"single_file",
"Single File",
"A single file database, as in Korman <=0.11",
),
],
"default": "database",
"options": set(),
},
),
"export_active": (
BoolProperty,
{
"name": "INTERNAL: Export currently running",
"default": False,
"options": {"SKIP_SAVE"},
},
),
} }
# This wigs out and very bad things happen if it's not directly on the operator... # This wigs out and very bad things happen if it's not directly on the operator...
filepath = StringProperty(subtype="FILE_PATH") filepath = StringProperty(subtype="FILE_PATH")
filter_glob = StringProperty(default="*.age;*.zip", options={'HIDDEN'}) filter_glob = StringProperty(default="*.age;*.zip", options={"HIDDEN"})
version = EnumProperty(name="Version", version = EnumProperty(
description="Plasma version to export this age for", name="Version",
items=game_versions, description="Plasma version to export this age for",
default="pvPots", items=game_versions,
options=set()) default="pvPots",
options=set(),
dat_only = BoolProperty(name="Export Only PRPs", )
description="Only the Age PRPs should be exported",
default=True, dat_only = BoolProperty(
options={"HIDDEN"}) name="Export Only PRPs",
description="Only the Age PRPs should be exported",
actions = EnumProperty(name="Actions", default=True,
description="Actions for the exporter to perform", options={"HIDDEN"},
default={"EXPORT"}, )
items=[("EXPORT", "Export", "Export the age data"),
("PROFILE", "Profile", "Profile the exporter"), actions = EnumProperty(
("LAUNCH", "Launch Age", "Launch the age in Plasma")], name="Actions",
options={"ENUM_FLAG"}) description="Actions for the exporter to perform",
default={"EXPORT"},
ki = IntProperty(name="KI", items=[
description="KI Number of the player to use when launching the game", ("EXPORT", "Export", "Export the age data"),
options=set()) ("PROFILE", "Profile", "Profile the exporter"),
("LAUNCH", "Launch Age", "Launch the age in Plasma"),
player = StringProperty(name="Player", ],
description="Name of the player to use when launching the game", options={"ENUM_FLAG"},
options=set()) )
serverini = StringProperty(name="Server INI", ki = IntProperty(
description="Name of the server configuation to use when launching the game", name="KI",
options=set()) description="KI Number of the player to use when launching the game",
options=set(),
)
player = StringProperty(
name="Player",
description="Name of the player to use when launching the game",
options=set(),
)
serverini = StringProperty(
name="Server INI",
description="Name of the server configuation to use when launching the game",
options=set(),
)
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
@ -204,7 +323,10 @@ class PlasmaAgeExportOperator(ExportOperator, bpy.types.Operator):
ageName = path.stem ageName = path.stem
if korlib.is_python_keyword(ageName): if korlib.is_python_keyword(ageName):
self.report({"ERROR"}, "The Age name conflicts with the Python keyword '{}'".format(ageName)) self.report(
{"ERROR"},
"The Age name conflicts with the Python keyword '{}'".format(ageName),
)
return {"CANCELLED"} return {"CANCELLED"}
# This prevents us from finding out at the very end that very, very bad things happened... # This prevents us from finding out at the very end that very, very bad things happened...
@ -222,8 +344,12 @@ class PlasmaAgeExportOperator(ExportOperator, bpy.types.Operator):
try: try:
self.export_active = True self.export_active = True
if "PROFILE" in self.actions: if "PROFILE" in self.actions:
profile_path = str(path.with_name("{}_cProfile".format(ageName))) profile_path = str(
profile = cProfile.runctx("e.run()", globals(), locals(), profile_path) path.with_name("{}_cProfile".format(ageName))
)
profile = cProfile.runctx(
"e.run()", globals(), locals(), profile_path
)
else: else:
e.run() e.run()
except exporter.ExportError as error: except exporter.ExportError as error:
@ -274,13 +400,19 @@ class PlasmaAgeExportOperator(ExportOperator, bpy.types.Operator):
def _sanity_check_run_plasma(self): def _sanity_check_run_plasma(self):
if not bpy.app.binary_path_python: if not bpy.app.binary_path_python:
raise exporter.PlasmaLaunchError("Can't Launch Plasma: No Python executable available") raise exporter.PlasmaLaunchError(
"Can't Launch Plasma: No Python executable available"
)
if self.version == "pvMoul": if self.version == "pvMoul":
if not self.ki: if not self.ki:
raise exporter.PlasmaLaunchError("Can't Launch Plasma: Player KI not set") raise exporter.PlasmaLaunchError(
"Can't Launch Plasma: Player KI not set"
)
else: else:
if not self.player: if not self.player:
raise exporter.PlasmaLaunchError("Can't Launch Plasma: Player Name not set") raise exporter.PlasmaLaunchError(
"Can't Launch Plasma: Player Name not set"
)
def _run_plasma(self, context): def _run_plasma(self, context):
path = Path(self.filepath) path = Path(self.filepath)
@ -289,8 +421,13 @@ class PlasmaAgeExportOperator(ExportOperator, bpy.types.Operator):
# It would be nice to launch URU right here. Unfortunately, for single player URUs, we will # It would be nice to launch URU right here. Unfortunately, for single player URUs, we will
# need to actually wait for the whole rigamaroll to finish. Therefore, we need to kick # need to actually wait for the whole rigamaroll to finish. Therefore, we need to kick
# open a separate python exe to launch URU and wait. # open a separate python exe to launch URU and wait.
args = [bpy.app.binary_path_python, plasma_launcher.__file__, args = [
str(client_dir), path.stem, self.version] bpy.app.binary_path_python,
plasma_launcher.__file__,
str(client_dir),
path.stem,
self.version,
]
if self.version == "pvMoul": if self.version == "pvMoul":
if self.serverini: if self.serverini:
args.append("--serverini") args.append("--serverini")
@ -300,8 +437,13 @@ class PlasmaAgeExportOperator(ExportOperator, bpy.types.Operator):
args.append(self.player) args.append(self.player)
with exporter.ExportVerboseLogger() as log: with exporter.ExportVerboseLogger() as log:
proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, proc = subprocess.Popen(
cwd=str(client_dir), universal_newlines=True) args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=str(client_dir),
universal_newlines=True,
)
while True: while True:
line = proc.stdout.readline().strip() line = proc.stdout.readline().strip()
if line == "DIE": if line == "DIE":
@ -320,13 +462,15 @@ class PlasmaLocalizationExportOperator(ExportOperator, bpy.types.Operator):
bl_description = "Export Age localization data" bl_description = "Export Age localization data"
filepath = StringProperty(subtype="DIR_PATH") filepath = StringProperty(subtype="DIR_PATH")
filter_glob = StringProperty(default="*.pak", options={'HIDDEN'}) filter_glob = StringProperty(default="*.pak", options={"HIDDEN"})
version = EnumProperty(name="Version", version = EnumProperty(
description="Plasma version to export this age for", name="Version",
items=game_versions, description="Plasma version to export this age for",
default="pvPots", items=game_versions,
options=set()) default="pvPots",
options=set(),
)
def execute(self, context): def execute(self, context):
path = Path(self.filepath) path = Path(self.filepath)
@ -344,12 +488,16 @@ class PlasmaLocalizationExportOperator(ExportOperator, bpy.types.Operator):
# Age names cannot be python keywords # Age names cannot be python keywords
age_name = context.scene.world.plasma_age.age_name age_name = context.scene.world.plasma_age.age_name
if korlib.is_python_keyword(age_name): if korlib.is_python_keyword(age_name):
self.report({"ERROR"}, "The Age name conflicts with the Python keyword '{}'".format(age_name)) self.report(
{"ERROR"},
"The Age name conflicts with the Python keyword '{}'".format(age_name),
)
return {"CANCELLED"} return {"CANCELLED"}
# Bonus Fun: Implement Profile-mode here (later...) # Bonus Fun: Implement Profile-mode here (later...)
e = exporter.LocalizationConverter(age_name=age_name, path=self.filepath, e = exporter.LocalizationConverter(
version=globals()[self.version]) age_name=age_name, path=self.filepath, version=globals()[self.version]
)
try: try:
e.run() e.run()
except exporter.ExportError as error: except exporter.ExportError as error:
@ -368,13 +516,15 @@ class PlasmaPythonExportOperator(ExportOperator, bpy.types.Operator):
bl_description = "Export Age python script package" bl_description = "Export Age python script package"
filepath = StringProperty(subtype="FILE_PATH") filepath = StringProperty(subtype="FILE_PATH")
filter_glob = StringProperty(default="*.pak", options={'HIDDEN'}) filter_glob = StringProperty(default="*.pak", options={"HIDDEN"})
version = EnumProperty(name="Version", version = EnumProperty(
description="Plasma version to export this age for", name="Version",
items=game_versions, description="Plasma version to export this age for",
default="pvPots", items=game_versions,
options=set()) default="pvPots",
options=set(),
)
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
@ -407,12 +557,16 @@ class PlasmaPythonExportOperator(ExportOperator, bpy.types.Operator):
# Age names cannot be python keywords # Age names cannot be python keywords
age_name = context.scene.world.plasma_age.age_name age_name = context.scene.world.plasma_age.age_name
if korlib.is_python_keyword(age_name): if korlib.is_python_keyword(age_name):
self.report({"ERROR"}, "The Age name conflicts with the Python keyword '{}'".format(age_name)) self.report(
{"ERROR"},
"The Age name conflicts with the Python keyword '{}'".format(age_name),
)
return {"CANCELLED"} return {"CANCELLED"}
# Bonus Fun: Implement Profile-mode here (later...) # Bonus Fun: Implement Profile-mode here (later...)
e = exporter.PythonPackageExporter(filepath=self.filepath, e = exporter.PythonPackageExporter(
version=globals()[self.version]) filepath=self.filepath, version=globals()[self.version]
)
try: try:
e.run() e.run()
except exporter.ExportError as error: except exporter.ExportError as error:
@ -439,12 +593,17 @@ class PlasmaPythonExportOperator(ExportOperator, bpy.types.Operator):
def menu_cb(self, context): def menu_cb(self, context):
if context.scene.render.engine == "PLASMA_GAME": if context.scene.render.engine == "PLASMA_GAME":
self.layout.operator_context = "INVOKE_DEFAULT" self.layout.operator_context = "INVOKE_DEFAULT"
self.layout.operator(PlasmaAgeExportOperator.bl_idname, text="Plasma Age (.age)") self.layout.operator(
self.layout.operator(PlasmaPythonExportOperator.bl_idname, text="Plasma Scripts (.pak)") PlasmaAgeExportOperator.bl_idname, text="Plasma Age (.age)"
)
self.layout.operator(
PlasmaPythonExportOperator.bl_idname, text="Plasma Scripts (.pak)"
)
def register(): def register():
bpy.types.INFO_MT_file_export.append(menu_cb) bpy.types.INFO_MT_file_export.append(menu_cb)
def unregister(): def unregister():
bpy.types.INFO_MT_file_export.remove(menu_cb) bpy.types.INFO_MT_file_export.remove(menu_cb)

83
korman/operators/op_image.py

@ -33,6 +33,7 @@ _CUBE_FACES = {
"frontFace": "FR", "frontFace": "FR",
} }
class ImageOperator: class ImageOperator:
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@ -44,19 +45,25 @@ class PlasmaBuildCubeMapOperator(ImageOperator, bpy.types.Operator):
bl_label = "Build Cubemap" bl_label = "Build Cubemap"
bl_description = "Builds a Blender cubemap from six images" bl_description = "Builds a Blender cubemap from six images"
overwrite_existing = BoolProperty(name="Check Existing", overwrite_existing = BoolProperty(
description="Checks for an existing image and overwrites it", name="Check Existing",
default=True, description="Checks for an existing image and overwrites it",
options=set()) default=True,
options=set(),
)
filepath = StringProperty(subtype="FILE_PATH") filepath = StringProperty(subtype="FILE_PATH")
require_cube = BoolProperty(name="Require Square Faces", require_cube = BoolProperty(
description="Resize cubemap faces to be square if they are not", name="Require Square Faces",
default=True, description="Resize cubemap faces to be square if they are not",
options=set()) default=True,
texture_name = StringProperty(name="Texture", options=set(),
description="Environment Map Texture to stuff this into", )
default="", texture_name = StringProperty(
options={"HIDDEN"}) name="Texture",
description="Environment Map Texture to stuff this into",
default="",
options={"HIDDEN"},
)
def __init__(self): def __init__(self):
self._report = ExportProgressLogger() self._report = ExportProgressLogger()
@ -91,15 +98,17 @@ class PlasmaBuildCubeMapOperator(ImageOperator, bpy.types.Operator):
face_widths, face_heights, face_data = zip(*face_data) face_widths, face_heights, face_data = zip(*face_data)
# All widths and heights must be the same... so, if needed, scale the stupid images. # All widths and heights must be the same... so, if needed, scale the stupid images.
width, height, face_data = self._scale_images(face_widths, face_heights, face_data) width, height, face_data = self._scale_images(
face_widths, face_heights, face_data
)
# Now generate the stoopid cube map # Now generate the stoopid cube map
image_name = Path(self.filepath).name image_name = Path(self.filepath).name
idx = image_name.rfind('_') idx = image_name.rfind("_")
if idx != -1: if idx != -1:
suffix = image_name[idx+1:idx+3] suffix = image_name[idx + 1 : idx + 3]
if suffix in _CUBE_FACES.values(): if suffix in _CUBE_FACES.values():
image_name = image_name[:idx] + image_name[idx+3:] image_name = image_name[:idx] + image_name[idx + 3 :]
cubemap_image = self._generate_cube_map(image_name, width, height, face_data) cubemap_image = self._generate_cube_map(image_name, width, height, face_data)
# If a texture was provided, we can assign this generated cube map to it... # If a texture was provided, we can assign this generated cube map to it...
@ -116,18 +125,22 @@ class PlasmaBuildCubeMapOperator(ImageOperator, bpy.types.Operator):
self._report.progress_range = len(BLENDER_CUBE_MAP) self._report.progress_range = len(BLENDER_CUBE_MAP)
self._report.msg("Searching for cubemap faces...") self._report.msg("Searching for cubemap faces...")
idx = filepath.rfind('_') idx = filepath.rfind("_")
if idx != -1: if idx != -1:
files = [] files = []
for key in BLENDER_CUBE_MAP: for key in BLENDER_CUBE_MAP:
suffix = _CUBE_FACES[key] suffix = _CUBE_FACES[key]
face_path = filepath[:idx+1] + suffix + filepath[idx+3:] face_path = filepath[: idx + 1] + suffix + filepath[idx + 3 :]
face_name = key[:-4].upper() face_name = key[:-4].upper()
if Path(face_path).is_file(): if Path(face_path).is_file():
self._report.msg("Found face '{}': {}", face_name, face_path, indent=1) self._report.msg(
"Found face '{}': {}", face_name, face_path, indent=1
)
files.append(face_path) files.append(face_path)
else: else:
self._report.warn("Using default face data for face '{}'", face_name, indent=1) self._report.warn(
"Using default face data for face '{}'", face_name, indent=1
)
files.append(None) files.append(None)
self._report.progress_increment() self._report.progress_increment()
return tuple(files) return tuple(files)
@ -138,7 +151,9 @@ class PlasmaBuildCubeMapOperator(ImageOperator, bpy.types.Operator):
self._report.msg("Generating cubemap image...") self._report.msg("Generating cubemap image...")
# If a texture was provided, we should check to see if we have an image we can replace... # If a texture was provided, we should check to see if we have an image we can replace...
image = bpy.data.textures[self.texture_name].image if self.texture_name else None image = (
bpy.data.textures[self.texture_name].image if self.texture_name else None
)
# Init our image # Init our image
image_width = face_width * 3 image_width = face_width * 3
@ -167,9 +182,13 @@ class PlasmaBuildCubeMapOperator(ImageOperator, bpy.types.Operator):
for row_current in range(row_start, row_end, 1): for row_current in range(row_start, row_end, 1):
src_start_idx = (row_current - row_start) * face_width * 4 src_start_idx = (row_current - row_start) * face_width * 4
src_end_idx = src_start_idx + (face_width * 4) src_end_idx = src_start_idx + (face_width * 4)
dst_start_idx = (row_current * image_width * 4) + (col_id * face_width * 4) dst_start_idx = (row_current * image_width * 4) + (
col_id * face_width * 4
)
dst_end_idx = dst_start_idx + (face_width * 4) dst_end_idx = dst_start_idx + (face_width * 4)
image_data[dst_start_idx:dst_end_idx] = face_data[j][src_start_idx:src_end_idx] image_data[dst_start_idx:dst_end_idx] = face_data[j][
src_start_idx:src_end_idx
]
# FFFUUUUU... Blender wants a list of floats # FFFUUUUU... Blender wants a list of floats
pixels = [None] * image_datasz pixels = [None] * image_datasz
@ -183,7 +202,6 @@ class PlasmaBuildCubeMapOperator(ImageOperator, bpy.types.Operator):
image.plasma_image.texcache_method = "rebuild" image.plasma_image.texcache_method = "rebuild"
return image return image
def invoke(self, context, event): def invoke(self, context, event):
context.window_manager.fileselect_add(self) context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"} return {"RUNNING_MODAL"}
@ -230,10 +248,17 @@ class PlasmaBuildCubeMapOperator(ImageOperator, bpy.types.Operator):
face_width, face_height = face_widths[i], face_heights[i] face_width, face_height = face_widths[i], face_heights[i]
if face_width != min_width or face_height != min_height: if face_width != min_width or face_height != min_height:
face_name = BLENDER_CUBE_MAP[i][:-4].upper() face_name = BLENDER_CUBE_MAP[i][:-4].upper()
self._report.msg("Resizing face '{}' from {}x{} to {}x{}", face_name, self._report.msg(
face_width, face_height, min_width, min_height, "Resizing face '{}' from {}x{} to {}x{}",
indent=1) face_name,
result_data[i] = scale_image(face_data[i], face_width, face_height, face_width,
min_width, min_height) face_height,
min_width,
min_height,
indent=1,
)
result_data[i] = scale_image(
face_data[i], face_width, face_height, min_width, min_height
)
self._report.progress_increment() self._report.progress_increment()
return min_width, min_height, tuple(result_data) return min_width, min_height, tuple(result_data)

55
korman/operators/op_lightmap.py

@ -25,6 +25,7 @@ from ..exporter.explosions import ExportError
from ..helpers import UiHelper from ..helpers import UiHelper
from ..korlib import ConsoleToggler from ..korlib import ConsoleToggler
class _LightingOperator: class _LightingOperator:
@contextmanager @contextmanager
def _oven(self, context): def _oven(self, context):
@ -34,7 +35,9 @@ class _LightingOperator:
else: else:
verbose = False verbose = False
console = True console = True
with UiHelper(context), ConsoleToggler(console), LightBaker(verbose=verbose) as oven: with UiHelper(context), ConsoleToggler(console), LightBaker(
verbose=verbose
) as oven:
yield oven yield oven
@classmethod @classmethod
@ -63,7 +66,11 @@ class LightmapAutobakePreviewOperator(_LightingOperator, bpy.types.Operator):
bake.lightmap_uvtex_name = "LIGHTMAPGEN_PREVIEW" bake.lightmap_uvtex_name = "LIGHTMAPGEN_PREVIEW"
bake.force = True bake.force = True
bake.retain_lightmap_uvtex = self.final bake.retain_lightmap_uvtex = self.final
if not bake.bake_static_lighting([context.object,]): if not bake.bake_static_lighting(
[
context.object,
]
):
self.report({"WARNING"}, "No valid lights found to bake.") self.report({"WARNING"}, "No valid lights found to bake.")
return {"FINISHED"} return {"FINISHED"}
@ -89,9 +96,11 @@ class LightmapBakeMultiOperator(_LightingOperator, bpy.types.Operator):
bl_label = "Bake Lighting" bl_label = "Bake Lighting"
bl_description = "Bake scene lighting to object(s)" bl_description = "Bake scene lighting to object(s)"
bake_selection = BoolProperty(name="Bake Selection", bake_selection = BoolProperty(
description="Bake only the selected objects (else all objects)", name="Bake Selection",
options=set()) description="Bake only the selected objects (else all objects)",
options=set(),
)
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -101,7 +110,9 @@ class LightmapBakeMultiOperator(_LightingOperator, bpy.types.Operator):
try: try:
if profile_me: if profile_me:
cProfile.runctx("self._run(context)", globals(), locals(), "bake_cProfile") cProfile.runctx(
"self._run(context)", globals(), locals(), "bake_cProfile"
)
else: else:
self._run(context) self._run(context)
except ExportError as error: except ExportError as error:
@ -116,8 +127,12 @@ class LightmapBakeMultiOperator(_LightingOperator, bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
def _run(self, context): def _run(self, context):
all_objects = context.selected_objects if self.bake_selection else context.scene.objects all_objects = (
filtered_objects = [i for i in all_objects if i.type == "MESH" and i.plasma_object.enabled] context.selected_objects if self.bake_selection else context.scene.objects
)
filtered_objects = [
i for i in all_objects if i.type == "MESH" and i.plasma_object.enabled
]
with self._oven(context) as bake: with self._oven(context) as bake:
bake.force = True bake.force = True
@ -136,21 +151,32 @@ class LightmapClearMultiOperator(_LightingOperator, bpy.types.Operator):
bl_label = "Clear Lighting" bl_label = "Clear Lighting"
bl_description = "Clear baked lighting" bl_description = "Clear baked lighting"
clear_selection = BoolProperty(name="Clear Selection", clear_selection = BoolProperty(
description="Clear only the selected objects (else all objects)", name="Clear Selection",
options=set()) description="Clear only the selected objects (else all objects)",
options=set(),
)
def __init__(self): def __init__(self):
super().__init__() super().__init__()
def _iter_lightmaps(self, objects): def _iter_lightmaps(self, objects):
yield from filter(lambda x: x.type == "MESH" and x.plasma_modifiers.lightmap.bake_lightmap, objects) yield from filter(
lambda x: x.type == "MESH" and x.plasma_modifiers.lightmap.bake_lightmap,
objects,
)
def _iter_vcols(self, objects): def _iter_vcols(self, objects):
yield from filter(lambda x: x.type == "MESH" and not x.plasma_modifiers.lightmap.bake_lightmap, objects) yield from filter(
lambda x: x.type == "MESH"
and not x.plasma_modifiers.lightmap.bake_lightmap,
objects,
)
def execute(self, context): def execute(self, context):
all_objects = context.selected_objects if self.clear_selection else context.scene.objects all_objects = (
context.selected_objects if self.clear_selection else context.scene.objects
)
for i in self._iter_lightmaps(all_objects): for i in self._iter_lightmaps(all_objects):
i.plasma_modifiers.lightmap.image = None i.plasma_modifiers.lightmap.image = None
@ -179,5 +205,6 @@ def _toss_garbage(scene):
if uvtex is not None: if uvtex is not None:
i.uv_textures.remove(uvtex) i.uv_textures.remove(uvtex)
# collects light baking garbage # collects light baking garbage
bpy.app.handlers.save_pre.append(_toss_garbage) bpy.app.handlers.save_pre.append(_toss_garbage)

357
korman/operators/op_mesh.py

@ -20,6 +20,7 @@ import mathutils
from ..exporter import utils from ..exporter import utils
class PlasmaMeshOperator: class PlasmaMeshOperator:
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@ -37,18 +38,25 @@ class PlasmaAddFlareOperator(PlasmaMeshOperator, bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
# Allows user to specify their own name stem # Allows user to specify their own name stem
flare_name = bpy.props.StringProperty(name="Name", flare_name = bpy.props.StringProperty(
description="Flare name stem", name="Name", description="Flare name stem", default="Flare", options=set()
default="Flare", )
options=set()) flare_distance = bpy.props.FloatProperty(
flare_distance = bpy.props.FloatProperty(name="Distance", name="Distance",
description="Flare's distance from the illuminating object", description="Flare's distance from the illuminating object",
min=0.1, max=2.0, step=10, precision=1, default=1.0, min=0.1,
options=set()) max=2.0,
flare_material_name = bpy.props.StringProperty(name="Material", step=10,
description="A specially-crafted material to use for this flare", precision=1,
default=FLARE_MATERIAL_BASE_NAME, default=1.0,
options=set()) options=set(),
)
flare_material_name = bpy.props.StringProperty(
name="Material",
description="A specially-crafted material to use for this flare",
default=FLARE_MATERIAL_BASE_NAME,
options=set(),
)
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@ -100,14 +108,26 @@ class PlasmaAddFlareOperator(PlasmaMeshOperator, bpy.types.Operator):
flare_root.plasma_modifiers.viewfacemod.preset_options = "Sprite" flare_root.plasma_modifiers.viewfacemod.preset_options = "Sprite"
# Create a textured Plane # Create a textured Plane
with utils.bmesh_object("{}_Visible".format(self.name_stem)) as (flare_plane, bm): with utils.bmesh_object("{}_Visible".format(self.name_stem)) as (
flare_plane,
bm,
):
flare_plane.hide_render = True flare_plane.hide_render = True
flare_plane.plasma_object.enabled = True flare_plane.plasma_object.enabled = True
bpyscene.objects.active = flare_plane bpyscene.objects.active = flare_plane
# Make the actual plane mesh, facing away from the empty # Make the actual plane mesh, facing away from the empty
bmesh.ops.create_grid(bm, size=(0.5 + self.flare_distance * 0.5), matrix=mathutils.Matrix.Rotation(math.radians(180.0), 4, 'X')) bmesh.ops.create_grid(
bmesh.ops.transform(bm, matrix=mathutils.Matrix.Translation((0.0, 0.0, -self.flare_distance)), space=flare_plane.matrix_world, verts=bm.verts) bm,
size=(0.5 + self.flare_distance * 0.5),
matrix=mathutils.Matrix.Rotation(math.radians(180.0), 4, "X"),
)
bmesh.ops.transform(
bm,
matrix=mathutils.Matrix.Translation((0.0, 0.0, -self.flare_distance)),
space=flare_plane.matrix_world,
verts=bm.verts,
)
bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY") bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY")
# Give the plane a basic UV unwrap, so that it's texture-ready # Give the plane a basic UV unwrap, so that it's texture-ready
@ -141,7 +161,9 @@ class PlasmaAddFlareOperator(PlasmaMeshOperator, bpy.types.Operator):
auto_mat.use_cast_shadows = False auto_mat.use_cast_shadows = False
self.flare_material_name = auto_mat.name self.flare_material_name = auto_mat.name
auto_tex = bpy.data.textures.new(name=FLARE_MATERIAL_BASE_NAME, type="IMAGE") auto_tex = bpy.data.textures.new(
name=FLARE_MATERIAL_BASE_NAME, type="IMAGE"
)
auto_tex.use_alpha = True auto_tex.use_alpha = True
auto_tex.plasma_layer.skip_depth_write = True auto_tex.plasma_layer.skip_depth_write = True
auto_tex.plasma_layer.skip_depth_test = True auto_tex.plasma_layer.skip_depth_test = True
@ -166,60 +188,104 @@ class PlasmaAddLadderMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
# Allows user to specify their own name stem # Allows user to specify their own name stem
ladder_name = bpy.props.StringProperty(name="Name", ladder_name = bpy.props.StringProperty(
description="Ladder name stem", name="Name", description="Ladder name stem", default="Ladder", options=set()
default="Ladder", )
options=set())
# Basic stats # Basic stats
ladder_height = bpy.props.FloatProperty(name="Height", ladder_height = bpy.props.FloatProperty(
description="Height of ladder in feet", name="Height",
min=6, max=1000, step=200, precision=0, default=6, description="Height of ladder in feet",
unit="LENGTH", subtype="DISTANCE", min=6,
options=set()) max=1000,
ladder_width = bpy.props.FloatProperty(name="Width", step=200,
description="Width of ladder in inches", precision=0,
min=30, max=42, step=100, precision=0, default=30, default=6,
options=set()) unit="LENGTH",
rung_height = bpy.props.FloatProperty(name="Rung height", subtype="DISTANCE",
description="Height of rungs in inches", options=set(),
min=1, max=6, step=100, precision=0, default=6, )
options=set()) ladder_width = bpy.props.FloatProperty(
name="Width",
description="Width of ladder in inches",
min=30,
max=42,
step=100,
precision=0,
default=30,
options=set(),
)
rung_height = bpy.props.FloatProperty(
name="Rung height",
description="Height of rungs in inches",
min=1,
max=6,
step=100,
precision=0,
default=6,
options=set(),
)
# Template generation # Template generation
gen_back_guide = bpy.props.BoolProperty(name="Ladder", gen_back_guide = bpy.props.BoolProperty(
description="Generates helper object where ladder back should be placed", name="Ladder",
default=True, description="Generates helper object where ladder back should be placed",
options=set()) default=True,
gen_ground_guides = bpy.props.BoolProperty(name="Ground", options=set(),
description="Generates helper objects where ground should be placed", )
default=True, gen_ground_guides = bpy.props.BoolProperty(
options=set()) name="Ground",
gen_rung_guides = bpy.props.BoolProperty(name="Rungs", description="Generates helper objects where ground should be placed",
description="Generates helper objects where rungs should be placed", default=True,
default=True, options=set(),
options=set()) )
rung_width_type = bpy.props.EnumProperty(name="Rung Width", gen_rung_guides = bpy.props.BoolProperty(
description="Type of rungs to generate", name="Rungs",
items=[("FULL", "Full Width Rungs", "The rungs cross the entire width of the ladder"), description="Generates helper objects where rungs should be placed",
("HALF", "Half Width Rungs", "The rungs only cross half the ladder's width, on the side where the avatar will contact them"),], default=True,
default="FULL", options=set(),
options=set()) )
rung_width_type = bpy.props.EnumProperty(
name="Rung Width",
description="Type of rungs to generate",
items=[
(
"FULL",
"Full Width Rungs",
"The rungs cross the entire width of the ladder",
),
(
"HALF",
"Half Width Rungs",
"The rungs only cross half the ladder's width, on the side where the avatar will contact them",
),
],
default="FULL",
options=set(),
)
# Game options # Game options
has_upper_entry = bpy.props.BoolProperty(name="Has Upper Entry Point", has_upper_entry = bpy.props.BoolProperty(
description="Specifies whether the ladder has an upper entry", name="Has Upper Entry Point",
default=True, description="Specifies whether the ladder has an upper entry",
options=set()) default=True,
upper_entry_enabled = bpy.props.BoolProperty(name="Upper Entry Enabled", options=set(),
description="Specifies whether the ladder's upper entry is enabled by default at Age start", )
default=True, upper_entry_enabled = bpy.props.BoolProperty(
options=set()) name="Upper Entry Enabled",
has_lower_entry = bpy.props.BoolProperty(name="Has Lower Entry Point", description="Specifies whether the ladder's upper entry is enabled by default at Age start",
description="Specifies whether the ladder has a lower entry", default=True,
default=True, options=set(),
options=set()) )
lower_entry_enabled = bpy.props.BoolProperty(name="Lower Entry Enabled", has_lower_entry = bpy.props.BoolProperty(
description="Specifies whether the ladder's lower entry is enabled by default at Age start", name="Has Lower Entry Point",
default=True, description="Specifies whether the ladder has a lower entry",
options=set()) default=True,
options=set(),
)
lower_entry_enabled = bpy.props.BoolProperty(
name="Lower Entry Enabled",
description="Specifies whether the ladder's lower entry is enabled by default at Age start",
default=True,
options=set(),
)
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
@ -269,7 +335,9 @@ class PlasmaAddLadderMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
else: else:
row = layout.row() row = layout.row()
row.label("Warning: Operator does not work in local view mode", icon="ERROR") row.label(
"Warning: Operator does not work in local view mode", icon="ERROR"
)
def execute(self, context): def execute(self, context):
if context.space_data.local_view: if context.space_data.local_view:
@ -292,15 +360,18 @@ class PlasmaAddLadderMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
rung_yoffset = rung_width_ft / 4 rung_yoffset = rung_width_ft / 4
rungs_scale = mathutils.Matrix( rungs_scale = mathutils.Matrix(
((0.5, 0.0, 0.0), ((0.5, 0.0, 0.0), (0.0, rung_width, 0.0), (0.0, 0.0, rung_height_ft))
(0.0, rung_width, 0.0), )
(0.0, 0.0, rung_height_ft)))
for rung_num in range(0, int(self.ladder_height)): for rung_num in range(0, int(self.ladder_height)):
side = "L" if (rung_num % 2) == 0 else "R" side = "L" if (rung_num % 2) == 0 else "R"
mesh = bpy.data.meshes.new("{}_Rung_{}_{}".format(self.name_stem, side, rung_num)) mesh = bpy.data.meshes.new(
rungs = bpy.data.objects.new("{}_Rung_{}_{}".format(self.name_stem, side, rung_num), mesh) "{}_Rung_{}_{}".format(self.name_stem, side, rung_num)
)
rungs = bpy.data.objects.new(
"{}_Rung_{}_{}".format(self.name_stem, side, rung_num), mesh
)
rungs.hide_render = True rungs.hide_render = True
rungs.draw_type = "BOUNDS" rungs.draw_type = "BOUNDS"
@ -314,11 +385,27 @@ class PlasmaAddLadderMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
# Move each rung up, based on: # Move each rung up, based on:
# its place in the array, aligned to the top of the rung position, shifted up to start at the ladder's base # its place in the array, aligned to the top of the rung position, shifted up to start at the ladder's base
if (rung_num % 2) == 0: if (rung_num % 2) == 0:
rung_pos = mathutils.Matrix.Translation((0.5, -rung_yoffset, rung_num + (1.0 - rung_height_ft) + (rung_height_ft / 2))) rung_pos = mathutils.Matrix.Translation(
(
0.5,
-rung_yoffset,
rung_num + (1.0 - rung_height_ft) + (rung_height_ft / 2),
)
)
else: else:
rung_pos = mathutils.Matrix.Translation((0.5, rung_yoffset, rung_num + (1.0 - rung_height_ft) + (rung_height_ft / 2))) rung_pos = mathutils.Matrix.Translation(
bmesh.ops.transform(bm, matrix=cursor_shift, space=rungs.matrix_world, verts=bm.verts) (
bmesh.ops.transform(bm, matrix=rung_pos, space=rungs.matrix_world, verts=bm.verts) 0.5,
rung_yoffset,
rung_num + (1.0 - rung_height_ft) + (rung_height_ft / 2),
)
)
bmesh.ops.transform(
bm, matrix=cursor_shift, space=rungs.matrix_world, verts=bm.verts
)
bmesh.ops.transform(
bm, matrix=rung_pos, space=rungs.matrix_world, verts=bm.verts
)
bm.to_mesh(mesh) bm.to_mesh(mesh)
bm.free() bm.free()
@ -341,15 +428,22 @@ class PlasmaAddLadderMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
# Construct the bmesh and assign it to the blender mesh. # Construct the bmesh and assign it to the blender mesh.
bm = bmesh.new() bm = bmesh.new()
ladder_scale = mathutils.Matrix( ladder_scale = mathutils.Matrix(
((0.5, 0.0, 0.0), (
(0.0, self.ladder_width / 12, 0.0), (0.5, 0.0, 0.0),
(0.0, 0.0, self.ladder_height))) (0.0, self.ladder_width / 12, 0.0),
(0.0, 0.0, self.ladder_height),
)
)
bmesh.ops.create_cube(bm, size=(1.0), matrix=ladder_scale) bmesh.ops.create_cube(bm, size=(1.0), matrix=ladder_scale)
# Shift the ladder up so that its base is at the 3D cursor # Shift the ladder up so that its base is at the 3D cursor
back_pos = mathutils.Matrix.Translation((0.0, 0.0, self.ladder_height / 2)) back_pos = mathutils.Matrix.Translation((0.0, 0.0, self.ladder_height / 2))
bmesh.ops.transform(bm, matrix=cursor_shift, space=back.matrix_world, verts=bm.verts) bmesh.ops.transform(
bmesh.ops.transform(bm, matrix=back_pos, space=back.matrix_world, verts=bm.verts) bm, matrix=cursor_shift, space=back.matrix_world, verts=bm.verts
)
bmesh.ops.transform(
bm, matrix=back_pos, space=back.matrix_world, verts=bm.verts
)
bm.to_mesh(mesh) bm.to_mesh(mesh)
bm.free() bm.free()
@ -374,17 +468,28 @@ class PlasmaAddLadderMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
bm = bmesh.new() bm = bmesh.new()
ground_depth = 3.0 ground_depth = 3.0
ground_scale = mathutils.Matrix( ground_scale = mathutils.Matrix(
((ground_depth, 0.0, 0.0), (
(0.0, self.ladder_width / 12, 0.0), (ground_depth, 0.0, 0.0),
(0.0, 0.0, 0.5))) (0.0, self.ladder_width / 12, 0.0),
(0.0, 0.0, 0.5),
)
)
bmesh.ops.create_cube(bm, size=(1.0), matrix=ground_scale) bmesh.ops.create_cube(bm, size=(1.0), matrix=ground_scale)
if pos == "Upper": if pos == "Upper":
ground_pos = mathutils.Matrix.Translation((-(ground_depth / 2) + 0.25, 0.0, self.ladder_height + 0.25)) ground_pos = mathutils.Matrix.Translation(
(-(ground_depth / 2) + 0.25, 0.0, self.ladder_height + 0.25)
)
else: else:
ground_pos = mathutils.Matrix.Translation(((ground_depth / 2) + 0.25, 0.0, 0.25)) ground_pos = mathutils.Matrix.Translation(
bmesh.ops.transform(bm, matrix=cursor_shift, space=ground.matrix_world, verts=bm.verts) ((ground_depth / 2) + 0.25, 0.0, 0.25)
bmesh.ops.transform(bm, matrix=ground_pos, space=ground.matrix_world, verts=bm.verts) )
bmesh.ops.transform(
bm, matrix=cursor_shift, space=ground.matrix_world, verts=bm.verts
)
bmesh.ops.transform(
bm, matrix=ground_pos, space=ground.matrix_world, verts=bm.verts
)
bm.to_mesh(mesh) bm.to_mesh(mesh)
bm.free() bm.free()
@ -408,14 +513,17 @@ class PlasmaAddLadderMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
# Construct the bmesh and assign it to the blender mesh. # Construct the bmesh and assign it to the blender mesh.
bm = bmesh.new() bm = bmesh.new()
rgn_scale = mathutils.Matrix( rgn_scale = mathutils.Matrix(
((self.ladder_width / 12, 0.0, 0.0), ((self.ladder_width / 12, 0.0, 0.0), (0.0, 2.5, 0.0), (0.0, 0.0, 2.0))
(0.0, 2.5, 0.0), )
(0.0, 0.0, 2.0)))
bmesh.ops.create_cube(bm, size=(1.0), matrix=rgn_scale) bmesh.ops.create_cube(bm, size=(1.0), matrix=rgn_scale)
rgn_pos = mathutils.Matrix.Translation((-1.80, 0.0, 1.5 + self.ladder_height)) rgn_pos = mathutils.Matrix.Translation((-1.80, 0.0, 1.5 + self.ladder_height))
bmesh.ops.transform(bm, matrix=cursor_shift, space=upper_rgn.matrix_world, verts=bm.verts) bmesh.ops.transform(
bmesh.ops.transform(bm, matrix=rgn_pos, space=upper_rgn.matrix_world, verts=bm.verts) bm, matrix=cursor_shift, space=upper_rgn.matrix_world, verts=bm.verts
)
bmesh.ops.transform(
bm, matrix=rgn_pos, space=upper_rgn.matrix_world, verts=bm.verts
)
bm.to_mesh(mesh) bm.to_mesh(mesh)
bm.free() bm.free()
@ -449,14 +557,17 @@ class PlasmaAddLadderMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
# Construct the bmesh and assign it to the blender mesh. # Construct the bmesh and assign it to the blender mesh.
bm = bmesh.new() bm = bmesh.new()
rgn_scale = mathutils.Matrix( rgn_scale = mathutils.Matrix(
((self.ladder_width / 12, 0.0, 0.0), ((self.ladder_width / 12, 0.0, 0.0), (0.0, 2.5, 0.0), (0.0, 0.0, 2.0))
(0.0, 2.5, 0.0), )
(0.0, 0.0, 2.0)))
bmesh.ops.create_cube(bm, size=(1.0), matrix=rgn_scale) bmesh.ops.create_cube(bm, size=(1.0), matrix=rgn_scale)
rgn_pos = mathutils.Matrix.Translation((2.70, 0.0, 1.5)) rgn_pos = mathutils.Matrix.Translation((2.70, 0.0, 1.5))
bmesh.ops.transform(bm, matrix=cursor_shift, space=lower_rgn.matrix_world, verts=bm.verts) bmesh.ops.transform(
bmesh.ops.transform(bm, matrix=rgn_pos, space=lower_rgn.matrix_world, verts=bm.verts) bm, matrix=cursor_shift, space=lower_rgn.matrix_world, verts=bm.verts
)
bmesh.ops.transform(
bm, matrix=rgn_pos, space=lower_rgn.matrix_world, verts=bm.verts
)
bm.to_mesh(mesh) bm.to_mesh(mesh)
bm.free() bm.free()
@ -495,6 +606,7 @@ class PlasmaAddLadderMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
def name_stem(self): def name_stem(self):
return self.ladder_name if self.ladder_name else "Ladder" return self.ladder_name if self.ladder_name else "Ladder"
def origin_to_bottom(obj): def origin_to_bottom(obj):
# Modified from https://blender.stackexchange.com/a/42110/3055 # Modified from https://blender.stackexchange.com/a/42110/3055
mw = obj.matrix_world mw = obj.matrix_world
@ -532,16 +644,30 @@ class PlasmaAddLinkingBookMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
} }
# Allows user to specify their own name stem # Allows user to specify their own name stem
panel_name = bpy.props.StringProperty(name="Name", panel_name = bpy.props.StringProperty(
description="Linking Book name stem", name="Name",
default="LinkingBook", description="Linking Book name stem",
options=set()) default="LinkingBook",
link_anim_type = bpy.props.EnumProperty(name="Link Animation", options=set(),
description="Type of Linking Animation to use", )
items=[("LinkOut", "Standing", "The avatar steps up to the book and places their hand on the panel"), link_anim_type = bpy.props.EnumProperty(
("FishBookLinkOut", "Kneeling", "The avatar kneels in front of the book and places their hand on the panel"),], name="Link Animation",
default="LinkOut", description="Type of Linking Animation to use",
options=set()) items=[
(
"LinkOut",
"Standing",
"The avatar steps up to the book and places their hand on the panel",
),
(
"FishBookLinkOut",
"Kneeling",
"The avatar kneels in front of the book and places their hand on the panel",
),
],
default="LinkOut",
options=set(),
)
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
@ -558,7 +684,9 @@ class PlasmaAddLinkingBookMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
row.prop(self, "link_anim_type", text="Type") row.prop(self, "link_anim_type", text="Type")
else: else:
row = layout.row() row = layout.row()
row.label("Warning: Operator does not work in local view mode", icon="ERROR") row.label(
"Warning: Operator does not work in local view mode", icon="ERROR"
)
def execute(self, context): def execute(self, context):
if context.space_data.local_view: if context.space_data.local_view:
@ -587,7 +715,9 @@ class PlasmaAddLinkingBookMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
bpy.context.scene.objects.link(seek_point) bpy.context.scene.objects.link(seek_point)
seek_point.show_name = True seek_point.show_name = True
seek_point.empty_draw_type = "ARROWS" seek_point.empty_draw_type = "ARROWS"
link_anim_offset = mathutils.Matrix.Translation(self.anim_offsets[self.link_anim_type]) link_anim_offset = mathutils.Matrix.Translation(
self.anim_offsets[self.link_anim_type]
)
seek_point.matrix_local = link_anim_offset seek_point.matrix_local = link_anim_offset
seek_point.plasma_object.enabled = True seek_point.plasma_object.enabled = True
@ -595,7 +725,9 @@ class PlasmaAddLinkingBookMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
clk_rgn_name = "{}_ClkRegion".format(self.name_stem) clk_rgn_name = "{}_ClkRegion".format(self.name_stem)
clk_rgn_size = 6.0 clk_rgn_size = 6.0
with utils.bmesh_object(clk_rgn_name) as (clk_rgn, bm): with utils.bmesh_object(clk_rgn_name) as (clk_rgn, bm):
bmesh.ops.create_cube(bm, size=(1.0), matrix=(mathutils.Matrix.Scale(clk_rgn_size, 4))) bmesh.ops.create_cube(
bm, size=(1.0), matrix=(mathutils.Matrix.Scale(clk_rgn_size, 4))
)
clk_rgn.hide_render = True clk_rgn.hide_render = True
clk_rgn.plasma_object.enabled = True clk_rgn.plasma_object.enabled = True
@ -625,5 +757,6 @@ class PlasmaAddLinkingBookMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
def register(): def register():
bpy.utils.register_module(__name__) bpy.utils.register_module(__name__)
def unregister(): def unregister():
bpy.utils.unregister_module(__name__) bpy.utils.unregister_module(__name__)

74
korman/operators/op_modifier.py

@ -20,6 +20,7 @@ import time
from ..ordered_set import OrderedSet from ..ordered_set import OrderedSet
from ..properties import modifiers from ..properties import modifiers
def _fetch_modifiers(): def _fetch_modifiers():
items = [] items = []
@ -27,16 +28,19 @@ def _fetch_modifiers():
for i in sorted(mapping.keys()): for i in sorted(mapping.keys()):
items.append(("", i, "")) items.append(("", i, ""))
items.extend(mapping[i]) items.extend(mapping[i])
#yield ("", i, "") # yield ("", i, "")
#yield mapping[i] # yield mapping[i]
return items return items
class ModifierOperator: class ModifierOperator:
def _get_modifier(self, context): def _get_modifier(self, context):
if self.active_modifier == -1: if self.active_modifier == -1:
return None return None
pl_mods = context.object.plasma_modifiers.modifiers pl_mods = context.object.plasma_modifiers.modifiers
pl_mod = next((i for i in pl_mods if self.active_modifier == i.display_order), None) pl_mod = next(
(i for i in pl_mods if self.active_modifier == i.display_order), None
)
if pl_mod is None: if pl_mod is None:
raise IndexError(self.active_modifier) raise IndexError(self.active_modifier)
return pl_mod return pl_mod
@ -51,9 +55,11 @@ class ModifierAddOperator(ModifierOperator, bpy.types.Operator):
bl_label = "Add Modifier" bl_label = "Add Modifier"
bl_description = "Adds a Plasma Modifier" bl_description = "Adds a Plasma Modifier"
types = EnumProperty(name="Modifier Type", types = EnumProperty(
description="The type of modifier we add to the list", name="Modifier Type",
items=_fetch_modifiers()) description="The type of modifier we add to the list",
items=_fetch_modifiers(),
)
def execute(self, context): def execute(self, context):
plmods = context.object.plasma_modifiers plmods = context.object.plasma_modifiers
@ -122,9 +128,9 @@ class ModifierCopyOperator(ModifierOperator, bpy.types.Operator):
bl_label = "Copy Modifiers" bl_label = "Copy Modifiers"
bl_description = "Copy Modifiers from an Object" bl_description = "Copy Modifiers from an Object"
active_modifier = IntProperty(name="Modifier Display Order", active_modifier = IntProperty(
default=-1, name="Modifier Display Order", default=-1, options={"HIDDEN"}
options={"HIDDEN"}) )
def execute(self, context): def execute(self, context):
pl_scene = context.scene.plasma_scene pl_scene = context.scene.plasma_scene
@ -181,7 +187,9 @@ class ModifierPasteOperator(ModifierClipboard, ModifierOperator, bpy.types.Opera
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
pl_scene = context.scene.plasma_scene pl_scene = context.scene.plasma_scene
return super().poll(context) and pl_scene.is_property_set("modifier_copy_object") return super().poll(context) and pl_scene.is_property_set(
"modifier_copy_object"
)
class ModifierRemoveOperator(ModifierOperator, bpy.types.Operator): class ModifierRemoveOperator(ModifierOperator, bpy.types.Operator):
@ -189,9 +197,9 @@ class ModifierRemoveOperator(ModifierOperator, bpy.types.Operator):
bl_label = "Remove Modifier" bl_label = "Remove Modifier"
bl_description = "Removes this Plasma Modifier" bl_description = "Removes this Plasma Modifier"
active_modifier = IntProperty(name="Modifier Display Order", active_modifier = IntProperty(
default=-1, name="Modifier Display Order", default=-1, options={"HIDDEN"}
options={"HIDDEN"}) )
mods2delete = CollectionProperty(type=modifiers.PlasmaModifierSpec, options=set()) mods2delete = CollectionProperty(type=modifiers.PlasmaModifierSpec, options=set())
@ -203,11 +211,15 @@ class ModifierRemoveOperator(ModifierOperator, bpy.types.Operator):
layout = layout.column_flow(align=True) layout = layout.column_flow(align=True)
for i in self.mods2delete: for i in self.mods2delete:
mod = getattr(mods, i.name) mod = getattr(mods, i.name)
layout.label(" {}".format(mod.bl_label), icon=getattr(mod, "bl_icon", "NONE")) layout.label(
" {}".format(mod.bl_label), icon=getattr(mod, "bl_icon", "NONE")
)
def execute(self, context): def execute(self, context):
want2delete = set((i.name for i in self.mods2delete)) want2delete = set((i.name for i in self.mods2delete))
mods = sorted(context.object.plasma_modifiers.modifiers, key=lambda x: x.display_order) mods = sorted(
context.object.plasma_modifiers.modifiers, key=lambda x: x.display_order
)
subtract = 0 subtract = 0
for mod in mods: for mod in mods:
@ -225,7 +237,9 @@ class ModifierRemoveOperator(ModifierOperator, bpy.types.Operator):
want2delete = OrderedSet() want2delete = OrderedSet()
if self.active_modifier == -1: if self.active_modifier == -1:
want2delete.update((i.pl_id for i in context.object.plasma_modifiers.modifiers)) want2delete.update(
(i.pl_id for i in context.object.plasma_modifiers.modifiers)
)
else: else:
want2delete.add(self._get_modifier(context).pl_id) want2delete.add(self._get_modifier(context).pl_id)
@ -254,9 +268,9 @@ class ModifierResetOperator(ModifierOperator, bpy.types.Operator):
bl_label = "Reset the modifier(s) to the default state?" bl_label = "Reset the modifier(s) to the default state?"
bl_description = "Reset the modifier(s) to the default state" bl_description = "Reset the modifier(s) to the default state"
active_modifier = IntProperty(name="Modifier Display Order", active_modifier = IntProperty(
default=-1, name="Modifier Display Order", default=-1, options={"HIDDEN"}
options={"HIDDEN"}) )
def draw(self, context): def draw(self, context):
pass pass
@ -297,15 +311,17 @@ class ModifierMoveUpOperator(ModifierMoveOperator, bpy.types.Operator):
bl_label = "Move Up" bl_label = "Move Up"
bl_description = "Move the modifier up in the stack" bl_description = "Move the modifier up in the stack"
active_modifier = IntProperty(name="Modifier Display Order", active_modifier = IntProperty(
default=-1, name="Modifier Display Order", default=-1, options={"HIDDEN"}
options={"HIDDEN"}) )
def execute(self, context): def execute(self, context):
assert self.active_modifier >= 0 assert self.active_modifier >= 0
if self.active_modifier > 0: if self.active_modifier > 0:
plmods = context.object.plasma_modifiers plmods = context.object.plasma_modifiers
self.swap_modifier_ids(plmods, self.active_modifier, self.active_modifier-1) self.swap_modifier_ids(
plmods, self.active_modifier, self.active_modifier - 1
)
return {"FINISHED"} return {"FINISHED"}
@ -314,9 +330,9 @@ class ModifierMoveDownOperator(ModifierMoveOperator, bpy.types.Operator):
bl_label = "Move Down" bl_label = "Move Down"
bl_description = "Move the modifier down in the stack" bl_description = "Move the modifier down in the stack"
active_modifier = IntProperty(name="Modifier Display Order", active_modifier = IntProperty(
default=-1, name="Modifier Display Order", default=-1, options={"HIDDEN"}
options={"HIDDEN"}) )
def execute(self, context): def execute(self, context):
assert self.active_modifier >= 0 assert self.active_modifier >= 0
@ -324,7 +340,9 @@ class ModifierMoveDownOperator(ModifierMoveOperator, bpy.types.Operator):
plmods = context.object.plasma_modifiers plmods = context.object.plasma_modifiers
last = max([mod.display_order for mod in plmods.modifiers]) last = max([mod.display_order for mod in plmods.modifiers])
if self.active_modifier < last: if self.active_modifier < last:
self.swap_modifier_ids(plmods, self.active_modifier, self.active_modifier+1) self.swap_modifier_ids(
plmods, self.active_modifier, self.active_modifier + 1
)
return {"FINISHED"} return {"FINISHED"}
@ -348,5 +366,5 @@ class ModifierLogicWizOperator(ModifierOperator, bpy.types.Operator):
start = time.process_time() start = time.process_time()
mod.create_logic(obj) mod.create_logic(obj)
end = time.process_time() end = time.process_time()
print("\nLogicWiz finished in {:.2f} seconds".format(end-start)) print("\nLogicWiz finished in {:.2f} seconds".format(end - start))
return {"FINISHED"} return {"FINISHED"}

134
korman/operators/op_nodes.py

@ -18,6 +18,7 @@ from bpy.props import *
import itertools import itertools
import pickle import pickle
class NodeOperator: class NodeOperator:
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@ -40,7 +41,9 @@ class CreateLinkNodeOperator(NodeOperator, bpy.types.Operator):
_hack = [] _hack = []
def _link_search_list(self, context): def _link_search_list(self, context):
CreateLinkNodeOperator._hack = list(CreateLinkNodeOperator._link_search_list_imp(self, context)) CreateLinkNodeOperator._hack = list(
CreateLinkNodeOperator._link_search_list_imp(self, context)
)
return CreateLinkNodeOperator._hack return CreateLinkNodeOperator._hack
def _link_search_list_imp(self, context): def _link_search_list_imp(self, context):
@ -50,14 +53,18 @@ class CreateLinkNodeOperator(NodeOperator, bpy.types.Operator):
src_node = tree.nodes[self.node_name] src_node = tree.nodes[self.node_name]
src_socket = CreateLinkNodeOperator._find_source_socket(self, src_node) src_socket = CreateLinkNodeOperator._find_source_socket(self, src_node)
links = list(src_node.generate_valid_links_for(context, src_socket, self.is_output)) links = list(
src_node.generate_valid_links_for(context, src_socket, self.is_output)
)
max_node = max((len(i["node_text"]) for i in links)) if links else 0 max_node = max((len(i["node_text"]) for i in links)) if links else 0
for i, link in enumerate(links): for i, link in enumerate(links):
# Pickle protocol 0 uses only ASCII bytes, so we can pretend it's a string easily... # Pickle protocol 0 uses only ASCII bytes, so we can pretend it's a string easily...
id_string = pickle.dumps(link, protocol=0).decode() id_string = pickle.dumps(link, protocol=0).decode()
desc_string = "{node}:{node_sock_space}{sock}".format(node=link["node_text"], desc_string = "{node}:{node_sock_space}{sock}".format(
node=link["node_text"],
node_sock_space=(" " * (max_node - len(link["node_text"]) + 4)), node_sock_space=(" " * (max_node - len(link["node_text"]) + 4)),
sock=link["socket_text"]) sock=link["socket_text"],
)
yield (id_string, desc_string, "", i) yield (id_string, desc_string, "", i)
node_item = EnumProperty(items=_link_search_list) node_item = EnumProperty(items=_link_search_list)
@ -101,7 +108,11 @@ class CreateLinkNodeOperator(NodeOperator, bpy.types.Operator):
src_node = tree.nodes[self.node_name] src_node = tree.nodes[self.node_name]
src_socket = self._find_source_socket(src_node) src_socket = self._find_source_socket(src_node)
# We need to use Korman's functions because they may generate a node socket. # We need to use Korman's functions because they may generate a node socket.
find_socket = dest_node.find_input_socket if self.is_output else dest_node.find_output_socket find_socket = (
dest_node.find_input_socket
if self.is_output
else dest_node.find_output_socket
)
dest_socket = find_socket(link["socket_name"], True) dest_socket = find_socket(link["socket_name"], True)
if self.is_output: if self.is_output:
@ -113,7 +124,9 @@ class CreateLinkNodeOperator(NodeOperator, bpy.types.Operator):
def modal(self, context, event): def modal(self, context, event):
# Ugh. The Blender API sucks so much. We can only get the cursor pos from here??? # Ugh. The Blender API sucks so much. We can only get the cursor pos from here???
context.space_data.cursor_location_from_region(event.mouse_region_x, event.mouse_region_y) context.space_data.cursor_location_from_region(
event.mouse_region_x, event.mouse_region_y
)
if len(self._hack) == 1: if len(self._hack) == 1:
self._create_link_node(context, self._hack[0][0]) self._create_link_node(context, self._hack[0][0])
self._hack.clear() self._hack.clear()
@ -132,9 +145,12 @@ class CreateLinkNodeOperator(NodeOperator, bpy.types.Operator):
def poll(cls, context): def poll(cls, context):
space = context.space_data space = context.space_data
# needs active node editor and a tree to add nodes to # needs active node editor and a tree to add nodes to
return (space.type == 'NODE_EDITOR' and return (
space.edit_tree and not space.edit_tree.library and space.type == "NODE_EDITOR"
context.scene.render.engine == "PLASMA_GAME") and space.edit_tree
and not space.edit_tree.library
and context.scene.render.engine == "PLASMA_GAME"
)
class SelectFileOperator(NodeOperator, bpy.types.Operator): class SelectFileOperator(NodeOperator, bpy.types.Operator):
@ -147,14 +163,23 @@ class SelectFileOperator(NodeOperator, bpy.types.Operator):
filename = StringProperty(options={"HIDDEN"}) filename = StringProperty(options={"HIDDEN"})
data_path = StringProperty(options={"HIDDEN"}) data_path = StringProperty(options={"HIDDEN"})
filepath_property = StringProperty(description="Name of property to store filepath in", options={"HIDDEN"}) filepath_property = StringProperty(
filename_property = StringProperty(description="Name of property to store filename in", options={"HIDDEN"}) description="Name of property to store filepath in", options={"HIDDEN"}
)
filename_property = StringProperty(
description="Name of property to store filename in", options={"HIDDEN"}
)
def execute(self, context): def execute(self, context):
if bpy.data.texts.get(self.filename, None) is None: if bpy.data.texts.get(self.filename, None) is None:
bpy.data.texts.load(self.filepath) bpy.data.texts.load(self.filepath)
else: else:
self.report({"WARNING"}, "A file named '{}' is already loaded. It will be used.".format(self.filename)) self.report(
{"WARNING"},
"A file named '{}' is already loaded. It will be used.".format(
self.filename
),
)
dest = eval(self.data_path) dest = eval(self.data_path)
if self.filepath_property: if self.filepath_property:
@ -167,46 +192,28 @@ class SelectFileOperator(NodeOperator, bpy.types.Operator):
context.window_manager.fileselect_add(self) context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"} return {"RUNNING_MODAL"}
pyAttribArgMap= {
"ptAttribute": pyAttribArgMap = {
["vislistid", "visliststates"], "ptAttribute": ["vislistid", "visliststates"],
"ptAttribBoolean": "ptAttribBoolean": ["default"],
["default"], "ptAttribInt": ["default", "rang"],
"ptAttribInt": "ptAttribFloat": ["default", "rang"],
["default", "rang"], "ptAttribString": ["default"],
"ptAttribFloat": "ptAttribDropDownList": ["options"],
["default", "rang"], "ptAttribSceneobject": ["netForce"],
"ptAttribString": "ptAttribSceneobjectList": ["byObject", "netForce"],
["default"], "ptAttributeKeyList": ["byObject", "netForce"],
"ptAttribDropDownList": "ptAttribActivator": ["byObject", "netForce"],
["options"], "ptAttribActivatorList": ["byObject", "netForce"],
"ptAttribSceneobject": "ptAttribResponder": ["stateList", "byObject", "netForce", "netPropagate"],
["netForce"], "ptAttribResponderList": ["stateList", "byObject", "netForce", "netPropagate"],
"ptAttribSceneobjectList": "ptAttribNamedActivator": ["byObject", "netForce"],
["byObject", "netForce"], "ptAttribNamedResponder": ["stateList", "byObject", "netForce", "netPropagate"],
"ptAttributeKeyList": "ptAttribDynamicMap": ["netForce"],
["byObject", "netForce"], "ptAttribAnimation": ["byObject", "netForce"],
"ptAttribActivator": "ptAttribBehavior": ["netForce", "netProp"],
["byObject", "netForce"], "ptAttribMaterialList": ["byObject", "netForce"],
"ptAttribActivatorList": }
["byObject", "netForce"],
"ptAttribResponder":
["stateList", "byObject", "netForce", "netPropagate"],
"ptAttribResponderList":
["stateList", "byObject", "netForce", "netPropagate"],
"ptAttribNamedActivator":
["byObject", "netForce"],
"ptAttribNamedResponder":
["stateList", "byObject", "netForce", "netPropagate"],
"ptAttribDynamicMap":
["netForce"],
"ptAttribAnimation":
["byObject", "netForce"],
"ptAttribBehavior":
["netForce", "netProp"],
"ptAttribMaterialList":
["byObject", "netForce"],
}
class PlPyAttributeNodeOperator(NodeOperator, bpy.types.Operator): class PlPyAttributeNodeOperator(NodeOperator, bpy.types.Operator):
@ -220,6 +227,7 @@ class PlPyAttributeNodeOperator(NodeOperator, bpy.types.Operator):
def execute(self, context): def execute(self, context):
from ..plasma_attributes import get_attributes_from_str from ..plasma_attributes import get_attributes_from_str
text_id = bpy.data.texts[self.text_path] text_id = bpy.data.texts[self.text_path]
attribs = get_attributes_from_str(text_id.as_string()) attribs = get_attributes_from_str(text_id.as_string())
@ -250,12 +258,26 @@ class PlPyAttributeNodeOperator(NodeOperator, bpy.types.Operator):
# Load our default argument mapping # Load our default argument mapping
if args is not None: if args is not None:
if cached.attribute_type in pyAttribArgMap.keys(): if cached.attribute_type in pyAttribArgMap.keys():
argmap.update(dict(zip(pyAttribArgMap[cached.attribute_type], args))) argmap.update(
dict(zip(pyAttribArgMap[cached.attribute_type], args))
)
else: else:
print("Found ptAttribute type '{}' with unknown arguments: {}".format(cached.attribute_type, args)) print(
"Found ptAttribute type '{}' with unknown arguments: {}".format(
cached.attribute_type, args
)
)
# Add in/set any arguments provided by keyword # Add in/set any arguments provided by keyword
if cached.attribute_type in pyAttribArgMap.keys() and not set(pyAttribArgMap[cached.attribute_type]).isdisjoint(attrib.keys()): if cached.attribute_type in pyAttribArgMap.keys() and not set(
argmap.update({key: attrib[key] for key in attrib if key in pyAttribArgMap[cached.attribute_type]}) pyAttribArgMap[cached.attribute_type]
).isdisjoint(attrib.keys()):
argmap.update(
{
key: attrib[key]
for key in attrib
if key in pyAttribArgMap[cached.attribute_type]
}
)
# Attach the arguments to the attribute # Attach the arguments to the attribute
if argmap: if argmap:
cached.attribute_arguments.set_arguments(argmap) cached.attribute_arguments.set_arguments(argmap)

20
korman/operators/op_sound.py

@ -16,6 +16,7 @@
import bpy import bpy
from bpy.props import * from bpy.props import *
class SoundOperator: class SoundOperator:
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@ -69,13 +70,18 @@ class PlasmaSoundUnpackOperator(SoundOperator, bpy.types.Operator):
bl_label = "Unpack" bl_label = "Unpack"
bl_options = {"INTERNAL"} bl_options = {"INTERNAL"}
method = EnumProperty(name="Method", description="How to unpack", method = EnumProperty(
# See blender/makesrna/intern/rna_packedfile.c name="Method",
items=[("USE_LOCAL", "Use local file", "", 5), description="How to unpack",
("WRITE_LOCAL", "Write Local File (overwrite existing)", "", 4), # See blender/makesrna/intern/rna_packedfile.c
("USE_ORIGINAL", "Use Original File", "", 6), items=[
("WRITE_ORIGINAL", "Write Original File (overwrite existing)", "", 3)], ("USE_LOCAL", "Use local file", "", 5),
options=set()) ("WRITE_LOCAL", "Write Local File (overwrite existing)", "", 4),
("USE_ORIGINAL", "Use Original File", "", 6),
("WRITE_ORIGINAL", "Write Original File (overwrite existing)", "", 3),
],
options=set(),
)
def execute(self, context): def execute(self, context):
soundemit = context.active_object.plasma_modifiers.soundemit soundemit = context.active_object.plasma_modifiers.soundemit

47
korman/operators/op_toolbox.py

@ -18,6 +18,7 @@ from bpy.props import *
import pickle import pickle
import itertools import itertools
class ToolboxOperator: class ToolboxOperator:
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@ -40,7 +41,9 @@ class PageSearchOperator(ToolboxOperator):
pages = [(pickle.dumps(i.name, 0).decode(), i.name, "") for i in page_defns] pages = [(pickle.dumps(i.name, 0).decode(), i.name, "") for i in page_defns]
# Ensure an entry exists for the default page # Ensure an entry exists for the default page
manual_default_page = next((i.name for i in page_defns if i.seq_suffix == 0), None) manual_default_page = next(
(i.name for i in page_defns if i.seq_suffix == 0), None
)
if not manual_default_page: if not manual_default_page:
pages.append((pickle.dumps("", 0).decode(), "Default", "Default Page")) pages.append((pickle.dumps("", 0).decode(), "Default", "Default Page"))
@ -87,7 +90,7 @@ class PlasmaConvertPlasmaObjectOperator(ToolboxOperator, bpy.types.Operator):
# is either inserted into a valid page using the old-style text properties or is lacking # is either inserted into a valid page using the old-style text properties or is lacking
# a page property. Unfortunately, unless we start bundling some YAML interpreter, we cannot # a page property. Unfortunately, unless we start bundling some YAML interpreter, we cannot
# use the old AlcScript schtuff. # use the old AlcScript schtuff.
pages = { i.seq_suffix: i.name for i in context.scene.world.plasma_age.pages } pages = {i.seq_suffix: i.name for i in context.scene.world.plasma_age.pages}
for i in bpy.data.objects: for i in bpy.data.objects:
pageid = i.game.properties.get("page_num", None) pageid = i.game.properties.get("page_num", None)
if pageid is None: if pageid is None:
@ -99,7 +102,11 @@ class PlasmaConvertPlasmaObjectOperator(ToolboxOperator, bpy.types.Operator):
# a common hack to prevent exporting in PyPRP was to set page_num == -1, # a common hack to prevent exporting in PyPRP was to set page_num == -1,
# so don't warn about that. # so don't warn about that.
if pageid.value != -1: if pageid.value != -1:
print("Object '{}' in page_num '{}', which is not available :/".format(i.name, pageid.value)) print(
"Object '{}' in page_num '{}', which is not available :/".format(
i.name, pageid.value
)
)
else: else:
i.plasma_object.enabled = True i.plasma_object.enabled = True
i.plasma_object.page = page_name i.plasma_object.page = page_name
@ -130,10 +137,12 @@ class PlasmaMovePageObjectsOperator(PageSearchOperator, bpy.types.Operator):
bl_description = "Moves all selected objects to a new page" bl_description = "Moves all selected objects to a new page"
bl_property = "page" bl_property = "page"
page = EnumProperty(name="Page", page = EnumProperty(
description= "Page whose objects should be selected", name="Page",
items=PageSearchOperator._get_pages, description="Page whose objects should be selected",
options=set()) items=PageSearchOperator._get_pages,
options=set(),
)
def execute(self, context): def execute(self, context):
desired_page = self.desired_page desired_page = self.desired_page
@ -148,10 +157,12 @@ class PlasmaSelectPageObjectsOperator(PageSearchOperator, bpy.types.Operator):
bl_description = "Selects all objects in a specific page" bl_description = "Selects all objects in a specific page"
bl_property = "page" bl_property = "page"
page = EnumProperty(name="Page", page = EnumProperty(
description= "Page whose objects should be selected", name="Page",
items=PageSearchOperator._get_pages, description="Page whose objects should be selected",
options=set()) items=PageSearchOperator._get_pages,
options=set(),
)
def execute(self, context): def execute(self, context):
desired_page = self.desired_page desired_page = self.desired_page
@ -232,7 +243,9 @@ class PlasmaTogglePlasmaObjectsOperator(ToolboxOperator, bpy.types.Operator):
return super().poll(context) and hasattr(bpy.context, "selected_objects") return super().poll(context) and hasattr(bpy.context, "selected_objects")
def execute(self, context): def execute(self, context):
enable = not all((i.plasma_object.enabled for i in bpy.context.selected_objects)) enable = not all(
(i.plasma_object.enabled for i in bpy.context.selected_objects)
)
for i in context.selected_objects: for i in context.selected_objects:
i.plasma_object.enabled = enable i.plasma_object.enabled = enable
return {"FINISHED"} return {"FINISHED"}
@ -265,7 +278,15 @@ class PlasmaToggleSoundExportSelectedOperator(ToolboxOperator, bpy.types.Operato
return super().poll(context) and hasattr(bpy.context, "selected_objects") return super().poll(context) and hasattr(bpy.context, "selected_objects")
def execute(self, context): def execute(self, context):
enable = not all((i.package for i in itertools.chain.from_iterable(i.plasma_modifiers.soundemit.sounds for i in bpy.context.selected_objects))) enable = not all(
(
i.package
for i in itertools.chain.from_iterable(
i.plasma_modifiers.soundemit.sounds
for i in bpy.context.selected_objects
)
)
)
for i in context.selected_objects: for i in context.selected_objects:
if i.plasma_modifiers.soundemit is None: if i.plasma_modifiers.soundemit is None:
continue continue

89
korman/operators/op_ui.py

@ -17,6 +17,7 @@ import addon_utils
import bpy import bpy
from bpy.props import * from bpy.props import *
class UIOperator: class UIOperator:
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@ -28,25 +29,37 @@ class CollectionAddOperator(UIOperator, bpy.types.Operator):
bl_label = "Add Item" bl_label = "Add Item"
bl_description = "Adds an item to the collection" bl_description = "Adds an item to the collection"
context = StringProperty(name="ID Path", context = StringProperty(
description="Path to the relevant datablock from the current context", name="ID Path",
options=set()) description="Path to the relevant datablock from the current context",
group_path = StringProperty(name="Property Group Path", options=set(),
description="Path to the property group from the ID", )
options=set()) group_path = StringProperty(
collection_prop = StringProperty(name="Collection Property", name="Property Group Path",
description="Name of the collection property", description="Path to the property group from the ID",
options=set()) options=set(),
index_prop = StringProperty(name="Index Property", )
description="Name of the active element index property", collection_prop = StringProperty(
options=set()) name="Collection Property",
name_prefix = StringProperty(name="Name Prefix", description="Name of the collection property",
description="Prefix for autogenerated item names", options=set(),
default="Item", )
options=set()) index_prop = StringProperty(
name_prop = StringProperty(name="Name Property", name="Index Property",
description="Attribute name of the item name property", description="Name of the active element index property",
options=set()) options=set(),
)
name_prefix = StringProperty(
name="Name Prefix",
description="Prefix for autogenerated item names",
default="Item",
options=set(),
)
name_prop = StringProperty(
name="Name Property",
description="Attribute name of the item name property",
options=set(),
)
def execute(self, context): def execute(self, context):
props = getattr(context, self.context).path_resolve(self.group_path) props = getattr(context, self.context).path_resolve(self.group_path)
@ -54,7 +67,11 @@ class CollectionAddOperator(UIOperator, bpy.types.Operator):
idx = len(collection) idx = len(collection)
collection.add() collection.add()
if self.name_prop: if self.name_prop:
setattr(collection[idx], self.name_prop, "{} {}".format(self.name_prefix, idx+1)) setattr(
collection[idx],
self.name_prop,
"{} {}".format(self.name_prefix, idx + 1),
)
if self.index_prop: if self.index_prop:
setattr(props, self.index_prop, idx) setattr(props, self.index_prop, idx)
return {"FINISHED"} return {"FINISHED"}
@ -65,18 +82,26 @@ class CollectionRemoveOperator(UIOperator, bpy.types.Operator):
bl_label = "Remove Item" bl_label = "Remove Item"
bl_description = "Removes an item from the collection" bl_description = "Removes an item from the collection"
context = StringProperty(name="ID Path", context = StringProperty(
description="Path to the relevant datablock from the current context", name="ID Path",
options=set()) description="Path to the relevant datablock from the current context",
group_path = StringProperty(name="Property Group Path", options=set(),
description="Path to the property group from the ID", )
options=set()) group_path = StringProperty(
collection_prop = StringProperty(name="Collection Property", name="Property Group Path",
description="Name of the collection property", description="Path to the property group from the ID",
options=set()) options=set(),
index_prop = StringProperty(name="Index Property", )
description="Name of the active element index property", collection_prop = StringProperty(
options=set()) name="Collection Property",
description="Name of the collection property",
options=set(),
)
index_prop = StringProperty(
name="Index Property",
description="Name of the active element index property",
options=set(),
)
def execute(self, context): def execute(self, context):
props = getattr(context, self.context).path_resolve(self.group_path) props = getattr(context, self.context).path_resolve(self.group_path)

6
korman/operators/op_world.py

@ -17,6 +17,7 @@ import bpy
from bpy.props import * from bpy.props import *
from pathlib import Path from pathlib import Path
class AgeOperator: class AgeOperator:
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@ -40,7 +41,9 @@ class GameAddOperator(AgeOperator, bpy.types.Operator):
# Blendsucks likes to tack filenames onto our doggone directories... # Blendsucks likes to tack filenames onto our doggone directories...
if not path.is_dir(): if not path.is_dir():
path = path.parent path = path.parent
if not ((path / "UruExplorer.exe").is_file() or (path / "plClient.exe").is_file()): if not (
(path / "UruExplorer.exe").is_file() or (path / "plClient.exe").is_file()
):
self.report({"ERROR"}, "The selected directory is not a copy of URU.") self.report({"ERROR"}, "The selected directory is not a copy of URU.")
return {"CANCELLED"} return {"CANCELLED"}
@ -62,7 +65,6 @@ class GameAddOperator(AgeOperator, bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
def invoke(self, context, event): def invoke(self, context, event):
context.window_manager.fileselect_add(self) context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"} return {"RUNNING_MODAL"}

26
korman/ordered_set.py

@ -19,7 +19,7 @@ Updated for Python 3.5 by Adam Johnson
import collections.abc import collections.abc
SLICE_ALL = slice(None) SLICE_ALL = slice(None)
__version__ = '2.0.1' __version__ = "2.0.1"
def is_iterable(obj): def is_iterable(obj):
@ -35,7 +35,11 @@ def is_iterable(obj):
We don't need to check for the Python 2 `unicode` type, because it doesn't We don't need to check for the Python 2 `unicode` type, because it doesn't
have an `__iter__` attribute anyway. have an `__iter__` attribute anyway.
""" """
return hasattr(obj, '__iter__') and not isinstance(obj, str) and not isinstance(obj, tuple) return (
hasattr(obj, "__iter__")
and not isinstance(obj, str)
and not isinstance(obj, tuple)
)
class OrderedSet(collections.abc.MutableSet): class OrderedSet(collections.abc.MutableSet):
@ -43,6 +47,7 @@ class OrderedSet(collections.abc.MutableSet):
An OrderedSet is a custom MutableSet that remembers its order, so that An OrderedSet is a custom MutableSet that remembers its order, so that
every entry has an index that can be looked up. every entry has an index that can be looked up.
""" """
def __init__(self, iterable=None): def __init__(self, iterable=None):
self.items = [] self.items = []
self.map = {} self.map = {}
@ -66,7 +71,7 @@ class OrderedSet(collections.abc.MutableSet):
""" """
if index == SLICE_ALL: if index == SLICE_ALL:
return self return self
elif hasattr(index, '__index__') or isinstance(index, slice): elif hasattr(index, "__index__") or isinstance(index, slice):
result = self.items[index] result = self.items[index]
if isinstance(result, list): if isinstance(result, list):
return OrderedSet(result) return OrderedSet(result)
@ -75,8 +80,7 @@ class OrderedSet(collections.abc.MutableSet):
elif is_iterable(index): elif is_iterable(index):
return OrderedSet([self.items[i] for i in index]) return OrderedSet([self.items[i] for i in index])
else: else:
raise TypeError("Don't know how to index an OrderedSet by %r" % raise TypeError("Don't know how to index an OrderedSet by %r" % index)
index)
def copy(self): def copy(self):
return OrderedSet(self) return OrderedSet(self)
@ -113,6 +117,7 @@ class OrderedSet(collections.abc.MutableSet):
self.map[key] = len(self.items) self.map[key] = len(self.items)
self.items.append(key) self.items.append(key)
return self.map[key] return self.map[key]
append = add append = add
def update(self, sequence): def update(self, sequence):
@ -125,7 +130,9 @@ class OrderedSet(collections.abc.MutableSet):
for item in sequence: for item in sequence:
item_index = self.add(item) item_index = self.add(item)
except TypeError: except TypeError:
raise ValueError('Argument needs to be an iterable, got %s' % type(sequence)) raise ValueError(
"Argument needs to be an iterable, got %s" % type(sequence)
)
return item_index return item_index
def index(self, key): def index(self, key):
@ -147,7 +154,7 @@ class OrderedSet(collections.abc.MutableSet):
Raises KeyError if the set is empty. Raises KeyError if the set is empty.
""" """
if not self.items: if not self.items:
raise KeyError('Set is empty') raise KeyError("Set is empty")
elem = self.items[-1] elem = self.items[-1]
del self.items[-1] del self.items[-1]
@ -184,8 +191,8 @@ class OrderedSet(collections.abc.MutableSet):
def __repr__(self): def __repr__(self):
if not self: if not self:
return '%s()' % (self.__class__.__name__,) return "%s()" % (self.__class__.__name__,)
return '%s(%r)' % (self.__class__.__name__, list(self)) return "%s(%r)" % (self.__class__.__name__, list(self))
def __eq__(self, other): def __eq__(self, other):
if isinstance(other, OrderedSet): if isinstance(other, OrderedSet):
@ -197,4 +204,3 @@ class OrderedSet(collections.abc.MutableSet):
return False return False
else: else:
return set(self) == other_as_set return set(self) == other_as_set

22
korman/plasma_attributes.py

@ -37,10 +37,12 @@ class PlasmaAttributeVisitor(ast.NodeVisitor):
# - assignments with targets # - assignments with targets
# - that are taking a function call (the ptAttrib Constructor) # - that are taking a function call (the ptAttrib Constructor)
# - whose name starts with ptAttrib # - whose name starts with ptAttrib
if (len(assign.targets) == 1 if (
len(assign.targets) == 1
and isinstance(assign.value, ast.Call) and isinstance(assign.value, ast.Call)
and hasattr(assign.value.func, "id") and hasattr(assign.value.func, "id")
and assign.value.func.id.startswith("ptAttrib")): and assign.value.func.id.startswith("ptAttrib")
):
# Start pulling apart that delicious information # Start pulling apart that delicious information
ptVar = assign.targets[0].id ptVar = assign.targets[0].id
ptType = assign.value.func.id ptType = assign.value.func.id
@ -53,7 +55,11 @@ class PlasmaAttributeVisitor(ast.NodeVisitor):
# which only have an index. We don't want those. # which only have an index. We don't want those.
if len(ptArgs) > 1: if len(ptArgs) > 1:
# Add the common arguments as named items. # Add the common arguments as named items.
self._attributes[ptArgs[0]] = {"name": ptVar, "type": ptType, "desc": ptArgs[1]} self._attributes[ptArgs[0]] = {
"name": ptVar,
"type": ptType,
"desc": ptArgs[1],
}
# Add the class-specific arguments under the 'args' item. # Add the class-specific arguments under the 'args' item.
if ptArgs[2:]: if ptArgs[2:]:
self._attributes[ptArgs[0]]["args"] = ptArgs[2:] self._attributes[ptArgs[0]]["args"] = ptArgs[2:]
@ -61,13 +67,15 @@ class PlasmaAttributeVisitor(ast.NodeVisitor):
# Add the keyword arguments, if any. # Add the keyword arguments, if any.
if assign.value.keywords: if assign.value.keywords:
for keyword in assign.value.keywords: for keyword in assign.value.keywords:
self._attributes[ptArgs[0]][keyword.arg] = self.visit(keyword.value) self._attributes[ptArgs[0]][keyword.arg] = self.visit(
keyword.value
)
return self.generic_visit(node) return self.generic_visit(node)
def visit_Name(self, node): def visit_Name(self, node):
# Workaround for old Cyan scripts: replace variables named "true" or "false" # Workaround for old Cyan scripts: replace variables named "true" or "false"
# with the respective constant values True or False. # with the respective constant values True or False.
if node.id.lower() in {"true", "false"}: if node.id.lower() in {"true", "false"}:
return ast.literal_eval(node.id.capitalize()) return ast.literal_eval(node.id.capitalize())
return node.id return node.id
@ -104,10 +112,11 @@ class PlasmaAttributeVisitor(ast.NodeVisitor):
def get_attributes_from_file(filepath): def get_attributes_from_file(filepath):
"""Scan the file for assignments matching our regex, let our visitor parse them, and return the """Scan the file for assignments matching our regex, let our visitor parse them, and return the
file's ptAttribs, if any.""" file's ptAttribs, if any."""
with open(str(filepath)) as script: with open(str(filepath)) as script:
return get_attributes_from_str(script.read()) return get_attributes_from_str(script.read())
def get_attributes_from_str(code): def get_attributes_from_str(code):
results = funcregex.findall(code) results = funcregex.findall(code)
if results: if results:
@ -119,6 +128,7 @@ def get_attributes_from_str(code):
return v._attributes return v._attributes
return {} return {}
if __name__ == "__main__": if __name__ == "__main__":
import json import json
from pathlib import Path from pathlib import Path

68
korman/plasma_launcher.py

@ -26,7 +26,10 @@ main_parser = argparse.ArgumentParser(description="Korman Plasma Launcher")
main_parser.add_argument("cwd", type=Path, help="Working directory of the client") main_parser.add_argument("cwd", type=Path, help="Working directory of the client")
main_parser.add_argument("age", type=str, help="Name of the age to launch into") main_parser.add_argument("age", type=str, help="Name of the age to launch into")
sub_parsers = main_parser.add_subparsers(title="Plasma Version", dest="version",) sub_parsers = main_parser.add_subparsers(
title="Plasma Version",
dest="version",
)
moul_parser = sub_parsers.add_parser("pvMoul") moul_parser = sub_parsers.add_parser("pvMoul")
moul_parser.add_argument("ki", type=int, help="KI Number of the desired player") moul_parser.add_argument("ki", type=int, help="KI Number of the desired player")
moul_parser.add_argument("--serverini", type=str, default="server.ini") moul_parser.add_argument("--serverini", type=str, default="server.ini")
@ -36,15 +39,10 @@ sp_parser.add_argument("player", type=str, help="Name of the desired player")
autolink_chron_name = "OfflineKIAutoLink" autolink_chron_name = "OfflineKIAutoLink"
if sys.platform == "win32": if sys.platform == "win32":
client_executables = { client_executables = {"pvMoul": "plClient.exe", "pvPots": "UruExplorer.exe"}
"pvMoul": "plClient.exe",
"pvPots": "UruExplorer.exe"
}
else: else:
client_executables = { client_executables = {"pvMoul": "plClient", "pvPots": "UruExplorer"}
"pvMoul": "plClient",
"pvPots": "UruExplorer"
}
def die(*args, **kwargs): def die(*args, **kwargs):
assert args assert args
@ -55,6 +53,7 @@ def die(*args, **kwargs):
sys.stdout.write("DIE\n") sys.stdout.write("DIE\n")
sys.exit(1) sys.exit(1)
def write(*args, **kwargs): def write(*args, **kwargs):
assert args assert args
if len(args) == 1 and not kwargs: if len(args) == 1 and not kwargs:
@ -65,18 +64,32 @@ def write(*args, **kwargs):
# And this is why we aren't using print()... # And this is why we aren't using print()...
sys.stdout.flush() sys.stdout.flush()
def backup_vault_dat(path): def backup_vault_dat(path):
backup_path = path.with_suffix(".dat.korman_backup") backup_path = path.with_suffix(".dat.korman_backup")
shutil.copy2(str(path), str(backup_path)) shutil.copy2(str(path), str(backup_path))
write("DBG: Copied vault backup: {}", backup_path) write("DBG: Copied vault backup: {}", backup_path)
def set_link_chronicle(store, new_value, cond_value=None): def set_link_chronicle(store, new_value, cond_value=None):
chron_folder = next((i for i in store.getChildren(store.firstNodeID) chron_folder = next(
if getattr(i, "folderType", None) == plVault.kChronicleFolder), None) (
i
for i in store.getChildren(store.firstNodeID)
if getattr(i, "folderType", None) == plVault.kChronicleFolder
),
None,
)
if chron_folder is None: if chron_folder is None:
die("Could not locate vault chronicle folder.") die("Could not locate vault chronicle folder.")
autolink_chron = next((i for i in store.getChildren(chron_folder.nodeID) autolink_chron = next(
if getattr(i, "entryName", None) == autolink_chron_name), None) (
i
for i in store.getChildren(chron_folder.nodeID)
if getattr(i, "entryName", None) == autolink_chron_name
),
None,
)
if autolink_chron is None: if autolink_chron is None:
write("DBG: Creating AutoLink chronicle...") write("DBG: Creating AutoLink chronicle...")
autolink_chron = plVaultChronicleNode() autolink_chron = plVaultChronicleNode()
@ -93,10 +106,15 @@ def set_link_chronicle(store, new_value, cond_value=None):
autolink_chron.entryValue = new_value autolink_chron.entryValue = new_value
store.addNode(autolink_chron) store.addNode(autolink_chron)
else: else:
write("DBG: ***Not*** changing chronicle! AutoLink = '{}' (expected: '{}')", previous_value, cond_value) write(
"DBG: ***Not*** changing chronicle! AutoLink = '{}' (expected: '{}')",
previous_value,
cond_value,
)
return previous_value return previous_value
def find_player_vault(cwd, name): def find_player_vault(cwd, name):
sav_dir = cwd.joinpath("sav") sav_dir = cwd.joinpath("sav")
if not sav_dir.is_dir(): if not sav_dir.is_dir():
@ -121,6 +139,7 @@ def find_player_vault(cwd, name):
return vault_dat, store return vault_dat, store
die("Could not locate the requested player vault.") die("Could not locate the requested player vault.")
def main(): def main():
print("DBG: alive") print("DBG: alive")
args = main_parser.parse_args() args = main_parser.parse_args()
@ -139,11 +158,13 @@ def main():
# Update init file for this schtuff... # Update init file for this schtuff...
init_path = args.cwd.joinpath("init", "net_age.fni") init_path = args.cwd.joinpath("init", "net_age.fni")
with plEncryptedStream().open(str(init_path), fmWrite, plEncryptedStream.kEncXtea) as ini: with plEncryptedStream().open(
str(init_path), fmWrite, plEncryptedStream.kEncXtea
) as ini:
ini.writeLine("# This file was automatically generated by Korman.") ini.writeLine("# This file was automatically generated by Korman.")
ini.writeLine("Nav.PageInHoldList GlobalAnimations") ini.writeLine("Nav.PageInHoldList GlobalAnimations")
ini.writeLine("Net.SetPlayer {}".format(vault_store.firstNodeID)) ini.writeLine("Net.SetPlayer {}".format(vault_store.firstNodeID))
ini.writeLine("Net.SetPlayerByName \"{}\"".format(args.player)) ini.writeLine('Net.SetPlayerByName "{}"'.format(args.player))
# BUT WHY??? You ask... # BUT WHY??? You ask...
# Because, sayeth Hoikas, if this command is not executed, you will remain ensconsed # Because, sayeth Hoikas, if this command is not executed, you will remain ensconsed
# in the black void of the Link... forever... Sadly, it accepts no arguments and determines # in the black void of the Link... forever... Sadly, it accepts no arguments and determines
@ -161,8 +182,14 @@ def main():
plasma_args = [str(executable), "-iinit", "To_Dni"] plasma_args = [str(executable), "-iinit", "To_Dni"]
else: else:
write("DBG: Using a superior client :) :) :)") write("DBG: Using a superior client :) :) :)")
plasma_args = [str(executable), "-LocalData", "-SkipLoginDialog", "-ServerIni={}".format(args.serverini), plasma_args = [
"-PlayerId={}".format(args.ki), "-Age={}".format(args.age)] str(executable),
"-LocalData",
"-SkipLoginDialog",
"-ServerIni={}".format(args.serverini),
"-PlayerId={}".format(args.ki),
"-Age={}".format(args.age),
]
try: try:
proc = subprocess.Popen(plasma_args, cwd=str(args.cwd), shell=True) proc = subprocess.Popen(plasma_args, cwd=str(args.cwd), shell=True)
@ -180,7 +207,9 @@ def main():
vault_store = plVaultStore() vault_store = plVaultStore()
vault_store.Import(str(vault_path)) vault_store.Import(str(vault_path))
new_prev_autolink = set_link_chronicle(vault_store, vault_prev_autolink, args.age) new_prev_autolink = set_link_chronicle(
vault_store, vault_prev_autolink, args.age
)
if new_prev_autolink != args.age: if new_prev_autolink != args.age:
write("DBG: ***Not*** resaving the vault!") write("DBG: ***Not*** resaving the vault!")
else: else:
@ -191,6 +220,7 @@ def main():
write("DONE") write("DONE")
sys.exit(0) sys.exit(0)
if __name__ == "__main__": if __name__ == "__main__":
try: try:
main() main()

20
korman/properties/modifiers/__init__.py

@ -26,6 +26,7 @@ from .render import *
from .sound import * from .sound import *
from .water import * from .water import *
class PlasmaModifiers(bpy.types.PropertyGroup): class PlasmaModifiers(bpy.types.PropertyGroup):
def determine_next_id(self): def determine_next_id(self):
"""Gets the ID for the next modifier in the UI""" """Gets the ID for the next modifier in the UI"""
@ -40,7 +41,7 @@ class PlasmaModifiers(bpy.types.PropertyGroup):
@property @property
def modifiers(self): def modifiers(self):
"""Generates all of the enabled modifiers. """Generates all of the enabled modifiers.
NOTE: We do not promise to return modifiers in their display_order! NOTE: We do not promise to return modifiers in their display_order!
""" """
for i in dir(self): for i in dir(self):
attr = getattr(self, i, None) attr = getattr(self, i, None)
@ -66,7 +67,7 @@ class PlasmaModifiers(bpy.types.PropertyGroup):
setattr(cls, i.pl_id, bpy.props.PointerProperty(type=i)) setattr(cls, i.pl_id, bpy.props.PointerProperty(type=i))
bpy.types.Object.plasma_modifiers = bpy.props.PointerProperty(type=cls) bpy.types.Object.plasma_modifiers = bpy.props.PointerProperty(type=cls)
def test_property(self, property : str) -> bool: def test_property(self, property: str) -> bool:
"""Tests a property on all enabled Plasma modifiers""" """Tests a property on all enabled Plasma modifiers"""
return any((getattr(i, property) for i in self.modifiers)) return any((getattr(i, property) for i in self.modifiers))
@ -79,17 +80,24 @@ def modifier_mapping():
"""This returns a dict mapping Plasma Modifier categories to names""" """This returns a dict mapping Plasma Modifier categories to names"""
d = {} d = {}
sorted_modifiers = sorted(PlasmaModifierProperties.__subclasses__(), key=lambda x: x.bl_label) sorted_modifiers = sorted(
PlasmaModifierProperties.__subclasses__(), key=lambda x: x.bl_label
)
for i, mod in enumerate(sorted_modifiers): for i, mod in enumerate(sorted_modifiers):
pl_id, category, label, description = mod.pl_id, mod.bl_category, mod.bl_label, mod.bl_description pl_id, category, label, description = (
mod.pl_id,
mod.bl_category,
mod.bl_label,
mod.bl_description,
)
icon = getattr(mod, "bl_icon", "") icon = getattr(mod, "bl_icon", "")
# The modifier might include the cateogry name in its name, so we'll strip that. # The modifier might include the cateogry name in its name, so we'll strip that.
if label != category: if label != category:
if label.startswith(category): if label.startswith(category):
label = label[len(category)+1:] label = label[len(category) + 1 :]
if label.endswith(category): if label.endswith(category):
label = label[:-len(category)-1] label = label[: -len(category) - 1]
tup = (pl_id, label, description, icon, i) tup = (pl_id, label, description, icon, i)
d_cat = d.setdefault(category, []) d_cat = d.setdefault(category, [])

155
korman/properties/modifiers/anim.py

@ -25,10 +25,12 @@ from ..prop_anim import PlasmaAnimationCollection
from ...exporter import ExportError, utils from ...exporter import ExportError, utils
from ... import idprops 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
return frame_num / fps return frame_num / fps
class ActionModifier: class ActionModifier:
@property @property
def blender_action(self): def blender_action(self):
@ -36,19 +38,30 @@ class ActionModifier:
if bo.animation_data is not None and bo.animation_data.action is not None: if bo.animation_data is not None and bo.animation_data.action is not None:
return bo.animation_data.action return bo.animation_data.action
if bo.data is not None: if bo.data is not None:
if bo.data.animation_data is not None and bo.data.animation_data.action is not None: if (
bo.data.animation_data is not None
and bo.data.animation_data.action is not None
):
# we will not use this action for any animation logic. that must be stored on the Object # we will not use this action for any animation logic. that must be stored on the Object
# datablock for simplicity's sake. # datablock for simplicity's sake.
return None return None
raise ExportError("'{}': Object has an animation modifier but is not animated".format(bo.name)) raise ExportError(
"'{}': Object has an animation modifier but is not animated".format(bo.name)
)
def sanity_check(self) -> None: def sanity_check(self) -> None:
if not self.id_data.plasma_object.has_animation_data: if not self.id_data.plasma_object.has_animation_data:
raise ExportError("'{}': Has an animation modifier but no animation data.", self.id_data.name) raise ExportError(
"'{}': Has an animation modifier but no animation data.",
self.id_data.name,
)
if self.id_data.type == "CAMERA": if self.id_data.type == "CAMERA":
if not self.id_data.data.plasma_camera.allow_animations: if not self.id_data.data.plasma_camera.allow_animations:
raise ExportError("'{}': Animation modifiers are not allowed on this camera type.", self.id_data.name) raise ExportError(
"'{}': Animation modifiers are not allowed on this camera type.",
self.id_data.name,
)
class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties): class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties):
@ -67,7 +80,9 @@ class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties):
so = exporter.mgr.find_create_object(plSceneObject, bl=bo) so = exporter.mgr.find_create_object(plSceneObject, bl=bo)
self.convert_object_animations(exporter, bo, so, self.subanimations) self.convert_object_animations(exporter, bo, so, self.subanimations)
def convert_object_animations(self, exporter, bo, so, anims: Optional[Iterable] = None): def convert_object_animations(
self, exporter, bo, so, anims: Optional[Iterable] = None
):
if not anims: if not anims:
anims = [self.subanimations.entire_animation] anims = [self.subanimations.entire_animation]
aganims = list(self._export_ag_anims(exporter, bo, so, anims)) aganims = list(self._export_ag_anims(exporter, bo, so, anims))
@ -98,16 +113,25 @@ class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties):
else: else:
start, end = None, None start, end = None, None
applicators = converter.convert_object_animations(bo, so, anim_name, start=start, end=end) applicators = converter.convert_object_animations(
bo, so, anim_name, start=start, end=end
)
if not applicators: if not applicators:
exporter.report.warn("Animation '{}' generated no applicators. Nothing will be exported.", exporter.report.warn(
anim_name, indent=2) "Animation '{}' generated no applicators. Nothing will be exported.",
anim_name,
indent=2,
)
continue continue
pClass = plAgeGlobalAnim if anim.sdl_var else plATCAnim pClass = plAgeGlobalAnim if anim.sdl_var else plATCAnim
aganim = exporter.mgr.find_create_object(pClass, bl=bo, so=so, name="{}_{}".format(bo.name, anim_name)) aganim = exporter.mgr.find_create_object(
pClass, bl=bo, so=so, name="{}_{}".format(bo.name, anim_name)
)
aganim.name = anim_name aganim.name = anim_name
aganim.start, aganim.end = converter.get_frame_time_range(*applicators, so=so) aganim.start, aganim.end = converter.get_frame_time_range(
*applicators, so=so
)
for i in applicators: for i in applicators:
aganim.addApplicator(i) aganim.addApplicator(i)
@ -119,18 +143,24 @@ class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties):
markers = action.pose_markers markers = action.pose_markers
initial_marker = markers.get(anim.initial_marker) initial_marker = markers.get(anim.initial_marker)
if initial_marker is not None: if initial_marker is not None:
aganim.initial = converter.convert_frame_time(initial_marker.frame) aganim.initial = converter.convert_frame_time(
initial_marker.frame
)
else: else:
aganim.initial = -1.0 aganim.initial = -1.0
if anim.loop: if anim.loop:
loop_start = markers.get(anim.loop_start) loop_start = markers.get(anim.loop_start)
if loop_start is not None: if loop_start is not None:
aganim.loopStart = converter.convert_frame_time(loop_start.frame) aganim.loopStart = converter.convert_frame_time(
loop_start.frame
)
else: else:
aganim.loopStart = aganim.start aganim.loopStart = aganim.start
loop_end = markers.get(anim.loop_end) loop_end = markers.get(anim.loop_end)
if loop_end is not None: if loop_end is not None:
aganim.loopEnd = converter.convert_frame_time(loop_end.frame) aganim.loopEnd = converter.convert_frame_time(
loop_end.frame
)
else: else:
aganim.loopEnd = aganim.end aganim.loopEnd = aganim.end
else: else:
@ -157,10 +187,12 @@ class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties):
class AnimGroupObject(idprops.IDPropObjectMixin, bpy.types.PropertyGroup): class AnimGroupObject(idprops.IDPropObjectMixin, bpy.types.PropertyGroup):
child_anim = PointerProperty(name="Child Animation", child_anim = PointerProperty(
description="Object whose action is a child animation", name="Child Animation",
type=bpy.types.Object, description="Object whose action is a child animation",
poll=idprops.poll_animated_objects) type=bpy.types.Object,
poll=idprops.poll_animated_objects,
)
@classmethod @classmethod
def _idprop_mapping(cls): def _idprop_mapping(cls):
@ -175,19 +207,25 @@ class PlasmaAnimationFilterModifier(PlasmaModifierProperties):
bl_description = "Filter animation components" bl_description = "Filter animation components"
bl_icon = "UNLINKED" bl_icon = "UNLINKED"
no_rotation = BoolProperty(name="Filter Rotation", no_rotation = BoolProperty(
description="Filter rotations", name="Filter Rotation", description="Filter rotations", options=set()
options=set()) )
no_transX = BoolProperty(name="Filter X Translation", no_transX = BoolProperty(
description="Filter the X component of translations", name="Filter X Translation",
options=set()) description="Filter the X component of translations",
no_transY = BoolProperty(name="Filter Y Translation", options=set(),
description="Filter the Y component of translations", )
options=set()) no_transY = BoolProperty(
no_transZ = BoolProperty(name="Filter Z Translation", name="Filter Y Translation",
description="Filter the Z component of translations", description="Filter the Y component of translations",
options=set()) options=set(),
)
no_transZ = BoolProperty(
name="Filter Z Translation",
description="Filter the Z component of translations",
options=set(),
)
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
# By this point, the object should already have a plFilterCoordInterface # By this point, the object should already have a plFilterCoordInterface
@ -219,9 +257,11 @@ class PlasmaAnimationGroupModifier(ActionModifier, PlasmaModifierProperties):
bl_description = "Defines related animations" bl_description = "Defines related animations"
bl_icon = "GROUP" bl_icon = "GROUP"
children = CollectionProperty(name="Child Animations", children = CollectionProperty(
description="Animations that will execute the same commands as this one", name="Child Animations",
type=AnimGroupObject) description="Animations that will execute the same commands as this one",
type=AnimGroupObject,
)
active_child_index = IntProperty(options={"HIDDEN"}) active_child_index = IntProperty(options={"HIDDEN"})
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
@ -229,7 +269,9 @@ class PlasmaAnimationGroupModifier(ActionModifier, PlasmaModifierProperties):
raise ExportError("'{}': Object is not animated".format(bo.name)) raise ExportError("'{}': Object is not animated".format(bo.name))
# The message forwarder is the guy that makes sure that everybody knows WTF is going on # The message forwarder is the guy that makes sure that everybody knows WTF is going on
msgfwd = exporter.mgr.find_create_object(plMsgForwarder, so=so, name=self.key_name) msgfwd = exporter.mgr.find_create_object(
plMsgForwarder, so=so, name=self.key_name
)
# Now, this is da swhiz... # Now, this is da swhiz...
agmod, agmaster = exporter.animation.get_anigraph_objects(bo, so) agmod, agmaster = exporter.animation.get_anigraph_objects(bo, so)
@ -250,7 +292,9 @@ class PlasmaAnimationGroupModifier(ActionModifier, PlasmaModifierProperties):
msg = "Animation Group '{}' specifies an object '{}' with no Plasma Animation modifier. Ignoring..." msg = "Animation Group '{}' specifies an object '{}' with no Plasma Animation modifier. Ignoring..."
exporter.report.warn(msg, self.key_name, child_bo.name, indent=2) exporter.report.warn(msg, self.key_name, child_bo.name, indent=2)
continue continue
child_agmod, child_agmaster = exporter.animation.get_anigraph_objects(bo=child_bo) child_agmod, child_agmaster = exporter.animation.get_anigraph_objects(
bo=child_bo
)
msgfwd.addForwardKey(child_agmaster.key) msgfwd.addForwardKey(child_agmaster.key)
msgfwd.addForwardKey(agmaster.key) msgfwd.addForwardKey(agmaster.key)
@ -260,12 +304,13 @@ class PlasmaAnimationGroupModifier(ActionModifier, PlasmaModifierProperties):
class LoopMarker(bpy.types.PropertyGroup): class LoopMarker(bpy.types.PropertyGroup):
loop_name = StringProperty(name="Loop Name", loop_name = StringProperty(name="Loop Name", description="Name of this loop")
description="Name of this loop") loop_start = StringProperty(
loop_start = StringProperty(name="Loop Start", name="Loop Start", description="Marker name from whence the loop begins"
description="Marker name from whence the loop begins") )
loop_end = StringProperty(name="Loop End", loop_end = StringProperty(
description="Marker name from whence the loop ends") name="Loop End", description="Marker name from whence the loop ends"
)
class PlasmaAnimationLoopModifier(ActionModifier, PlasmaModifierProperties): class PlasmaAnimationLoopModifier(ActionModifier, PlasmaModifierProperties):
@ -277,9 +322,9 @@ class PlasmaAnimationLoopModifier(ActionModifier, PlasmaModifierProperties):
bl_description = "Animation loop settings" bl_description = "Animation loop settings"
bl_icon = "PMARKER_SEL" bl_icon = "PMARKER_SEL"
loops = CollectionProperty(name="Loops", loops = CollectionProperty(
description="Loop points within the animation", name="Loops", description="Loop points within the animation", type=LoopMarker
type=LoopMarker) )
active_loop_index = IntProperty(options={"HIDDEN"}) active_loop_index = IntProperty(options={"HIDDEN"})
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
@ -293,11 +338,23 @@ class PlasmaAnimationLoopModifier(ActionModifier, PlasmaModifierProperties):
start = markers.get(loop.loop_start) start = markers.get(loop.loop_start)
end = markers.get(loop.loop_end) end = markers.get(loop.loop_end)
if start is None: if start is None:
exporter.report.warn("Animation '{}' Loop '{}': Marker '{}' not found. This loop will not be exported".format( exporter.report.warn(
action.name, loop.loop_name, loop.loop_start), indent=2) "Animation '{}' Loop '{}': Marker '{}' not found. This loop will not be exported".format(
action.name, loop.loop_name, loop.loop_start
),
indent=2,
)
if end is None: if end is None:
exporter.report.warn("Animation '{}' Loop '{}': Marker '{}' not found. This loop will not be exported".format( exporter.report.warn(
action.name, loop.loop_name, loop.loop_end), indent=2) "Animation '{}' Loop '{}': Marker '{}' not found. This loop will not be exported".format(
action.name, loop.loop_name, loop.loop_end
),
indent=2,
)
if start is None or end is None: if start is None or end is None:
continue continue
atcanim.setLoop(loop.loop_name, _convert_frame_time(start.frame), _convert_frame_time(end.frame)) atcanim.setLoop(
loop.loop_name,
_convert_frame_time(start.frame),
_convert_frame_time(end.frame),
)

140
korman/properties/modifiers/avatar.py

@ -32,21 +32,32 @@ class PlasmaLadderModifier(PlasmaModifierProperties):
bl_description = "Climbable Ladder" bl_description = "Climbable Ladder"
bl_icon = "COLLAPSEMENU" bl_icon = "COLLAPSEMENU"
is_enabled = BoolProperty(name="Enabled", is_enabled = BoolProperty(
description="Ladder enabled by default at Age start", name="Enabled",
default=True) description="Ladder enabled by default at Age start",
direction = EnumProperty(name="Direction", default=True,
description="Direction of climb", )
items=[("UP", "Up", "The avatar will mount the ladder and climb upward"), direction = EnumProperty(
("DOWN", "Down", "The avatar will mount the ladder and climb downward"),], name="Direction",
default="DOWN") description="Direction of climb",
num_loops = IntProperty(name="Loops", items=[
description="How many full animation loops after the first to play before dismounting", ("UP", "Up", "The avatar will mount the ladder and climb upward"),
min=0, default=4) ("DOWN", "Down", "The avatar will mount the ladder and climb downward"),
facing_object = PointerProperty(name="Facing Object", ],
description="Target object the avatar must be facing through this region to trigger climb (optional)", default="DOWN",
type=bpy.types.Object, )
poll=idprops.poll_mesh_objects) num_loops = IntProperty(
name="Loops",
description="How many full animation loops after the first to play before dismounting",
min=0,
default=4,
)
facing_object = PointerProperty(
name="Facing Object",
description="Target object the avatar must be facing through this region to trigger climb (optional)",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects,
)
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
# Create the ladder modifier # Create the ladder modifier
@ -61,7 +72,10 @@ class PlasmaLadderModifier(PlasmaModifierProperties):
# engine-defined (45 degree) tolerance # engine-defined (45 degree) tolerance
if self.facing_object is not None: if self.facing_object is not None:
# Use object if one has been selected # Use object if one has been selected
ladderVec = self.facing_object.matrix_world.translation - bo.matrix_world.translation ladderVec = (
self.facing_object.matrix_world.translation
- bo.matrix_world.translation
)
else: else:
# Make our own artificial target -1.0 units back on the local Y axis. # Make our own artificial target -1.0 units back on the local Y axis.
ladderVec = mathutils.Vector((0, -1, 0)) * bo.matrix_world.inverted() ladderVec = mathutils.Vector((0, -1, 0)) * bo.matrix_world.inverted()
@ -69,48 +83,75 @@ class PlasmaLadderModifier(PlasmaModifierProperties):
mod.ladderView.normalize() mod.ladderView.normalize()
# Generate the detector's physical bounds # Generate the detector's physical bounds
bounds = "hull" if not bo.plasma_modifiers.collision.enabled else bo.plasma_modifiers.collision.bounds bounds = (
exporter.physics.generate_physical(bo, so, bounds=bounds, member_group="kGroupDetector", "hull"
report_groups=["kGroupAvatar"], properties=["kPinned"]) if not bo.plasma_modifiers.collision.enabled
else bo.plasma_modifiers.collision.bounds
)
exporter.physics.generate_physical(
bo,
so,
bounds=bounds,
member_group="kGroupDetector",
report_groups=["kGroupAvatar"],
properties=["kPinned"],
)
@property @property
def requires_actor(self): def requires_actor(self):
return True return True
sitting_approach_flags = [("kApproachFront", "Front", "Approach from the font"), sitting_approach_flags = [
("kApproachLeft", "Left", "Approach from the left"), ("kApproachFront", "Front", "Approach from the font"),
("kApproachRight", "Right", "Approach from the right"), ("kApproachLeft", "Left", "Approach from the left"),
("kApproachRear", "Rear", "Approach from the rear guard")] ("kApproachRight", "Right", "Approach from the right"),
("kApproachRear", "Rear", "Approach from the rear guard"),
]
class PlasmaSittingBehavior(idprops.IDPropObjectMixin, PlasmaModifierProperties, PlasmaModifierLogicWiz):
class PlasmaSittingBehavior(
idprops.IDPropObjectMixin, PlasmaModifierProperties, PlasmaModifierLogicWiz
):
pl_id = "sittingmod" pl_id = "sittingmod"
bl_category = "Avatar" bl_category = "Avatar"
bl_label = "Sitting Behavior" bl_label = "Sitting Behavior"
bl_description = "Avatar sitting position" bl_description = "Avatar sitting position"
approach = EnumProperty(name="Approach", approach = EnumProperty(
description="Directions an avatar can approach the seat from", name="Approach",
items=sitting_approach_flags, description="Directions an avatar can approach the seat from",
default={"kApproachFront", "kApproachLeft", "kApproachRight"}, items=sitting_approach_flags,
options={"ENUM_FLAG"}) default={"kApproachFront", "kApproachLeft", "kApproachRight"},
options={"ENUM_FLAG"},
clickable_object = PointerProperty(name="Clickable", )
description="Object that defines the clickable area",
type=bpy.types.Object, clickable_object = PointerProperty(
poll=idprops.poll_mesh_objects) name="Clickable",
region_object = PointerProperty(name="Region", description="Object that defines the clickable area",
description="Object that defines the region mesh", type=bpy.types.Object,
type=bpy.types.Object, poll=idprops.poll_mesh_objects,
poll=idprops.poll_mesh_objects) )
region_object = PointerProperty(
facing_enabled = BoolProperty(name="Avatar Facing", name="Region",
description="The avatar must be facing the clickable's Y-axis", description="Object that defines the region mesh",
default=True) type=bpy.types.Object,
facing_degrees = IntProperty(name="Tolerance", poll=idprops.poll_mesh_objects,
description="How far away we will tolerate the avatar facing the clickable", )
min=-180, max=180, default=45)
facing_enabled = BoolProperty(
name="Avatar Facing",
description="The avatar must be facing the clickable's Y-axis",
default=True,
)
facing_degrees = IntProperty(
name="Tolerance",
description="How far away we will tolerate the avatar facing the clickable",
min=-180,
max=180,
default=45,
)
def harvest_actors(self): def harvest_actors(self):
if self.facing_enabled: if self.facing_enabled:
@ -153,8 +194,7 @@ class PlasmaSittingBehavior(idprops.IDPropObjectMixin, PlasmaModifierProperties,
@classmethod @classmethod
def _idprop_mapping(cls): def _idprop_mapping(cls):
return {"clickable_object": "clickable_obj", return {"clickable_object": "clickable_obj", "region_object": "region_obj"}
"region_object": "region_obj"}
@property @property
def key_name(self): def key_name(self):
@ -168,4 +208,8 @@ class PlasmaSittingBehavior(idprops.IDPropObjectMixin, PlasmaModifierProperties,
def sanity_check(self): def sanity_check(self):
# 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.
if self.clickable_object is None: if self.clickable_object 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
)
)

90
korman/properties/modifiers/base.py

@ -19,6 +19,7 @@ from bpy.props import *
import abc import abc
from typing import Any, Dict, Generator, Optional from typing import Any, Dict, Generator, Optional
class PlasmaModifierProperties(bpy.types.PropertyGroup): class PlasmaModifierProperties(bpy.types.PropertyGroup):
@property @property
def copy_material(self): def copy_material(self):
@ -52,8 +53,8 @@ class PlasmaModifierProperties(bpy.types.PropertyGroup):
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
"""This is the main phase of the modifier export where most, if not all, PRP objects should """This is the main phase of the modifier export where most, if not all, PRP objects should
be generated. No new Blender objects should be created unless their lifespan is constrained be generated. No new Blender objects should be created unless their lifespan is constrained
to the duration of this method. to the duration of this method.
""" """
pass pass
@ -86,7 +87,7 @@ class PlasmaModifierProperties(bpy.types.PropertyGroup):
@property @property
def no_span_sort(self): def no_span_sort(self):
"""Indicates that the geometry's Spans should never be sorted with those from other """Indicates that the geometry's Spans should never be sorted with those from other
Drawables that will render in the same pass""" Drawables that will render in the same pass"""
return False return False
# This is temporarily commented out to prevent MRO failure. Revisit in Python 3.7 # This is temporarily commented out to prevent MRO failure. Revisit in Python 3.7
@ -110,24 +111,35 @@ class PlasmaModifierProperties(bpy.types.PropertyGroup):
# you see... So, we'll store our definitions in a dict and make those properties on each subclass # you see... So, we'll store our definitions in a dict and make those properties on each subclass
# at runtime. What joy. Python FTW. See register() in __init__.py # at runtime. What joy. Python FTW. See register() in __init__.py
_subprops = { _subprops = {
"display_order": (IntProperty, {"name": "INTERNAL: Display Ordering", "display_order": (
"description": "Position in the list of buttons", IntProperty,
"default": -1, {
"options": {"HIDDEN"}}), "name": "INTERNAL: Display Ordering",
"show_expanded": (BoolProperty, {"name": "INTERNAL: Actually draw the modifier", "description": "Position in the list of buttons",
"default": True, "default": -1,
"options": {"HIDDEN"}}), "options": {"HIDDEN"},
"current_version": (IntProperty, {"name": "INTERNAL: Modifier version", },
"default": 1, ),
"options": {"HIDDEN"}}), "show_expanded": (
BoolProperty,
{
"name": "INTERNAL: Actually draw the modifier",
"default": True,
"options": {"HIDDEN"},
},
),
"current_version": (
IntProperty,
{"name": "INTERNAL: Modifier version", "default": 1, "options": {"HIDDEN"}},
),
} }
class PlasmaModifierLogicWiz: class PlasmaModifierLogicWiz:
def convert_logic(self, bo, **kwargs): def convert_logic(self, bo, **kwargs):
"""Creates, converts, and returns an unmanaged NodeTree for this logic wizard. If the wizard """Creates, converts, and returns an unmanaged NodeTree for this logic wizard. If the wizard
fails during conversion, the temporary tree is deleted for you. However, on success, you fails during conversion, the temporary tree is deleted for you. However, on success, you
are responsible for removing the tree from Blender, if applicable.""" are responsible for removing the tree from Blender, if applicable."""
name = kwargs.pop("name", self.key_name) name = kwargs.pop("name", self.key_name)
assert not "tree" in kwargs assert not "tree" in kwargs
tree = bpy.data.node_groups.new(name, "PlasmaNodeTree") tree = bpy.data.node_groups.new(name, "PlasmaNodeTree")
@ -140,7 +152,9 @@ class PlasmaModifierLogicWiz:
else: else:
return tree return tree
def _create_python_file_node(self, tree, filename: str, attributes: Dict[str, Any]) -> bpy.types.Node: def _create_python_file_node(
self, tree, filename: str, attributes: Dict[str, Any]
) -> bpy.types.Node:
pfm_node = tree.nodes.new("PlasmaPythonFileNode") pfm_node = tree.nodes.new("PlasmaPythonFileNode")
with pfm_node.NoUpdate(): with pfm_node.NoUpdate():
pfm_node.filename = filename pfm_node.filename = filename
@ -152,19 +166,36 @@ class PlasmaModifierLogicWiz:
pfm_node.update() pfm_node.update()
return pfm_node return pfm_node
def _create_python_attribute(self, pfm_node, attribute_name: str, attribute_type: Optional[str] = None, **kwargs): def _create_python_attribute(
self,
pfm_node,
attribute_name: str,
attribute_type: Optional[str] = None,
**kwargs
):
"""Creates and links a Python Attribute Node to the Python File Node given by `pfm_node`. """Creates and links a Python Attribute Node to the Python File Node given by `pfm_node`.
This will automatically handle simple attribute types such as numbers and strings, however, This will automatically handle simple attribute types such as numbers and strings, however,
for object linkage, you should specify the optional `attribute_type` to ensure the proper for object linkage, you should specify the optional `attribute_type` to ensure the proper
attribute type is found. For attribute nodes that require multiple values, the `value` may attribute type is found. For attribute nodes that require multiple values, the `value` may
be set to None and handled in your code.""" be set to None and handled in your code."""
from ...nodes.node_python import PlasmaAttribute, PlasmaAttribNodeBase from ...nodes.node_python import PlasmaAttribute, PlasmaAttribNodeBase
if attribute_type is None: if attribute_type is None:
assert len(kwargs) == 1 and "value" in kwargs, \ assert (
"In order to deduce the attribute_type, exactly one attribute value must be passed as a kw named `value`" len(kwargs) == 1 and "value" in kwargs
), "In order to deduce the attribute_type, exactly one attribute value must be passed as a kw named `value`"
attribute_type = PlasmaAttribute.type_LUT.get(kwargs["value"].__class__) attribute_type = PlasmaAttribute.type_LUT.get(kwargs["value"].__class__)
node_cls = next((i for i in PlasmaAttribNodeBase.__subclasses__() if attribute_type in i.pl_attrib), None) node_cls = next(
assert node_cls is not None, "'{}': Unable to find attribute node type for '{}' ('{}')".format( (
i
for i in PlasmaAttribNodeBase.__subclasses__()
if attribute_type in i.pl_attrib
),
None,
)
assert (
node_cls is not None
), "'{}': Unable to find attribute node type for '{}' ('{}')".format(
self.id_data.name, attribute_name, attribute_type self.id_data.name, attribute_name, attribute_type
) )
@ -180,7 +211,7 @@ class PlasmaModifierLogicWiz:
def pre_export(self, exporter, bo): def pre_export(self, exporter, bo):
"""Default implementation of the pre_export phase for logic wizards that simply triggers """Default implementation of the pre_export phase for logic wizards that simply triggers
the logic nodes to be created and for their export to be scheduled.""" the logic nodes to be created and for their export to be scheduled."""
yield self.convert_logic(bo) yield self.convert_logic(bo)
@ -213,10 +244,13 @@ def _restore_properties(dummy):
# Unregistered propertes are a sequence of (property function, # Unregistered propertes are a sequence of (property function,
# property keyword arguments). Interesting design decision :) # property keyword arguments). Interesting design decision :)
prop_cb, prop_kwargs = getattr(mod_cls, prop_name) prop_cb, prop_kwargs = getattr(mod_cls, prop_name)
del prop_kwargs["attr"] # Prevents proper registration del prop_kwargs["attr"] # Prevents proper registration
setattr(mod_cls, prop_name, prop_cb(**prop_kwargs)) setattr(mod_cls, prop_name, prop_cb(**prop_kwargs))
bpy.app.handlers.load_pre.append(_restore_properties) bpy.app.handlers.load_pre.append(_restore_properties)
@bpy.app.handlers.persistent @bpy.app.handlers.persistent
def _upgrade_modifiers(dummy): def _upgrade_modifiers(dummy):
# First, run all the upgrades # First, run all the upgrades
@ -231,4 +265,6 @@ def _upgrade_modifiers(dummy):
for mod_cls in PlasmaModifierUpgradable.__subclasses__(): for mod_cls in PlasmaModifierUpgradable.__subclasses__():
for prop in mod_cls.deprecated_properties: for prop in mod_cls.deprecated_properties:
RemoveProperty(mod_cls, attr=prop) RemoveProperty(mod_cls, attr=prop)
bpy.app.handlers.load_post.append(_upgrade_modifiers) bpy.app.handlers.load_post.append(_upgrade_modifiers)

630
korman/properties/modifiers/gui.py

@ -25,65 +25,75 @@ from PyHSPlasma import *
from ...addon_prefs import game_versions from ...addon_prefs import game_versions
from ...exporter import ExportError, utils from ...exporter import ExportError, utils
from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz, PlasmaModifierUpgradable from .base import (
PlasmaModifierProperties,
PlasmaModifierLogicWiz,
PlasmaModifierUpgradable,
)
from ... import idprops from ... import idprops
journal_pfms = { journal_pfms = {
pvPots : { pvPots: {
# Supplied by the OfflineKI script: # Supplied by the OfflineKI script:
# https://gitlab.com/diafero/offline-ki/blob/master/offlineki/xSimpleJournal.py # https://gitlab.com/diafero/offline-ki/blob/master/offlineki/xSimpleJournal.py
"filename": "xSimpleJournal.py", "filename": "xSimpleJournal.py",
"attribs": ( "attribs": (
{ 'id': 1, 'type': "ptAttribActivator", "name": "bookClickable" }, {"id": 1, "type": "ptAttribActivator", "name": "bookClickable"},
{ 'id': 2, 'type': "ptAttribString", "name": "journalFileName" }, {"id": 2, "type": "ptAttribString", "name": "journalFileName"},
{ 'id': 3, 'type': "ptAttribBoolean", "name": "isNotebook" }, {"id": 3, "type": "ptAttribBoolean", "name": "isNotebook"},
{ 'id': 4, 'type': "ptAttribFloat", "name": "BookWidth" }, {"id": 4, "type": "ptAttribFloat", "name": "BookWidth"},
{ 'id': 5, 'type': "ptAttribFloat", "name": "BookHeight" }, {"id": 5, "type": "ptAttribFloat", "name": "BookHeight"},
) ),
}, },
pvMoul : { pvMoul: {
"filename": "xJournalBookGUIPopup.py", "filename": "xJournalBookGUIPopup.py",
"attribs": ( "attribs": (
{ 'id': 1, 'type': "ptAttribActivator", 'name': "actClickableBook" }, {"id": 1, "type": "ptAttribActivator", "name": "actClickableBook"},
{ 'id': 10, 'type': "ptAttribBoolean", 'name': "StartOpen" }, {"id": 10, "type": "ptAttribBoolean", "name": "StartOpen"},
{ 'id': 11, 'type': "ptAttribFloat", 'name': "BookWidth" }, {"id": 11, "type": "ptAttribFloat", "name": "BookWidth"},
{ 'id': 12, 'type': "ptAttribFloat", 'name': "BookHeight" }, {"id": 12, "type": "ptAttribFloat", "name": "BookHeight"},
{ 'id': 13, 'type': "ptAttribString", 'name': "LocPath" }, {"id": 13, "type": "ptAttribString", "name": "LocPath"},
{ 'id': 14, 'type': "ptAttribString", 'name': "GUIType" }, {"id": 14, "type": "ptAttribString", "name": "GUIType"},
) ),
}, },
} }
# Do not change the numeric IDs. They allow the list to be rearranged. # Do not change the numeric IDs. They allow the list to be rearranged.
_languages = [("Dutch", "Nederlands", "Dutch", 0), _languages = [
("English", "English", "", 1), ("Dutch", "Nederlands", "Dutch", 0),
("Finnish", "Suomi", "Finnish", 2), ("English", "English", "", 1),
("French", "Français", "French", 3), ("Finnish", "Suomi", "Finnish", 2),
("German", "Deutsch", "German", 4), ("French", "Français", "French", 3),
("Hungarian", "Magyar", "Hungarian", 5), ("German", "Deutsch", "German", 4),
("Italian", "Italiano ", "Italian", 6), ("Hungarian", "Magyar", "Hungarian", 5),
# Blender 2.79b can't render 日本語 by default ("Italian", "Italiano ", "Italian", 6),
("Japanese", "Nihongo", "Japanese", 7), # Blender 2.79b can't render 日本語 by default
("Norwegian", "Norsk", "Norwegian", 8), ("Japanese", "Nihongo", "Japanese", 7),
("Polish", "Polski", "Polish", 9), ("Norwegian", "Norsk", "Norwegian", 8),
("Romanian", "Română", "Romanian", 10), ("Polish", "Polski", "Polish", 9),
("Russian", "Pyccĸий", "Russian", 11), ("Romanian", "Română", "Romanian", 10),
("Spanish", "Español", "Spanish", 12), ("Russian", "Pyccĸий", "Russian", 11),
("Swedish", "Svenska", "Swedish", 13)] ("Spanish", "Español", "Spanish", 12),
("Swedish", "Svenska", "Swedish", 13),
]
languages = sorted(_languages, key=lambda x: x[1]) languages = sorted(_languages, key=lambda x: x[1])
_DEFAULT_LANGUAGE_NAME = "English" _DEFAULT_LANGUAGE_NAME = "English"
_DEFAULT_LANGUAGE_ID = 1 _DEFAULT_LANGUAGE_ID = 1
class ImageLibraryItem(bpy.types.PropertyGroup): class ImageLibraryItem(bpy.types.PropertyGroup):
image = bpy.props.PointerProperty(name="Image Item", image = bpy.props.PointerProperty(
description="Image stored for export.", name="Image Item",
type=bpy.types.Image, description="Image stored for export.",
options=set()) type=bpy.types.Image,
enabled = bpy.props.BoolProperty(name="Enabled", options=set(),
description="Specifies whether this image will be stored during export.", )
default=True, enabled = bpy.props.BoolProperty(
options=set()) name="Enabled",
description="Specifies whether this image will be stored during export.",
default=True,
options=set(),
)
class PlasmaImageLibraryModifier(PlasmaModifierProperties): class PlasmaImageLibraryModifier(PlasmaModifierProperties):
@ -99,43 +109,66 @@ class PlasmaImageLibraryModifier(PlasmaModifierProperties):
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
if self.images: if self.images:
ilmod = exporter.mgr.find_create_object(plImageLibMod, so=so, name=self.key_name) ilmod = exporter.mgr.find_create_object(
plImageLibMod, so=so, name=self.key_name
)
for item in self.images: for item in self.images:
if item.image and item.enabled: if item.image and item.enabled:
exporter.mesh.material.export_prepared_image(owner=ilmod, image=item.image, allowed_formats={"JPG", "PNG"}, extension="hsm") exporter.mesh.material.export_prepared_image(
owner=ilmod,
image=item.image,
allowed_formats={"JPG", "PNG"},
extension="hsm",
)
class PlasmaJournalTranslation(bpy.types.PropertyGroup): class PlasmaJournalTranslation(bpy.types.PropertyGroup):
def _poll_nonpytext(self, value): def _poll_nonpytext(self, value):
return not value.name.endswith(".py") return not value.name.endswith(".py")
language = EnumProperty(name="Language", language = EnumProperty(
description="Language of this translation", name="Language",
items=languages, description="Language of this translation",
default=_DEFAULT_LANGUAGE_NAME, items=languages,
options=set()) default=_DEFAULT_LANGUAGE_NAME,
text_id = PointerProperty(name="Contents", options=set(),
description="Text data block containing the text for this language", )
type=bpy.types.Text, text_id = PointerProperty(
poll=_poll_nonpytext, name="Contents",
options=set()) description="Text data block containing the text for this language",
type=bpy.types.Text,
poll=_poll_nonpytext,
options=set(),
)
class TranslationMixin: class TranslationMixin:
def export_localization(self, exporter): def export_localization(self, exporter):
translations = [i for i in self.translations if i.text_id is not None] translations = [i for i in self.translations if i.text_id is not None]
if not translations: if not translations:
exporter.report.error("'{}': '{}' No content translations available. The localization will not be exported.", exporter.report.error(
self.id_data.name, self.bl_label, indent=1) "'{}': '{}' No content translations available. The localization will not be exported.",
self.id_data.name,
self.bl_label,
indent=1,
)
return return
for i in translations: for i in translations:
exporter.locman.add_string(self.localization_set, self.key_name, i.language, i.text_id, indent=1) exporter.locman.add_string(
self.localization_set, self.key_name, i.language, i.text_id, indent=1
)
def _get_translation(self): def _get_translation(self):
# Ensure there is always a default (read: English) translation available. # Ensure there is always a default (read: English) translation available.
default_idx, default = next(((idx, translation) for idx, translation in enumerate(self.translations) default_idx, default = next(
if translation.language == _DEFAULT_LANGUAGE_NAME), (None, None)) (
(idx, translation)
for idx, translation in enumerate(self.translations)
if translation.language == _DEFAULT_LANGUAGE_NAME
),
(None, None),
)
if default is None: if default is None:
default_idx = len(self.translations) default_idx = len(self.translations)
default = self.translations.add() default = self.translations.add()
@ -153,8 +186,14 @@ class TranslationMixin:
def _set_translation(self, value): def _set_translation(self, value):
# We were given an int here, must change to a string # We were given an int here, must change to a string
language_name = next((key for key, _, _, i in languages if i == value)) language_name = next((key for key, _, _, i in languages if i == value))
idx = next((idx for idx, translation in enumerate(self.translations) idx = next(
if translation.language == language_name), None) (
idx
for idx, translation in enumerate(self.translations)
if translation.language == language_name
),
None,
)
if idx is None: if idx is None:
self.active_translation_index = len(self.translations) self.active_translation_index = len(self.translations)
translation = self.translations.add() translation = self.translations.add()
@ -171,7 +210,9 @@ class TranslationMixin:
raise RuntimeError("TranslationMixin subclass needs a translation getter!") raise RuntimeError("TranslationMixin subclass needs a translation getter!")
class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz, TranslationMixin): class PlasmaJournalBookModifier(
PlasmaModifierProperties, PlasmaModifierLogicWiz, TranslationMixin
):
pl_id = "journalbookmod" pl_id = "journalbookmod"
bl_category = "GUI" bl_category = "GUI"
@ -179,59 +220,94 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
bl_description = "Journal Book" bl_description = "Journal Book"
bl_icon = "WORDWRAP_ON" bl_icon = "WORDWRAP_ON"
versions = EnumProperty(name="Export Targets", versions = EnumProperty(
description="Plasma versions for which this journal exports", name="Export Targets",
items=game_versions, description="Plasma versions for which this journal exports",
options={"ENUM_FLAG"}, items=game_versions,
default={"pvMoul"}) options={"ENUM_FLAG"},
start_state = EnumProperty(name="Start", default={"pvMoul"},
description="State of journal when activated", )
items=[("OPEN", "Open", "Journal will start opened to first page"), start_state = EnumProperty(
("CLOSED", "Closed", "Journal will start closed showing cover")], name="Start",
default="CLOSED") description="State of journal when activated",
book_type = EnumProperty(name="Book Type", items=[
description="GUI type to be used for the journal", ("OPEN", "Open", "Journal will start opened to first page"),
items=[("bkBook", "Book", "A journal written on worn, yellowed paper"), ("CLOSED", "Closed", "Journal will start closed showing cover"),
("bkNotebook", "Notebook", "A journal written on white, lined paper")], ],
default="bkBook") default="CLOSED",
book_scale_w = IntProperty(name="Book Width Scale", )
description="Width scale", book_type = EnumProperty(
default=100, min=0, max=100, name="Book Type",
subtype="PERCENTAGE") description="GUI type to be used for the journal",
book_scale_h = IntProperty(name="Book Height Scale", items=[
description="Height scale", ("bkBook", "Book", "A journal written on worn, yellowed paper"),
default=100, min=0, max=100, ("bkNotebook", "Notebook", "A journal written on white, lined paper"),
subtype="PERCENTAGE") ],
clickable_region = PointerProperty(name="Region", default="bkBook",
description="Region inside which the avatar must stand to be able to open the journal (optional)", )
type=bpy.types.Object, book_scale_w = IntProperty(
poll=idprops.poll_mesh_objects) name="Book Width Scale",
description="Width scale",
journal_translations = CollectionProperty(name="Journal Translations", default=100,
type=PlasmaJournalTranslation, min=0,
options=set()) max=100,
subtype="PERCENTAGE",
)
book_scale_h = IntProperty(
name="Book Height Scale",
description="Height scale",
default=100,
min=0,
max=100,
subtype="PERCENTAGE",
)
clickable_region = PointerProperty(
name="Region",
description="Region inside which the avatar must stand to be able to open the journal (optional)",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects,
)
journal_translations = CollectionProperty(
name="Journal Translations", type=PlasmaJournalTranslation, options=set()
)
active_translation_index = IntProperty(options={"HIDDEN"}) active_translation_index = IntProperty(options={"HIDDEN"})
active_translation = EnumProperty(name="Language", active_translation = EnumProperty(
description="Language of this translation", name="Language",
items=languages, description="Language of this translation",
get=TranslationMixin._get_translation, items=languages,
set=TranslationMixin._set_translation, get=TranslationMixin._get_translation,
options=set()) set=TranslationMixin._set_translation,
options=set(),
)
def pre_export(self, exporter, bo): def pre_export(self, exporter, bo):
our_versions = (globals()[j] for j in self.versions) our_versions = (globals()[j] for j in self.versions)
version = exporter.mgr.getVer() version = exporter.mgr.getVer()
if version not in our_versions: if version not in our_versions:
# We aren't needed here # We aren't needed here
exporter.report.port("Object '{}' has a JournalMod not enabled for export to the selected engine. Skipping.", exporter.report.port(
bo.name, version, indent=2) "Object '{}' has a JournalMod not enabled for export to the selected engine. Skipping.",
bo.name,
version,
indent=2,
)
return return
if self.clickable_region is None: if self.clickable_region is None:
with utils.bmesh_object("{}_Journal_ClkRgn".format(self.key_name)) as (rgn_obj, bm): with utils.bmesh_object("{}_Journal_ClkRgn".format(self.key_name)) as (
rgn_obj,
bm,
):
bmesh.ops.create_cube(bm, size=(6.0)) bmesh.ops.create_cube(bm, size=(6.0))
bmesh.ops.transform(bm, matrix=mathutils.Matrix.Translation(bo.matrix_world.translation - rgn_obj.matrix_world.translation), bmesh.ops.transform(
space=rgn_obj.matrix_world, verts=bm.verts) bm,
matrix=mathutils.Matrix.Translation(
bo.matrix_world.translation - rgn_obj.matrix_world.translation
),
space=rgn_obj.matrix_world,
verts=bm.verts,
)
rgn_obj.plasma_object.enabled = True rgn_obj.plasma_object.enabled = True
rgn_obj.hide_render = True rgn_obj.hide_render = True
yield rgn_obj yield rgn_obj
@ -240,18 +316,24 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
rgn_obj = self.clickable_region rgn_obj = self.clickable_region
# Generate the logic nodes # Generate the logic nodes
yield self.convert_logic(bo, age_name=exporter.age_name, rgn_obj=rgn_obj, version=version) yield self.convert_logic(
bo, age_name=exporter.age_name, rgn_obj=rgn_obj, version=version
)
def logicwiz(self, bo, tree, age_name, rgn_obj, version): def logicwiz(self, bo, tree, age_name, rgn_obj, version):
# Assign journal script based on target version # Assign journal script based on target version
journal_pfm = journal_pfms[version] journal_pfm = journal_pfms[version]
journalnode = self._create_python_file_node(tree, journal_pfm["filename"], journal_pfm["attribs"]) journalnode = self._create_python_file_node(
tree, journal_pfm["filename"], journal_pfm["attribs"]
)
if version <= pvPots: if version <= pvPots:
self._create_pots_nodes(bo, tree.nodes, journalnode, age_name, rgn_obj) self._create_pots_nodes(bo, tree.nodes, journalnode, age_name, rgn_obj)
else: else:
self._create_moul_nodes(bo, tree.nodes, journalnode, age_name, rgn_obj) self._create_moul_nodes(bo, tree.nodes, journalnode, age_name, rgn_obj)
def _create_pots_nodes(self, clickable_object, nodes, journalnode, age_name, rgn_obj): def _create_pots_nodes(
self, clickable_object, nodes, journalnode, age_name, rgn_obj
):
clickable_region = nodes.new("PlasmaClickableRegionNode") clickable_region = nodes.new("PlasmaClickableRegionNode")
clickable_region.region_object = rgn_obj clickable_region.region_object = rgn_obj
@ -281,7 +363,9 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
height.link_output(journalnode, "pfm", "BookHeight") height.link_output(journalnode, "pfm", "BookHeight")
height.value_float = self.book_scale_h / 100.0 height.value_float = self.book_scale_h / 100.0
def _create_moul_nodes(self, clickable_object, nodes, journalnode, age_name, rgn_obj): def _create_moul_nodes(
self, clickable_object, nodes, journalnode, age_name, rgn_obj
):
clickable_region = nodes.new("PlasmaClickableRegionNode") clickable_region = nodes.new("PlasmaClickableRegionNode")
clickable_region.region_object = rgn_obj clickable_region.region_object = rgn_obj
@ -309,7 +393,9 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
locpath = nodes.new("PlasmaAttribStringNode") locpath = nodes.new("PlasmaAttribStringNode")
locpath.link_output(journalnode, "pfm", "LocPath") locpath.link_output(journalnode, "pfm", "LocPath")
locpath.value = "{}.{}.{}".format(age_name, self.localization_set, self.key_name) locpath.value = "{}.{}.{}".format(
age_name, self.localization_set, self.key_name
)
guitype = nodes.new("PlasmaAttribStringNode") guitype = nodes.new("PlasmaAttribStringNode")
guitype.link_output(journalnode, "pfm", "GUIType") guitype.link_output(journalnode, "pfm", "GUIType")
@ -331,38 +417,38 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
linking_pfms = { linking_pfms = {
pvPots : { pvPots: {
# Supplied by the OfflineKI script: # Supplied by the OfflineKI script:
# https://gitlab.com/diafero/offline-ki/blob/master/offlineki/xSimpleLinkingBook.py # https://gitlab.com/diafero/offline-ki/blob/master/offlineki/xSimpleLinkingBook.py
"filename": "xSimpleLinkingBook.py", "filename": "xSimpleLinkingBook.py",
"attribs": ( "attribs": (
{ 'id': 1, 'type': "ptAttribActivator", "name": "bookClickable" }, {"id": 1, "type": "ptAttribActivator", "name": "bookClickable"},
{ 'id': 2, 'type': "ptAttribString", "name": "destinationAge" }, {"id": 2, "type": "ptAttribString", "name": "destinationAge"},
{ 'id': 3, 'type': "ptAttribString", "name": "spawnPoint" }, {"id": 3, "type": "ptAttribString", "name": "spawnPoint"},
{ 'id': 4, 'type': "ptAttribString", "name": "linkPanel" }, {"id": 4, "type": "ptAttribString", "name": "linkPanel"},
{ 'id': 5, 'type': "ptAttribString", "name": "bookCover" }, {"id": 5, "type": "ptAttribString", "name": "bookCover"},
{ 'id': 6, 'type': "ptAttribString", "name": "stampTexture" }, {"id": 6, "type": "ptAttribString", "name": "stampTexture"},
{ 'id': 7, 'type': "ptAttribFloat", "name": "stampX" }, {"id": 7, "type": "ptAttribFloat", "name": "stampX"},
{ 'id': 8, 'type': "ptAttribFloat", "name": "stampY" }, {"id": 8, "type": "ptAttribFloat", "name": "stampY"},
{ 'id': 9, 'type': "ptAttribFloat", "name": "bookWidth" }, {"id": 9, "type": "ptAttribFloat", "name": "bookWidth"},
{ 'id': 10, 'type': "ptAttribFloat", "name": "BookHeight" }, {"id": 10, "type": "ptAttribFloat", "name": "BookHeight"},
{ 'id': 11, 'type': "ptAttribBehavior", "name": "msbSeekBeforeUI" }, {"id": 11, "type": "ptAttribBehavior", "name": "msbSeekBeforeUI"},
{ 'id': 12, 'type': "ptAttribResponder", "name": "respOneShot" }, {"id": 12, "type": "ptAttribResponder", "name": "respOneShot"},
) ),
}, },
pvMoul : { pvMoul: {
"filename": "xLinkingBookGUIPopup.py", "filename": "xLinkingBookGUIPopup.py",
"attribs": ( "attribs": (
{ 'id': 1, 'type': "ptAttribActivator", 'name': "actClickableBook" }, {"id": 1, "type": "ptAttribActivator", "name": "actClickableBook"},
{ 'id': 2, 'type': "ptAttribBehavior", 'name': "SeekBehavior" }, {"id": 2, "type": "ptAttribBehavior", "name": "SeekBehavior"},
{ 'id': 3, 'type': "ptAttribResponder", 'name': "respLinkResponder" }, {"id": 3, "type": "ptAttribResponder", "name": "respLinkResponder"},
{ 'id': 4, 'type': "ptAttribString", 'name': "TargetAge" }, {"id": 4, "type": "ptAttribString", "name": "TargetAge"},
{ 'id': 5, 'type': "ptAttribActivator", 'name': "actBookshelf" }, {"id": 5, "type": "ptAttribActivator", "name": "actBookshelf"},
{ 'id': 6, 'type': "ptAttribActivator", 'name': "shareRegion" }, {"id": 6, "type": "ptAttribActivator", "name": "shareRegion"},
{ 'id': 7, 'type': "ptAttribBehavior", 'name': "shareBookSeek" }, {"id": 7, "type": "ptAttribBehavior", "name": "shareBookSeek"},
{ 'id': 10, 'type': "ptAttribBoolean", 'name': "IsDRCStamped" }, {"id": 10, "type": "ptAttribBoolean", "name": "IsDRCStamped"},
{ 'id': 11, 'type': "ptAttribBoolean", 'name': "ForceThirdPerson" }, {"id": 11, "type": "ptAttribBoolean", "name": "ForceThirdPerson"},
) ),
}, },
} }
@ -375,81 +461,138 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
bl_description = "Linking Book" bl_description = "Linking Book"
bl_icon = "FILE_IMAGE" bl_icon = "FILE_IMAGE"
versions = EnumProperty(name="Export Targets", versions = EnumProperty(
description="Plasma versions for which this journal exports", name="Export Targets",
items=game_versions, description="Plasma versions for which this journal exports",
options={"ENUM_FLAG"}, items=game_versions,
default={"pvMoul"}) options={"ENUM_FLAG"},
default={"pvMoul"},
)
# Link Info # Link Info
link_type = EnumProperty(name="Linking Type", link_type = EnumProperty(
description="The type of Link this Linking Book will use", name="Linking Type",
items=[ description="The type of Link this Linking Book will use",
("kBasicLink", "Public Link", "Links to a public instance of the specified Age"), items=[
("kOriginalBook", "Private Link", "Links to a new or existing private instance of the specified Age"), (
("kSubAgeBook", "Closed Loop Link", "Links between instances of the specifed Age and the current one"),], "kBasicLink",
options=set(), "Public Link",
default="kOriginalBook") "Links to a public instance of the specified Age",
age_name = StringProperty(name="Age Name", ),
description="Filename of the Age to link to (e.g. Garrison)",) (
age_instance = StringProperty(name="Age Instance", "kOriginalBook",
description="Friendly name of the Age to link to (e.g. Gahreesen)",) "Private Link",
age_uuid = StringProperty(name="Age GUID", "Links to a new or existing private instance of the specified Age",
description="GUID for a specific instance (used with public Ages)",) ),
age_parent = StringProperty(name="Parent Age", (
description="Name of the Child Age's parent Age",) "kSubAgeBook",
"Closed Loop Link",
spawn_title = StringProperty(name="Spawn Title", "Links between instances of the specifed Age and the current one",
description="Title of the Spawn Point", ),
default="Default") ],
spawn_point = StringProperty(name="Spawn Point", options=set(),
description="Name of the Spawn Point to arrive at after the link", default="kOriginalBook",
default="LinkInPointDefault") )
anim_type = EnumProperty(name="Link Animation", age_name = StringProperty(
description="Type of Linking Animation to use", name="Age Name",
items=[("LinkOut", "Standing", "The avatar steps up to the book and places their hand on the panel"), description="Filename of the Age to link to (e.g. Garrison)",
("FishBookLinkOut", "Kneeling", "The avatar kneels in front of the book and places their hand on the panel"),], )
default="LinkOut", age_instance = StringProperty(
options=set()) name="Age Instance",
link_destination = StringProperty(name="Linking Panel Name", description="Friendly name of the Age to link to (e.g. Gahreesen)",
description="Optional: Name of Linking Panel to use for this link-in point if it differs from the Age Name",) )
age_uuid = StringProperty(
name="Age GUID",
description="GUID for a specific instance (used with public Ages)",
)
age_parent = StringProperty(
name="Parent Age",
description="Name of the Child Age's parent Age",
)
spawn_title = StringProperty(
name="Spawn Title", description="Title of the Spawn Point", default="Default"
)
spawn_point = StringProperty(
name="Spawn Point",
description="Name of the Spawn Point to arrive at after the link",
default="LinkInPointDefault",
)
anim_type = EnumProperty(
name="Link Animation",
description="Type of Linking Animation to use",
items=[
(
"LinkOut",
"Standing",
"The avatar steps up to the book and places their hand on the panel",
),
(
"FishBookLinkOut",
"Kneeling",
"The avatar kneels in front of the book and places their hand on the panel",
),
],
default="LinkOut",
options=set(),
)
link_destination = StringProperty(
name="Linking Panel Name",
description="Optional: Name of Linking Panel to use for this link-in point if it differs from the Age Name",
)
# Interactables # Interactables
seek_point = PointerProperty(name="Seek Point", seek_point = PointerProperty(
description="The point the avatar will seek to before opening the Linking Book GUI", name="Seek Point",
type=bpy.types.Object, description="The point the avatar will seek to before opening the Linking Book GUI",
poll=idprops.poll_empty_objects) type=bpy.types.Object,
clickable_region = PointerProperty(name="Clickable Region", poll=idprops.poll_empty_objects,
description="The region in which the avatar must be standing before they can click on the Linking Book", )
type=bpy.types.Object, clickable_region = PointerProperty(
poll=idprops.poll_mesh_objects) name="Clickable Region",
clickable = PointerProperty(name="Clickable", description="The region in which the avatar must be standing before they can click on the Linking Book",
description="The object the avatar will click on to activate the Linking Book GUI", type=bpy.types.Object,
type=bpy.types.Object, poll=idprops.poll_mesh_objects,
poll=idprops.poll_mesh_objects) )
clickable = PointerProperty(
name="Clickable",
description="The object the avatar will click on to activate the Linking Book GUI",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects,
)
# -- Path of the Shell options -- # -- Path of the Shell options --
# Popup Appearance # Popup Appearance
book_cover_image = PointerProperty(name="Book Cover", book_cover_image = PointerProperty(
description="Image to use for the Linking Book's cover (Optional: book starts open if left blank)", name="Book Cover",
type=bpy.types.Image, description="Image to use for the Linking Book's cover (Optional: book starts open if left blank)",
options=set()) type=bpy.types.Image,
link_panel_image = PointerProperty(name="Linking Panel", options=set(),
description="Image to use for the Linking Panel", )
type=bpy.types.Image, link_panel_image = PointerProperty(
options=set()) name="Linking Panel",
stamp_image = PointerProperty(name="Stamp Image", description="Image to use for the Linking Panel",
description="Image to use for the stamp on the page opposite the book's linking panel, if any", type=bpy.types.Image,
type=bpy.types.Image, options=set(),
options=set()) )
stamp_x = IntProperty(name="Stamp Position X", stamp_image = PointerProperty(
description="X position of Stamp", name="Stamp Image",
default=140, description="Image to use for the stamp on the page opposite the book's linking panel, if any",
subtype="UNSIGNED") type=bpy.types.Image,
stamp_y = IntProperty(name="Stamp Position Y", options=set(),
description="Y position of Stamp", )
default=255, stamp_x = IntProperty(
subtype="UNSIGNED") name="Stamp Position X",
description="X position of Stamp",
default=140,
subtype="UNSIGNED",
)
stamp_y = IntProperty(
name="Stamp Position Y",
description="Y position of Stamp",
default=255,
subtype="UNSIGNED",
)
def _check_version(self, *args) -> bool: def _check_version(self, *args) -> bool:
our_versions = frozenset((globals()[j] for j in self.versions)) our_versions = frozenset((globals()[j] for j in self.versions))
@ -458,34 +601,63 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
def pre_export(self, exporter, bo): def pre_export(self, exporter, bo):
if not self._check_version(exporter.mgr.getVer()): if not self._check_version(exporter.mgr.getVer()):
# We aren't needed here # We aren't needed here
exporter.report.port("Object '{}' has a LinkingBookMod not enabled for export to the selected engine. Skipping.", exporter.report.port(
self.id_data.name, indent=2) "Object '{}' has a LinkingBookMod not enabled for export to the selected engine. Skipping.",
self.id_data.name,
indent=2,
)
return return
# Auto-generate a six-foot cube region around the clickable if none was provided. # Auto-generate a six-foot cube region around the clickable if none was provided.
if self.clickable_region is None: if self.clickable_region is None:
with utils.bmesh_object("{}_LinkingBook_ClkRgn".format(self.key_name)) as (rgn_obj, bm): with utils.bmesh_object("{}_LinkingBook_ClkRgn".format(self.key_name)) as (
rgn_obj,
bm,
):
bmesh.ops.create_cube(bm, size=(6.0)) bmesh.ops.create_cube(bm, size=(6.0))
rgn_offset = mathutils.Matrix.Translation(self.clickable.matrix_world.translation - rgn_obj.matrix_world.translation) rgn_offset = mathutils.Matrix.Translation(
bmesh.ops.transform(bm, matrix=rgn_offset, space=rgn_obj.matrix_world, verts=bm.verts) self.clickable.matrix_world.translation
- rgn_obj.matrix_world.translation
)
bmesh.ops.transform(
bm, matrix=rgn_offset, space=rgn_obj.matrix_world, verts=bm.verts
)
rgn_obj.plasma_object.enabled = True rgn_obj.plasma_object.enabled = True
rgn_obj.hide_render = True rgn_obj.hide_render = True
yield rgn_obj yield rgn_obj
else: else:
rgn_obj = self.clickable_region rgn_obj = self.clickable_region
yield self.convert_logic(bo, age_name=exporter.age_name, version=exporter.mgr.getVer(), region=rgn_obj) yield self.convert_logic(
bo,
age_name=exporter.age_name,
version=exporter.mgr.getVer(),
region=rgn_obj,
)
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
if self._check_version(pvPrime, pvPots): if self._check_version(pvPrime, pvPots):
# Create ImageLibraryMod in which to store the Cover, Linking Panel, and Stamp images # Create ImageLibraryMod in which to store the Cover, Linking Panel, and Stamp images
ilmod = exporter.mgr.find_create_object(plImageLibMod, so=so, name=self.key_name) ilmod = exporter.mgr.find_create_object(
plImageLibMod, so=so, name=self.key_name
user_images = (i for i in (self.book_cover_image, self.link_panel_image, self.stamp_image) )
if i is not None)
user_images = (
i
for i in (
self.book_cover_image,
self.link_panel_image,
self.stamp_image,
)
if i is not None
)
for image in user_images: for image in user_images:
exporter.mesh.material.export_prepared_image(owner=ilmod, image=image, exporter.mesh.material.export_prepared_image(
allowed_formats={"JPG", "PNG"}, extension="hsm") owner=ilmod,
image=image,
allowed_formats={"JPG", "PNG"},
extension="hsm",
)
def harvest_actors(self): def harvest_actors(self):
if self.seek_point is not None: if self.seek_point is not None:
@ -494,13 +666,17 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
def logicwiz(self, bo, tree, age_name, version, region): def logicwiz(self, bo, tree, age_name, version, region):
# Assign linking book script based on target version # Assign linking book script based on target version
linking_pfm = linking_pfms[version] linking_pfm = linking_pfms[version]
linkingnode = self._create_python_file_node(tree, linking_pfm["filename"], linking_pfm["attribs"]) linkingnode = self._create_python_file_node(
tree, linking_pfm["filename"], linking_pfm["attribs"]
)
if version <= pvPots: if version <= pvPots:
self._create_pots_nodes(bo, tree.nodes, linkingnode, age_name, region) self._create_pots_nodes(bo, tree.nodes, linkingnode, age_name, region)
else: else:
self._create_moul_nodes(bo, tree.nodes, linkingnode, age_name, region) self._create_moul_nodes(bo, tree.nodes, linkingnode, age_name, region)
def _create_pots_nodes(self, clickable_object, nodes, linkingnode, age_name, clk_region): def _create_pots_nodes(
self, clickable_object, nodes, linkingnode, age_name, clk_region
):
# Clickable # Clickable
clickable_region = nodes.new("PlasmaClickableRegionNode") clickable_region = nodes.new("PlasmaClickableRegionNode")
clickable_region.region_object = clk_region clickable_region.region_object = clk_region
@ -524,19 +700,25 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
# Book Cover Image # Book Cover Image
if self.book_cover_image: if self.book_cover_image:
book_cover_name = nodes.new("PlasmaAttribStringNode") book_cover_name = nodes.new("PlasmaAttribStringNode")
book_cover_name.value = str(Path(self.book_cover_image.name).with_suffix(".hsm")) book_cover_name.value = str(
Path(self.book_cover_image.name).with_suffix(".hsm")
)
book_cover_name.link_output(linkingnode, "pfm", "bookCover") book_cover_name.link_output(linkingnode, "pfm", "bookCover")
# Linking Panel Image # Linking Panel Image
if self.link_panel_image: if self.link_panel_image:
linking_panel_name = nodes.new("PlasmaAttribStringNode") linking_panel_name = nodes.new("PlasmaAttribStringNode")
linking_panel_name.value = str(Path(self.link_panel_image.name).with_suffix(".hsm")) linking_panel_name.value = str(
Path(self.link_panel_image.name).with_suffix(".hsm")
)
linking_panel_name.link_output(linkingnode, "pfm", "linkPanel") linking_panel_name.link_output(linkingnode, "pfm", "linkPanel")
# Stamp Image # Stamp Image
if self.stamp_image: if self.stamp_image:
stamp_texture_name = nodes.new("PlasmaAttribStringNode") stamp_texture_name = nodes.new("PlasmaAttribStringNode")
stamp_texture_name.value = str(Path(self.stamp_image.name).with_suffix(".hsm")) stamp_texture_name.value = str(
Path(self.stamp_image.name).with_suffix(".hsm")
)
stamp_texture_name.link_output(linkingnode, "pfm", "stampTexture") stamp_texture_name.link_output(linkingnode, "pfm", "stampTexture")
# Stamp X Position # Stamp X Position
@ -578,7 +760,9 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
responder.link_output(responder_state, "state_refs", "resp") responder.link_output(responder_state, "state_refs", "resp")
responder.link_output(linkingnode, "keyref", "respOneShot") responder.link_output(linkingnode, "keyref", "respOneShot")
def _create_moul_nodes(self, clickable_object, nodes, linkingnode, age_name, clk_region): def _create_moul_nodes(
self, clickable_object, nodes, linkingnode, age_name, clk_region
):
# Clickable # Clickable
clickable_region = nodes.new("PlasmaClickableRegionNode") clickable_region = nodes.new("PlasmaClickableRegionNode")
clickable_region.region_object = clk_region clickable_region.region_object = clk_region
@ -630,11 +814,17 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
# Linking Panel Name # Linking Panel Name
linking_panel_name = nodes.new("PlasmaAttribStringNode") linking_panel_name = nodes.new("PlasmaAttribStringNode")
linking_panel_name.value = self.link_destination if self.link_destination else self.age_name linking_panel_name.value = (
self.link_destination if self.link_destination else self.age_name
)
linking_panel_name.link_output(linkingnode, "pfm", "TargetAge") linking_panel_name.link_output(linkingnode, "pfm", "TargetAge")
def sanity_check(self): def sanity_check(self):
if self.clickable is None: if self.clickable is None:
raise ExportError("{}: Linking Book modifier requires a clickable!", self.id_data.name) raise ExportError(
"{}: Linking Book modifier requires a clickable!", self.id_data.name
)
if self.seek_point is None: if self.seek_point is None:
raise ExportError("{}: Linking Book modifier requires a seek point!", self.id_data.name) raise ExportError(
"{}: Linking Book modifier requires a seek point!", self.id_data.name
)

68
korman/properties/modifiers/logic.py

@ -22,15 +22,18 @@ from .base import PlasmaModifierProperties
from ...exporter import ExportError from ...exporter import ExportError
from ... import idprops from ... import idprops
class PlasmaVersionedNodeTree(idprops.IDPropMixin, bpy.types.PropertyGroup): class PlasmaVersionedNodeTree(idprops.IDPropMixin, bpy.types.PropertyGroup):
version = EnumProperty(name="Version", version = EnumProperty(
description="Plasma versions this node tree exports under", name="Version",
items=game_versions, description="Plasma versions this node tree exports under",
options={"ENUM_FLAG"}, items=game_versions,
default=set(list(zip(*game_versions))[0])) options={"ENUM_FLAG"},
node_tree = PointerProperty(name="Node Tree", default=set(list(zip(*game_versions))[0]),
description="Node Tree to export", )
type=bpy.types.NodeTree) node_tree = PointerProperty(
name="Node Tree", description="Node Tree to export", type=bpy.types.NodeTree
)
@classmethod @classmethod
def _idprop_mapping(cls): def _idprop_mapping(cls):
@ -57,7 +60,11 @@ class PlasmaAdvancedLogic(PlasmaModifierProperties):
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: if i.node_tree is None:
raise ExportError("'{}': Advanced Logic is missing a node tree for '{}'".format(bo.name, i.name)) raise ExportError(
"'{}': Advanced Logic is missing a node tree for '{}'".format(
bo.name, i.name
)
)
# Defer node tree export until all trees are harvested. # Defer node tree export until all trees are harvested.
exporter.want_node_trees[i.node_tree.name].add((bo, so)) exporter.want_node_trees[i.node_tree.name].add((bo, so))
@ -71,7 +78,9 @@ class PlasmaAdvancedLogic(PlasmaModifierProperties):
@property @property
def requires_actor(self): def requires_actor(self):
return any((i.node_tree.requires_actor for i in self.logic_groups if i.node_tree)) return any(
(i.node_tree.requires_actor for i in self.logic_groups if i.node_tree)
)
class PlasmaSpawnPoint(PlasmaModifierProperties): class PlasmaSpawnPoint(PlasmaModifierProperties):
@ -96,22 +105,37 @@ class PlasmaMaintainersMarker(PlasmaModifierProperties):
bl_category = "Logic" bl_category = "Logic"
bl_label = "Maintainer's Marker" bl_label = "Maintainer's Marker"
bl_description = "Designates an object as the D'ni coordinate origin point of the Age." bl_description = (
"Designates an object as the D'ni coordinate origin point of the Age."
)
bl_icon = "OUTLINER_DATA_EMPTY" bl_icon = "OUTLINER_DATA_EMPTY"
calibration = EnumProperty(name="Calibration", calibration = EnumProperty(
description="State of repair for the Marker", name="Calibration",
items=[ description="State of repair for the Marker",
("kBroken", "Broken", items=[
"A marker which reports scrambled coordinates to the KI."), (
("kRepaired", "Repaired", "kBroken",
"A marker which reports blank coordinates to the KI."), "Broken",
("kCalibrated", "Calibrated", "A marker which reports scrambled coordinates to the KI.",
"A marker which reports accurate coordinates to the KI.") ),
]) (
"kRepaired",
"Repaired",
"A marker which reports blank coordinates to the KI.",
),
(
"kCalibrated",
"Calibrated",
"A marker which reports accurate coordinates to the KI.",
),
],
)
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
maintmark = exporter.mgr.add_object(pl=plMaintainersMarkerModifier, so=so, name=self.key_name) maintmark = exporter.mgr.add_object(
pl=plMaintainersMarkerModifier, so=so, name=self.key_name
)
maintmark.calibration = getattr(plMaintainersMarkerModifier, self.calibration) maintmark.calibration = getattr(plMaintainersMarkerModifier, self.calibration)
@property @property

130
korman/properties/modifiers/physics.py

@ -27,7 +27,7 @@ bounds_types = (
("box", "Bounding Box", "Use a perfect bounding box"), ("box", "Bounding Box", "Use a perfect bounding box"),
("sphere", "Bounding Sphere", "Use a perfect bounding sphere"), ("sphere", "Bounding Sphere", "Use a perfect bounding sphere"),
("hull", "Convex Hull", "Use a convex set encompasing all vertices"), ("hull", "Convex Hull", "Use a convex set encompasing all vertices"),
("trimesh", "Triangle Mesh", "Use the exact triangle mesh (SLOW!)") ("trimesh", "Triangle Mesh", "Use the exact triangle mesh (SLOW!)"),
) )
# These are the collision sound surface types # These are the collision sound surface types
@ -49,12 +49,15 @@ surface_types = (
("kUser3", "User 3", ""), ("kUser3", "User 3", ""),
) )
def bounds_type_index(key): def bounds_type_index(key):
return list(zip(*bounds_types))[0].index(key) return list(zip(*bounds_types))[0].index(key)
def bounds_type_str(idx): def bounds_type_str(idx):
return bounds_types[idx][0] return bounds_types[idx][0]
def _set_phys_prop(prop, sim, phys, value=True): def _set_phys_prop(prop, sim, phys, value=True):
"""Sets properties on plGenericPhysical and plSimulationInterface (seeing as how they are duped)""" """Sets properties on plGenericPhysical and plSimulationInterface (seeing as how they are duped)"""
sim.setProperty(prop, value) sim.setProperty(prop, value)
@ -69,29 +72,58 @@ class PlasmaCollider(PlasmaModifierProperties):
bl_icon = "MOD_PHYSICS" bl_icon = "MOD_PHYSICS"
bl_description = "Simple physical collider" bl_description = "Simple physical collider"
bounds = EnumProperty(name="Bounds Type", description="", items=bounds_types, default="hull") bounds = EnumProperty(
name="Bounds Type", description="", items=bounds_types, default="hull"
)
avatar_blocker = BoolProperty(name="Blocks Avatars", description="Object blocks avatars", default=True) avatar_blocker = BoolProperty(
camera_blocker = BoolProperty(name="Blocks Camera LOS", description="Object blocks camera line-of-sight", default=True) name="Blocks Avatars", description="Object blocks avatars", default=True
)
camera_blocker = BoolProperty(
name="Blocks Camera LOS",
description="Object blocks camera line-of-sight",
default=True,
)
friction = FloatProperty(name="Friction", min=0.0, default=0.5) friction = FloatProperty(name="Friction", min=0.0, default=0.5)
restitution = FloatProperty(name="Restitution", description="Coefficient of collision elasticity", min=0.0, max=1.0) restitution = FloatProperty(
terrain = BoolProperty(name="Terrain", description="Object represents the ground", default=False) name="Restitution",
description="Coefficient of collision elasticity",
dynamic = BoolProperty(name="Dynamic", description="Object can be influenced by other objects (ie is kickable)", default=False) min=0.0,
mass = FloatProperty(name="Mass", description="Mass of object in pounds", min=0.0, default=1.0) max=1.0,
start_asleep = BoolProperty(name="Start Asleep", description="Object is not active until influenced by another object", default=False) )
terrain = BoolProperty(
proxy_object = PointerProperty(name="Proxy", name="Terrain", description="Object represents the ground", default=False
description="Object used as the collision geometry", )
type=bpy.types.Object,
poll=idprops.poll_mesh_objects) dynamic = BoolProperty(
name="Dynamic",
surface = EnumProperty(name="Surface Type", description="Object can be influenced by other objects (ie is kickable)",
description="Type of surface sound effect to play on collision", default=False,
items=surface_types, )
default="kNone", mass = FloatProperty(
options=set()) name="Mass", description="Mass of object in pounds", min=0.0, default=1.0
)
start_asleep = BoolProperty(
name="Start Asleep",
description="Object is not active until influenced by another object",
default=False,
)
proxy_object = PointerProperty(
name="Proxy",
description="Object used as the collision geometry",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects,
)
surface = EnumProperty(
name="Surface Type",
description="Type of surface sound effect to play on collision",
items=surface_types,
default="kNone",
options=set(),
)
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
# All modifier properties are examined by this little stinker... # All modifier properties are examined by this little stinker...
@ -110,17 +142,34 @@ class PlasmaSubworld(PlasmaModifierProperties):
bl_description = "Subworld definition" bl_description = "Subworld definition"
bl_icon = "WORLD" bl_icon = "WORLD"
sub_type = EnumProperty(name="Subworld Type", sub_type = EnumProperty(
description="Specifies the physics strategy to use for this subworld", name="Subworld Type",
items=[("auto", "Auto", "Korman will decide which physics strategy to use"), description="Specifies the physics strategy to use for this subworld",
("dynamicav", "Dynamic Avatar", "Allows the avatar to affected by dynamic physicals"), items=[
("subworld", "Separate World", "Causes all objects to be placed in a separate physics simulation")], ("auto", "Auto", "Korman will decide which physics strategy to use"),
default="auto", (
options=set()) "dynamicav",
gravity = FloatVectorProperty(name="Gravity", "Dynamic Avatar",
"Allows the avatar to affected by dynamic physicals",
),
(
"subworld",
"Separate World",
"Causes all objects to be placed in a separate physics simulation",
),
],
default="auto",
options=set(),
)
gravity = FloatVectorProperty(
name="Gravity",
description="Subworld's gravity defined in feet per second squared", description="Subworld's gravity defined in feet per second squared",
size=3, default=(0.0, 0.0, -32.174), precision=3, size=3,
subtype="ACCELERATION", unit="ACCELERATION") default=(0.0, 0.0, -32.174),
precision=3,
subtype="ACCELERATION",
unit="ACCELERATION",
)
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
if self.is_dedicated_subworld(exporter): if self.is_dedicated_subworld(exporter):
@ -147,9 +196,22 @@ class PlasmaSubworld(PlasmaModifierProperties):
# plCoordinateInterface::IGetRoot. Not really sure why this happens (nor do I care), # plCoordinateInterface::IGetRoot. Not really sure why this happens (nor do I care),
# but we definitely don't want it to happen. # but we definitely don't want it to happen.
if bo.type != "EMPTY": if bo.type != "EMPTY":
exporter.report.warn("Subworld '{}' is attached to a '{}'--this should be an empty.", bo.name, bo.type, indent=1) exporter.report.warn(
"Subworld '{}' is attached to a '{}'--this should be an empty.",
bo.name,
bo.type,
indent=1,
)
if so.sim: if so.sim:
if exporter.mgr.getVer() > pvPots: if exporter.mgr.getVer() > pvPots:
exporter.report.port("Subworld '{}' has physics data--this will cause PotS to crash.", bo.name, indent=1) exporter.report.port(
"Subworld '{}' has physics data--this will cause PotS to crash.",
bo.name,
indent=1,
)
else: else:
raise ExportError("Subworld '{}' cannot have physics data (should be an empty).".format(bo.name)) raise ExportError(
"Subworld '{}' cannot have physics data (should be an empty).".format(
bo.name
)
)

234
korman/properties/modifiers/region.py

@ -48,17 +48,20 @@ footstep_surface_ids = {
# 18 = swimming (why would you want this?) # 18 = swimming (why would you want this?)
} }
footstep_surfaces = [("dirt", "Dirt", "Dirt"), footstep_surfaces = [
("grass", "Grass", "Grass"), ("dirt", "Dirt", "Dirt"),
("metal", "Metal", "Metal Catwalk"), ("grass", "Grass", "Grass"),
("puddle", "Puddle", "Shallow Water"), ("metal", "Metal", "Metal Catwalk"),
("rope", "Rope", "Rope Ladder"), ("puddle", "Puddle", "Shallow Water"),
("rug", "Rug", "Carpet Rug"), ("rope", "Rope", "Rope Ladder"),
("stone", "Stone", "Stone Tile"), ("rug", "Rug", "Carpet Rug"),
("water", "Water", "Deep Water"), ("stone", "Stone", "Stone Tile"),
("woodbridge", "Wood Bridge", "Wood Bridge"), ("water", "Water", "Deep Water"),
("woodfloor", "Wood Floor", "Wood Floor"), ("woodbridge", "Wood Bridge", "Wood Bridge"),
("woodladder", "Wood Ladder", "Wood Ladder")] ("woodfloor", "Wood Floor", "Wood Floor"),
("woodladder", "Wood Ladder", "Wood Ladder"),
]
class PlasmaCameraRegion(PlasmaModifierProperties): class PlasmaCameraRegion(PlasmaModifierProperties):
pl_id = "camera_rgn" pl_id = "camera_rgn"
@ -68,24 +71,40 @@ class PlasmaCameraRegion(PlasmaModifierProperties):
bl_description = "Camera Region" bl_description = "Camera Region"
bl_icon = "CAMERA_DATA" bl_icon = "CAMERA_DATA"
camera_type = EnumProperty(name="Camera Type", camera_type = EnumProperty(
description="What kind of camera should be used?", name="Camera Type",
items=[("auto_follow", "Auto Follow Camera", "Automatically generated follow camera"), description="What kind of camera should be used?",
("manual", "Manual Camera", "User specified camera object")], items=[
default="manual", (
options=set()) "auto_follow",
camera_object = PointerProperty(name="Camera", "Auto Follow Camera",
description="Switches to this camera", "Automatically generated follow camera",
type=bpy.types.Object, ),
poll=idprops.poll_camera_objects, ("manual", "Manual Camera", "User specified camera object"),
options=set()) ],
default="manual",
options=set(),
)
camera_object = PointerProperty(
name="Camera",
description="Switches to this camera",
type=bpy.types.Object,
poll=idprops.poll_camera_objects,
options=set(),
)
auto_camera = PointerProperty(type=PlasmaCameraProperties, options=set()) auto_camera = PointerProperty(type=PlasmaCameraProperties, options=set())
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
if self.camera_type == "manual": if self.camera_type == "manual":
if self.camera_object is None: if self.camera_object is None:
raise ExportError("Camera Modifier '{}' does not specify a valid camera object".format(self.id_data.name)) raise ExportError(
camera_so_key = exporter.mgr.find_create_key(plSceneObject, bl=self.camera_object) "Camera Modifier '{}' does not specify a valid camera object".format(
self.id_data.name
)
)
camera_so_key = exporter.mgr.find_create_key(
plSceneObject, bl=self.camera_object
)
camera_props = self.camera_object.data.plasma_camera.settings camera_props = self.camera_object.data.plasma_camera.settings
else: else:
assert self.camera_type[:4] == "auto" assert self.camera_type[:4] == "auto"
@ -98,9 +117,13 @@ class PlasmaCameraRegion(PlasmaModifierProperties):
# Setup physical stuff # Setup physical stuff
phys_mod = bo.plasma_modifiers.collision phys_mod = bo.plasma_modifiers.collision
exporter.physics.generate_physical(bo, so, member_group="kGroupDetector", exporter.physics.generate_physical(
report_groups=["kGroupAvatar"], bo,
properties=["kPinned"]) so,
member_group="kGroupDetector",
report_groups=["kGroupAvatar"],
properties=["kPinned"],
)
# I don't feel evil enough to make this generate a logic tree... # I don't feel evil enough to make this generate a logic tree...
msg = plCameraMsg() msg = plCameraMsg()
@ -116,8 +139,14 @@ class PlasmaCameraRegion(PlasmaModifierProperties):
actors = set() actors = set()
if self.camera_type == "manual": if self.camera_type == "manual":
if self.camera_object is None: if self.camera_object is None:
raise ExportError("Camera Modifier '{}' does not specify a valid camera object".format(self.id_data.name)) raise ExportError(
actors.update(self.camera_object.data.plasma_camera.settings.harvest_actors()) "Camera Modifier '{}' does not specify a valid camera object".format(
self.id_data.name
)
)
actors.update(
self.camera_object.data.plasma_camera.settings.harvest_actors()
)
else: else:
actors.update(self.auto_camera.harvest_actors()) actors.update(self.auto_camera.harvest_actors())
return actors return actors
@ -134,14 +163,18 @@ class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierLogicWiz):
bl_label = "Footstep" bl_label = "Footstep"
bl_description = "Footstep Region" bl_description = "Footstep Region"
surface = EnumProperty(name="Surface", surface = EnumProperty(
description="What kind of surface are we walking on?", name="Surface",
items=footstep_surfaces, description="What kind of surface are we walking on?",
default="stone") items=footstep_surfaces,
bounds = EnumProperty(name="Region Bounds", default="stone",
description="Physical object's bounds", )
items=bounds_types, bounds = EnumProperty(
default="hull") name="Region Bounds",
description="Physical object's bounds",
items=bounds_types,
default="hull",
)
def logicwiz(self, bo, tree): def logicwiz(self, bo, tree):
nodes = tree.nodes nodes = tree.nodes
@ -178,13 +211,16 @@ class PlasmaPanicLinkRegion(PlasmaModifierProperties):
bl_label = "Panic Link" bl_label = "Panic Link"
bl_description = "Panic Link Region" bl_description = "Panic Link Region"
play_anim = BoolProperty(name="Play Animation", play_anim = BoolProperty(
description="Play the link-out animation when panic linking", name="Play Animation",
default=True) description="Play the link-out animation when panic linking",
default=True,
)
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
exporter.physics.generate_physical(bo, so, member_group="kGroupDetector", exporter.physics.generate_physical(
report_groups=["kGroupAvatar"]) bo, so, member_group="kGroupDetector", report_groups=["kGroupAvatar"]
)
# Finally, the panic link region proper # Finally, the panic link region proper
reg = exporter.mgr.add_object(plPanicLinkRegion, name=self.key_name, so=so) reg = exporter.mgr.add_object(plPanicLinkRegion, name=self.key_name, so=so)
@ -207,22 +243,38 @@ class PlasmaSoftVolume(idprops.IDPropMixin, PlasmaModifierProperties):
bl_description = "Soft-Boundary Region" bl_description = "Soft-Boundary Region"
# Advanced # Advanced
use_nodes = BoolProperty(name="Use Nodes", use_nodes = BoolProperty(
description="Make this a node-based Soft Volume", name="Use Nodes",
default=False) description="Make this a node-based Soft Volume",
node_tree = PointerProperty(name="Node Tree", default=False,
description="Node Tree detailing soft volume logic", )
type=bpy.types.NodeTree) node_tree = PointerProperty(
name="Node Tree",
description="Node Tree detailing soft volume logic",
type=bpy.types.NodeTree,
)
# Basic # Basic
invert = BoolProperty(name="Invert", invert = BoolProperty(name="Invert", description="Invert the soft region")
description="Invert the soft region") inside_strength = IntProperty(
inside_strength = IntProperty(name="Inside", description="Strength inside the region", name="Inside",
subtype="PERCENTAGE", default=100, min=0, max=100) description="Strength inside the region",
outside_strength = IntProperty(name="Outside", description="Strength outside the region", subtype="PERCENTAGE",
subtype="PERCENTAGE", default=0, min=0, max=100) default=100,
soft_distance = FloatProperty(name="Distance", description="Soft Distance", min=0,
default=0.0, min=0.0, max=500.0) max=100,
)
outside_strength = IntProperty(
name="Outside",
description="Strength outside the region",
subtype="PERCENTAGE",
default=0,
min=0,
max=100,
)
soft_distance = FloatProperty(
name="Distance", description="Soft Distance", default=0.0, min=0.0, max=500.0
)
def _apply_settings(self, sv): def _apply_settings(self, sv):
sv.insideStrength = self.inside_strength / 100.0 sv.insideStrength = self.inside_strength / 100.0
@ -237,7 +289,11 @@ class PlasmaSoftVolume(idprops.IDPropMixin, PlasmaModifierProperties):
tree = self.get_node_tree() tree = self.get_node_tree()
output = tree.find_output("PlasmaSoftVolumeOutputNode") 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, tree.name)) 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
@ -251,7 +307,11 @@ class PlasmaSoftVolume(idprops.IDPropMixin, PlasmaModifierProperties):
def _export_convex_region(self, exporter, bo, so): def _export_convex_region(self, exporter, bo, so):
if bo.type != "MESH": if bo.type != "MESH":
raise ExportError("SoftVolume '{}': Simple SoftVolumes can only be meshes!".format(bo.name)) raise ExportError(
"SoftVolume '{}': Simple SoftVolumes can only be meshes!".format(
bo.name
)
)
# Grab the SoftVolume KO # Grab the SoftVolume KO
sv = self.get_key(exporter, so).object sv = self.get_key(exporter, so).object
@ -296,7 +356,11 @@ class PlasmaSoftVolume(idprops.IDPropMixin, PlasmaModifierProperties):
def get_node_tree(self): def get_node_tree(self):
if self.node_tree is None: if self.node_tree is None:
raise ExportError("SoftVolume '{}' does not specify a valid Node Tree!".format(self.key_name)) raise ExportError(
"SoftVolume '{}' does not specify a valid Node Tree!".format(
self.key_name
)
)
return self.node_tree return self.node_tree
@classmethod @classmethod
@ -314,16 +378,22 @@ class PlasmaSubworldRegion(PlasmaModifierProperties):
bl_label = "Subworld Region" bl_label = "Subworld Region"
bl_description = "Subworld transition region" bl_description = "Subworld transition region"
subworld = PointerProperty(name="Subworld", subworld = PointerProperty(
description="Subworld to transition into", name="Subworld",
type=bpy.types.Object, description="Subworld to transition into",
poll=idprops.poll_subworld_objects) type=bpy.types.Object,
transition = EnumProperty(name="Transition", poll=idprops.poll_subworld_objects,
description="When to transition to the new subworld", )
items=[("enter", "On Enter", "Transition when the avatar enters the region"), transition = EnumProperty(
("exit", "On Exit", "Transition when the avatar exits the region")], name="Transition",
default="enter", description="When to transition to the new subworld",
options=set()) items=[
("enter", "On Enter", "Transition when the avatar enters the region"),
("exit", "On Exit", "Transition when the avatar exits the region"),
],
default="enter",
options=set(),
)
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
# Due to the fact that our subworld modifier can produce both RidingAnimatedPhysical # Due to the fact that our subworld modifier can produce both RidingAnimatedPhysical
@ -333,15 +403,22 @@ class PlasmaSubworldRegion(PlasmaModifierProperties):
from_isded = exporter.physics.is_dedicated_subworld(from_sub) from_isded = exporter.physics.is_dedicated_subworld(from_sub)
to_isded = exporter.physics.is_dedicated_subworld(to_sub) to_isded = exporter.physics.is_dedicated_subworld(to_sub)
if 1: if 1:
def get_log_text(bo, isded): def get_log_text(bo, isded):
main = "[Main World]" if bo is None else bo.name main = "[Main World]" if bo is None else bo.name
sub = "Subworld" if isded or bo is None else "RidingAnimatedPhysical" sub = "Subworld" if isded or bo is None else "RidingAnimatedPhysical"
return main, sub return main, sub
from_name, from_type = get_log_text(from_sub, from_isded) from_name, from_type = get_log_text(from_sub, from_isded)
to_name, to_type = get_log_text(to_sub, to_isded) to_name, to_type = get_log_text(to_sub, to_isded)
exporter.report.msg("Transition from '{}' ({}) to '{}' ({})", exporter.report.msg(
from_name, from_type, to_name, to_type, "Transition from '{}' ({}) to '{}' ({})",
indent=2) from_name,
from_type,
to_name,
to_type,
indent=2,
)
# I think the best solution here is to not worry about the excitement mentioned above. # I think the best solution here is to not worry about the excitement mentioned above.
# If we encounter anything truly interesting, we can fix it in CWE more easily IMO because # If we encounter anything truly interesting, we can fix it in CWE more easily IMO because
@ -353,7 +430,9 @@ class PlasmaSubworldRegion(PlasmaModifierProperties):
region.onExit = self.transition == "exit" region.onExit = self.transition == "exit"
else: else:
msg = plRideAnimatedPhysMsg() msg = plRideAnimatedPhysMsg()
msg.BCastFlags |= plMessage.kLocalPropagate | plMessage.kPropagateToModifiers msg.BCastFlags |= (
plMessage.kLocalPropagate | plMessage.kPropagateToModifiers
)
msg.sender = so.key msg.sender = so.key
msg.entering = to_sub is not None msg.entering = to_sub is not None
@ -362,7 +441,9 @@ class PlasmaSubworldRegion(PlasmaModifierProperties):
# reverts on region exit. We're going for an approach that is backwards compatible # reverts on region exit. We're going for an approach that is backwards compatible
# with subworlds, so our enter/exit regions are separate. Here, enter/exit message # with subworlds, so our enter/exit regions are separate. Here, enter/exit message
# corresponds with when we should trigger the transition. # corresponds with when we should trigger the transition.
region = exporter.mgr.find_create_object(plRidingAnimatedPhysicalDetector, so=so) region = exporter.mgr.find_create_object(
plRidingAnimatedPhysicalDetector, so=so
)
if self.transition == "enter": if self.transition == "enter":
region.enterMsg = msg region.enterMsg = msg
elif self.transition == "exit": elif self.transition == "exit":
@ -371,5 +452,6 @@ class PlasmaSubworldRegion(PlasmaModifierProperties):
raise ExportAssertionError() raise ExportAssertionError()
# Fancy pants region collider type shit # Fancy pants region collider type shit
exporter.physics.generate_physical(bo, so, member_group="kGroupDetector", exporter.physics.generate_physical(
report_groups=["kGroupAvatar"]) bo, so, member_group="kGroupDetector", report_groups=["kGroupAvatar"]
)

946
korman/properties/modifiers/render.py

File diff suppressed because it is too large Load Diff

467
korman/properties/modifiers/sound.py

@ -30,9 +30,10 @@ _randomsound_modes = {
"normal": plRandomSoundMod.kNormal, "normal": plRandomSoundMod.kNormal,
"norepeat": plRandomSoundMod.kNoRepeats, "norepeat": plRandomSoundMod.kNoRepeats,
"coverall": plRandomSoundMod.kCoverall | plRandomSoundMod.kNoRepeats, "coverall": plRandomSoundMod.kCoverall | plRandomSoundMod.kNoRepeats,
"sequential": plRandomSoundMod.kSequential "sequential": plRandomSoundMod.kSequential,
} }
class PlasmaRandomSound(PlasmaModifierProperties): class PlasmaRandomSound(PlasmaModifierProperties):
pl_id = "random_sound" pl_id = "random_sound"
pl_depends = {"soundemit"} pl_depends = {"soundemit"}
@ -41,55 +42,101 @@ class PlasmaRandomSound(PlasmaModifierProperties):
bl_label = "Random Sound" bl_label = "Random Sound"
bl_description = "" bl_description = ""
mode = EnumProperty(name="Mode", mode = EnumProperty(
description="Playback Type", name="Mode",
items=[("random", "Random Time", "Plays a random sound from the emitter at a random time"), description="Playback Type",
("collision", "Collision Surface", "Plays a random sound when the object's parent collides")], items=[
default="random", (
options=set()) "random",
"Random Time",
"Plays a random sound from the emitter at a random time",
),
(
"collision",
"Collision Surface",
"Plays a random sound when the object's parent collides",
),
],
default="random",
options=set(),
)
# Physical (read: collision) sounds # Physical (read: collision) sounds
play_on = EnumProperty(name="Play On", play_on = EnumProperty(
description="Play sounds on this collision event", name="Play On",
items=[("slide", "Slide", "Plays a random sound on object slide"), description="Play sounds on this collision event",
("impact", "Impact", "Plays a random sound on object slide")], items=[
options=set()) ("slide", "Slide", "Plays a random sound on object slide"),
surfaces = EnumProperty(name="Play Against", ("impact", "Impact", "Plays a random sound on object slide"),
description="Sounds are played on collision against these surfaces", ],
items=surface_types[1:], options=set(),
options={"ENUM_FLAG"}) )
surfaces = EnumProperty(
name="Play Against",
description="Sounds are played on collision against these surfaces",
items=surface_types[1:],
options={"ENUM_FLAG"},
)
# Timed random sounds # Timed random sounds
auto_start = BoolProperty(name="Auto Start", auto_start = BoolProperty(
description="Start playing when the Age loads", name="Auto Start",
default=True, description="Start playing when the Age loads",
options=set()) default=True,
play_mode = EnumProperty(name="Play Mode", options=set(),
description="", )
items=[("normal", "Any", "Plays any attached sound"), play_mode = EnumProperty(
("norepeat", "No Repeats", "Do not replay a sound immediately after itself"), name="Play Mode",
("coverall", "Full Set", "Once a sound is played, do not replay it until after all sounds are played"), description="",
("sequential", "Sequential", "Play sounds in the order they appear in the emitter")], items=[
default="norepeat", ("normal", "Any", "Plays any attached sound"),
options=set()) (
stop_after_set = BoolProperty(name="Stop After Set", "norepeat",
description="Stop playing after all sounds are played", "No Repeats",
default=False, "Do not replay a sound immediately after itself",
options=set()) ),
stop_after_play = BoolProperty(name="Stop After Play", (
description="Stop playing after one sound is played", "coverall",
default=False, "Full Set",
options=set()) "Once a sound is played, do not replay it until after all sounds are played",
min_delay = FloatProperty(name="Min Delay", ),
description="Minimum delay length", (
min=0.0, "sequential",
subtype="TIME", unit="TIME", "Sequential",
options=set()) "Play sounds in the order they appear in the emitter",
max_delay = FloatProperty(name="Max Delay", ),
description="Maximum delay length", ],
min=0.0, default="norepeat",
subtype="TIME", unit="TIME", options=set(),
options=set()) )
stop_after_set = BoolProperty(
name="Stop After Set",
description="Stop playing after all sounds are played",
default=False,
options=set(),
)
stop_after_play = BoolProperty(
name="Stop After Play",
description="Stop playing after one sound is played",
default=False,
options=set(),
)
min_delay = FloatProperty(
name="Min Delay",
description="Minimum delay length",
min=0.0,
subtype="TIME",
unit="TIME",
options=set(),
)
max_delay = FloatProperty(
name="Max Delay",
description="Maximum delay length",
min=0.0,
subtype="TIME",
unit="TIME",
options=set(),
)
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
rndmod = exporter.mgr.find_create_object(plRandomSoundMod, bl=bo, so=so) rndmod = exporter.mgr.find_create_object(plRandomSoundMod, bl=bo, so=so)
@ -116,15 +163,23 @@ class PlasmaRandomSound(PlasmaModifierProperties):
if self.mode == "collision" and self.surfaces: if self.mode == "collision" and self.surfaces:
parent_bo = bo.parent parent_bo = bo.parent
if parent_bo is None: if parent_bo is None:
raise ExportError("[{}]: Collision sound objects MUST be parented directly to the collider object.", bo.name) raise ExportError(
"[{}]: Collision sound objects MUST be parented directly to the collider object.",
bo.name,
)
phys = exporter.mgr.find_object(plGenericPhysical, bl=parent_bo) phys = exporter.mgr.find_object(plGenericPhysical, bl=parent_bo)
if phys is None: if phys is None:
raise ExportError("[{}]: Collision sound objects MUST be parented directly to the collider object.", bo.name) raise ExportError(
"[{}]: Collision sound objects MUST be parented directly to the collider object.",
bo.name,
)
# The soundGroup on the physical may or may not be the generic "this is my surface type" # The soundGroup on the physical may or may not be the generic "this is my surface type"
# soundGroup with no actual sounds attached. So, we need to lookup the actual one. # soundGroup with no actual sounds attached. So, we need to lookup the actual one.
sndgroup = exporter.mgr.find_create_object(plPhysicalSndGroup, bl=parent_bo) sndgroup = exporter.mgr.find_create_object(plPhysicalSndGroup, bl=parent_bo)
sndgroup.group = getattr(plPhysicalSndGroup, parent_bo.plasma_modifiers.collision.surface) sndgroup.group = getattr(
plPhysicalSndGroup, parent_bo.plasma_modifiers.collision.surface
)
phys.soundGroup = sndgroup.key phys.soundGroup = sndgroup.key
rndmod = exporter.mgr.find_key(plRandomSoundMod, bl=bo, so=so) rndmod = exporter.mgr.find_key(plRandomSoundMod, bl=bo, so=so)
@ -135,32 +190,55 @@ class PlasmaRandomSound(PlasmaModifierProperties):
else: else:
raise RuntimeError() raise RuntimeError()
sounds = { i: sound for i, sound in enumerate(getattr(sndgroup, groupattr)) } sounds = {i: sound for i, sound in enumerate(getattr(sndgroup, groupattr))}
for surface_name in self.surfaces: for surface_name in self.surfaces:
surface_id = getattr(plPhysicalSndGroup, surface_name) surface_id = getattr(plPhysicalSndGroup, surface_name)
if surface_id in sounds: if surface_id in sounds:
exporter.report.warn("Overwriting physical {} surface '{}' ID:{}", exporter.report.warn(
groupattr, surface_name, surface_id, indent=2) "Overwriting physical {} surface '{}' ID:{}",
groupattr,
surface_name,
surface_id,
indent=2,
)
else: else:
exporter.report.msg("Got physical {} surface '{}' ID:{}", exporter.report.msg(
groupattr, surface_name, surface_id, indent=2) "Got physical {} surface '{}' ID:{}",
groupattr,
surface_name,
surface_id,
indent=2,
)
sounds[surface_id] = rndmod sounds[surface_id] = rndmod
# Keeps the LUT (or should that be lookup vector?) as small as possible # Keeps the LUT (or should that be lookup vector?) as small as possible
setattr(sndgroup, groupattr, [sounds.get(i) for i in range(max(sounds.keys()) + 1)]) setattr(
sndgroup,
groupattr,
[sounds.get(i) for i in range(max(sounds.keys()) + 1)],
)
class PlasmaSfxFade(bpy.types.PropertyGroup): class PlasmaSfxFade(bpy.types.PropertyGroup):
fade_type = EnumProperty(name="Type", fade_type = EnumProperty(
description="Fade Type", name="Type",
items=[("NONE", "[Disable]", "Don't fade"), description="Fade Type",
("kLinear", "Linear", "Linear fade"), items=[
("kLogarithmic", "Logarithmic", "Log fade"), ("NONE", "[Disable]", "Don't fade"),
("kExponential", "Exponential", "Exponential fade")], ("kLinear", "Linear", "Linear fade"),
options=set()) ("kLogarithmic", "Logarithmic", "Log fade"),
length = FloatProperty(name="Length", ("kExponential", "Exponential", "Exponential fade"),
description="Seconds to spend fading", ],
default=1.0, min=0.0, options=set(),
options=set(), subtype="TIME", unit="TIME") )
length = FloatProperty(
name="Length",
description="Seconds to spend fading",
default=1.0,
min=0.0,
options=set(),
subtype="TIME",
unit="TIME",
)
class PlasmaSound(idprops.IDPropMixin, bpy.types.PropertyGroup): class PlasmaSound(idprops.IDPropMixin, bpy.types.PropertyGroup):
@ -195,86 +273,125 @@ class PlasmaSound(idprops.IDPropMixin, bpy.types.PropertyGroup):
def _update_name(self, context=None): 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_name, "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_name self.name = self._sound_name
enabled = BoolProperty(name="Enabled", default=True, options=set()) enabled = BoolProperty(name="Enabled", default=True, options=set())
sound = PointerProperty(name="Sound", sound = PointerProperty(
description="Sound Datablock", name="Sound",
type=bpy.types.Sound, description="Sound Datablock",
update=_update_sound) type=bpy.types.Sound,
updating_sound = BoolProperty(default=False, update=_update_sound,
options={"HIDDEN", "SKIP_SAVE"}) )
updating_sound = BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"})
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"})
sfx_region = PointerProperty(name="Soft Volume", sfx_region = PointerProperty(
description="Soft region this sound can be heard in", name="Soft Volume",
type=bpy.types.Object, description="Soft region this sound can be heard in",
poll=idprops.poll_softvolume_objects) type=bpy.types.Object,
poll=idprops.poll_softvolume_objects,
sfx_type = EnumProperty(name="Category", )
description="Describes the purpose of this sound",
items=[("kSoundFX", "3D", "3D Positional SoundFX"), sfx_type = EnumProperty(
("kAmbience", "Ambience", "Ambient Sounds"), name="Category",
("kBackgroundMusic", "Music", "Background Music"), description="Describes the purpose of this sound",
("kGUISound", "GUI", "GUI Effect"), items=[
("kNPCVoices", "NPC", "NPC Speech")], ("kSoundFX", "3D", "3D Positional SoundFX"),
options=set()) ("kAmbience", "Ambience", "Ambient Sounds"),
channel = EnumProperty(name="Channel", ("kBackgroundMusic", "Music", "Background Music"),
description="Which channel(s) to play", ("kGUISound", "GUI", "GUI Effect"),
items=[("L", "Left", "Left Channel"), ("kNPCVoices", "NPC", "NPC Speech"),
("R", "Right", "Right Channel")], ],
options={"ENUM_FLAG"}, options=set(),
default={"L", "R"}, )
update=_update_name) channel = EnumProperty(
name="Channel",
auto_start = BoolProperty(name="Auto Start", description="Which channel(s) to play",
description="Start playing when the age is loaded", items=[("L", "Left", "Left Channel"), ("R", "Right", "Right Channel")],
default=False, options={"ENUM_FLAG"},
options=set()) default={"L", "R"},
incidental = BoolProperty(name="Incidental", update=_update_name,
description="Sound is a low-priority incident and the engine may forgo playback", )
default=False,
options=set()) auto_start = BoolProperty(
loop = BoolProperty(name="Loop", name="Auto Start",
description="Loop the sound", description="Start playing when the age is loaded",
default=False, default=False,
options=set()) options=set(),
)
inner_cone = FloatProperty(name="Inner Angle", incidental = BoolProperty(
description="Angle of the inner cone from the negative Z-axis", name="Incidental",
min=0, max=math.radians(360), default=0, step=100, description="Sound is a low-priority incident and the engine may forgo playback",
options=set(), default=False,
subtype="ANGLE") options=set(),
outer_cone = FloatProperty(name="Outer Angle", )
description="Angle of the outer cone from the negative Z-axis", loop = BoolProperty(
min=0, max=math.radians(360), default=math.radians(360), step=100, name="Loop", description="Loop the sound", default=False, options=set()
options=set(), )
subtype="ANGLE")
outside_volume = IntProperty(name="Outside Volume", inner_cone = FloatProperty(
description="Sound's volume when outside the outer cone", name="Inner Angle",
min=0, max=100, default=100, description="Angle of the inner cone from the negative Z-axis",
options=set(), min=0,
subtype="PERCENTAGE") max=math.radians(360),
default=0,
min_falloff = IntProperty(name="Begin Falloff", step=100,
description="Distance where volume attenuation begins", options=set(),
min=0, max=1000000000, default=1, subtype="ANGLE",
options=set(), )
subtype="DISTANCE") outer_cone = FloatProperty(
max_falloff = IntProperty(name="End Falloff", name="Outer Angle",
description="Distance where the sound is inaudible", description="Angle of the outer cone from the negative Z-axis",
min=0, max=1000000000, default=1000, min=0,
options=set(), max=math.radians(360),
subtype="DISTANCE") default=math.radians(360),
volume = IntProperty(name="Volume", step=100,
description="Volume to play the sound", options=set(),
min=0, max=100, default=100, subtype="ANGLE",
options={"ANIMATABLE"}, )
subtype="PERCENTAGE") outside_volume = IntProperty(
name="Outside Volume",
description="Sound's volume when outside the outer cone",
min=0,
max=100,
default=100,
options=set(),
subtype="PERCENTAGE",
)
min_falloff = IntProperty(
name="Begin Falloff",
description="Distance where volume attenuation begins",
min=0,
max=1000000000,
default=1,
options=set(),
subtype="DISTANCE",
)
max_falloff = IntProperty(
name="End Falloff",
description="Distance where the sound is inaudible",
min=0,
max=1000000000,
default=1000,
options=set(),
subtype="DISTANCE",
)
volume = IntProperty(
name="Volume",
description="Volume to play the sound",
min=0,
max=100,
default=100,
options={"ANIMATABLE"},
subtype="PERCENTAGE",
)
fade_in = PointerProperty(type=PlasmaSfxFade, options=set()) fade_in = PointerProperty(type=PlasmaSfxFade, options=set())
fade_out = PointerProperty(type=PlasmaSfxFade, options=set()) fade_out = PointerProperty(type=PlasmaSfxFade, options=set())
@ -291,10 +408,13 @@ class PlasmaSound(idprops.IDPropMixin, bpy.types.PropertyGroup):
# This is really a property of the sound itself, not of this particular emitter instance. # This is really a property of the sound itself, not of this particular emitter instance.
# However, to prevent weird UI inconsistencies where the button might be missing or change # However, to prevent weird UI inconsistencies where the button might be missing or change
# states when clearing the sound pointer, we'll cache the actual value here. # states when clearing the sound pointer, we'll cache the actual value here.
package = BoolProperty(name="Export", package = BoolProperty(
description="Package this file in the age export", name="Export",
get=_get_package_value, set=_set_package_value, description="Package this file in the age export",
options=set()) get=_get_package_value,
set=_set_package_value,
options=set(),
)
package_value = BoolProperty(options={"HIDDEN", "SKIP_SAVE"}) package_value = BoolProperty(options={"HIDDEN", "SKIP_SAVE"})
@property @property
@ -331,10 +451,23 @@ class PlasmaSound(idprops.IDPropMixin, bpy.types.PropertyGroup):
header.blockAlign = int(header.blockAlign / 2) header.blockAlign = int(header.blockAlign / 2)
dataSize = int(dataSize / 2) dataSize = int(dataSize / 2)
if self.is_3d_stereo: if self.is_3d_stereo:
audible.addSound(self._convert_sound(exporter, so, pClass, header, dataSize, channel="L")) audible.addSound(
audible.addSound(self._convert_sound(exporter, so, pClass, header, dataSize, channel="R")) self._convert_sound(exporter, so, pClass, header, dataSize, channel="L")
)
audible.addSound(
self._convert_sound(exporter, so, pClass, header, dataSize, channel="R")
)
else: else:
audible.addSound(self._convert_sound(exporter, so, pClass, header, dataSize, channel=self.channel_override)) audible.addSound(
self._convert_sound(
exporter,
so,
pClass,
header,
dataSize,
channel=self.channel_override,
)
)
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:
@ -352,10 +485,18 @@ class PlasmaSound(idprops.IDPropMixin, bpy.types.PropertyGroup):
elif self.sfx_region: elif self.sfx_region:
sv_mod = self.sfx_region.plasma_modifiers.softvolume sv_mod = self.sfx_region.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_name, self.sfx_region.name)) 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
)
sound.softRegion = sv_key sound.softRegion = sv_key
# Sound # Sound
@ -368,7 +509,9 @@ class PlasmaSound(idprops.IDPropMixin, bpy.types.PropertyGroup):
sound.properties |= plSound.kPropLooping sound.properties |= plSound.kPropLooping
if self.incidental: if self.incidental:
sound.properties |= plSound.kPropIncidental sound.properties |= plSound.kPropIncidental
sound.dataBuffer = self._find_sound_buffer(exporter, so, wavHeader, dataSize, channel) sound.dataBuffer = self._find_sound_buffer(
exporter, so, wavHeader, dataSize, channel
)
# Cone effect # Cone effect
# I have observed that Blender 2.77's UI doesn't show the appropriate unit (degrees) for # I have observed that Blender 2.77's UI doesn't show the appropriate unit (degrees) for
@ -473,20 +616,26 @@ class PlasmaSound(idprops.IDPropMixin, bpy.types.PropertyGroup):
@classmethod @classmethod
def _idprop_mapping(cls): def _idprop_mapping(cls):
return {"sound": "sound_data", return {"sound": "sound_data", "sfx_region": "soft_region"}
"sfx_region": "soft_region"}
def _idprop_sources(self): def _idprop_sources(self):
return {"sound_data": bpy.data.sounds, return {"sound_data": bpy.data.sounds, "soft_region": bpy.data.objects}
"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):
if self.sound: if self.sound:
raise ExportError("SoundEmitter '{}': Sound '{}' {}".format(self.id_data.name, self.sound.name, msg)) raise ExportError(
"SoundEmitter '{}': Sound '{}' {}".format(
self.id_data.name, self.sound.name, msg
)
)
else: else:
raise ExportError("SoundEmitter '{}': {}".format(self.id_data.name, msg)) raise ExportError("SoundEmitter '{}': {}".format(self.id_data.name, msg))
@ -515,9 +664,13 @@ class PlasmaSoundEmitter(PlasmaModifierProperties):
active_sound_index = IntProperty(options={"HIDDEN"}) active_sound_index = IntProperty(options={"HIDDEN"})
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
winaud = exporter.mgr.find_create_object(plWinAudible, so=so, name=self.key_name) winaud = exporter.mgr.find_create_object(
plWinAudible, so=so, name=self.key_name
)
winaud.sceneNode = exporter.mgr.get_scene_node(so.key.location) winaud.sceneNode = exporter.mgr.get_scene_node(so.key.location)
aiface = exporter.mgr.find_create_object(plAudioInterface, so=so, name=self.key_name) aiface = exporter.mgr.find_create_object(
plAudioInterface, so=so, name=self.key_name
)
aiface.audible = winaud.key aiface.audible = winaud.key
# Pass this off to each individual sound for conversion # Pass this off to each individual sound for conversion
@ -527,7 +680,7 @@ class PlasmaSoundEmitter(PlasmaModifierProperties):
def get_sound_indices(self, name=None, sound=None): def get_sound_indices(self, name=None, sound=None):
"""Returns the index of the given sound in the plWin32Sound. This is needed because stereo """Returns the index of the given sound in the plWin32Sound. This is needed because stereo
3D sounds export as two mono sound objects -- wheeeeee""" 3D sounds export as two mono sound objects -- wheeeeee"""
assert name or sound assert name or sound
idx = 0 idx = 0

498
korman/properties/modifiers/water.py

@ -22,7 +22,10 @@ from .base import PlasmaModifierProperties
from ...exporter import ExportError, ExportAssertionError from ...exporter import ExportError, ExportAssertionError
from ... import idprops from ... import idprops
class PlasmaSwimRegion(idprops.IDPropObjectMixin, PlasmaModifierProperties, bpy.types.PropertyGroup):
class PlasmaSwimRegion(
idprops.IDPropObjectMixin, PlasmaModifierProperties, bpy.types.PropertyGroup
):
pl_id = "swimregion" pl_id = "swimregion"
bl_category = "Water" bl_category = "Water"
@ -36,54 +39,94 @@ class PlasmaSwimRegion(idprops.IDPropObjectMixin, PlasmaModifierProperties, bpy.
"STRAIGHT": plSwimStraightCurrentRegion, "STRAIGHT": plSwimStraightCurrentRegion,
} }
region = PointerProperty(name="Region", region = PointerProperty(
description="Swimming detector region", name="Region",
type=bpy.types.Object, description="Swimming detector region",
poll=idprops.poll_mesh_objects) type=bpy.types.Object,
poll=idprops.poll_mesh_objects,
down_buoyancy = FloatProperty(name="Downward Buoyancy", )
description="Distance the avatar sinks into the water",
min=0.0, max=100.0, default=3.0, down_buoyancy = FloatProperty(
options=set()) name="Downward Buoyancy",
up_buoyancy = FloatProperty(name="Up Buoyancy", description="Distance the avatar sinks into the water",
description="Distance the avatar rises up after sinking", min=0.0,
min=0.0, max=100.0, default=0.05, max=100.0,
options=set()) default=3.0,
up_velocity = FloatProperty(name="Up Velcocity", options=set(),
description="Rate at which the avatar rises", )
min=0.0, max=100.0, default=3.0, up_buoyancy = FloatProperty(
options=set()) name="Up Buoyancy",
description="Distance the avatar rises up after sinking",
current_type = EnumProperty(name="Water Current", min=0.0,
description="", max=100.0,
items=[("NONE", "None", "No current"), default=0.05,
("CIRCULAR", "Circular", "Circular current"), options=set(),
("STRAIGHT", "Straight", "Straight current")], )
options=set()) up_velocity = FloatProperty(
rotation = FloatProperty(name="Rotation", name="Up Velcocity",
description="Rate of rotation about the current object", description="Rate at which the avatar rises",
min=-100.0, max=100.0, default=1.0, min=0.0,
options=set()) max=100.0,
near_distance = FloatProperty(name="Near Distance", default=3.0,
description="Maximum distance at which the current is at the Near Velocity rate", options=set(),
min=0.0, max=10000.0, default=1.0, )
options=set())
far_distance = FloatProperty(name="Far Distance", current_type = EnumProperty(
description="Distance at which the current is at the Far Velocity rate", name="Water Current",
min=0.0, max=10000.0, default=1.0, description="",
options=set()) items=[
near_velocity = FloatProperty(name="Near Velocity", ("NONE", "None", "No current"),
description="Current velocity near the region center", ("CIRCULAR", "Circular", "Circular current"),
min=-100.0, max=100.0, default=0.0, ("STRAIGHT", "Straight", "Straight current"),
options=set()) ],
far_velocity = FloatProperty(name="Far Velocity", options=set(),
description="Current velocity far from the region center", )
min=-100.0, max=100.0, default=0.0, rotation = FloatProperty(
options=set()) name="Rotation",
current = PointerProperty(name="Current Object", description="Rate of rotation about the current object",
description="Object whose Y-axis defines the direction of the current", min=-100.0,
type=bpy.types.Object, max=100.0,
poll=idprops.poll_empty_objects) default=1.0,
options=set(),
)
near_distance = FloatProperty(
name="Near Distance",
description="Maximum distance at which the current is at the Near Velocity rate",
min=0.0,
max=10000.0,
default=1.0,
options=set(),
)
far_distance = FloatProperty(
name="Far Distance",
description="Distance at which the current is at the Far Velocity rate",
min=0.0,
max=10000.0,
default=1.0,
options=set(),
)
near_velocity = FloatProperty(
name="Near Velocity",
description="Current velocity near the region center",
min=-100.0,
max=100.0,
default=0.0,
options=set(),
)
far_velocity = FloatProperty(
name="Far Velocity",
description="Current velocity far from the region center",
min=-100.0,
max=100.0,
default=0.0,
options=set(),
)
current = PointerProperty(
name="Current Object",
description="Object whose Y-axis defines the direction of the current",
type=bpy.types.Object,
poll=idprops.poll_empty_objects,
)
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
swimIface = self.get_key(exporter, so).object swimIface = self.get_key(exporter, so).object
@ -101,10 +144,18 @@ class PlasmaSwimRegion(idprops.IDPropObjectMixin, PlasmaModifierProperties, bpy.
swimIface.farDist = self.far_distance swimIface.farDist = self.far_distance
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 self.current is None: if self.current is None:
raise ExportError("Swimming Surface '{}' does not specify a current object".format(bo.name)) raise ExportError(
swimIface.currentObj = exporter.mgr.find_create_key(plSceneObject, bl=self.current) "Swimming Surface '{}' does not specify a current object".format(
bo.name
)
)
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... # 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
@ -112,25 +163,38 @@ class PlasmaSwimRegion(idprops.IDPropObjectMixin, PlasmaModifierProperties, bpy.
# pool. Therefore, we need to flatten out a temporary mesh in that case. # pool. Therefore, we need to flatten out a temporary mesh in that case.
# Ohey! CWE doesn't let you swim at all if the surface isn't flat... # Ohey! CWE doesn't let you swim at all if the surface isn't flat...
losdbs = ["kLOSDBSwimRegion"] losdbs = ["kLOSDBSwimRegion"]
member_group = "kGroupLOSOnly" if exporter.mgr.getVer() != pvMoul else "kGroupStatic" member_group = (
"kGroupLOSOnly" if exporter.mgr.getVer() != pvMoul else "kGroupStatic"
)
if bo.plasma_modifiers.water_basic.enabled: if bo.plasma_modifiers.water_basic.enabled:
exporter.physics.generate_flat_proxy(bo, so, z_coord=bo.matrix_world.translation[2], exporter.physics.generate_flat_proxy(
member_group=member_group, bo,
losdbs=losdbs) so,
z_coord=bo.matrix_world.translation[2],
member_group=member_group,
losdbs=losdbs,
)
else: else:
exporter.physics.generate_physical(bo, so, bounds="trimesh", exporter.physics.generate_physical(
member_group=member_group, bo, so, bounds="trimesh", member_group=member_group, losdbs=losdbs
losdbs=losdbs) )
# Detector region bounds # Detector region bounds
if self.region is not None: if self.region is not None:
region_so = exporter.mgr.find_create_object(plSceneObject, bl=self.region) 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 # Good news: if this phys has already been exported, this is basically a noop
member_group = "kGroupDetector" if exporter.mgr.getVer() == "pvMoul" else "kGroupLOSOnly" member_group = (
exporter.physics.generate_physical(self.region, region_so, "kGroupDetector"
member_group=member_group, if exporter.mgr.getVer() == "pvMoul"
report_groups=["kGroupAvatar"]) else "kGroupLOSOnly"
)
exporter.physics.generate_physical(
self.region,
region_so,
member_group=member_group,
report_groups=["kGroupAvatar"],
)
# I am a little concerned if we already have a plSwimDetector... I am not certain how # I am a little concerned if we already have a plSwimDetector... I am not certain how
# well Plasma would tolerate having a plSwimMsg with multiple regions referenced. # well Plasma would tolerate having a plSwimMsg with multiple regions referenced.
@ -141,7 +205,9 @@ class PlasmaSwimRegion(idprops.IDPropObjectMixin, PlasmaModifierProperties, bpy.
if exporter.mgr.find_key(plSwimDetector, so=region_so) is None: if exporter.mgr.find_key(plSwimDetector, so=region_so) is None:
enter_msg, exit_msg = plSwimMsg(), plSwimMsg() enter_msg, exit_msg = plSwimMsg(), plSwimMsg()
for i in (enter_msg, exit_msg): for i in (enter_msg, exit_msg):
i.BCastFlags = plMessage.kLocalPropagate | plMessage.kPropagateToModifiers i.BCastFlags = (
plMessage.kLocalPropagate | plMessage.kPropagateToModifiers
)
i.sender = region_so.key i.sender = region_so.key
i.swimRegion = swimIface.key i.swimRegion = swimIface.key
enter_msg.isEntering = True enter_msg.isEntering = True
@ -156,7 +222,12 @@ class PlasmaSwimRegion(idprops.IDPropObjectMixin, PlasmaModifierProperties, bpy.
# swimming surface should have a detector. m'kay? But still, we might want to make note # swimming surface should have a detector. m'kay? But still, we might want to make note
# of this sitation. Just in case someone is like "WTF! Why am I not swimming?!?!1111111" # of this sitation. Just in case someone is like "WTF! Why am I not swimming?!?!1111111"
# Because you need to have a detector, dummy. # Because you need to have a detector, dummy.
exporter.report.warn("Swimming Surface '{}' does not specify a detector region".format(bo.name), indent=2) exporter.report.warn(
"Swimming Surface '{}' does not specify a detector region".format(
bo.name
),
indent=2,
)
def get_key(self, exporter, so=None): def get_key(self, exporter, so=None):
pClass = self._CURRENTS[self.current_type] pClass = self._CURRENTS[self.current_type]
@ -169,72 +240,66 @@ class PlasmaSwimRegion(idprops.IDPropObjectMixin, PlasmaModifierProperties, bpy.
@classmethod @classmethod
def _idprop_mapping(cls): def _idprop_mapping(cls):
return {"current": "current_object", return {"current": "current_object", "region": "region_name"}
"region": "region_name"}
class PlasmaWaterModifier(idprops.IDPropMixin, 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 = PointerProperty(name="Wind Object", wind_object = PointerProperty(
description="Object whose Y axis represents the wind direction", name="Wind Object",
type=bpy.types.Object, description="Object whose Y axis represents the wind direction",
poll=idprops.poll_empty_objects) type=bpy.types.Object,
wind_speed = FloatProperty(name="Wind Speed", poll=idprops.poll_empty_objects,
description="Magnitude of the wind", )
default=1.0) wind_speed = FloatProperty(
envmap = PointerProperty(name="EnvMap", name="Wind Speed", description="Magnitude of the wind", default=1.0
description="Texture defining an environment map for this water object", )
type=bpy.types.Texture, envmap = PointerProperty(
poll=idprops.poll_envmap_textures) name="EnvMap",
envmap_radius = FloatProperty(name="Environment Sphere Radius", description="Texture defining an environment map for this water object",
description="How far away the first object you want to see is", type=bpy.types.Texture,
min=5.0, max=10000.0, poll=idprops.poll_envmap_textures,
default=500.0) )
envmap_radius = FloatProperty(
specular_tint = FloatVectorProperty(name="Specular Tint", name="Environment Sphere Radius",
subtype="COLOR", description="How far away the first object you want to see is",
min=0.0, max=1.0, min=5.0,
default=(1.0, 1.0, 1.0)) max=10000.0,
specular_alpha = FloatProperty(name="Specular Alpha", default=500.0,
min=0.0, max=1.0, )
default=0.3)
noise = IntProperty(name="Noise", specular_tint = FloatVectorProperty(
subtype="PERCENTAGE", name="Specular Tint", subtype="COLOR", min=0.0, max=1.0, default=(1.0, 1.0, 1.0)
min=0, max=300, )
default=50) specular_alpha = FloatProperty(name="Specular Alpha", min=0.0, max=1.0, default=0.3)
specular_start = FloatProperty(name="Specular Start", noise = IntProperty(name="Noise", subtype="PERCENTAGE", min=0, max=300, default=50)
min=0.0, max=1000.0, specular_start = FloatProperty(
default=50.0) name="Specular Start", min=0.0, max=1000.0, default=50.0
specular_end = FloatProperty(name="Specular End", )
min=0.0, max=10000.0, specular_end = FloatProperty(
default=1000.0) name="Specular End", min=0.0, max=10000.0, default=1000.0
ripple_scale = FloatProperty(name="Ripple Scale", )
min=5.0, max=1000.0, ripple_scale = FloatProperty(name="Ripple Scale", min=5.0, max=1000.0, default=25.0)
default=25.0)
depth_opacity = FloatProperty(name="Opacity End", min=0.5, max=20.0, default=3.0)
depth_opacity = FloatProperty(name="Opacity End", depth_reflection = FloatProperty(
min=0.5, max=20.0, name="Reflection End", min=0.5, max=20.0, default=3.0
default=3.0) )
depth_reflection = FloatProperty(name="Reflection End", depth_wave = FloatProperty(name="Wave End", min=0.5, max=20.0, default=4.0)
min=0.5, max=20.0, zero_opacity = FloatProperty(
default=3.0) name="Opacity Start", min=-10.0, max=10.0, default=-1.0
depth_wave = FloatProperty(name="Wave End", )
min=0.5, max=20.0, zero_reflection = FloatProperty(
default=4.0) name="Reflection Start", min=-10.0, max=10.0, default=0.0
zero_opacity = FloatProperty(name="Opacity Start", )
min=-10.0, max=10.0, zero_wave = FloatProperty(name="Wave Start", min=-10.0, max=10.0, default=0.0)
default=-1.0)
zero_reflection = FloatProperty(name="Reflection Start",
min=-10.0, max=10.0,
default=0.0)
zero_wave = FloatProperty(name="Wave Start",
min=-10.0, max=10.0,
default=0.0)
@property @property
def copy_material(self): def copy_material(self):
@ -243,14 +308,21 @@ class PlasmaWaterModifier(idprops.IDPropMixin, PlasmaModifierProperties, bpy.typ
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: if self.wind_object:
if self.wind_object.plasma_object.enabled and self.wind_object.plasma_modifiers.animation.enabled: if (
waveset.refObj = exporter.mgr.find_create_key(plSceneObject, bl=self.wind_object) 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) waveset.setFlag(plWaveSet7.kHasRefObject, True)
# This is much like what happened in PyPRP # This is much like what happened in PyPRP
speed = self.wind_speed speed = self.wind_speed
matrix = self.wind_object.matrix_world matrix = self.wind_object.matrix_world
wind_dir = hsVector3(matrix[1][0] * speed, matrix[1][1] * speed, matrix[1][2] * speed) wind_dir = hsVector3(
matrix[1][0] * speed, matrix[1][1] * speed, matrix[1][2] * speed
)
else: else:
# Stolen shamelessly from PyPRP # Stolen shamelessly from PyPRP
wind_dir = hsVector3(0.0871562, 0.996195, 0.0) wind_dir = hsVector3(0.0871562, 0.996195, 0.0)
@ -260,15 +332,23 @@ class PlasmaWaterModifier(idprops.IDPropMixin, PlasmaModifierProperties, bpy.typ
state.rippleScale = self.ripple_scale state.rippleScale = self.ripple_scale
state.waterHeight = bo.matrix_world.translation[2] state.waterHeight = bo.matrix_world.translation[2]
state.windDir = wind_dir state.windDir = wind_dir
state.specVector = hsVector3(self.noise / 100.0, self.specular_start, self.specular_end) state.specVector = hsVector3(
self.noise / 100.0, self.specular_start, self.specular_end
)
state.specularTint = hsColorRGBA(*self.specular_tint, alpha=self.specular_alpha) state.specularTint = hsColorRGBA(*self.specular_tint, alpha=self.specular_alpha)
state.waterOffset = hsVector3(self.zero_opacity * -1.0, self.zero_reflection * -1.0, self.zero_wave * -1.0) state.waterOffset = hsVector3(
state.depthFalloff = hsVector3(self.depth_opacity, self.depth_reflection, self.depth_wave) self.zero_opacity * -1.0, self.zero_reflection * -1.0, self.zero_wave * -1.0
)
state.depthFalloff = hsVector3(
self.depth_opacity, self.depth_reflection, self.depth_wave
)
# Environment Map # Environment Map
if self.envmap: if self.envmap:
# 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, self.envmap, 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
@ -296,12 +376,10 @@ class PlasmaWaterModifier(idprops.IDPropMixin, PlasmaModifierProperties, bpy.typ
@classmethod @classmethod
def _idprop_mapping(cls): def _idprop_mapping(cls):
return {"wind_object": "wind_object_name", return {"wind_object": "wind_object_name", "envmap": "envmap_name"}
"envmap": "envmap_name"}
def _idprop_sources(self): def _idprop_sources(self):
return {"wind_object_name": bpy.data.objects, return {"wind_object_name": bpy.data.objects, "envmap_name": bpy.data.textures}
"envmap_name": bpy.data.textures}
@property @property
def key_name(self): def key_name(self):
@ -310,10 +388,12 @@ class PlasmaWaterModifier(idprops.IDPropMixin, PlasmaModifierProperties, bpy.typ
class PlasmaShoreObject(idprops.IDPropObjectMixin, bpy.types.PropertyGroup): class PlasmaShoreObject(idprops.IDPropObjectMixin, bpy.types.PropertyGroup):
display_name = StringProperty(name="Display Name") display_name = StringProperty(name="Display Name")
shore_object = PointerProperty(name="Shore Object", shore_object = PointerProperty(
description="Object that waves crash upon", name="Shore Object",
type=bpy.types.Object, description="Object that waves crash upon",
poll=idprops.poll_mesh_objects) type=bpy.types.Object,
poll=idprops.poll_mesh_objects,
)
@classmethod @classmethod
def _idprop_mapping(cls): def _idprop_mapping(cls):
@ -340,37 +420,50 @@ class PlasmaWaterShoreModifier(PlasmaModifierProperties):
shores = CollectionProperty(type=PlasmaShoreObject) shores = CollectionProperty(type=PlasmaShoreObject)
active_shore_index = IntProperty(options={"HIDDEN"}) active_shore_index = IntProperty(options={"HIDDEN"})
shore_tint = FloatVectorProperty(name="Shore Tint", shore_tint = FloatVectorProperty(
subtype="COLOR", name="Shore Tint",
min=0.0, max=1.0, subtype="COLOR",
default=_shore_tint_default) min=0.0,
shore_opacity = IntProperty(name="Shore Opacity", max=1.0,
subtype="PERCENTAGE", default=_shore_tint_default,
min=0, max=100, )
default=_shore_opacity_default) shore_opacity = IntProperty(
wispiness = IntProperty(name="Wispiness", name="Shore Opacity",
subtype="PERCENTAGE", subtype="PERCENTAGE",
min=0, max=200, min=0,
default=_wispiness_default) max=100,
default=_shore_opacity_default,
period = FloatProperty(name="Period", )
min=0.0, max=200.0, wispiness = IntProperty(
default=_period_default) name="Wispiness",
finger = FloatProperty(name="Finger", subtype="PERCENTAGE",
min=50.0, max=300.0, min=0,
default=_finger_default) max=200,
edge_opacity = IntProperty(name="Edge Opacity", default=_wispiness_default,
subtype="PERCENTAGE", )
min=0, max=100,
default=_edge_opacity_default) period = FloatProperty(name="Period", min=0.0, max=200.0, default=_period_default)
edge_radius = FloatProperty(name="Edge Radius", finger = FloatProperty(name="Finger", min=50.0, max=300.0, default=_finger_default)
subtype="PERCENTAGE", edge_opacity = IntProperty(
min=50, max=300, name="Edge Opacity",
default=_edge_radius_default) subtype="PERCENTAGE",
min=0,
max=100,
default=_edge_opacity_default,
)
edge_radius = FloatProperty(
name="Edge Radius",
subtype="PERCENTAGE",
min=50,
max=300,
default=_edge_radius_default,
)
def convert_default(self, wavestate): def convert_default(self, wavestate):
wavestate.wispiness = self._wispiness_default / 100.0 wavestate.wispiness = self._wispiness_default / 100.0
wavestate.minColor = hsColorRGBA(*self._shore_tint_default, alpha=(self._shore_opacity_default / 100.0)) wavestate.minColor = hsColorRGBA(
*self._shore_tint_default, alpha=(self._shore_opacity_default / 100.0)
)
wavestate.edgeOpacity = self._edge_opacity_default / 100.0 wavestate.edgeOpacity = self._edge_opacity_default / 100.0
wavestate.edgeRadius = self._edge_radius_default / 100.0 wavestate.edgeRadius = self._edge_radius_default / 100.0
wavestate.period = self._period_default / 100.0 wavestate.period = self._period_default / 100.0
@ -382,11 +475,19 @@ class PlasmaWaterShoreModifier(PlasmaModifierProperties):
for i in self.shores: for i in self.shores:
if i.shore_object is None: if i.shore_object is None:
raise ExportError("'{}': Shore Object for '{}' is invalid".format(self.key_name, i.display_name)) raise ExportError(
waveset.addShore(exporter.mgr.find_create_key(plSceneObject, bl=i.shore_object)) "'{}': 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.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)
)
wavestate.edgeOpacity = self.edge_opacity / 100.0 wavestate.edgeOpacity = self.edge_opacity / 100.0
wavestate.edgeRadius = self.edge_radius / 100.0 wavestate.edgeRadius = self.edge_radius / 100.0
wavestate.period = self.period / 100.0 wavestate.period = self.period / 100.0
@ -413,28 +514,43 @@ class PlasmaWaveState:
@classmethod @classmethod
def register(cls): def register(cls):
cls.min_length = FloatProperty(name="Min Length", cls.min_length = FloatProperty(
description="Smallest wave length", name="Min Length",
min=0.1, max=50.0, description="Smallest wave length",
default=cls._min_length_default) min=0.1,
cls.max_length = FloatProperty(name="Max Length", max=50.0,
description="Largest wave length", default=cls._min_length_default,
min=0.1, max=50.0, )
default=cls._max_length_default) cls.max_length = FloatProperty(
cls.amplitude = IntProperty(name="Amplitude", name="Max Length",
description="Multiplier for wave height", description="Largest wave length",
subtype="PERCENTAGE", min=0.1,
min=0, max=100, max=50.0,
default=cls._amplitude_default) default=cls._max_length_default,
cls.chop = IntProperty(name="Choppiness", )
description="Sharpness of wave crests", cls.amplitude = IntProperty(
subtype="PERCENTAGE", name="Amplitude",
min=0, max=500, description="Multiplier for wave height",
default=cls._chop_default) subtype="PERCENTAGE",
cls.angle_dev = FloatProperty(name="Wave Spread", min=0,
subtype="ANGLE", max=100,
min=math.radians(0.0), max=math.radians(180.0), default=cls._amplitude_default,
default=cls._angle_dev_default) )
cls.chop = IntProperty(
name="Choppiness",
description="Sharpness of wave crests",
subtype="PERCENTAGE",
min=0,
max=500,
default=cls._chop_default,
)
cls.angle_dev = FloatProperty(
name="Wave Spread",
subtype="ANGLE",
min=math.radians(0.0),
max=math.radians(180.0),
default=cls._angle_dev_default,
)
class PlasmaWaveGeoState(PlasmaWaveState, PlasmaModifierProperties): class PlasmaWaveGeoState(PlasmaWaveState, PlasmaModifierProperties):

62
korman/properties/prop_anim.py

@ -23,6 +23,7 @@ import functools
import itertools import itertools
from typing import Iterable, Iterator from typing import Iterable, Iterator
class PlasmaAnimation(bpy.types.PropertyGroup): class PlasmaAnimation(bpy.types.PropertyGroup):
ENTIRE_ANIMATION = "(Entire Animation)" ENTIRE_ANIMATION = "(Entire Animation)"
@ -103,7 +104,7 @@ class PlasmaAnimation(bpy.types.PropertyGroup):
"entire_animation": { "entire_animation": {
bpy.types.Object: "plasma_modifiers.animation.initial_marker", bpy.types.Object: "plasma_modifiers.animation.initial_marker",
bpy.types.Texture: "plasma_layer.anim_initial_marker", bpy.types.Texture: "plasma_layer.anim_initial_marker",
} },
}, },
"loop_start": { "loop_start": {
"type": StringProperty, "type": StringProperty,
@ -144,10 +145,16 @@ class PlasmaAnimation(bpy.types.PropertyGroup):
def iter_frame_numbers(cls, id_data) -> Iterator[int]: def iter_frame_numbers(cls, id_data) -> Iterator[int]:
# It would be nice if this could use self.iter_fcurves, but the property that uses this # It would be nice if this could use self.iter_fcurves, but the property that uses this
# is not actually of type PlasmaAnimation. Meaning that self is some other object (great). # is not actually of type PlasmaAnimation. Meaning that self is some other object (great).
fcurves = itertools.chain.from_iterable((id.animation_data.action.fcurves fcurves = itertools.chain.from_iterable(
for id in cls._iter_my_ids(id_data) (
if id.animation_data and id.animation_data.action)) id.animation_data.action.fcurves
frame_numbers = (keyframe.co[0] for fcurve in fcurves for keyframe in fcurve.keyframe_points) for id in cls._iter_my_ids(id_data)
if id.animation_data and id.animation_data.action
)
)
frame_numbers = (
keyframe.co[0] for fcurve in fcurves for keyframe in fcurve.keyframe_points
)
yield from frame_numbers yield from frame_numbers
@classmethod @classmethod
@ -203,13 +210,14 @@ class PlasmaAnimation(bpy.types.PropertyGroup):
if self.is_entire_animation: if self.is_entire_animation:
attr_path = cls._get_from_class_lut(self.id_data, lut) attr_path = cls._get_from_class_lut(self.id_data, lut)
if attr_path is not None: if attr_path is not None:
prop_delim = attr_path.rfind('.') prop_delim = attr_path.rfind(".")
prop_group = self.id_data.path_resolve(attr_path[:prop_delim]) prop_group = self.id_data.path_resolve(attr_path[:prop_delim])
return getattr(prop_group, attr_path[prop_delim+1:]) return getattr(prop_group, attr_path[prop_delim + 1 :])
else: else:
return default return default
else: else:
return getattr(self, "{}_value".format(prop_name)) return getattr(self, "{}_value".format(prop_name))
return proc return proc
@classmethod @classmethod
@ -218,11 +226,12 @@ class PlasmaAnimation(bpy.types.PropertyGroup):
if self.is_entire_animation: if self.is_entire_animation:
attr_path = cls._get_from_class_lut(self.id_data, lut) attr_path = cls._get_from_class_lut(self.id_data, lut)
if attr_path is not None: if attr_path is not None:
prop_delim = attr_path.rfind('.') prop_delim = attr_path.rfind(".")
prop_group = self.id_data.path_resolve(attr_path[:prop_delim]) prop_group = self.id_data.path_resolve(attr_path[:prop_delim])
setattr(prop_group, attr_path[prop_delim+1:], value) setattr(prop_group, attr_path[prop_delim + 1 :], value)
else: else:
setattr(self, "{}_value".format(prop_name), value) setattr(self, "{}_value".format(prop_name), value)
return proc return proc
@classmethod @classmethod
@ -238,27 +247,39 @@ class PlasmaAnimation(bpy.types.PropertyGroup):
value_kwargs = deepcopy(kwargs) value_kwargs = deepcopy(kwargs)
value_kwargs["options"].add("HIDDEN") value_kwargs["options"].add("HIDDEN")
value_props = { key: value for key, value in props.items() if key not in {"get", "set", "update"} } value_props = {
setattr(cls, "{}_value".format(prop_name), definitions["type"](**value_props, **value_kwargs)) key: value
for key, value in props.items()
if key not in {"get", "set", "update"}
}
setattr(
cls,
"{}_value".format(prop_name),
definitions["type"](**value_props, **value_kwargs),
)
needs_accessors = "get" not in props and "set" not in props needs_accessors = "get" not in props and "set" not in props
if needs_accessors: if needs_accessors:
# We have to use these weirdo wrappers because Blender only accepts function objects # We have to use these weirdo wrappers because Blender only accepts function objects
# for its property callbacks, not arbitrary callables eg lambdas, functools.partials. # for its property callbacks, not arbitrary callables eg lambdas, functools.partials.
kwargs["get"] = cls._make_prop_getter(prop_name, definitions["entire_animation"], props.get("default")) kwargs["get"] = cls._make_prop_getter(
kwargs["set"] = cls._make_prop_setter(prop_name, definitions["entire_animation"]) prop_name, definitions["entire_animation"], props.get("default")
)
kwargs["set"] = cls._make_prop_setter(
prop_name, definitions["entire_animation"]
)
setattr(cls, prop_name, definitions["type"](**props, **kwargs)) setattr(cls, prop_name, definitions["type"](**props, **kwargs))
@classmethod @classmethod
def register_entire_animation(cls, id_type, rna_type): def register_entire_animation(cls, id_type, rna_type):
"""Registers all of the properties for the old style single animation per ID animations onto """Registers all of the properties for the old style single animation per ID animations onto
the property group given by `rna_type`. These were previously directly registered but are the property group given by `rna_type`. These were previously directly registered but are
now abstracted away to serve as the backing store for the new "entire animation" method.""" now abstracted away to serve as the backing store for the new "entire animation" method."""
for prop_name, definitions in cls._PROPERTIES.items(): for prop_name, definitions in cls._PROPERTIES.items():
lut = definitions.get("entire_animation", {}) lut = definitions.get("entire_animation", {})
path_from_id = lut.get(id_type) path_from_id = lut.get(id_type)
if path_from_id: if path_from_id:
attr_name = path_from_id[path_from_id.rfind('.')+1:] attr_name = path_from_id[path_from_id.rfind(".") + 1 :]
kwargs = deepcopy(definitions["property"]) kwargs = deepcopy(definitions["property"])
kwargs.update(cls._ENTIRE_ANIMATION_PROPERTIES.get(prop_name, {})) kwargs.update(cls._ENTIRE_ANIMATION_PROPERTIES.get(prop_name, {}))
setattr(rna_type, attr_name, definitions["type"](**kwargs)) setattr(rna_type, attr_name, definitions["type"](**kwargs))
@ -277,8 +298,9 @@ class PlasmaAnimationCollection(bpy.types.PropertyGroup):
def _set_active_index(self, value: int) -> None: def _set_active_index(self, value: int) -> None:
self.active_animation_index_value = value self.active_animation_index_value = value
active_animation_index = IntProperty(get=_get_active_index, set=_set_active_index, active_animation_index = IntProperty(
options={"HIDDEN"}) get=_get_active_index, set=_set_active_index, options={"HIDDEN"}
)
active_animation_index_value = IntProperty(options={"HIDDEN"}) active_animation_index_value = IntProperty(options={"HIDDEN"})
# Animations backing store--don't use this except to display the list in Blender's UI. # Animations backing store--don't use this except to display the list in Blender's UI.
@ -298,7 +320,9 @@ class PlasmaAnimationCollection(bpy.types.PropertyGroup):
# want to observe it in the UI. That restriction is dropped, however, when RNA poperties are # want to observe it in the UI. That restriction is dropped, however, when RNA poperties are
# being observed or set. So, this will allow us to initialize the entire animation in the # being observed or set. So, this will allow us to initialize the entire animation in the
# UI phase at the penalty of potentially having to loop through the animation collection twice. # UI phase at the penalty of potentially having to loop through the animation collection twice.
request_entire_animation = BoolProperty(get=_get_hack, set=_set_hack, options={"HIDDEN"}) request_entire_animation = BoolProperty(
get=_get_hack, set=_set_hack, options={"HIDDEN"}
)
@property @property
def animations(self) -> Iterable[PlasmaAnimation]: def animations(self) -> Iterable[PlasmaAnimation]:

529
korman/properties/prop_camera.py

@ -19,213 +19,395 @@ import math
from .. import idprops from .. import idprops
camera_types = [("circle", "Circle Camera", "The camera circles a fixed point"), camera_types = [
("follow", "Follow Camera", "The camera follows an object"), ("circle", "Circle Camera", "The camera circles a fixed point"),
("fixed", "Fixed Camera", "The camera is fixed in one location"), ("follow", "Follow Camera", "The camera follows an object"),
("rail", "Rail Camera", "The camera follows an object by moving along a line"), ("fixed", "Fixed Camera", "The camera is fixed in one location"),
("firstperson", "First Person", "Simulates first person view and disappears avatar")] ("rail", "Rail Camera", "The camera follows an object by moving along a line"),
(
"firstperson",
"First Person",
"Simulates first person view and disappears avatar",
),
]
class PlasmaTransition(bpy.types.PropertyGroup): class PlasmaTransition(bpy.types.PropertyGroup):
poa_acceleration = FloatProperty(name="PoA Acceleration", poa_acceleration = FloatProperty(
description="Rate the camera's Point of Attention tracking velocity increases in feet per second squared", name="PoA Acceleration",
min=-100.0, max=100.0, precision=0, default=60.0, description="Rate the camera's Point of Attention tracking velocity increases in feet per second squared",
unit="ACCELERATION", options=set()) min=-100.0,
poa_deceleration = FloatProperty(name="PoA Deceleration", max=100.0,
description="Rate the camera's Point of Attention tracking velocity decreases in feet per second squared", precision=0,
min=-100.0, max=100.0, precision=0, default=60.0, default=60.0,
unit="ACCELERATION", options=set()) unit="ACCELERATION",
poa_velocity = FloatProperty(name="PoA Velocity", options=set(),
description="Maximum velocity of the camera's Point of Attention tracking", )
min=-100.0, max=100.0, precision=0, default=60.0, poa_deceleration = FloatProperty(
unit="VELOCITY", options=set()) name="PoA Deceleration",
poa_cut = BoolProperty(name="Cut", description="Rate the camera's Point of Attention tracking velocity decreases in feet per second squared",
description="The camera immediately begins tracking the Point of Attention", min=-100.0,
options=set()) max=100.0,
precision=0,
default=60.0,
unit="ACCELERATION",
options=set(),
)
poa_velocity = FloatProperty(
name="PoA Velocity",
description="Maximum velocity of the camera's Point of Attention tracking",
min=-100.0,
max=100.0,
precision=0,
default=60.0,
unit="VELOCITY",
options=set(),
)
poa_cut = BoolProperty(
name="Cut",
description="The camera immediately begins tracking the Point of Attention",
options=set(),
)
pos_acceleration = FloatProperty(name="Position Acceleration", pos_acceleration = FloatProperty(
description="Rate the camera's positional velocity increases in feet per second squared", name="Position Acceleration",
min=-100.0, max=100.0, precision=0, default=60.0, description="Rate the camera's positional velocity increases in feet per second squared",
unit="ACCELERATION", options=set()) min=-100.0,
pos_deceleration = FloatProperty(name="Position Deceleration", max=100.0,
description="Rate the camera's positional velocity decreases in feet per second squared", precision=0,
min=-100.0, max=100.0, precision=0, default=60.0, default=60.0,
unit="ACCELERATION", options=set()) unit="ACCELERATION",
pos_velocity = FloatProperty(name="Position Max Velocity", options=set(),
description="Maximum positional velocity of the camera", )
min=-100.0, max=100.0, precision=0, default=60.0, pos_deceleration = FloatProperty(
unit="VELOCITY", options=set()) name="Position Deceleration",
pos_cut = BoolProperty(name="Cut", description="Rate the camera's positional velocity decreases in feet per second squared",
description="The camera immediately moves to its new position", min=-100.0,
options=set()) max=100.0,
precision=0,
default=60.0,
unit="ACCELERATION",
options=set(),
)
pos_velocity = FloatProperty(
name="Position Max Velocity",
description="Maximum positional velocity of the camera",
min=-100.0,
max=100.0,
precision=0,
default=60.0,
unit="VELOCITY",
options=set(),
)
pos_cut = BoolProperty(
name="Cut",
description="The camera immediately moves to its new position",
options=set(),
)
class PlasmaManualTransition(bpy.types.PropertyGroup): class PlasmaManualTransition(bpy.types.PropertyGroup):
camera = PointerProperty(name="Camera", camera = PointerProperty(
description="The camera from which this transition is intended", name="Camera",
type=bpy.types.Object, description="The camera from which this transition is intended",
poll=idprops.poll_camera_objects, type=bpy.types.Object,
options=set()) poll=idprops.poll_camera_objects,
options=set(),
)
transition = PointerProperty(type=PlasmaTransition, options=set()) transition = PointerProperty(type=PlasmaTransition, options=set())
mode = EnumProperty(name="Transition Mode", mode = EnumProperty(
description="Type of transition that should occur between the two cameras", name="Transition Mode",
items=[("ignore", "Ignore Camera", "Ignore this camera and do not transition"), description="Type of transition that should occur between the two cameras",
("auto", "Auto", "Auto transition as defined by the two cameras' properies"), items=[
("manual", "Manual", "Manually defined transition")], ("ignore", "Ignore Camera", "Ignore this camera and do not transition"),
default="auto", (
options=set()) "auto",
enabled = BoolProperty(name="Enabled", "Auto",
description="Export this transition", "Auto transition as defined by the two cameras' properies",
default=True, ),
options=set()) ("manual", "Manual", "Manually defined transition"),
],
default="auto",
options=set(),
)
enabled = BoolProperty(
name="Enabled",
description="Export this transition",
default=True,
options=set(),
)
class PlasmaCameraProperties(bpy.types.PropertyGroup): class PlasmaCameraProperties(bpy.types.PropertyGroup):
# Point of Attention # Point of Attention
poa_type = EnumProperty(name="Point of Attention", poa_type = EnumProperty(
description="The point of attention that this camera tracks", name="Point of Attention",
items=[("avatar", "Track Local Player", "Camera tracks the player's avatar"), description="The point of attention that this camera tracks",
("object", "Track Object", "Camera tracks an object in the scene"), items=[
("none", "Don't Track", "Camera does not track anything")], ("avatar", "Track Local Player", "Camera tracks the player's avatar"),
options=set()) ("object", "Track Object", "Camera tracks an object in the scene"),
poa_object = PointerProperty(name="PoA Object", ("none", "Don't Track", "Camera does not track anything"),
description="Object the camera should track as its Point of Attention", ],
type=bpy.types.Object, options=set(),
options=set()) )
poa_offset = FloatVectorProperty(name="PoA Offset", poa_object = PointerProperty(
description="Offset from the point of attention's origin to track", name="PoA Object",
soft_min=-50.0, soft_max=50.0, description="Object the camera should track as its Point of Attention",
size=3, default=(0.0, 0.0, 3.0), type=bpy.types.Object,
options=set()) options=set(),
poa_worldspace = BoolProperty(name="Worldspace Offset", )
description="Point of Attention Offset is in worldspace coordinates", poa_offset = FloatVectorProperty(
options=set()) name="PoA Offset",
description="Offset from the point of attention's origin to track",
soft_min=-50.0,
soft_max=50.0,
size=3,
default=(0.0, 0.0, 3.0),
options=set(),
)
poa_worldspace = BoolProperty(
name="Worldspace Offset",
description="Point of Attention Offset is in worldspace coordinates",
options=set(),
)
# Position Offset # Position Offset
pos_offset = FloatVectorProperty(name="Position Offset", pos_offset = FloatVectorProperty(
description="Offset the camera's position", name="Position Offset",
soft_min=-50.0, soft_max=50.0, description="Offset the camera's position",
size=3, default=(0.0, 10.0, 3.0), soft_min=-50.0,
options=set()) soft_max=50.0,
pos_worldspace = BoolProperty(name="Worldspace Offset", size=3,
description="Position offset is in worldspace coordinates", default=(0.0, 10.0, 3.0),
options=set()) options=set(),
)
pos_worldspace = BoolProperty(
name="Worldspace Offset",
description="Position offset is in worldspace coordinates",
options=set(),
)
# Default Transition # Default Transition
transition = PointerProperty(type=PlasmaTransition, options=set()) transition = PointerProperty(type=PlasmaTransition, options=set())
# Limit Panning # Limit Panning
x_pan_angle = FloatProperty(name="X Degrees", x_pan_angle = FloatProperty(
description="Maximum camera pan angle in the X direction", name="X Degrees",
min=0.0, max=math.radians(180.0), precision=0, default=math.radians(90.0), description="Maximum camera pan angle in the X direction",
subtype="ANGLE", options=set()) min=0.0,
y_pan_angle = FloatProperty(name="Y Degrees", max=math.radians(180.0),
description="Maximum camera pan angle in the Y direction", precision=0,
min=0.0, max=math.radians(180.0), precision=0, default=math.radians(90.0), default=math.radians(90.0),
subtype="ANGLE", options=set()) subtype="ANGLE",
pan_rate = FloatProperty(name="Pan Velocity", options=set(),
description="", )
min=0.0, precision=1, default=50.0, y_pan_angle = FloatProperty(
unit="VELOCITY", options=set()) name="Y Degrees",
description="Maximum camera pan angle in the Y direction",
min=0.0,
max=math.radians(180.0),
precision=0,
default=math.radians(90.0),
subtype="ANGLE",
options=set(),
)
pan_rate = FloatProperty(
name="Pan Velocity",
description="",
min=0.0,
precision=1,
default=50.0,
unit="VELOCITY",
options=set(),
)
# Zooming # Zooming
fov = FloatProperty(name="Default FOV", fov = FloatProperty(
description="Horizontal Field of View angle", name="Default FOV",
min=0.0, max=math.radians(180.0), precision=0, default=math.radians(70.0), description="Horizontal Field of View angle",
subtype="ANGLE") min=0.0,
limit_zoom = BoolProperty(name="Limit Zoom", max=math.radians(180.0),
description="The camera allows zooming per artist limitations", precision=0,
options=set()) default=math.radians(70.0),
zoom_max = FloatProperty(name="Max FOV", subtype="ANGLE",
description="Maximum camera FOV when zooming", )
min=0.0, max=math.radians(180.0), precision=0, default=math.radians(120.0), limit_zoom = BoolProperty(
subtype="ANGLE", options=set()) name="Limit Zoom",
zoom_min = FloatProperty(name="Min FOV", description="The camera allows zooming per artist limitations",
description="Minimum camera FOV when zooming", options=set(),
min=0.0, max=math.radians(180.0), precision=0, default=math.radians(35.0), )
subtype="ANGLE", options=set()) zoom_max = FloatProperty(
zoom_rate = FloatProperty(name="Zoom Velocity", name="Max FOV",
description="Velocity of the camera's zoom in degrees per second", description="Maximum camera FOV when zooming",
min=0.0, max=180.0, precision=0, default=90.0, min=0.0,
unit="VELOCITY", options=set()) max=math.radians(180.0),
precision=0,
default=math.radians(120.0),
subtype="ANGLE",
options=set(),
)
zoom_min = FloatProperty(
name="Min FOV",
description="Minimum camera FOV when zooming",
min=0.0,
max=math.radians(180.0),
precision=0,
default=math.radians(35.0),
subtype="ANGLE",
options=set(),
)
zoom_rate = FloatProperty(
name="Zoom Velocity",
description="Velocity of the camera's zoom in degrees per second",
min=0.0,
max=180.0,
precision=0,
default=90.0,
unit="VELOCITY",
options=set(),
)
# Miscellaneous Movement Props # Miscellaneous Movement Props
maintain_los = BoolProperty(name="Maintain LOS", maintain_los = BoolProperty(
description="The camera should move to maintain line-of-sight with the object it's tracking", name="Maintain LOS",
default=True, description="The camera should move to maintain line-of-sight with the object it's tracking",
options=set()) default=True,
fall_vertical = BoolProperty(name="Fall Camera", options=set(),
description="The camera will orient itself vertically when the local player begins falling", )
options=set()) fall_vertical = BoolProperty(
fast_run = BoolProperty(name="Faster When Falling", name="Fall Camera",
description="The camera's velocity will be increased when the local player is falling", description="The camera will orient itself vertically when the local player begins falling",
options=set()) options=set(),
ignore_subworld = BoolProperty(name="Ignore Subworld Movement", )
description="The camera will not be parented to any subworlds", fast_run = BoolProperty(
options=set()) name="Faster When Falling",
description="The camera's velocity will be increased when the local player is falling",
options=set(),
)
ignore_subworld = BoolProperty(
name="Ignore Subworld Movement",
description="The camera will not be parented to any subworlds",
options=set(),
)
# Core Type Properties # Core Type Properties
primary_camera = BoolProperty(name="Primary Camera", primary_camera = BoolProperty(
description="The camera should be considered the Age's primary camera.", name="Primary Camera",
options=set()) description="The camera should be considered the Age's primary camera.",
options=set(),
)
# Cricle Camera # Cricle Camera
def _get_circle_radius(self): def _get_circle_radius(self):
# This is coming from the UI, so we need to get the active object from # This is coming from the UI, so we need to get the active object from
# Blender's context and pass that on to the actual getter. # Blender's context and pass that on to the actual getter.
return self.get_circle_radius(bpy.context.object) return self.get_circle_radius(bpy.context.object)
def _set_circle_radius(self, value): def _set_circle_radius(self, value):
# Don't really care about error checking... # Don't really care about error checking...
self.circle_radius_value = value self.circle_radius_value = value
circle_center = PointerProperty(name="Center", circle_center = PointerProperty(
description="Center of the circle camera's orbit", name="Center",
type=bpy.types.Object, description="Center of the circle camera's orbit",
options=set()) type=bpy.types.Object,
circle_pos = EnumProperty(name="Position on Circle", options=set(),
description="The point on the circle the camera moves to", )
items=[("closest", "Closest Point", "The camera moves to the point on the circle closest to the Point of Attention"), circle_pos = EnumProperty(
("farthest", "Farthest Point", "The camera moves to the point on the circle farthest from the Point of Attention")], name="Position on Circle",
options=set()) description="The point on the circle the camera moves to",
circle_velocity = FloatProperty(name="Velocity", items=[
description="Velocity of the circle camera in degrees per second", (
min=0.0, max=math.radians(360.0), precision=0, default=math.radians(36.0), "closest",
subtype="ANGLE", options=set()) "Closest Point",
circle_radius_ui = FloatProperty(name="Radius", "The camera moves to the point on the circle closest to the Point of Attention",
description="Radius at which the circle camera should orbit the Point of Attention", ),
min=0.0, get=_get_circle_radius, set=_set_circle_radius, options=set()) (
circle_radius_value = FloatProperty(name="INTERNAL: Radius", "farthest",
description="Radius at which the circle camera should orbit the Point of Attention", "Farthest Point",
min=0.0, default=8.5, options={"HIDDEN"}) "The camera moves to the point on the circle farthest from the Point of Attention",
),
],
options=set(),
)
circle_velocity = FloatProperty(
name="Velocity",
description="Velocity of the circle camera in degrees per second",
min=0.0,
max=math.radians(360.0),
precision=0,
default=math.radians(36.0),
subtype="ANGLE",
options=set(),
)
circle_radius_ui = FloatProperty(
name="Radius",
description="Radius at which the circle camera should orbit the Point of Attention",
min=0.0,
get=_get_circle_radius,
set=_set_circle_radius,
options=set(),
)
circle_radius_value = FloatProperty(
name="INTERNAL: Radius",
description="Radius at which the circle camera should orbit the Point of Attention",
min=0.0,
default=8.5,
options={"HIDDEN"},
)
# Animation # Animation
anim_enabled = BoolProperty(name="Animation Enabled", anim_enabled = BoolProperty(
description="Export the camera's animation", name="Animation Enabled",
default=True, description="Export the camera's animation",
options=set()) default=True,
start_on_push = BoolProperty(name="Start on Push", options=set(),
description="Start playing the camera's animation when the camera is activated", )
default=True, start_on_push = BoolProperty(
options=set()) name="Start on Push",
stop_on_pop = BoolProperty(name="Pause on Pop", description="Start playing the camera's animation when the camera is activated",
description="Pauses the camera's animation when the camera is no longer activated", default=True,
default=True, options=set(),
options=set()) )
reset_on_pop = BoolProperty(name="Reset on Pop", stop_on_pop = BoolProperty(
description="Reset the camera's animation to the beginning when the camera is no longer activated", name="Pause on Pop",
options=set()) description="Pauses the camera's animation when the camera is no longer activated",
default=True,
options=set(),
)
reset_on_pop = BoolProperty(
name="Reset on Pop",
description="Reset the camera's animation to the beginning when the camera is no longer activated",
options=set(),
)
# Rail # Rail
rail_pos = EnumProperty(name="Position on Rail", rail_pos = EnumProperty(
description="The point on the rail the camera moves to", name="Position on Rail",
items=[("closest", "Closest Point", "The camera moves to the point on the rail closest to the Point of Attention"), description="The point on the rail the camera moves to",
("farthest", "Farthest Point", "The camera moves to the point on the rail farthest from the Point of Attention")], items=[
options=set()) (
"closest",
"Closest Point",
"The camera moves to the point on the rail closest to the Point of Attention",
),
(
"farthest",
"Farthest Point",
"The camera moves to the point on the rail farthest from the Point of Attention",
),
],
options=set(),
)
def get_circle_radius(self, bo): def get_circle_radius(self, bo):
"""Gets the circle camera radius for this camera when it is attached to the given Object""" """Gets the circle camera radius for this camera when it is attached to the given Object"""
assert bo is not None assert bo is not None
if self.circle_center is not None: if self.circle_center is not None:
vec = bo.matrix_world.translation - self.circle_center.matrix_world.translation vec = (
bo.matrix_world.translation
- self.circle_center.matrix_world.translation
)
return vec.magnitude return vec.magnitude
return self.circle_radius_value return self.circle_radius_value
@ -236,22 +418,23 @@ class PlasmaCameraProperties(bpy.types.PropertyGroup):
# Dang! We need to escape out to the object to figure out if this is a circle camera... # Dang! We need to escape out to the object to figure out if this is a circle camera...
data = self.id_data data = self.id_data
if isinstance(data, bpy.types.Camera) and data.plasma_camera.camera_type == "circle": if (
isinstance(data, bpy.types.Camera)
and data.plasma_camera.camera_type == "circle"
):
if self.circle_center is not None: if self.circle_center is not None:
actors.add(self.circle_center.name) actors.add(self.circle_center.name)
return actors return actors
class PlasmaCamera(bpy.types.PropertyGroup): class PlasmaCamera(bpy.types.PropertyGroup):
camera_type = EnumProperty(name="Camera Type", camera_type = EnumProperty(
description="", name="Camera Type", description="", items=camera_types, options=set()
items=camera_types, )
options=set())
settings = PointerProperty(type=PlasmaCameraProperties, options=set()) settings = PointerProperty(type=PlasmaCameraProperties, options=set())
transitions = CollectionProperty(type=PlasmaManualTransition, transitions = CollectionProperty(
name="Transitions", type=PlasmaManualTransition, name="Transitions", description="", options=set()
description="", )
options=set())
active_transition_index = IntProperty(options={"HIDDEN"}) active_transition_index = IntProperty(options={"HIDDEN"})
@property @property

23
korman/properties/prop_image.py

@ -16,11 +16,20 @@
import bpy import bpy
from bpy.props import * from bpy.props import *
class PlasmaImage(bpy.types.PropertyGroup): class PlasmaImage(bpy.types.PropertyGroup):
texcache_method = EnumProperty(name="Texture Cache", texcache_method = EnumProperty(
description="Texture Cache Settings", name="Texture Cache",
items=[("skip", "Don't Cache Image", "This image is never cached."), description="Texture Cache Settings",
("use", "Use Image Cache", "This image should be cached."), items=[
("rebuild", "Refresh Image Cache", "Forces this image to be recached on the next export.")], ("skip", "Don't Cache Image", "This image is never cached."),
default="use", ("use", "Use Image Cache", "This image should be cached."),
options=set()) (
"rebuild",
"Refresh Image Cache",
"Forces this image to be recached on the next export.",
),
],
default="use",
options=set(),
)

105
korman/properties/prop_lamp.py

@ -18,51 +18,78 @@ from bpy.props import *
from .. import idprops from .. import idprops
class PlasmaLamp(idprops.IDPropObjectMixin, bpy.types.PropertyGroup): class PlasmaLamp(idprops.IDPropObjectMixin, bpy.types.PropertyGroup):
affect_characters = BoolProperty(name="Affect Avatars", affect_characters = BoolProperty(
description="This lamp affects avatars", name="Affect Avatars",
options=set(), description="This lamp affects avatars",
default=True) options=set(),
default=True,
)
# Shadow settings # Shadow settings
cast_shadows = BoolProperty(name="Cast", cast_shadows = BoolProperty(
description="This lamp casts runtime shadows", name="Cast", description="This lamp casts runtime shadows", default=True
default=True) )
shadow_falloff = FloatProperty(name="Falloff", shadow_falloff = FloatProperty(
description="Distance from the Lamp past which we don't cast shadows", name="Falloff",
min=5.0, max=50.0, default=10.0, description="Distance from the Lamp past which we don't cast shadows",
options=set()) min=5.0,
shadow_distance = FloatProperty(name="Fade Distance", max=50.0,
description="Distance at which the shadow has completely faded out", default=10.0,
min=0.0, max=500.0, default=0.0, options=set(),
options=set()) )
shadow_power = IntProperty(name="Power", shadow_distance = FloatProperty(
description="Multiplier for the shadow's intensity", name="Fade Distance",
min=0, max=200, default=100, description="Distance at which the shadow has completely faded out",
options=set(), subtype="PERCENTAGE") min=0.0,
shadow_self = BoolProperty(name="Self-Shadow", max=500.0,
description="This light can cause objects to cast shadows on themselves", default=0.0,
default=False, options=set(),
options=set()) )
shadow_quality = EnumProperty(name="Shadow Quality", shadow_power = IntProperty(
description="Maximum resolution the shadow is rendered", name="Power",
items=[("ABYSMAL", "Abysmal Quality", "64x64 pixels"), description="Multiplier for the shadow's intensity",
("LOW", "Low Quality", "128x128 pixels"), min=0,
("NORMAL", "Normal Quality", "256x256 pixels"), max=200,
("HIGH", "High Quality", "512x512 pixels")], default=100,
default="NORMAL", options=set(),
options=set()) subtype="PERCENTAGE",
)
shadow_self = BoolProperty(
name="Self-Shadow",
description="This light can cause objects to cast shadows on themselves",
default=False,
options=set(),
)
shadow_quality = EnumProperty(
name="Shadow Quality",
description="Maximum resolution the shadow is rendered",
items=[
("ABYSMAL", "Abysmal Quality", "64x64 pixels"),
("LOW", "Low Quality", "128x128 pixels"),
("NORMAL", "Normal Quality", "256x256 pixels"),
("HIGH", "High Quality", "512x512 pixels"),
],
default="NORMAL",
options=set(),
)
lamp_region = PointerProperty(name="Soft Volume", lamp_region = PointerProperty(
description="Soft region this light is active inside", name="Soft Volume",
type=bpy.types.Object, description="Soft region this light is active inside",
poll=idprops.poll_softvolume_objects) type=bpy.types.Object,
poll=idprops.poll_softvolume_objects,
)
# For LimitedDirLights # For LimitedDirLights
size_height = FloatProperty(name="Height", size_height = FloatProperty(
description="Size of the area for the Area Lamp in the Z direction", name="Height",
min=0.0, default=200.0, description="Size of the area for the Area Lamp in the Z direction",
options=set()) min=0.0,
default=200.0,
options=set(),
)
def has_light_group(self, bo): def has_light_group(self, bo):
return bool(bo.users_group) return bool(bo.users_group)

76
korman/properties/prop_object.py

@ -17,6 +17,7 @@ import bpy
from bpy.props import * from bpy.props import *
from PyHSPlasma import * from PyHSPlasma import *
class PlasmaObject(bpy.types.PropertyGroup): class PlasmaObject(bpy.types.PropertyGroup):
def _enabled(self, context): def _enabled(self, context):
if not self.is_property_set("page"): if not self.is_property_set("page"):
@ -37,18 +38,18 @@ class PlasmaObject(bpy.types.PropertyGroup):
o.plasma_object.page = page.name o.plasma_object.page = page.name
break break
enabled = BoolProperty(
enabled = BoolProperty(name="Export", name="Export",
description="Export this as a discrete object", description="Export this as a discrete object",
default=False, default=False,
update=_enabled) update=_enabled,
page = StringProperty(name="Page", )
description="Page this object will be exported to") page = StringProperty(
name="Page", description="Page this object will be exported to"
)
# DEAD - leaving in just in case external code uses it # DEAD - leaving in just in case external code uses it
is_inited = BoolProperty(description="DEAD", is_inited = BoolProperty(description="DEAD", default=False, options={"HIDDEN"})
default=False,
options={"HIDDEN"})
@property @property
def ci_type(self): def ci_type(self):
@ -75,8 +76,16 @@ class PlasmaObject(bpy.types.PropertyGroup):
bo = self.id_data bo = self.id_data
if bo.animation_data is not None: if bo.animation_data is not None:
if bo.animation_data.action is not None: if bo.animation_data.action is not None:
data_paths = frozenset((i.data_path for i in bo.animation_data.action.fcurves)) data_paths = frozenset(
return {"location", "rotation_euler", "rotation_quaternion", "rotation_axis_angle", "scale"} & data_paths (i.data_path for i in bo.animation_data.action.fcurves)
)
return {
"location",
"rotation_euler",
"rotation_quaternion",
"rotation_axis_angle",
"scale",
} & data_paths
return False return False
@property @property
@ -91,9 +100,11 @@ class PlasmaObject(bpy.types.PropertyGroup):
class PlasmaNet(bpy.types.PropertyGroup): class PlasmaNet(bpy.types.PropertyGroup):
manual_sdl = BoolProperty(name="Override SDL", manual_sdl = BoolProperty(
description="ADVANCED: Manually track high level states on this object", name="Override SDL",
default=False) description="ADVANCED: Manually track high level states on this object",
default=False,
)
sdl_names = set() sdl_names = set()
def propagate_synch_options(self, scnobj, synobj): def propagate_synch_options(self, scnobj, synobj):
@ -108,14 +119,16 @@ class PlasmaNet(bpy.types.PropertyGroup):
else: else:
# This SynchedObject may have already excluded or volatile'd everything # This SynchedObject may have already excluded or volatile'd everything
# If so, bail. # If so, bail.
if synobj.synchFlags & plSynchedObject.kExcludeAllPersistentState or \ if (
synobj.synchFlags & plSynchedObject.kAllStateIsVolatile: synobj.synchFlags & plSynchedObject.kExcludeAllPersistentState
or synobj.synchFlags & plSynchedObject.kAllStateIsVolatile
):
return return
# Is this a kickable? # Is this a kickable?
if scnobj.sim is not None: if scnobj.sim is not None:
phys = scnobj.sim.object.physical.object phys = scnobj.sim.object.physical.object
has_kickable = (phys.memberGroup == plSimDefs.kGroupDynamic) has_kickable = phys.memberGroup == plSimDefs.kGroupDynamic
else: else:
has_kickable = False has_kickable = False
@ -159,16 +172,27 @@ class PlasmaNet(bpy.types.PropertyGroup):
@classmethod @classmethod
def register(cls): def register(cls):
def SdlEnumProperty(name): def SdlEnumProperty(name):
value = bpy.props.EnumProperty(name=name, value = bpy.props.EnumProperty(
description="{} state synchronization".format(name), name=name,
items=[ description="{} state synchronization".format(name),
("save", "Save to Server", "Save state on the server"), items=[
("volatile", "Volatile on Server", "Throw away state when the age shuts down"), ("save", "Save to Server", "Save state on the server"),
("exclude", "Don't Send to Server", "Don't synchronize with the server"), (
], "volatile",
default="exclude") "Volatile on Server",
"Throw away state when the age shuts down",
),
(
"exclude",
"Don't Send to Server",
"Don't synchronize with the server",
),
],
default="exclude",
)
setattr(PlasmaNet, name, value) setattr(PlasmaNet, name, value)
PlasmaNet.sdl_names.add(name) PlasmaNet.sdl_names.add(name)
agmaster = SdlEnumProperty("AGMaster") agmaster = SdlEnumProperty("AGMaster")
avatar = SdlEnumProperty("Avatar") avatar = SdlEnumProperty("Avatar")
avatar_phys = SdlEnumProperty("AvatarPhysical") avatar_phys = SdlEnumProperty("AvatarPhysical")

182
korman/properties/prop_scene.py

@ -19,9 +19,11 @@ import itertools
from ..exporter.etlight import _NUM_RENDER_LAYERS from ..exporter.etlight import _NUM_RENDER_LAYERS
class PlasmaBakePass(bpy.types.PropertyGroup): class PlasmaBakePass(bpy.types.PropertyGroup):
def _get_display_name(self): def _get_display_name(self):
return self.name return self.name
def _set_display_name(self, value): def _set_display_name(self, value):
for i in bpy.data.objects: for i in bpy.data.objects:
lm = i.plasma_modifiers.lightmap lm = i.plasma_modifiers.lightmap
@ -29,32 +31,32 @@ class PlasmaBakePass(bpy.types.PropertyGroup):
lm.bake_pass_name = value lm.bake_pass_name = value
self.name = value self.name = value
display_name = StringProperty(name="Pass Name", display_name = StringProperty(
get=_get_display_name, name="Pass Name", get=_get_display_name, set=_set_display_name, options=set()
set=_set_display_name, )
options=set())
render_layers = BoolVectorProperty(name="Layers to Bake", render_layers = BoolVectorProperty(
description="Render layers to use for baking", name="Layers to Bake",
options=set(), description="Render layers to use for baking",
subtype="LAYER", options=set(),
size=_NUM_RENDER_LAYERS, subtype="LAYER",
default=((True,) * _NUM_RENDER_LAYERS)) size=_NUM_RENDER_LAYERS,
default=((True,) * _NUM_RENDER_LAYERS),
)
class PlasmaWetDecalRef(bpy.types.PropertyGroup): class PlasmaWetDecalRef(bpy.types.PropertyGroup):
enabled = BoolProperty(name="Enabled", enabled = BoolProperty(name="Enabled", default=True, options=set())
default=True,
options=set())
name = StringProperty(name="Decal Name", name = StringProperty(
description="Wet decal manager", name="Decal Name", description="Wet decal manager", options=set()
options=set()) )
class PlasmaDecalManager(bpy.types.PropertyGroup): class PlasmaDecalManager(bpy.types.PropertyGroup):
def _get_display_name(self): def _get_display_name(self):
return self.name return self.name
def _set_display_name(self, value): def _set_display_name(self, value):
prev_value = self.name prev_value = self.name
for i in bpy.data.objects: for i in bpy.data.objects:
@ -69,59 +71,88 @@ class PlasmaDecalManager(bpy.types.PropertyGroup):
j.name = value j.name = value
self.name = value self.name = value
name = StringProperty(name="Decal Name", name = StringProperty(name="Decal Name", options=set())
options=set()) display_name = StringProperty(
display_name = StringProperty(name="Display Name", name="Display Name", get=_get_display_name, set=_set_display_name, options=set()
get=_get_display_name, )
set=_set_display_name,
options=set()) decal_type = EnumProperty(
name="Decal Type",
decal_type = EnumProperty(name="Decal Type", description="",
description="", items=[
items=[("footprint_dry", "Footprint (Dry)", ""), ("footprint_dry", "Footprint (Dry)", ""),
("footprint_wet", "Footprint (Wet)", ""), ("footprint_wet", "Footprint (Wet)", ""),
("puddle", "Water Ripple (Shallow)", ""), ("puddle", "Water Ripple (Shallow)", ""),
("ripple", "Water Ripple (Deep)", "")], ("ripple", "Water Ripple (Deep)", ""),
default="footprint_dry", ],
options=set()) default="footprint_dry",
image = PointerProperty(name="Image", options=set(),
description="", )
type=bpy.types.Image, image = PointerProperty(
options=set()) name="Image", description="", type=bpy.types.Image, options=set()
blend = EnumProperty(name="Blend Mode", )
description="", blend = EnumProperty(
items=[("kBlendAdd", "Add", ""), name="Blend Mode",
("kBlendAlpha", "Alpha", ""), description="",
("kBlendMADD", "Brighten", ""), items=[
("kBlendMult", "Multiply", "")], ("kBlendAdd", "Add", ""),
default="kBlendAlpha", ("kBlendAlpha", "Alpha", ""),
options=set()) ("kBlendMADD", "Brighten", ""),
("kBlendMult", "Multiply", ""),
length = IntProperty(name="Length", ],
description="", default="kBlendAlpha",
subtype="PERCENTAGE", options=set(),
min=0, soft_min=25, soft_max=400, default=100, )
options=set())
width = IntProperty(name="Width", length = IntProperty(
description="", name="Length",
subtype="PERCENTAGE", description="",
min=0, soft_min=25, soft_max=400, default=100, subtype="PERCENTAGE",
options=set()) min=0,
intensity = IntProperty(name="Intensity", soft_min=25,
description="", soft_max=400,
subtype="PERCENTAGE", default=100,
min=0, soft_max=100, default=100, options=set(),
options=set()) )
life_span = FloatProperty(name="Life Span", width = IntProperty(
description="", name="Width",
subtype="TIME", unit="TIME", description="",
min=0.0, soft_max=300.0, default=30.0, subtype="PERCENTAGE",
options=set()) min=0,
wet_time = FloatProperty(name="Wet Time", soft_min=25,
description="How long the decal print shapes stay wet after losing contact with this surface", soft_max=400,
subtype="TIME", unit="TIME", default=100,
min=0.0, soft_max=300.0, default=10.0, options=set(),
options=set()) )
intensity = IntProperty(
name="Intensity",
description="",
subtype="PERCENTAGE",
min=0,
soft_max=100,
default=100,
options=set(),
)
life_span = FloatProperty(
name="Life Span",
description="",
subtype="TIME",
unit="TIME",
min=0.0,
soft_max=300.0,
default=30.0,
options=set(),
)
wet_time = FloatProperty(
name="Wet Time",
description="How long the decal print shapes stay wet after losing contact with this surface",
subtype="TIME",
unit="TIME",
min=0.0,
soft_max=300.0,
default=10.0,
options=set(),
)
# Footprints to wet-ize # Footprints to wet-ize
wet_managers = CollectionProperty(type=PlasmaWetDecalRef) wet_managers = CollectionProperty(type=PlasmaWetDecalRef)
@ -135,8 +166,11 @@ class PlasmaScene(bpy.types.PropertyGroup):
decal_managers = CollectionProperty(type=PlasmaDecalManager) decal_managers = CollectionProperty(type=PlasmaDecalManager)
active_decal_index = IntProperty(options={"HIDDEN"}) active_decal_index = IntProperty(options={"HIDDEN"})
modifier_copy_object = PointerProperty(name="INTERNAL: Object to copy modifiers from", modifier_copy_object = PointerProperty(
options={"HIDDEN", "SKIP_SAVE"}, name="INTERNAL: Object to copy modifiers from",
type=bpy.types.Object) options={"HIDDEN", "SKIP_SAVE"},
modifier_copy_id = StringProperty(name="INTERNAL: Modifier to copy from", type=bpy.types.Object,
options={"HIDDEN", "SKIP_SAVE"}) )
modifier_copy_id = StringProperty(
name="INTERNAL: Modifier to copy from", options={"HIDDEN", "SKIP_SAVE"}
)

11
korman/properties/prop_sound.py

@ -16,8 +16,11 @@
import bpy import bpy
from bpy.props import * from bpy.props import *
class PlasmaSound(bpy.types.PropertyGroup): class PlasmaSound(bpy.types.PropertyGroup):
package = BoolProperty(name="Export", package = BoolProperty(
description="Package this file in the age export", name="Export",
default=True, description="Package this file in the age export",
options=set()) default=True,
options=set(),
)

7
korman/properties/prop_text.py

@ -16,7 +16,8 @@
import bpy import bpy
from bpy.props import * from bpy.props import *
class PlasmaText(bpy.types.PropertyGroup): class PlasmaText(bpy.types.PropertyGroup):
package = BoolProperty(name="Export", package = BoolProperty(
description="Package this file in the age export", name="Export", description="Package this file in the age export", options=set()
options=set()) )

179
korman/properties/prop_texture.py

@ -19,12 +19,15 @@ from bpy.props import *
from .. import idprops from .. import idprops
from .prop_anim import PlasmaAnimationCollection from .prop_anim import PlasmaAnimationCollection
class EnvMapVisRegion(idprops.IDPropObjectMixin, bpy.types.PropertyGroup): class EnvMapVisRegion(idprops.IDPropObjectMixin, bpy.types.PropertyGroup):
enabled = BoolProperty(default=True) enabled = BoolProperty(default=True)
control_region = PointerProperty(name="Control", control_region = PointerProperty(
description="Object defining a Plasma Visibility Control", name="Control",
type=bpy.types.Object, description="Object defining a Plasma Visibility Control",
poll=idprops.poll_visregion_objects) type=bpy.types.Object,
poll=idprops.poll_visregion_objects,
)
@classmethod @classmethod
def _idprop_mapping(cls): def _idprop_mapping(cls):
@ -34,71 +37,113 @@ class EnvMapVisRegion(idprops.IDPropObjectMixin, bpy.types.PropertyGroup):
class PlasmaLayer(bpy.types.PropertyGroup): class PlasmaLayer(bpy.types.PropertyGroup):
bl_idname = "texture.plasma_layer" bl_idname = "texture.plasma_layer"
opacity = FloatProperty(name="Layer Opacity", opacity = FloatProperty(
description="Opacity of the texture", name="Layer Opacity",
default=100.0, min=0.0, max=100.0, description="Opacity of the texture",
precision=0, subtype="PERCENTAGE") default=100.0,
alpha_halo = BoolProperty(name="High Alpha Test", min=0.0,
description="Fixes halos seen around semitransparent objects resulting from sorting errors", max=100.0,
default=False) precision=0,
subtype="PERCENTAGE",
envmap_color = FloatVectorProperty(name="Environment Map Color", )
description="The default background color rendered onto the Environment Map", alpha_halo = BoolProperty(
min=0.0, name="High Alpha Test",
max=1.0, description="Fixes halos seen around semitransparent objects resulting from sorting errors",
default=(1.0, 1.0, 1.0), default=False,
subtype="COLOR") )
envmap_addavatar = BoolProperty(name="Render Avatars", envmap_color = FloatVectorProperty(
description="Toggle the rendering of avatars in the environment map", name="Environment Map Color",
default=True) description="The default background color rendered onto the Environment Map",
min=0.0,
vis_regions = CollectionProperty(name="Visibility Regions", max=1.0,
type=EnvMapVisRegion) default=(1.0, 1.0, 1.0),
subtype="COLOR",
)
envmap_addavatar = BoolProperty(
name="Render Avatars",
description="Toggle the rendering of avatars in the environment map",
default=True,
)
vis_regions = CollectionProperty(name="Visibility Regions", type=EnvMapVisRegion)
active_region_index = IntProperty(options={"HIDDEN"}) active_region_index = IntProperty(options={"HIDDEN"})
is_detail_map = BoolProperty(name="Detail Fade", is_detail_map = BoolProperty(
description="Texture fades out as distance from the camera increases", name="Detail Fade",
default=False, description="Texture fades out as distance from the camera increases",
options=set()) default=False,
detail_fade_start = IntProperty(name="Falloff Start", options=set(),
description="", )
min=0, max=100, default=0, detail_fade_start = IntProperty(
options=set(), subtype="PERCENTAGE") name="Falloff Start",
detail_fade_stop = IntProperty(name="Falloff Stop", description="",
description="", min=0,
min=0, max=100, default=100, max=100,
options=set(), subtype="PERCENTAGE") default=0,
detail_opacity_start = IntProperty(name="Opacity Start", options=set(),
description="", subtype="PERCENTAGE",
min=0, max=100, default=50, )
options=set(), subtype="PERCENTAGE") detail_fade_stop = IntProperty(
detail_opacity_stop = IntProperty(name="Opacity Stop", name="Falloff Stop",
description="", description="",
min=0, max=100, default=0, min=0,
options=set(), subtype="PERCENTAGE") max=100,
default=100,
z_bias = BoolProperty(name="Z Bias", options=set(),
description="Request Z bias offset to defeat Z-fighting", subtype="PERCENTAGE",
default=False, )
options=set()) detail_opacity_start = IntProperty(
skip_depth_test = BoolProperty(name="Skip Depth Test", name="Opacity Start",
description="Causes this layer to be rendered, even if behind others", description="",
default=False, min=0,
options=set()) max=100,
skip_depth_write = BoolProperty(name="Skip Depth Write", default=50,
description="Don't save the depth information, allowing rendering of layers behind this one", options=set(),
default=False, subtype="PERCENTAGE",
options=set()) )
detail_opacity_stop = IntProperty(
dynatext_resolution = EnumProperty(name="Dynamic Text Map Resolution", name="Opacity Stop",
description="Size of the Dynamic Text Map's underlying image", description="",
items=[("128", "128x128", ""), min=0,
("256", "256x256", ""), max=100,
("512", "512x512", ""), default=0,
("1024", "1024x1024", "")], options=set(),
default="1024", subtype="PERCENTAGE",
options=set()) )
z_bias = BoolProperty(
name="Z Bias",
description="Request Z bias offset to defeat Z-fighting",
default=False,
options=set(),
)
skip_depth_test = BoolProperty(
name="Skip Depth Test",
description="Causes this layer to be rendered, even if behind others",
default=False,
options=set(),
)
skip_depth_write = BoolProperty(
name="Skip Depth Write",
description="Don't save the depth information, allowing rendering of layers behind this one",
default=False,
options=set(),
)
dynatext_resolution = EnumProperty(
name="Dynamic Text Map Resolution",
description="Size of the Dynamic Text Map's underlying image",
items=[
("128", "128x128", ""),
("256", "256x256", ""),
("512", "512x512", ""),
("1024", "1024x1024", ""),
],
default="1024",
options=set(),
)
subanimations = PointerProperty(type=PlasmaAnimationCollection) subanimations = PointerProperty(type=PlasmaAnimationCollection)

222
korman/properties/prop_world.py

@ -19,42 +19,44 @@ from PyHSPlasma import *
from ..addon_prefs import game_versions from ..addon_prefs import game_versions
class PlasmaFni(bpy.types.PropertyGroup): class PlasmaFni(bpy.types.PropertyGroup):
bl_idname = "world.plasma_fni" bl_idname = "world.plasma_fni"
fog_color = FloatVectorProperty(name="Fog Color", fog_color = FloatVectorProperty(
description="The default fog color used in your age", name="Fog Color",
default=(0.4, 0.3, 0.1), description="The default fog color used in your age",
min=0.0, default=(0.4, 0.3, 0.1),
max=1.0, min=0.0,
subtype="COLOR") max=1.0,
fog_method = EnumProperty(name="Fog Type", subtype="COLOR",
items=[ )
("linear", "Linear", "Fog Based on Linear Distance"), fog_method = EnumProperty(
("exp", "Exponential", "Fog Based on Exponential Distance"), name="Fog Type",
("exp2", "Exponential 2", "Fog Based on Exponential Distance Squared"), items=[
("none", "None", "No Fog") ("linear", "Linear", "Fog Based on Linear Distance"),
]) ("exp", "Exponential", "Fog Based on Exponential Distance"),
fog_start = FloatProperty(name="Start", ("exp2", "Exponential 2", "Fog Based on Exponential Distance Squared"),
description="", ("none", "None", "No Fog"),
default=-1500.0) ],
fog_end = FloatProperty(name="End", )
description="", fog_start = FloatProperty(name="Start", description="", default=-1500.0)
default=20000.0) fog_end = FloatProperty(name="End", description="", default=20000.0)
fog_density = FloatProperty(name="Density", fog_density = FloatProperty(name="Density", description="", default=1.0, min=0.0)
description="", clear_color = FloatVectorProperty(
default=1.0, name="Clear Color",
min=0.0) description="The default background color rendered in your age",
clear_color = FloatVectorProperty(name="Clear Color", min=0.0,
description="The default background color rendered in your age", max=1.0,
min=0.0, subtype="COLOR",
max=1.0, )
subtype="COLOR") yon = IntProperty(
yon = IntProperty(name="Draw Distance", name="Draw Distance",
description="The distance (in feet) Plasma will draw", description="The distance (in feet) Plasma will draw",
default=100000, default=100000,
soft_min=100, soft_min=100,
min=1) min=1,
)
class PlasmaGames(bpy.types.PropertyGroup): class PlasmaGames(bpy.types.PropertyGroup):
@ -114,37 +116,47 @@ class PlasmaPage(bpy.types.PropertyGroup):
self.name = "Page%02i" % suffix self.name = "Page%02i" % suffix
self.check_suffixes = True self.check_suffixes = True
name = StringProperty(name="Name", name = StringProperty(
description="Name of the specified page", name="Name", description="Name of the specified page", update=_rename_page
update=_rename_page) )
seq_suffix = IntProperty(name="ID", seq_suffix = IntProperty(
description="A numerical ID for this page", name="ID",
soft_min=0, # Negatives indicate global--advanced users only description="A numerical ID for this page",
default=0, # The add operator will autogen a default soft_min=0, # Negatives indicate global--advanced users only
update=_check_suffix) default=0, # The add operator will autogen a default
auto_load = BoolProperty(name="Auto Load", update=_check_suffix,
description="Load this page on link-in", )
default=True) auto_load = BoolProperty(
local_only = BoolProperty(name="Local Only", name="Auto Load", description="Load this page on link-in", default=True
description="This page should not synchronize with the server", )
default=False) local_only = BoolProperty(
enabled = BoolProperty(name="Export Page", name="Local Only",
description="Export this page", description="This page should not synchronize with the server",
default=True) default=False,
version = EnumProperty(name="Export Versions", )
description="Plasma versions this page exports under", enabled = BoolProperty(
items=game_versions, name="Export Page", description="Export this page", default=True
options={"ENUM_FLAG"}, )
default=set(list(zip(*game_versions))[0])) version = EnumProperty(
name="Export Versions",
description="Plasma versions this page exports under",
items=game_versions,
options={"ENUM_FLAG"},
default=set(list(zip(*game_versions))[0]),
)
# Implementation details... # Implementation details...
last_name = StringProperty(description="INTERNAL: Cached page name", last_name = StringProperty(
options={"HIDDEN"}) description="INTERNAL: Cached page name", options={"HIDDEN"}
last_seq_suffix = IntProperty(description="INTERNAL: Cached sequence suffix", )
options={"HIDDEN"}) last_seq_suffix = IntProperty(
check_suffixes = BoolProperty(description="INTERNAL: Should we sanity-check suffixes?", description="INTERNAL: Cached sequence suffix", options={"HIDDEN"}
options={"HIDDEN"}, )
default=False) check_suffixes = BoolProperty(
description="INTERNAL: Should we sanity-check suffixes?",
options={"HIDDEN"},
default=False,
)
class PlasmaAge(bpy.types.PropertyGroup): class PlasmaAge(bpy.types.PropertyGroup):
@ -153,14 +165,20 @@ class PlasmaAge(bpy.types.PropertyGroup):
log_func = exporter.report.warn log_func = exporter.report.warn
else: else:
log_func = exporter.report.port log_func = exporter.report.port
if self.seq_prefix <= self.MOUL_PREFIX_RANGE[0] or self.seq_prefix >= self.MOUL_PREFIX_RANGE[1]: if (
log_func("Age Sequence Prefix {} is potentially out of range (should be between {} and {})", self.seq_prefix <= self.MOUL_PREFIX_RANGE[0]
self.seq_prefix, *self.MOUL_PREFIX_RANGE) or self.seq_prefix >= self.MOUL_PREFIX_RANGE[1]
):
log_func(
"Age Sequence Prefix {} is potentially out of range (should be between {} and {})",
self.seq_prefix,
*self.MOUL_PREFIX_RANGE
)
_age_info = plAgeInfo() _age_info = plAgeInfo()
_age_info.dayLength = self.day_length _age_info.dayLength = self.day_length
_age_info.lingerTime = 180 # this is fairly standard _age_info.lingerTime = 180 # this is fairly standard
_age_info.maxCapacity = 50 # the server currently ignores this _age_info.maxCapacity = 50 # the server currently ignores this
_age_info.name = exporter.age_name _age_info.name = exporter.age_name
_age_info.seqPrefix = self.seq_prefix _age_info.seqPrefix = self.seq_prefix
_age_info.startDateTime = self.start_time _age_info.startDateTime = self.start_time
@ -170,35 +188,47 @@ class PlasmaAge(bpy.types.PropertyGroup):
MOUL_PREFIX_RANGE = ((pow(2, 16) - pow(2, 15)) * -1, pow(2, 15) - 1) MOUL_PREFIX_RANGE = ((pow(2, 16) - pow(2, 15)) * -1, pow(2, 15) - 1)
SP_PRFIX_RANGE = ((pow(2, 24) - pow(2, 23)) * -1, pow(2, 23) - 1) SP_PRFIX_RANGE = ((pow(2, 24) - pow(2, 23)) * -1, pow(2, 23) - 1)
day_length = FloatProperty(name="Day Length", day_length = FloatProperty(
description="Length of a day (in hours) on this age", name="Day Length",
default=30.230000, description="Length of a day (in hours) on this age",
soft_min=0.1, default=30.230000,
min=0.0) soft_min=0.1,
start_time = IntProperty(name="Start Time", min=0.0,
description="Seconds from 1/1/1970 until the first day on this age", )
subtype="UNSIGNED", start_time = IntProperty(
default=672211080, name="Start Time",
min=0) description="Seconds from 1/1/1970 until the first day on this age",
seq_prefix = IntProperty(name="Sequence Prefix", subtype="UNSIGNED",
description="A unique numerical ID for this age", default=672211080,
min=SP_PRFIX_RANGE[0], min=0,
soft_min=0, # Negative indicates global--advanced users only )
soft_max=MOUL_PREFIX_RANGE[1], seq_prefix = IntProperty(
max=SP_PRFIX_RANGE[1], name="Sequence Prefix",
default=100) description="A unique numerical ID for this age",
pages = CollectionProperty(name="Pages", min=SP_PRFIX_RANGE[0],
description="Registry pages for this age", soft_min=0, # Negative indicates global--advanced users only
type=PlasmaPage) soft_max=MOUL_PREFIX_RANGE[1],
age_sdl = BoolProperty(name="Age Global SDL", max=SP_PRFIX_RANGE[1],
description="This age has its own SDL file", default=100,
default=False) )
use_texture_page = BoolProperty(name="Use Textures Page", pages = CollectionProperty(
description="Exports all textures to a dedicated Textures page", name="Pages", description="Registry pages for this age", type=PlasmaPage
default=True) )
age_name = StringProperty(name="Age Name", age_sdl = BoolProperty(
description="Name of the Age to be used for data files", name="Age Global SDL",
options=set()) description="This age has its own SDL file",
default=False,
)
use_texture_page = BoolProperty(
name="Use Textures Page",
description="Exports all textures to a dedicated Textures page",
default=True,
)
age_name = StringProperty(
name="Age Name",
description="Name of the Age to be used for data files",
options=set(),
)
# Implementation details # Implementation details
active_page_index = IntProperty(name="Active Page Index") active_page_index = IntProperty(name="Active Page Index")

8
korman/render.py

@ -26,6 +26,7 @@ class PlasmaRenderEngine(bpy.types.RenderEngine):
# Explicitly whitelist compatible Blender panels... # Explicitly whitelist compatible Blender panels...
from bl_ui import properties_material from bl_ui import properties_material
properties_material.MATERIAL_PT_context_material.COMPAT_ENGINES.add("PLASMA_GAME") properties_material.MATERIAL_PT_context_material.COMPAT_ENGINES.add("PLASMA_GAME")
properties_material.MATERIAL_PT_diffuse.COMPAT_ENGINES.add("PLASMA_GAME") properties_material.MATERIAL_PT_diffuse.COMPAT_ENGINES.add("PLASMA_GAME")
properties_material.MATERIAL_PT_shading.COMPAT_ENGINES.add("PLASMA_GAME") properties_material.MATERIAL_PT_shading.COMPAT_ENGINES.add("PLASMA_GAME")
@ -37,18 +38,22 @@ properties_material.MATERIAL_PT_shadow.COMPAT_ENGINES.add("PLASMA_GAME")
del properties_material del properties_material
from bl_ui import properties_data_mesh from bl_ui import properties_data_mesh
properties_data_mesh.DATA_PT_normals.COMPAT_ENGINES.add("PLASMA_GAME") properties_data_mesh.DATA_PT_normals.COMPAT_ENGINES.add("PLASMA_GAME")
properties_data_mesh.DATA_PT_uv_texture.COMPAT_ENGINES.add("PLASMA_GAME") properties_data_mesh.DATA_PT_uv_texture.COMPAT_ENGINES.add("PLASMA_GAME")
properties_data_mesh.DATA_PT_vertex_colors.COMPAT_ENGINES.add("PLASMA_GAME") properties_data_mesh.DATA_PT_vertex_colors.COMPAT_ENGINES.add("PLASMA_GAME")
del properties_data_mesh del properties_data_mesh
def _whitelist_all(mod): def _whitelist_all(mod):
for i in dir(mod): for i in dir(mod):
attr = getattr(mod, i) attr = getattr(mod, i)
if hasattr(attr, "COMPAT_ENGINES"): if hasattr(attr, "COMPAT_ENGINES"):
getattr(attr, "COMPAT_ENGINES").add("PLASMA_GAME") getattr(attr, "COMPAT_ENGINES").add("PLASMA_GAME")
from bl_ui import properties_data_lamp from bl_ui import properties_data_lamp
properties_data_lamp.DATA_PT_context_lamp.COMPAT_ENGINES.add("PLASMA_GAME") properties_data_lamp.DATA_PT_context_lamp.COMPAT_ENGINES.add("PLASMA_GAME")
properties_data_lamp.DATA_PT_preview.COMPAT_ENGINES.add("PLASMA_GAME") properties_data_lamp.DATA_PT_preview.COMPAT_ENGINES.add("PLASMA_GAME")
properties_data_lamp.DATA_PT_lamp.COMPAT_ENGINES.add("PLASMA_GAME") properties_data_lamp.DATA_PT_lamp.COMPAT_ENGINES.add("PLASMA_GAME")
@ -60,14 +65,17 @@ properties_data_lamp.DATA_PT_custom_props_lamp.COMPAT_ENGINES.add("PLASMA_GAME")
del properties_data_lamp del properties_data_lamp
from bl_ui import properties_render from bl_ui import properties_render
_whitelist_all(properties_render) _whitelist_all(properties_render)
del properties_render del properties_render
from bl_ui import properties_texture from bl_ui import properties_texture
_whitelist_all(properties_texture) _whitelist_all(properties_texture)
del properties_texture del properties_texture
from bl_ui import properties_world from bl_ui import properties_world
properties_world.WORLD_PT_context_world.COMPAT_ENGINES.add("PLASMA_GAME") properties_world.WORLD_PT_context_world.COMPAT_ENGINES.add("PLASMA_GAME")
properties_world.WORLD_PT_ambient_occlusion.COMPAT_ENGINES.add("PLASMA_GAME") properties_world.WORLD_PT_ambient_occlusion.COMPAT_ENGINES.add("PLASMA_GAME")
properties_world.WORLD_PT_environment_lighting.COMPAT_ENGINES.add("PLASMA_GAME") properties_world.WORLD_PT_environment_lighting.COMPAT_ENGINES.add("PLASMA_GAME")

1
korman/ui/__init__.py

@ -32,5 +32,6 @@ from .ui_world import *
def register(): def register():
ui_menus.register() ui_menus.register()
def unregister(): def unregister():
ui_menus.unregister() ui_menus.unregister()

69
korman/ui/modifiers/anim.py

@ -18,6 +18,7 @@ import bpy
from .. import ui_list from .. import ui_list
from .. import ui_anim from .. import ui_anim
def _check_for_anim(layout, modifier): def _check_for_anim(layout, modifier):
try: try:
action = modifier.blender_action action = modifier.blender_action
@ -27,6 +28,7 @@ def _check_for_anim(layout, modifier):
else: else:
return action if action is not None else False return action if action is not None else False
def animation(modifier, layout, context): def animation(modifier, layout, context):
action = _check_for_anim(layout, modifier) action = _check_for_anim(layout, modifier)
if action is None: if action is None:
@ -34,11 +36,14 @@ def animation(modifier, layout, context):
if modifier.id_data.type == "CAMERA": if modifier.id_data.type == "CAMERA":
if not modifier.id_data.data.plasma_camera.allow_animations: if not modifier.id_data.data.plasma_camera.allow_animations:
layout.label("Animation modifiers are not allowed on this camera type!", icon="ERROR") layout.label(
"Animation modifiers are not allowed on this camera type!", icon="ERROR"
)
return return
ui_anim.draw_multi_animation(layout, "object", modifier, "subanimations") ui_anim.draw_multi_animation(layout, "object", modifier, "subanimations")
def animation_filter(modifier, layout, context): def animation_filter(modifier, layout, context):
split = layout.split() split = layout.split()
@ -52,9 +57,25 @@ def animation_filter(modifier, layout, context):
col.label("Rotation:") col.label("Rotation:")
col.prop(modifier, "no_rotation", text="Filter Rotation") col.prop(modifier, "no_rotation", text="Filter Rotation")
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(
label = item.child_anim.name if item.child_anim is not None else "[No Child Specified]" self,
context,
layout,
data,
item,
icon,
active_data,
active_property,
index=0,
flt_flag=0,
):
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" icon = "ACTION" if item.child_anim is not None else "ERROR"
layout.label(text=label, icon=icon) layout.label(text=label, icon=icon)
@ -64,14 +85,34 @@ def animation_group(modifier, layout, context):
if action is None: if action is None:
return return
ui_list.draw_modifier_list(layout, "GroupListUI", modifier, "children", ui_list.draw_modifier_list(
"active_child_index", rows=3, maxrows=4) layout,
"GroupListUI",
modifier,
"children",
"active_child_index",
rows=3,
maxrows=4,
)
if modifier.children: if modifier.children:
layout.prop(modifier.children[modifier.active_child_index], "child_anim", icon="ACTION") layout.prop(
modifier.children[modifier.active_child_index], "child_anim", icon="ACTION"
)
class LoopListUI(bpy.types.UIList): class LoopListUI(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,
):
layout.prop(item, "loop_name", emboss=False, text="", icon="PMARKER_ACT") layout.prop(item, "loop_name", emboss=False, text="", icon="PMARKER_ACT")
@ -83,9 +124,17 @@ def animation_loop(modifier, layout, context):
elif action is None: elif action is None:
return return
ui_list.draw_modifier_list(layout, "LoopListUI", modifier, "loops", ui_list.draw_modifier_list(
"active_loop_index", name_prefix="Loop", layout,
name_prop="loop_name", rows=2, maxrows=3) "LoopListUI",
modifier,
"loops",
"active_loop_index",
name_prefix="Loop",
name_prop="loop_name",
rows=2,
maxrows=3,
)
# Modify the loop points # Modify the loop points
if modifier.loops: if modifier.loops:
loop = modifier.loops[modifier.active_loop_index] loop = modifier.loops[modifier.active_loop_index]

2
korman/ui/modifiers/avatar.py

@ -17,6 +17,7 @@ import bpy
from ...helpers import find_modifier from ...helpers import find_modifier
def laddermod(modifier, layout, context): def laddermod(modifier, layout, context):
layout.label(text="Avatar climbs facing negative Y.") layout.label(text="Avatar climbs facing negative Y.")
@ -26,6 +27,7 @@ def laddermod(modifier, layout, context):
layout.prop(modifier, "facing_object", icon="MESH_DATA") layout.prop(modifier, "facing_object", icon="MESH_DATA")
def sittingmod(modifier, layout, context): def sittingmod(modifier, layout, context):
layout.row().prop(modifier, "approach") layout.row().prop(modifier, "approach")

35
korman/ui/modifiers/gui.py

@ -18,21 +18,47 @@ from pathlib import Path
from . import ui_list from . import ui_list
class ImageListUI(bpy.types.UIList): class ImageListUI(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.image is None: if item.image is None:
layout.label("[No Image Specified]", icon="ERROR") layout.label("[No Image Specified]", icon="ERROR")
else: else:
layout.label(str(Path(item.image.name).with_suffix(".hsm")), icon_value=item.image.preview.icon_id) layout.label(
str(Path(item.image.name).with_suffix(".hsm")),
icon_value=item.image.preview.icon_id,
)
layout.prop(item, "enabled", text="") layout.prop(item, "enabled", text="")
def imagelibmod(modifier, layout, context): def imagelibmod(modifier, layout, context):
ui_list.draw_modifier_list(layout, "ImageListUI", modifier, "images", "active_image_index", rows=3, maxrows=6) ui_list.draw_modifier_list(
layout,
"ImageListUI",
modifier,
"images",
"active_image_index",
rows=3,
maxrows=6,
)
if modifier.images: if modifier.images:
row = layout.row(align=True) row = layout.row(align=True)
row.template_ID(modifier.images[modifier.active_image_index], "image", open="image.open") row.template_ID(
modifier.images[modifier.active_image_index], "image", open="image.open"
)
def journalbookmod(modifier, layout, context): def journalbookmod(modifier, layout, context):
layout.prop_menu_enum(modifier, "versions") layout.prop_menu_enum(modifier, "versions")
@ -68,6 +94,7 @@ def journalbookmod(modifier, layout, context):
main_col.label("Clickable Region:") main_col.label("Clickable Region:")
main_col.prop(modifier, "clickable_region", text="") main_col.prop(modifier, "clickable_region", text="")
def linkingbookmod(modifier, layout, context): def linkingbookmod(modifier, layout, context):
def row_alert(prop_name, **kwargs): def row_alert(prop_name, **kwargs):
row = layout.row() row = layout.row()

27
korman/ui/modifiers/logic.py

@ -17,8 +17,20 @@ import bpy
from .. import ui_list from .. import ui_list
class LogicListUI(bpy.types.UIList): class LogicListUI(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.node_tree: if item.node_tree:
# Using layout.prop on the pointer prevents clicking on the item O.o # Using layout.prop on the pointer prevents clicking on the item O.o
layout.label(item.node_tree.name, icon="NODETREE") layout.label(item.node_tree.name, icon="NODETREE")
@ -27,8 +39,15 @@ class LogicListUI(bpy.types.UIList):
def advanced_logic(modifier, layout, context): def advanced_logic(modifier, layout, context):
ui_list.draw_modifier_list(layout, "LogicListUI", modifier, "logic_groups", ui_list.draw_modifier_list(
"active_group_index", rows=2, maxrows=3) layout,
"LogicListUI",
modifier,
"logic_groups",
"active_group_index",
rows=2,
maxrows=3,
)
# Modify the logic groups # Modify the logic groups
if modifier.logic_groups: if modifier.logic_groups:
@ -36,9 +55,11 @@ def advanced_logic(modifier, layout, context):
layout.row().prop_menu_enum(logic, "version") layout.row().prop_menu_enum(logic, "version")
layout.prop(logic, "node_tree", icon="NODETREE") layout.prop(logic, "node_tree", icon="NODETREE")
def spawnpoint(modifier, layout, context): def spawnpoint(modifier, layout, context):
layout.label(text="Avatar faces negative Y.") layout.label(text="Avatar faces negative Y.")
def maintainersmarker(modifier, layout, context): def maintainersmarker(modifier, layout, context):
layout.label(text="Positive Y is North, positive Z is up.") layout.label(text="Positive Y is North, positive Z is up.")
layout.prop(modifier, "calibration") layout.prop(modifier, "calibration")

2
korman/ui/modifiers/physics.py

@ -13,6 +13,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Korman. If not, see <http://www.gnu.org/licenses/>. # along with Korman. If not, see <http://www.gnu.org/licenses/>.
def collision(modifier, layout, context): def collision(modifier, layout, context):
layout.prop(modifier, "bounds") layout.prop(modifier, "bounds")
layout.prop(modifier, "surface") layout.prop(modifier, "surface")
@ -45,6 +46,7 @@ def collision(modifier, layout, context):
row.active = modifier.bounds == "trimesh" row.active = modifier.bounds == "trimesh"
row.prop(modifier, "proxy_object") row.prop(modifier, "proxy_object")
def subworld_def(modifier, layout, context): def subworld_def(modifier, layout, context):
layout.prop(modifier, "sub_type") layout.prop(modifier, "sub_type")
if modifier.sub_type != "dynamicav": if modifier.sub_type != "dynamicav":

18
korman/ui/modifiers/region.py

@ -16,6 +16,7 @@
import bpy import bpy
from .. import ui_camera from .. import ui_camera
def camera_rgn(modifier, layout, context): def camera_rgn(modifier, layout, context):
layout.prop(modifier, "camera_type") layout.prop(modifier, "camera_type")
if modifier.camera_type == "manual": if modifier.camera_type == "manual":
@ -29,20 +30,28 @@ def camera_rgn(modifier, layout, context):
layout.separator() layout.separator()
i(layout, cam_type, cam_props) i(layout, cam_type, cam_props)
_draw_props(layout, (ui_camera.draw_camera_mode_props, _draw_props(
ui_camera.draw_camera_poa_props, layout,
ui_camera.draw_camera_pos_props, (
ui_camera.draw_camera_manipulation_props)) ui_camera.draw_camera_mode_props,
ui_camera.draw_camera_poa_props,
ui_camera.draw_camera_pos_props,
ui_camera.draw_camera_manipulation_props,
),
)
def footstep(modifier, layout, context): def footstep(modifier, layout, context):
layout.prop(modifier, "bounds") layout.prop(modifier, "bounds")
layout.prop(modifier, "surface") layout.prop(modifier, "surface")
def paniclink(modifier, layout, context): def paniclink(modifier, layout, context):
phys_mod = context.object.plasma_modifiers.collision phys_mod = context.object.plasma_modifiers.collision
layout.prop(phys_mod, "bounds") layout.prop(phys_mod, "bounds")
layout.prop(modifier, "play_anim") layout.prop(modifier, "play_anim")
def softvolume(modifier, layout, context): 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")
@ -59,6 +68,7 @@ def softvolume(modifier, layout, context):
col.prop(modifier, "invert") col.prop(modifier, "invert")
col.prop(modifier, "soft_distance") col.prop(modifier, "soft_distance")
def subworld_rgn(modifier, layout, context): def subworld_rgn(modifier, layout, context):
layout.prop(modifier, "subworld") layout.prop(modifier, "subworld")
collision_mod = modifier.id_data.plasma_modifiers.collision collision_mod = modifier.id_data.plasma_modifiers.collision

187
korman/ui/modifiers/render.py

@ -18,8 +18,20 @@ import bpy
from .. import ui_list from .. import ui_list
from ...exporter.mesh import _VERTEX_COLOR_LAYERS from ...exporter.mesh import _VERTEX_COLOR_LAYERS
class BlendOntoListUI(bpy.types.UIList): class BlendOntoListUI(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.blend_onto is None: if item.blend_onto is None:
layout.label("[No Object Specified]", icon="ERROR") layout.label("[No Object Specified]", icon="ERROR")
else: else:
@ -37,8 +49,15 @@ def blend(modifier, layout, context):
layout.separator() layout.separator()
layout.label("Render Dependencies:") layout.label("Render Dependencies:")
ui_list.draw_modifier_list(layout, "BlendOntoListUI", modifier, "dependencies", ui_list.draw_modifier_list(
"active_dependency_index", rows=2, maxrows=4) layout,
"BlendOntoListUI",
modifier,
"dependencies",
"active_dependency_index",
rows=2,
maxrows=4,
)
try: try:
dependency_ref = modifier.dependencies[modifier.active_dependency_index] dependency_ref = modifier.dependencies[modifier.active_dependency_index]
except: except:
@ -49,7 +68,18 @@ def blend(modifier, layout, context):
class DecalMgrListUI(bpy.types.UIList): class DecalMgrListUI(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.name: if item.name:
layout.label(item.name, icon="BRUSH_DATA") layout.label(item.name, icon="BRUSH_DATA")
layout.prop(item, "enabled", text="") layout.prop(item, "enabled", text="")
@ -69,34 +99,54 @@ def decal_print(modifier, layout, context):
row.prop(modifier, "height") row.prop(modifier, "height")
layout.separator() layout.separator()
ui_list.draw_modifier_list(layout, "DecalMgrListUI", modifier, "managers", ui_list.draw_modifier_list(
"active_manager_index", rows=2, maxrows=3) layout,
"DecalMgrListUI",
modifier,
"managers",
"active_manager_index",
rows=2,
maxrows=3,
)
try: try:
mgr_ref = modifier.managers[modifier.active_manager_index] mgr_ref = modifier.managers[modifier.active_manager_index]
except: except:
pass pass
else: else:
scene = context.scene.plasma_scene scene = context.scene.plasma_scene
decal_mgr = next((i for i in scene.decal_managers if i.display_name == mgr_ref), None) decal_mgr = next(
(i for i in scene.decal_managers if i.display_name == mgr_ref), None
)
layout.alert = decal_mgr is None layout.alert = decal_mgr is None
layout.prop_search(mgr_ref, "name", scene, "decal_managers", icon="BRUSH_DATA") layout.prop_search(mgr_ref, "name", scene, "decal_managers", icon="BRUSH_DATA")
layout.alert = False layout.alert = False
def decal_receive(modifier, layout, context): def decal_receive(modifier, layout, context):
ui_list.draw_modifier_list(layout, "DecalMgrListUI", modifier, "managers", ui_list.draw_modifier_list(
"active_manager_index", rows=2, maxrows=3) layout,
"DecalMgrListUI",
modifier,
"managers",
"active_manager_index",
rows=2,
maxrows=3,
)
try: try:
mgr_ref = modifier.managers[modifier.active_manager_index] mgr_ref = modifier.managers[modifier.active_manager_index]
except: except:
pass pass
else: else:
scene = context.scene.plasma_scene scene = context.scene.plasma_scene
decal_mgr = next((i for i in scene.decal_managers if i.display_name == mgr_ref), None) decal_mgr = next(
(i for i in scene.decal_managers if i.display_name == mgr_ref), None
)
layout.alert = decal_mgr is None layout.alert = decal_mgr is None
layout.prop_search(mgr_ref, "name", scene, "decal_managers", icon="BRUSH_DATA") layout.prop_search(mgr_ref, "name", scene, "decal_managers", icon="BRUSH_DATA")
def dynatext(modifier, layout, context): def dynatext(modifier, layout, context):
col = layout.column() col = layout.column()
col.alert = modifier.texture is None col.alert = modifier.texture is None
@ -128,12 +178,16 @@ def dynatext(modifier, layout, context):
split = layout.split() split = layout.split()
col = split.column(align=True) col = split.column(align=True)
if modifier.texture is not None: if modifier.texture is not None:
col.alert = modifier.margin_top + modifier.margin_bottom >= int(modifier.texture.plasma_layer.dynatext_resolution) col.alert = modifier.margin_top + modifier.margin_bottom >= int(
modifier.texture.plasma_layer.dynatext_resolution
)
col.prop(modifier, "margin_top") col.prop(modifier, "margin_top")
col.prop(modifier, "margin_bottom") col.prop(modifier, "margin_bottom")
col = split.column(align=True) col = split.column(align=True)
if modifier.texture is not None: if modifier.texture is not None:
col.alert = modifier.margin_left + modifier.margin_right >= int(modifier.texture.plasma_layer.dynatext_resolution) col.alert = modifier.margin_left + modifier.margin_right >= int(
modifier.texture.plasma_layer.dynatext_resolution
)
col.prop(modifier, "margin_left") col.prop(modifier, "margin_left")
col.prop(modifier, "margin_right") col.prop(modifier, "margin_right")
@ -142,6 +196,7 @@ def dynatext(modifier, layout, context):
flow.prop_menu_enum(modifier, "justify") flow.prop_menu_enum(modifier, "justify")
flow.prop(modifier, "line_spacing") flow.prop(modifier, "line_spacing")
def fademod(modifier, layout, context): def fademod(modifier, layout, context):
layout.prop(modifier, "fader_type") layout.prop(modifier, "fader_type")
@ -162,17 +217,23 @@ def fademod(modifier, layout, context):
col.prop(modifier, "far_opaq") col.prop(modifier, "far_opaq")
col.prop(modifier, "far_trans") col.prop(modifier, "far_trans")
if (modifier.fader_type in ("SimpleDist", "DistOpacity") and if modifier.fader_type in ("SimpleDist", "DistOpacity") and not (
not (modifier.near_trans <= modifier.near_opaq <= modifier.far_opaq <= modifier.far_trans)): modifier.near_trans
<= modifier.near_opaq
<= modifier.far_opaq
<= modifier.far_trans
):
# Warn the user that the values are not recommended. # Warn the user that the values are not recommended.
layout.label("Distance values must be equal or increasing!", icon="ERROR") layout.label("Distance values must be equal or increasing!", icon="ERROR")
def followmod(modifier, layout, context): 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(modifier, "leader", icon="OUTLINER_OB_MESH") layout.prop(modifier, "leader", icon="OUTLINER_OB_MESH")
def grass_shader(modifier, layout, context): def grass_shader(modifier, layout, context):
layout.prop(modifier, "wave_selector", icon="SMOOTHCURVE") layout.prop(modifier, "wave_selector", icon="SMOOTHCURVE")
layout.separator() layout.separator()
@ -188,6 +249,7 @@ def grass_shader(modifier, layout, context):
col.prop(wave, "direction", text="") col.prop(wave, "direction", text="")
box.prop(wave, "speed") box.prop(wave, "speed")
def lighting(modifier, layout, context): def lighting(modifier, layout, context):
split = layout.split() split = layout.split()
col = split.column() col = split.column()
@ -208,34 +270,65 @@ def lighting(modifier, layout, context):
col.label("Satan remains ensconced deep in the abyss...", icon="GHOST_ENABLED") col.label("Satan remains ensconced deep in the abyss...", icon="GHOST_ENABLED")
col.label("Animated lights will be cast at runtime.", icon="LAYER_USED") col.label("Animated lights will be cast at runtime.", icon="LAYER_USED")
col.label("Projection lights will be cast at runtime.", icon="LAYER_USED") col.label("Projection lights will be cast at runtime.", icon="LAYER_USED")
col.label("Specular lights will be cast to specular materials at runtime.", icon="LAYER_USED") col.label(
col.label("Other Plasma lights {} be cast at runtime.".format("will" if modifier.rt_lights else "will NOT"), "Specular lights will be cast to specular materials at runtime.",
icon="LAYER_USED") icon="LAYER_USED",
)
col.label(
"Other Plasma lights {} be cast at runtime.".format(
"will" if modifier.rt_lights else "will NOT"
),
icon="LAYER_USED",
)
map_type = "a lightmap" if lightmap.bake_lightmap else "vertex colors" map_type = "a lightmap" if lightmap.bake_lightmap else "vertex colors"
if lightmap.enabled and lightmap.lights: if lightmap.enabled and lightmap.lights:
col.label("All '{}' lights will be baked to {}".format(lightmap.lights.name, map_type), col.label(
icon="LAYER_USED") "All '{}' lights will be baked to {}".format(
lightmap.lights.name, map_type
),
icon="LAYER_USED",
)
elif have_static_lights: elif have_static_lights:
light_type = "Blender-only" if modifier.rt_lights else "unanimated" light_type = "Blender-only" if modifier.rt_lights else "unanimated"
col.label("Other {} lights will be baked to {} (if applicable).".format(light_type, map_type), icon="LAYER_USED") col.label(
"Other {} lights will be baked to {} (if applicable).".format(
light_type, map_type
),
icon="LAYER_USED",
)
else: else:
col.label("No static lights will be baked.", icon="LAYER_USED") col.label("No static lights will be baked.", icon="LAYER_USED")
def lightmap(modifier, layout, context): def lightmap(modifier, layout, context):
pl_scene = context.scene.plasma_scene pl_scene = context.scene.plasma_scene
is_texture = modifier.bake_type == "lightmap" is_texture = modifier.bake_type == "lightmap"
layout.prop(modifier, "bake_type") layout.prop(modifier, "bake_type")
if modifier.bake_type == "vcol": if modifier.bake_type == "vcol":
col_layer = next((i for i in modifier.id_data.data.vertex_colors if i.name.lower() in _VERTEX_COLOR_LAYERS), None) col_layer = next(
(
i
for i in modifier.id_data.data.vertex_colors
if i.name.lower() in _VERTEX_COLOR_LAYERS
),
None,
)
if col_layer is not None: if col_layer is not None:
layout.label("Mesh color layer '{}' will override this lighting.".format(col_layer.name), icon="ERROR") layout.label(
"Mesh color layer '{}' will override this lighting.".format(
col_layer.name
),
icon="ERROR",
)
col = layout.column() col = layout.column()
col.active = is_texture col.active = is_texture
col.prop(modifier, "quality") col.prop(modifier, "quality")
layout.prop_search(modifier, "bake_pass_name", pl_scene, "bake_passes", icon="RENDERLAYERS") layout.prop_search(
modifier, "bake_pass_name", pl_scene, "bake_passes", icon="RENDERLAYERS"
)
layout.prop(modifier, "lights") layout.prop(modifier, "lights")
col = layout.column() col = layout.column()
col.active = is_texture col.active = is_texture
@ -247,15 +340,23 @@ def lightmap(modifier, layout, context):
col.prop(modifier, "image", icon="IMAGE_RGB") col.prop(modifier, "image", icon="IMAGE_RGB")
# Lightmaps can only be applied to objects with opaque materials. # Lightmaps can only be applied to objects with opaque materials.
if is_texture and any((i.use_transparency for i in modifier.id_data.data.materials if i is not None)): if is_texture and any(
(i.use_transparency for i in modifier.id_data.data.materials if i is not None)
):
layout.label("Transparent objects cannot be lightmapped.", icon="ERROR") layout.label("Transparent objects cannot be lightmapped.", icon="ERROR")
else: else:
row = layout.row(align=True) row = layout.row(align=True)
if modifier.bake_lightmap: if modifier.bake_lightmap:
row.operator("object.plasma_lightmap_preview", "Preview", icon="RENDER_STILL").final = False row.operator(
row.operator("object.plasma_lightmap_preview", "Bake for Export", icon="RENDER_STILL").final = True "object.plasma_lightmap_preview", "Preview", icon="RENDER_STILL"
).final = False
row.operator(
"object.plasma_lightmap_preview", "Bake for Export", icon="RENDER_STILL"
).final = True
else: else:
row.operator("object.plasma_lightmap_preview", "Bake", icon="RENDER_STILL").final = True row.operator(
"object.plasma_lightmap_preview", "Bake", icon="RENDER_STILL"
).final = True
# 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
@ -264,10 +365,13 @@ def lightmap(modifier, layout, context):
if is_texture: if is_texture:
tex = bpy.data.textures.get("LIGHTMAPGEN_PREVIEW") tex = bpy.data.textures.get("LIGHTMAPGEN_PREVIEW")
if tex is not None and tex.image is not None: if tex is not None and tex.image is not None:
im_name = "{}_LIGHTMAPGEN_PREVIEW.png".format(context.active_object.name) im_name = "{}_LIGHTMAPGEN_PREVIEW.png".format(
context.active_object.name
)
if tex.image.name == im_name: if tex.image.name == im_name:
layout.template_preview(tex, show_buttons=False) layout.template_preview(tex, show_buttons=False)
def rtshadow(modifier, layout, context): def rtshadow(modifier, layout, context):
split = layout.split() split = layout.split()
col = split.column() col = split.column()
@ -279,6 +383,7 @@ def rtshadow(modifier, layout, context):
col.prop(modifier, "limit_resolution") col.prop(modifier, "limit_resolution")
col.prop(modifier, "self_shadow") col.prop(modifier, "self_shadow")
def viewfacemod(modifier, layout, context): def viewfacemod(modifier, layout, context):
layout.prop(modifier, "preset_options") layout.prop(modifier, "preset_options")
@ -302,8 +407,20 @@ def viewfacemod(modifier, layout, context):
col.enabled = modifier.offset col.enabled = modifier.offset
col.prop(modifier, "offset_coord") col.prop(modifier, "offset_coord")
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,
):
if item.control_region is None: if item.control_region is None:
layout.label("[No Object Specified]", icon="ERROR") layout.label("[No Object Specified]", icon="ERROR")
else: else:
@ -312,12 +429,20 @@ class VisRegionListUI(bpy.types.UIList):
def visibility(modifier, layout, context): def visibility(modifier, layout, context):
ui_list.draw_modifier_list(layout, "VisRegionListUI", modifier, "regions", ui_list.draw_modifier_list(
"active_region_index", rows=2, maxrows=3) layout,
"VisRegionListUI",
modifier,
"regions",
"active_region_index",
rows=2,
maxrows=3,
)
if modifier.regions: if modifier.regions:
layout.prop(modifier.regions[modifier.active_region_index], "control_region") 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")

36
korman/ui/modifiers/sound.py

@ -17,10 +17,12 @@ import bpy
from .. import ui_list from .. import ui_list
def random_sound(modifier, layout, context): def random_sound(modifier, layout, context):
parent_bo = modifier.id_data.parent parent_bo = modifier.id_data.parent
collision_bad = (modifier.mode == "collision" and (parent_bo is None or collision_bad = modifier.mode == "collision" and (
not parent_bo.plasma_modifiers.collision.enabled)) parent_bo is None or not parent_bo.plasma_modifiers.collision.enabled
)
layout.alert = collision_bad layout.alert = collision_bad
layout.prop(modifier, "mode") layout.prop(modifier, "mode")
if collision_bad: if collision_bad:
@ -50,13 +52,26 @@ def random_sound(modifier, layout, context):
layout.alert = len(modifier.surfaces) == 0 layout.alert = len(modifier.surfaces) == 0
layout.prop_menu_enum(modifier, "surfaces") layout.prop_menu_enum(modifier, "surfaces")
def _draw_fade_ui(modifier, layout, label): def _draw_fade_ui(modifier, layout, label):
layout.label(label) layout.label(label)
layout.prop(modifier, "fade_type", text="") layout.prop(modifier, "fade_type", text="")
layout.prop(modifier, "length") layout.prop(modifier, "length")
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: if item.sound:
layout.prop(item, "name", emboss=False, icon="SOUND", text="") layout.prop(item, "name", emboss=False, icon="SOUND", text="")
layout.prop(item, "enabled", text="") layout.prop(item, "enabled", text="")
@ -65,8 +80,15 @@ class SoundListUI(bpy.types.UIList):
def soundemit(modifier, layout, context): def soundemit(modifier, layout, context):
ui_list.draw_modifier_list(layout, "SoundListUI", modifier, "sounds", ui_list.draw_modifier_list(
"active_sound_index", rows=2, maxrows=3) layout,
"SoundListUI",
modifier,
"sounds",
"active_sound_index",
rows=2,
maxrows=3,
)
try: try:
sound = modifier.sounds[modifier.active_sound_index] sound = modifier.sounds[modifier.active_sound_index]
@ -89,7 +111,9 @@ def soundemit(modifier, layout, context):
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="")
else: else:
row.operator_menu_enum("sound.plasma_unpack", "method", icon="PACKAGE", text="") row.operator_menu_enum(
"sound.plasma_unpack", "method", icon="PACKAGE", text=""
)
col = split.column() col = split.column()
col.enabled = data is not None col.enabled = data is not None

32
korman/ui/modifiers/water.py

@ -17,6 +17,7 @@ import bpy
from .. import ui_list from .. import ui_list
def swimregion(modifier, layout, context): def swimregion(modifier, layout, context):
split = layout.split() split = layout.split()
col = split.column() col = split.column()
@ -56,6 +57,7 @@ def swimregion(modifier, layout, context):
layout.prop(modifier, "current") layout.prop(modifier, "current")
def water_basic(modifier, layout, context): def water_basic(modifier, layout, context):
layout.prop(modifier, "wind_object") layout.prop(modifier, "wind_object")
layout.prop(modifier, "envmap") layout.prop(modifier, "envmap")
@ -91,6 +93,7 @@ def water_basic(modifier, layout, context):
col.prop(modifier, "zero_wave", text="Start") col.prop(modifier, "zero_wave", text="Start")
col.prop(modifier, "depth_wave", text="End") col.prop(modifier, "depth_wave", text="End")
def _wavestate(modifier, layout, context): def _wavestate(modifier, layout, context):
split = layout.split() split = layout.split()
col = split.column() col = split.column()
@ -104,18 +107,39 @@ def _wavestate(modifier, layout, context):
col.prop(modifier, "chop") col.prop(modifier, "chop")
col.prop(modifier, "angle_dev") col.prop(modifier, "angle_dev")
water_geostate = _wavestate water_geostate = _wavestate
water_texstate = _wavestate water_texstate = _wavestate
class ShoreListUI(bpy.types.UIList): class ShoreListUI(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,
):
layout.prop(item, "display_name", emboss=False, text="", icon="MOD_WAVE") layout.prop(item, "display_name", emboss=False, text="", icon="MOD_WAVE")
def water_shore(modifier, layout, context): def water_shore(modifier, layout, context):
ui_list.draw_modifier_list(layout, "ShoreListUI", modifier, "shores", ui_list.draw_modifier_list(
"active_shore_index", name_prefix="Shore", layout,
name_prop="display_name", rows=2, maxrows=3) "ShoreListUI",
modifier,
"shores",
"active_shore_index",
name_prefix="Shore",
name_prop="display_name",
rows=2,
maxrows=3,
)
# Display the active shore # Display the active shore
if modifier.shores: if modifier.shores:

38
korman/ui/ui_anim.py

@ -17,20 +17,41 @@ import bpy
from . import ui_list from . import ui_list
class AnimListUI(bpy.types.UIList): class AnimListUI(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,
):
layout.label(item.animation_name, icon="ANIM") layout.label(item.animation_name, icon="ANIM")
def draw_multi_animation(layout, context_attr, prop_base, anims_collection_name, *, use_box=False, **kwargs): def draw_multi_animation(
layout, context_attr, prop_base, anims_collection_name, *, use_box=False, **kwargs
):
# Yeah, I know this looks weird, but it lets us pretend that the PlasmaAnimationCollection # Yeah, I know this looks weird, but it lets us pretend that the PlasmaAnimationCollection
# is a first class collection property. Fancy. # is a first class collection property. Fancy.
anims = getattr(prop_base, anims_collection_name) anims = getattr(prop_base, anims_collection_name)
kwargs.setdefault("rows", 2) kwargs.setdefault("rows", 2)
ui_list.draw_list(layout, "AnimListUI", context_attr, anims, ui_list.draw_list(
"animation_collection", "active_animation_index", layout,
name_prop="animation_name", name_prefix="Animation", "AnimListUI",
**kwargs) context_attr,
anims,
"animation_collection",
"active_animation_index",
name_prop="animation_name",
name_prefix="Animation",
**kwargs
)
try: try:
anim = anims.animation_collection[anims.active_animation_index] anim = anims.animation_collection[anims.active_animation_index]
except IndexError: except IndexError:
@ -39,6 +60,7 @@ def draw_multi_animation(layout, context_attr, prop_base, anims_collection_name,
sub = layout.box() if use_box else layout sub = layout.box() if use_box else layout
draw_single_animation(sub, anim) draw_single_animation(sub, anim)
def draw_single_animation(layout, anim): def draw_single_animation(layout, anim):
row = layout.row() row = layout.row()
row.enabled = not anim.is_entire_animation row.enabled = not anim.is_entire_animation
@ -62,7 +84,9 @@ def draw_single_animation(layout, anim):
action = getattr(anim.id_data.animation_data, "action", None) action = getattr(anim.id_data.animation_data, "action", None)
if action: if action:
layout.separator() layout.separator()
layout.prop_search(anim, "initial_marker", action, "pose_markers", icon="PMARKER") layout.prop_search(
anim, "initial_marker", action, "pose_markers", icon="PMARKER"
)
col = layout.column() col = layout.column()
col.active = anim.loop and not anim.sdl_var col.active = anim.loop and not anim.sdl_var
col.prop_search(anim, "loop_start", action, "pose_markers", icon="PMARKER") col.prop_search(anim, "loop_start", action, "pose_markers", icon="PMARKER")

94
korman/ui/ui_camera.py

@ -18,7 +18,10 @@ import bpy
from .. import helpers from .. import helpers
from . import ui_list from . import ui_list
def _draw_alert_prop(layout, props, the_prop, cam_type, alert_cam="", min=None, max=None, **kwargs):
def _draw_alert_prop(
layout, props, the_prop, cam_type, alert_cam="", min=None, max=None, **kwargs
):
can_alert = not alert_cam or alert_cam == cam_type can_alert = not alert_cam or alert_cam == cam_type
if can_alert: if can_alert:
value = getattr(props, the_prop) value = getattr(props, the_prop)
@ -31,6 +34,7 @@ def _draw_alert_prop(layout, props, the_prop, cam_type, alert_cam="", min=None,
else: else:
layout.prop(props, the_prop, **kwargs) layout.prop(props, the_prop, **kwargs)
def _draw_gated_prop(layout, props, gate_prop, actual_prop): def _draw_gated_prop(layout, props, gate_prop, actual_prop):
row = layout.row(align=True) row = layout.row(align=True)
row.prop(props, gate_prop, text="") row.prop(props, gate_prop, text="")
@ -38,6 +42,7 @@ def _draw_gated_prop(layout, props, gate_prop, actual_prop):
row.active = getattr(props, gate_prop) row.active = getattr(props, gate_prop)
row.prop(props, actual_prop) row.prop(props, actual_prop)
def draw_camera_manipulation_props(layout, cam_type, props): def draw_camera_manipulation_props(layout, cam_type, props):
# Camera Panning # Camera Panning
split = layout.split() split = layout.split()
@ -54,6 +59,7 @@ def draw_camera_manipulation_props(layout, cam_type, props):
_draw_gated_prop(col, props, "limit_zoom", "zoom_max") _draw_gated_prop(col, props, "limit_zoom", "zoom_max")
_draw_gated_prop(col, props, "limit_zoom", "zoom_rate") _draw_gated_prop(col, props, "limit_zoom", "zoom_rate")
def draw_camera_mode_props(layout, cam_type, props): def draw_camera_mode_props(layout, cam_type, props):
# Point of Attention # Point of Attention
split = layout.split() split = layout.split()
@ -81,6 +87,7 @@ def draw_camera_mode_props(layout, cam_type, props):
col_target.active = props.poa_type != "none" col_target.active = props.poa_type != "none"
col_target.prop(props, "ignore_subworld") col_target.prop(props, "ignore_subworld")
def draw_camera_poa_props(layout, cam_type, props): def draw_camera_poa_props(layout, cam_type, props):
trans = props.transition trans = props.transition
@ -101,6 +108,7 @@ def draw_camera_poa_props(layout, cam_type, props):
col.prop(props, "poa_offset", text="") col.prop(props, "poa_offset", text="")
col.prop(props, "poa_worldspace") col.prop(props, "poa_worldspace")
def draw_camera_pos_props(layout, cam_type, props): def draw_camera_pos_props(layout, cam_type, props):
trans = props.transition trans = props.transition
@ -111,12 +119,33 @@ def draw_camera_pos_props(layout, cam_type, props):
# Position Transitions # Position Transitions
col.active = cam_type != "circle" col.active = cam_type != "circle"
col.label("Default Position Transition:") col.label("Default Position Transition:")
_draw_alert_prop(col, trans, "pos_acceleration", cam_type, _draw_alert_prop(
alert_cam="rail", max=10.0, text="Acceleration") col,
_draw_alert_prop(col, trans, "pos_deceleration", cam_type, trans,
alert_cam="rail", max=10.0, text="Deceleration") "pos_acceleration",
_draw_alert_prop(col, trans, "pos_velocity", cam_type, cam_type,
alert_cam="rail", max=10.0, text="Maximum Velocity") alert_cam="rail",
max=10.0,
text="Acceleration",
)
_draw_alert_prop(
col,
trans,
"pos_deceleration",
cam_type,
alert_cam="rail",
max=10.0,
text="Deceleration",
)
_draw_alert_prop(
col,
trans,
"pos_velocity",
cam_type,
alert_cam="rail",
max=10.0,
text="Maximum Velocity",
)
col = col.column() col = col.column()
col.active = cam_type in {"firstperson", "follow"} col.active = cam_type in {"firstperson", "follow"}
col.prop(trans, "pos_cut", text="Cut Animation") col.prop(trans, "pos_cut", text="Cut Animation")
@ -128,6 +157,7 @@ def draw_camera_pos_props(layout, cam_type, props):
col.prop(props, "pos_offset", text="") col.prop(props, "pos_offset", text="")
col.prop(props, "pos_worldspace") col.prop(props, "pos_worldspace")
def draw_circle_camera_props(layout, props): def draw_circle_camera_props(layout, props):
# Circle Camera Stuff # Circle Camera Stuff
layout.prop(props, "circle_center") layout.prop(props, "circle_center")
@ -137,6 +167,7 @@ def draw_circle_camera_props(layout, props):
row.active = props.circle_center is None row.active = props.circle_center is None
row.prop(props, "circle_radius_ui") row.prop(props, "circle_radius_ui")
class CameraButtonsPanel: class CameraButtonsPanel:
bl_space_type = "PROPERTIES" bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW" bl_region_type = "WINDOW"
@ -144,7 +175,7 @@ class CameraButtonsPanel:
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return (context.camera and context.scene.render.engine == "PLASMA_GAME") return context.camera and context.scene.render.engine == "PLASMA_GAME"
class PlasmaCameraTypePanel(CameraButtonsPanel, bpy.types.Panel): class PlasmaCameraTypePanel(CameraButtonsPanel, bpy.types.Panel):
@ -189,7 +220,10 @@ class PlasmaCameraCirclePanel(CameraButtonsPanel, bpy.types.Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return super().poll(context) and context.camera.plasma_camera.camera_type == "circle" return (
super().poll(context)
and context.camera.plasma_camera.camera_type == "circle"
)
class PlasmaCameraAnimationPanel(CameraButtonsPanel, bpy.types.Panel): class PlasmaCameraAnimationPanel(CameraButtonsPanel, bpy.types.Panel):
@ -204,7 +238,9 @@ class PlasmaCameraAnimationPanel(CameraButtonsPanel, bpy.types.Panel):
split = layout.split() split = layout.split()
col = split.column() col = split.column()
col.label("Animation:") col.label("Animation:")
anim_enabled = props.anim_enabled or context.object.plasma_modifiers.animation.enabled anim_enabled = (
props.anim_enabled or context.object.plasma_modifiers.animation.enabled
)
col.active = anim_enabled and context.object.plasma_object.has_animation_data col.active = anim_enabled and context.object.plasma_object.has_animation_data
col.prop(props, "start_on_push") col.prop(props, "start_on_push")
col.prop(props, "stop_on_pop") col.prop(props, "stop_on_pop")
@ -212,17 +248,24 @@ class PlasmaCameraAnimationPanel(CameraButtonsPanel, bpy.types.Panel):
col = split.column() col = split.column()
col.active = camera.camera_type == "rail" col.active = camera.camera_type == "rail"
invalid = camera.camera_type == "rail" and not context.object.plasma_object.has_transform_animation invalid = (
camera.camera_type == "rail"
and not context.object.plasma_object.has_transform_animation
)
col.alert = invalid col.alert = invalid
col.label("Rail:") col.label("Rail:")
col.prop(props, "rail_pos", text="") col.prop(props, "rail_pos", text="")
if invalid: if invalid:
col.label("Rail cameras must have a transformation animation!", icon="ERROR") col.label(
"Rail cameras must have a transformation animation!", icon="ERROR"
)
def draw_header(self, context): def draw_header(self, context):
self.layout.active = context.object.plasma_object.has_animation_data self.layout.active = context.object.plasma_object.has_animation_data
if not context.object.plasma_modifiers.animation.enabled: if not context.object.plasma_modifiers.animation.enabled:
self.layout.prop(context.camera.plasma_camera.settings, "anim_enabled", text="") self.layout.prop(
context.camera.plasma_camera.settings, "anim_enabled", text=""
)
class PlasmaCameraViewPanel(CameraButtonsPanel, bpy.types.Panel): class PlasmaCameraViewPanel(CameraButtonsPanel, bpy.types.Panel):
@ -234,7 +277,18 @@ class PlasmaCameraViewPanel(CameraButtonsPanel, bpy.types.Panel):
class TransitionListUI(bpy.types.UIList): class TransitionListUI(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.camera is None: if item.camera is None:
layout.label("[Default Transition]") layout.label("[Default Transition]")
else: else:
@ -249,8 +303,16 @@ class PlasmaCameraTransitionPanel(CameraButtonsPanel, bpy.types.Panel):
layout = self.layout layout = self.layout
camera = context.camera.plasma_camera camera = context.camera.plasma_camera
ui_list.draw_list(layout, "TransitionListUI", "camera", camera, "transitions", ui_list.draw_list(
"active_transition_index", rows=3, maxrows=4) layout,
"TransitionListUI",
"camera",
camera,
"transitions",
"active_transition_index",
rows=3,
maxrows=4,
)
try: try:
item = camera.transitions[camera.active_transition_index] item = camera.transitions[camera.active_transition_index]

1
korman/ui/ui_image.py

@ -15,6 +15,7 @@
import bpy import bpy
class PlasmaImageEditorHeader(bpy.types.Header): class PlasmaImageEditorHeader(bpy.types.Header):
bl_space_type = "IMAGE_EDITOR" bl_space_type = "IMAGE_EDITOR"

10
korman/ui/ui_lamp.py

@ -15,6 +15,7 @@
import bpy import bpy
class LampButtonsPanel: class LampButtonsPanel:
bl_space_type = "PROPERTIES" bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW" bl_region_type = "WINDOW"
@ -22,8 +23,11 @@ class LampButtonsPanel:
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return (context.object and context.scene.render.engine == "PLASMA_GAME" and return (
isinstance(context.object.data, bpy.types.Lamp)) context.object
and context.scene.render.engine == "PLASMA_GAME"
and isinstance(context.object.data, bpy.types.Lamp)
)
class PlasmaLampPanel(LampButtonsPanel, bpy.types.Panel): class PlasmaLampPanel(LampButtonsPanel, bpy.types.Panel):
@ -95,10 +99,12 @@ def _plasma_draw_area_lamp(self, context):
sub.prop(lamp, "size_y", text="D") sub.prop(lamp, "size_y", text="D")
sub.prop(plasma_lamp, "size_height", text="H") sub.prop(plasma_lamp, "size_height", text="H")
# Swap out the draw functions for the standard Area Shape panel # Swap out the draw functions for the standard Area Shape panel
# TODO: Maybe we should consider standardizing an interface for overriding # TODO: Maybe we should consider standardizing an interface for overriding
# standard Blender panels? This seems like a really useful approach. # standard Blender panels? This seems like a really useful approach.
from bl_ui import properties_data_lamp from bl_ui import properties_data_lamp
properties_data_lamp.DATA_PT_area._draw_blender = properties_data_lamp.DATA_PT_area.draw properties_data_lamp.DATA_PT_area._draw_blender = properties_data_lamp.DATA_PT_area.draw
properties_data_lamp.DATA_PT_area.draw = _draw_area_lamp properties_data_lamp.DATA_PT_area.draw = _draw_area_lamp
del properties_data_lamp del properties_data_lamp

49
korman/ui/ui_list.py

@ -15,28 +15,38 @@
import bpy import bpy
def draw_list(layout, listtype, context_attr, prop_base, collection_name, index_name, **kwargs):
def draw_list(
layout, listtype, context_attr, prop_base, collection_name, index_name, **kwargs
):
"""Draws a generic UI list, including add/remove buttons. Note that in order to use this, """Draws a generic UI list, including add/remove buttons. Note that in order to use this,
the parent datablock must be available in the context provided to operators. This should the parent datablock must be available in the context provided to operators. This should
always be true, but this is Blender... always be true, but this is Blender...
Arguments: Arguments:
- layout: required - layout: required
- listtype: bpy.types.UIList subclass - listtype: bpy.types.UIList subclass
- context_attr: attribute name to get the properties from in the current context - context_attr: attribute name to get the properties from in the current context
- prop_base: property group owning the collection - prop_base: property group owning the collection
- collection_name: name of the collection property - collection_name: name of the collection property
- index_name: name of the active element index property - index_name: name of the active element index property
- name_prefix: (optional) prefix to apply to display name of new elements - name_prefix: (optional) prefix to apply to display name of new elements
- name_prop: (optional) property for each element's display name - name_prop: (optional) property for each element's display name
*** any other arguments are passed as keyword arguments to the template_list call *** any other arguments are passed as keyword arguments to the template_list call
""" """
prop_path = prop_base.path_from_id() prop_path = prop_base.path_from_id()
name_prefix = kwargs.pop("name_prefix", "") name_prefix = kwargs.pop("name_prefix", "")
name_prop = kwargs.pop("name_prop", "") name_prop = kwargs.pop("name_prop", "")
row = layout.row() row = layout.row()
row.template_list(listtype, collection_name, prop_base, collection_name, row.template_list(
prop_base, index_name, **kwargs) listtype,
collection_name,
prop_base,
collection_name,
prop_base,
index_name,
**kwargs
)
col = row.column(align=True) col = row.column(align=True)
op = col.operator("ui.plasma_collection_add", icon="ZOOMIN", text="") op = col.operator("ui.plasma_collection_add", icon="ZOOMIN", text="")
op.context = context_attr op.context = context_attr
@ -51,5 +61,10 @@ def draw_list(layout, listtype, context_attr, prop_base, collection_name, index_
op.collection_prop = collection_name op.collection_prop = collection_name
op.index_prop = index_name op.index_prop = index_name
def draw_modifier_list(layout, listtype, prop_base, collection_name, index_name, **kwargs):
draw_list(layout, listtype, "object", prop_base, collection_name, index_name, **kwargs) def draw_modifier_list(
layout, listtype, prop_base, collection_name, index_name, **kwargs
):
draw_list(
layout, listtype, "object", prop_base, collection_name, index_name, **kwargs
)

19
korman/ui/ui_menus.py

@ -15,6 +15,7 @@
from ..operators.op_mesh import * from ..operators.op_mesh import *
class PlasmaMenu: class PlasmaMenu:
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@ -31,7 +32,9 @@ class PlasmaAddMenu(PlasmaMenu, bpy.types.Menu):
layout.operator("mesh.plasma_flare_add", text="Lamp Flare", icon="PARTICLES") layout.operator("mesh.plasma_flare_add", text="Lamp Flare", icon="PARTICLES")
layout.operator("mesh.plasma_ladder_add", text="Ladder", icon="COLLAPSEMENU") layout.operator("mesh.plasma_ladder_add", text="Ladder", icon="COLLAPSEMENU")
layout.operator("mesh.plasma_linkingbook_add", text="Linking Book", icon="FILE_IMAGE") layout.operator(
"mesh.plasma_linkingbook_add", text="Linking Book", icon="FILE_IMAGE"
)
def build_menu(self, context): def build_menu(self, context):
if context.scene.render.engine != "PLASMA_GAME": if context.scene.render.engine != "PLASMA_GAME":
@ -46,17 +49,25 @@ class PlasmaHelpMenu(PlasmaMenu, bpy.types.Menu):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
layout.operator("wm.url_open", text="About Korman", icon="URL").url = "https://guildofwriters.org/wiki/Korman" layout.operator(
layout.operator("wm.url_open", text="Getting Started", icon="URL").url = "https://guildofwriters.org/wiki/Korman:Getting_Started" "wm.url_open", text="About Korman", icon="URL"
layout.operator("wm.url_open", text="Tutorials", icon="URL").url = "https://guildofwriters.org/wiki/Category:Korman_Tutorials" ).url = "https://guildofwriters.org/wiki/Korman"
layout.operator(
"wm.url_open", text="Getting Started", icon="URL"
).url = "https://guildofwriters.org/wiki/Korman:Getting_Started"
layout.operator(
"wm.url_open", text="Tutorials", icon="URL"
).url = "https://guildofwriters.org/wiki/Category:Korman_Tutorials"
def build_menu(self, context): def build_menu(self, context):
self.layout.menu("menu.plasma_help", text="Korman", icon="URL") self.layout.menu("menu.plasma_help", text="Korman", icon="URL")
def register(): def register():
bpy.types.INFO_MT_add.append(PlasmaAddMenu.build_menu) bpy.types.INFO_MT_add.append(PlasmaAddMenu.build_menu)
bpy.types.INFO_MT_help.prepend(PlasmaHelpMenu.build_menu) bpy.types.INFO_MT_help.prepend(PlasmaHelpMenu.build_menu)
def unregister(): def unregister():
bpy.types.INFO_MT_add.remove(PlasmaAddMenu.build_menu) bpy.types.INFO_MT_add.remove(PlasmaAddMenu.build_menu)
bpy.types.INFO_MT_help.remove(PlasmaHelpMenu.build_menu) bpy.types.INFO_MT_help.remove(PlasmaHelpMenu.build_menu)

43
korman/ui/ui_modifiers.py

@ -17,6 +17,7 @@ import bpy
from . import modifiers as modifier_draw from . import modifiers as modifier_draw
class ModifierButtonsPanel: class ModifierButtonsPanel:
bl_space_type = "PROPERTIES" bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW" bl_region_type = "WINDOW"
@ -54,7 +55,9 @@ class PlasmaModifiersPanel(ModifierButtonsPanel, bpy.types.Panel):
# First, let's sort the list of modifiers based on their display order # First, let's sort the list of modifiers based on their display order
# We don't do this sort in the property itself because this is really just a UI hint. # We don't do this sort in the property itself because this is really just a UI hint.
modifiers = sorted(obj.plasma_modifiers.modifiers, key=lambda x: x.display_order) modifiers = sorted(
obj.plasma_modifiers.modifiers, key=lambda x: x.display_order
)
# Inside the modifier_draw module, we have draw callbables for each modifier # Inside the modifier_draw module, we have draw callbables for each modifier
# We'll loop through the list of active modifiers and call the drawprocs for the enabled mods # We'll loop through the list of active modifiers and call the drawprocs for the enabled mods
@ -65,7 +68,7 @@ class PlasmaModifiersPanel(ModifierButtonsPanel, bpy.types.Panel):
def _draw_modifier_template(self, modifier): def _draw_modifier_template(self, modifier):
"""This draws our lookalike modifier template and returns a UILayout object for each modifier """This draws our lookalike modifier template and returns a UILayout object for each modifier
to consume in order to draw its specific properties""" to consume in order to draw its specific properties"""
layout = self.layout.box() layout = self.layout.box()
# This is the main title row. It mimics the Blender template_modifier, which (unfortunately) # This is the main title row. It mimics the Blender template_modifier, which (unfortunately)
@ -77,11 +80,21 @@ class PlasmaModifiersPanel(ModifierButtonsPanel, bpy.types.Panel):
row.prop(modifier, "show_expanded", text="", icon=exicon, emboss=False) row.prop(modifier, "show_expanded", text="", icon=exicon, emboss=False)
row.label(text=modifier.bl_label, icon=getattr(modifier, "bl_icon", "NONE")) row.label(text=modifier.bl_label, icon=getattr(modifier, "bl_icon", "NONE"))
row.operator("object.plasma_modifier_move_up", text="", icon="TRIA_UP").active_modifier = modifier.display_order row.operator(
row.operator("object.plasma_modifier_move_down", text="", icon="TRIA_DOWN").active_modifier = modifier.display_order "object.plasma_modifier_move_up", text="", icon="TRIA_UP"
row.operator("object.plasma_modifier_copy", text="", icon="COPYDOWN").active_modifier = modifier.display_order ).active_modifier = modifier.display_order
row.operator("object.plasma_modifier_reset", text="", icon="FILE_REFRESH").active_modifier = modifier.display_order row.operator(
row.operator("object.plasma_modifier_remove", text="", icon="X").active_modifier = modifier.display_order "object.plasma_modifier_move_down", text="", icon="TRIA_DOWN"
).active_modifier = modifier.display_order
row.operator(
"object.plasma_modifier_copy", text="", icon="COPYDOWN"
).active_modifier = modifier.display_order
row.operator(
"object.plasma_modifier_reset", text="", icon="FILE_REFRESH"
).active_modifier = modifier.display_order
row.operator(
"object.plasma_modifier_remove", text="", icon="X"
).active_modifier = modifier.display_order
# Now we return the modifier box, which is populated with the modifier specific properties # Now we return the modifier box, which is populated with the modifier specific properties
# by whatever insanity is in the modifier module. modifier modifier modifier... # by whatever insanity is in the modifier module. modifier modifier modifier...
@ -97,8 +110,16 @@ class PlasmaModifiersSpecialMenu(ModifierButtonsPanel, bpy.types.Menu):
layout.operator("object.plasma_modifier_copy_to_selection", icon="PASTEDOWN") layout.operator("object.plasma_modifier_copy_to_selection", icon="PASTEDOWN")
layout.separator() layout.separator()
layout.operator("object.plasma_modifier_copy", icon="COPYDOWN", text="Copy Modifiers").active_modifier = -1 layout.operator(
layout.operator("object.plasma_modifier_paste", icon="PASTEDOWN", text="Paste Modifier(s)") "object.plasma_modifier_copy", icon="COPYDOWN", text="Copy Modifiers"
).active_modifier = -1
layout.operator(
"object.plasma_modifier_paste", icon="PASTEDOWN", text="Paste Modifier(s)"
)
layout.separator() layout.separator()
layout.operator("object.plasma_modifier_reset", text="Reset Modifiers", icon="FILE_REFRESH").active_modifier = -1 layout.operator(
layout.operator("object.plasma_modifier_remove", text="Remove Modifiers", icon="X").active_modifier = -1 "object.plasma_modifier_reset", text="Reset Modifiers", icon="FILE_REFRESH"
).active_modifier = -1
layout.operator(
"object.plasma_modifier_remove", text="Remove Modifiers", icon="X"
).active_modifier = -1

1
korman/ui/ui_object.py

@ -15,6 +15,7 @@
import bpy import bpy
class ObjectButtonsPanel: class ObjectButtonsPanel:
bl_space_type = "PROPERTIES" bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW" bl_region_type = "WINDOW"

29
korman/ui/ui_render_layer.py

@ -16,6 +16,7 @@
import bpy import bpy
from . import ui_list from . import ui_list
class RenderLayerButtonsPanel: class RenderLayerButtonsPanel:
bl_space_type = "PROPERTIES" bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW" bl_region_type = "WINDOW"
@ -27,7 +28,18 @@ class RenderLayerButtonsPanel:
class BakePassUI(bpy.types.UIList): class BakePassUI(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,
):
layout.prop(item, "display_name", emboss=False, text="", icon="RENDERLAYERS") layout.prop(item, "display_name", emboss=False, text="", icon="RENDERLAYERS")
@ -38,9 +50,18 @@ class PlasmaBakePassPanel(RenderLayerButtonsPanel, bpy.types.Panel):
layout = self.layout layout = self.layout
scene = context.scene.plasma_scene scene = context.scene.plasma_scene
ui_list.draw_list(layout, "BakePassUI", "scene", scene, "bake_passes", ui_list.draw_list(
"active_pass_index", name_prefix="Pass", layout,
name_prop="display_name", rows=3, maxrows=3) "BakePassUI",
"scene",
scene,
"bake_passes",
"active_pass_index",
name_prefix="Pass",
name_prop="display_name",
rows=3,
maxrows=3,
)
active_pass_index = scene.active_pass_index active_pass_index = scene.active_pass_index
try: try:

75
korman/ui/ui_scene.py

@ -17,6 +17,7 @@ import bpy
import functools import functools
from . import ui_list from . import ui_list
class SceneButtonsPanel: class SceneButtonsPanel:
bl_space_type = "PROPERTIES" bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW" bl_region_type = "WINDOW"
@ -28,12 +29,34 @@ class SceneButtonsPanel:
class DecalManagerListUI(bpy.types.UIList): class DecalManagerListUI(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,
):
layout.prop(item, "display_name", emboss=False, text="", icon="BRUSH_DATA") layout.prop(item, "display_name", emboss=False, text="", icon="BRUSH_DATA")
class WetManagerListUI(bpy.types.UIList): class WetManagerListUI(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.name: if item.name:
layout.label(item.name, icon="BRUSH_DATA") layout.label(item.name, icon="BRUSH_DATA")
layout.prop(item, "enabled", text="") layout.prop(item, "enabled", text="")
@ -47,9 +70,17 @@ class PlasmaDecalManagersPanel(SceneButtonsPanel, bpy.types.Panel):
def draw(self, context): def draw(self, context):
layout, scene = self.layout, context.scene.plasma_scene layout, scene = self.layout, context.scene.plasma_scene
ui_list.draw_list(layout, "DecalManagerListUI", "scene", scene, "decal_managers", ui_list.draw_list(
"active_decal_index", name_prefix="Decal", name_prop="display_name", layout,
rows=3) "DecalManagerListUI",
"scene",
scene,
"decal_managers",
"active_decal_index",
name_prefix="Decal",
name_prop="display_name",
rows=3,
)
try: try:
decal_mgr = scene.decal_managers[scene.active_decal_index] decal_mgr = scene.decal_managers[scene.active_decal_index]
@ -68,8 +99,10 @@ class PlasmaDecalManagersPanel(SceneButtonsPanel, bpy.types.Panel):
split = box.split() split = box.split()
col = split.column(align=True) col = split.column(align=True)
col.label("Scale:") col.label("Scale:")
col.alert = decal_mgr.decal_type in {"ripple", "puddle", "bullet", "torpedo"} \ col.alert = (
and decal_mgr.length != decal_mgr.width decal_mgr.decal_type in {"ripple", "puddle", "bullet", "torpedo"}
and decal_mgr.length != decal_mgr.width
)
col.prop(decal_mgr, "length") col.prop(decal_mgr, "length")
col.prop(decal_mgr, "width") col.prop(decal_mgr, "width")
@ -77,7 +110,12 @@ class PlasmaDecalManagersPanel(SceneButtonsPanel, bpy.types.Panel):
col.label("Draw Settings:") col.label("Draw Settings:")
col.prop(decal_mgr, "intensity") col.prop(decal_mgr, "intensity")
sub = col.row() sub = col.row()
sub.active = decal_mgr.decal_type in {"footprint_dry", "footprint_wet", "bullet", "torpedo"} sub.active = decal_mgr.decal_type in {
"footprint_dry",
"footprint_wet",
"bullet",
"torpedo",
}
sub.prop(decal_mgr, "life_span") sub.prop(decal_mgr, "life_span")
sub = col.row() sub = col.row()
sub.active = decal_mgr.decal_type in {"puddle", "ripple"} sub.active = decal_mgr.decal_type in {"puddle", "ripple"}
@ -86,15 +124,28 @@ class PlasmaDecalManagersPanel(SceneButtonsPanel, bpy.types.Panel):
if decal_mgr.decal_type in {"puddle", "ripple"}: if decal_mgr.decal_type in {"puddle", "ripple"}:
box.separator() box.separator()
box.label("Wet Footprints:") box.label("Wet Footprints:")
ui_list.draw_list(box, "WetManagerListUI", "scene", decal_mgr, "wet_managers", ui_list.draw_list(
"active_wet_index", rows=2, maxrows=3) box,
"WetManagerListUI",
"scene",
decal_mgr,
"wet_managers",
"active_wet_index",
rows=2,
maxrows=3,
)
try: try:
wet_ref = decal_mgr.wet_managers[decal_mgr.active_wet_index] wet_ref = decal_mgr.wet_managers[decal_mgr.active_wet_index]
except: except:
pass pass
else: else:
wet_mgr = next((i for i in scene.decal_managers if i.name == wet_ref.name), None) wet_mgr = next(
(i for i in scene.decal_managers if i.name == wet_ref.name),
None,
)
box.alert = getattr(wet_mgr, "decal_type", None) == "footprint_wet" box.alert = getattr(wet_mgr, "decal_type", None) == "footprint_wet"
box.prop_search(wet_ref, "name", scene, "decal_managers", icon="NONE") box.prop_search(
wet_ref, "name", scene, "decal_managers", icon="NONE"
)
if wet_ref.name == decal_mgr.name: if wet_ref.name == decal_mgr.name:
box.label(text="Circular reference", icon="ERROR") box.label(text="Circular reference", icon="ERROR")

1
korman/ui/ui_text.py

@ -15,6 +15,7 @@
import bpy import bpy
class PlasmaTextEditorHeader(bpy.types.Header): class PlasmaTextEditorHeader(bpy.types.Header):
bl_space_type = "TEXT_EDITOR" bl_space_type = "TEXT_EDITOR"

30
korman/ui/ui_texture.py

@ -18,6 +18,7 @@ import bpy
from . import ui_list from . import ui_list
from . import ui_anim from . import ui_anim
class TextureButtonsPanel: class TextureButtonsPanel:
bl_space_type = "PROPERTIES" bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW" bl_region_type = "WINDOW"
@ -48,15 +49,25 @@ class PlasmaEnvMapPanel(TextureButtonsPanel, bpy.types.Panel):
layout.separator() layout.separator()
layout.label("Visibility Sets:") layout.label("Visibility Sets:")
ui_list.draw_list(layout, "VisRegionListUI", "texture", layer_props, ui_list.draw_list(
"vis_regions", "active_region_index", rows=2, maxrows=3) layout,
"VisRegionListUI",
"texture",
layer_props,
"vis_regions",
"active_region_index",
rows=2,
maxrows=3,
)
rgns = layer_props.vis_regions rgns = layer_props.vis_regions
if layer_props.vis_regions: if layer_props.vis_regions:
layout.prop(rgns[layer_props.active_region_index], "control_region") layout.prop(rgns[layer_props.active_region_index], "control_region")
elif envmap.source == "IMAGE_FILE": elif envmap.source == "IMAGE_FILE":
op = layout.operator("image.plasma_build_cube_map", op = layout.operator(
text="Build Cubemap from Cube Faces", "image.plasma_build_cube_map",
icon="MATCUBE") text="Build Cubemap from Cube Faces",
icon="MATCUBE",
)
op.texture_name = context.texture.name op.texture_name = context.texture.name
@ -128,5 +139,10 @@ class PlasmaLayerAnimationPanel(TextureButtonsPanel, bpy.types.Panel):
return False return False
def draw(self, context): def draw(self, context):
ui_anim.draw_multi_animation(self.layout, "texture", context.texture.plasma_layer, ui_anim.draw_multi_animation(
"subanimations", use_box=True) self.layout,
"texture",
context.texture.plasma_layer,
"subanimations",
use_box=True,
)

118
korman/ui/ui_toolbox.py

@ -16,6 +16,7 @@
import bpy import bpy
import itertools import itertools
class ToolboxPanel: class ToolboxPanel:
bl_category = "Tools" bl_category = "Tools"
bl_space_type = "VIEW_3D" bl_space_type = "VIEW_3D"
@ -35,40 +36,113 @@ class PlasmaToolboxPanel(ToolboxPanel, bpy.types.Panel):
col = layout.column(align=True) col = layout.column(align=True)
col.label("Plasma Objects:") col.label("Plasma Objects:")
enable_all = col.operator("object.plasma_toggle_all_objects", icon="OBJECT_DATA", text="Enable All") enable_all = col.operator(
"object.plasma_toggle_all_objects", icon="OBJECT_DATA", text="Enable All"
)
enable_all.enable = True enable_all.enable = True
all_plasma_objects = all((i.plasma_object.enabled for i in bpy.context.selected_objects)) all_plasma_objects = all(
col.operator("object.plasma_toggle_selected_objects", icon="VIEW3D", text="Disable Selection" if all_plasma_objects else "Enable Selection") (i.plasma_object.enabled for i in bpy.context.selected_objects)
disable_all = col.operator("object.plasma_toggle_all_objects", icon="OBJECT_DATA", text="Disable All") )
col.operator(
"object.plasma_toggle_selected_objects",
icon="VIEW3D",
text="Disable Selection" if all_plasma_objects else "Enable Selection",
)
disable_all = col.operator(
"object.plasma_toggle_all_objects", icon="OBJECT_DATA", text="Disable All"
)
disable_all.enable = False disable_all.enable = False
col.label("Plasma Pages:") col.label("Plasma Pages:")
col.operator("object.plasma_move_selection_to_page", icon="BOOKMARKS", text="Move to Page") col.operator(
col.operator("object.plasma_select_page_objects", icon="RESTRICT_SELECT_OFF", text="Select Objects") "object.plasma_move_selection_to_page",
icon="BOOKMARKS",
text="Move to Page",
)
col.operator(
"object.plasma_select_page_objects",
icon="RESTRICT_SELECT_OFF",
text="Select Objects",
)
col.label("Lighting:") col.label("Lighting:")
col.operator("object.plasma_lightmap_bake", icon="RENDER_STILL", text="Bake All").bake_selection = False col.operator(
col.operator("object.plasma_lightmap_bake", icon="RENDER_REGION", text="Bake Selection").bake_selection = True "object.plasma_lightmap_bake", icon="RENDER_STILL", text="Bake All"
col.operator("object.plasma_lightmap_clear", icon="X", text="Clear All").clear_selection = False ).bake_selection = False
col.operator("object.plasma_lightmap_clear", icon="X", text="Clear Selection").clear_selection = True col.operator(
"object.plasma_lightmap_bake", icon="RENDER_REGION", text="Bake Selection"
).bake_selection = True
col.operator(
"object.plasma_lightmap_clear", icon="X", text="Clear All"
).clear_selection = False
col.operator(
"object.plasma_lightmap_clear", icon="X", text="Clear Selection"
).clear_selection = True
col.label("Package Sounds:") col.label("Package Sounds:")
col.operator("object.plasma_toggle_sound_export", icon="MUTE_IPO_OFF", text="Enable All").enable = True col.operator(
all_sounds_export = all((i.package for i in itertools.chain.from_iterable(i.plasma_modifiers.soundemit.sounds for i in bpy.context.selected_objects if i.plasma_modifiers.soundemit.enabled))) "object.plasma_toggle_sound_export", icon="MUTE_IPO_OFF", text="Enable All"
col.operator("object.plasma_toggle_sound_export_selected", icon="OUTLINER_OB_SPEAKER", text="Disable Selection" if all_sounds_export else "Enable Selection") ).enable = True
col.operator("object.plasma_toggle_sound_export", icon="MUTE_IPO_ON", text="Disable All").enable = False all_sounds_export = all(
(
i.package
for i in itertools.chain.from_iterable(
i.plasma_modifiers.soundemit.sounds
for i in bpy.context.selected_objects
if i.plasma_modifiers.soundemit.enabled
)
)
)
col.operator(
"object.plasma_toggle_sound_export_selected",
icon="OUTLINER_OB_SPEAKER",
text="Disable Selection" if all_sounds_export else "Enable Selection",
)
col.operator(
"object.plasma_toggle_sound_export", icon="MUTE_IPO_ON", text="Disable All"
).enable = False
col.label("Textures:") col.label("Textures:")
col.operator("texture.plasma_enable_all_textures", icon="TEXTURE", text="Enable All") col.operator(
col.operator("texture.plasma_toggle_environment_maps", icon="IMAGE_RGB", text="Enable All EnvMaps").enable = True "texture.plasma_enable_all_textures", icon="TEXTURE", text="Enable All"
col.operator("texture.plasma_toggle_environment_maps", icon="IMAGE_RGB_ALPHA", text="Disable All EnvMaps").enable = False )
col.operator(
"texture.plasma_toggle_environment_maps",
icon="IMAGE_RGB",
text="Enable All EnvMaps",
).enable = True
col.operator(
"texture.plasma_toggle_environment_maps",
icon="IMAGE_RGB_ALPHA",
text="Disable All EnvMaps",
).enable = False
# Double Sided Operators # Double Sided Operators
col.label("Double Sided:") col.label("Double Sided:")
col.operator("mesh.plasma_toggle_double_sided", icon="MESH_DATA", text="Disable All").enable = False col.operator(
all_double_sided = all((i.data.show_double_sided for i in bpy.context.selected_objects if i.type == "MESH")) "mesh.plasma_toggle_double_sided", icon="MESH_DATA", text="Disable All"
col.operator("mesh.plasma_toggle_double_sided_selected", icon="BORDER_RECT", text="Disable Selection" if all_double_sided else "Enable Selection") ).enable = False
all_double_sided = all(
(
i.data.show_double_sided
for i in bpy.context.selected_objects
if i.type == "MESH"
)
)
col.operator(
"mesh.plasma_toggle_double_sided_selected",
icon="BORDER_RECT",
text="Disable Selection" if all_double_sided else "Enable Selection",
)
col.label("Convert:") col.label("Convert:")
col.operator("object.plasma_convert_plasma_objects", icon="OBJECT_DATA", text="Plasma Objects") col.operator(
col.operator("texture.plasma_convert_layer_opacities", icon="IMAGE_RGB_ALPHA", text="Layer Opacities") "object.plasma_convert_plasma_objects",
icon="OBJECT_DATA",
text="Plasma Objects",
)
col.operator(
"texture.plasma_convert_layer_opacities",
icon="IMAGE_RGB_ALPHA",
text="Layer Opacities",
)

105
korman/ui/ui_world.py

@ -86,7 +86,9 @@ class PlasmaGameExportMenu(PlasmaGameHelper, bpy.types.Menu):
row.operator_context = "EXEC_DEFAULT" row.operator_context = "EXEC_DEFAULT"
age_path = self.format_path() age_path = self.format_path()
row.active = legal_game and active_game.can_launch and age_path.exists() row.active = legal_game and active_game.can_launch and age_path.exists()
op = row.operator("export.plasma_age", icon="RENDER_ANIMATION", text="Launch Age") op = row.operator(
"export.plasma_age", icon="RENDER_ANIMATION", text="Launch Age"
)
if active_game is not None: if active_game is not None:
op.actions = {"LAUNCH"} op.actions = {"LAUNCH"}
op.dat_only = False op.dat_only = False
@ -133,8 +135,15 @@ class PlasmaGamePanel(AgeButtonsPanel, PlasmaGameHelper, bpy.types.Panel):
row = layout.row() row = layout.row()
# Remember: game storage moved to addon preferences! # Remember: game storage moved to addon preferences!
row.template_list("PlasmaGameListRO", "games", prefs, "games", games, row.template_list(
"active_game_index", rows=2) "PlasmaGameListRO",
"games",
prefs,
"games",
games,
"active_game_index",
rows=2,
)
row.operator("ui.korman_open_prefs", icon="PREFERENCES", text="") row.operator("ui.korman_open_prefs", icon="PREFERENCES", text="")
layout.separator() layout.separator()
@ -168,20 +177,54 @@ class PlasmaGamePanel(AgeButtonsPanel, PlasmaGameHelper, bpy.types.Panel):
# Special Menu # Special Menu
row = row.row(align=True) row = row.row(align=True)
row.enabled = True row.enabled = True
row.menu("PlasmaGameExportMenu", icon='DOWNARROW_HLT', text="") row.menu("PlasmaGameExportMenu", icon="DOWNARROW_HLT", text="")
class PlasmaGameListRO(bpy.types.UIList): class PlasmaGameListRO(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,
):
layout.label(item.name, icon="BOOKMARKS") layout.label(item.name, icon="BOOKMARKS")
class PlasmaGameListRW(bpy.types.UIList): class PlasmaGameListRW(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,
):
layout.prop(item, "name", text="", emboss=False, icon="BOOKMARKS") layout.prop(item, "name", text="", emboss=False, icon="BOOKMARKS")
class PlasmaPageList(bpy.types.UIList): class PlasmaPageList(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,
):
layout.prop(item, "name", text="", emboss=False, icon="BOOKMARKS") layout.prop(item, "name", text="", emboss=False, icon="BOOKMARKS")
layout.prop(item, "enabled", text="") layout.prop(item, "enabled", text="")
@ -195,8 +238,9 @@ class PlasmaAgePanel(AgeButtonsPanel, bpy.types.Panel):
# We want a list of pages and an editor below that # We want a list of pages and an editor below that
row = layout.row() row = layout.row()
row.template_list("PlasmaPageList", "pages", age, "pages", age, row.template_list(
"active_page_index", rows=2) "PlasmaPageList", "pages", age, "pages", age, "active_page_index", rows=2
)
col = row.column(align=True) col = row.column(align=True)
col.operator("world.plasma_page_add", icon="ZOOMIN", text="") col.operator("world.plasma_page_add", icon="ZOOMIN", text="")
col.operator("world.plasma_page_remove", icon="ZOOMOUT", text="") col.operator("world.plasma_page_remove", icon="ZOOMOUT", text="")
@ -222,8 +266,11 @@ class PlasmaAgePanel(AgeButtonsPanel, bpy.types.Panel):
# Age Names should really be legal Python 2.x identifiers for AgeSDLHooks # Age Names should really be legal Python 2.x identifiers for AgeSDLHooks
legal_identifier = korlib.is_legal_python2_identifier(age.age_name) legal_identifier = korlib.is_legal_python2_identifier(age.age_name)
illegal_age_name = not legal_identifier or '_' in age.age_name illegal_age_name = not legal_identifier or "_" in age.age_name
bad_prefix = age.seq_prefix >= age.MOUL_PREFIX_RANGE[1] or age.seq_prefix <= age.MOUL_PREFIX_RANGE[0] bad_prefix = (
age.seq_prefix >= age.MOUL_PREFIX_RANGE[1]
or age.seq_prefix <= age.MOUL_PREFIX_RANGE[0]
)
# Core settings # Core settings
layout.separator() layout.separator()
@ -242,22 +289,36 @@ class PlasmaAgePanel(AgeButtonsPanel, bpy.types.Panel):
col.prop(age, "age_name", text="") col.prop(age, "age_name", text="")
if age.seq_prefix >= age.MOUL_PREFIX_RANGE[1]: if age.seq_prefix >= age.MOUL_PREFIX_RANGE[1]:
layout.label(text="Your sequence prefix is too high for Myst Online: Uru Live", icon="ERROR") layout.label(
text="Your sequence prefix is too high for Myst Online: Uru Live",
icon="ERROR",
)
elif age.seq_prefix <= age.MOUL_PREFIX_RANGE[0]: elif age.seq_prefix <= age.MOUL_PREFIX_RANGE[0]:
# Unlikely. # Unlikely.
layout.label(text="Your sequence prefix is too low for Myst Online: Uru Live", icon="ERROR") layout.label(
text="Your sequence prefix is too low for Myst Online: Uru Live",
icon="ERROR",
)
# Display a hint if the identifier is illegal # Display a hint if the identifier is illegal
if illegal_age_name: if illegal_age_name:
if not age.age_name: if not age.age_name:
layout.label(text="Age names cannot be empty", icon="ERROR") layout.label(text="Age names cannot be empty", icon="ERROR")
elif korlib.is_python_keyword(age.age_name): elif korlib.is_python_keyword(age.age_name):
layout.label(text="Ages should not be named the same as a Python keyword", icon="ERROR") layout.label(
text="Ages should not be named the same as a Python keyword",
icon="ERROR",
)
elif age.age_sdl: elif age.age_sdl:
fixed_identifier = korlib.replace_python2_identifier(age.age_name) fixed_identifier = korlib.replace_python2_identifier(age.age_name)
layout.label(text="Age's SDL will use the name '{}'".format(fixed_identifier), icon="ERROR") layout.label(
if '_' in age.age_name: text="Age's SDL will use the name '{}'".format(fixed_identifier),
layout.label(text="Age names should not contain underscores", icon="ERROR") icon="ERROR",
)
if "_" in age.age_name:
layout.label(
text="Age names should not contain underscores", icon="ERROR"
)
layout.separator() layout.separator()
split = layout.split() split = layout.split()
@ -289,8 +350,14 @@ class PlasmaEnvironmentPanel(AgeButtonsPanel, bpy.types.Panel):
fni = context.world.plasma_fni fni = context.world.plasma_fni
# warn about reversed linear fog values # warn about reversed linear fog values
if fni.fog_method == "linear" and fni.fog_start >= fni.fog_end and (fni.fog_start + fni.fog_end) != 0: if (
layout.label(text="Fog Start Value should be less than the End Value", icon="ERROR") fni.fog_method == "linear"
and fni.fog_start >= fni.fog_end
and (fni.fog_start + fni.fog_end) != 0
):
layout.label(
text="Fog Start Value should be less than the End Value", icon="ERROR"
)
# basic colors # basic colors
split = layout.split() split = layout.split()

Loading…
Cancel
Save