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. 54
      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. 389
      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. 63
      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. 236
      korman/properties/modifiers/region.py
  58. 948
      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
bl_info = {
"name": "Korman",
"author": "Guild of Writers",
"blender": (2, 79, 0),
"location": "File > Import-Export",
"name": "Korman",
"author": "Guild of Writers",
"blender": (2, 79, 0),
"location": "File > Import-Export",
"description": "Exporter for Cyan Worlds' Plasma Engine",
"warning": "beta",
"category": "System",
"warning": "beta",
"category": "System",
}

116
korman/addon_prefs.py

@ -17,31 +17,47 @@ import bpy
from bpy.props import *
from . import korlib
game_versions = [("pvPrime", "Ages Beyond Myst (63.11)", "Targets the original Uru (Live) game"),
("pvPots", "Path of the Shell (63.12)", "Targets the most recent offline expansion pack"),
("pvMoul", "Myst Online: Uru Live (70)", "Targets the most recent online game")]
game_versions = [
("pvPrime", "Ages Beyond Myst (63.11)", "Targets the original Uru (Live) game"),
(
"pvPots",
"Path of the Shell (63.12)",
"Targets the most recent offline expansion pack",
),
("pvMoul", "Myst Online: Uru Live (70)", "Targets the most recent online game"),
]
class PlasmaGame(bpy.types.PropertyGroup):
name = StringProperty(name="Name",
description="Name of the Plasma Game",
options=set())
path = StringProperty(name="Path",
description="Path to this Plasma Game",
options=set())
version = EnumProperty(name="Version",
description="Plasma version of this game",
items=game_versions,
options=set())
player = StringProperty(name="Player",
description="Name of the player to use when launching the game",
options=set())
ki = IntProperty(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())
name = StringProperty(
name="Name", description="Name of the Plasma Game", options=set()
)
path = StringProperty(
name="Path", description="Path to this Plasma Game", options=set()
)
version = EnumProperty(
name="Version",
description="Plasma version of this game",
items=game_versions,
options=set(),
)
player = StringProperty(
name="Player",
description="Name of the player to use when launching the game",
options=set(),
)
ki = IntProperty(
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
def can_launch(self):
@ -60,28 +76,36 @@ class KormanAddonPreferences(bpy.types.AddonPreferences):
def _check_py22_exe(self, context):
if self._ensure_abspath((2, 2)):
self._check_python((2, 2))
def _check_py23_exe(self, context):
if self._ensure_abspath((2, 3)):
self._check_python((2, 3))
def _check_py27_exe(self, context):
if self._ensure_abspath((2, 7)):
self._check_python((2, 7))
python22_executable = StringProperty(name="Python 2.2",
description="Path to the Python 2.2 executable",
options=set(),
subtype="FILE_PATH",
update=_check_py22_exe)
python23_executable = StringProperty(name="Python 2.3",
description="Path to the Python 2.3 executable",
options=set(),
subtype="FILE_PATH",
update=_check_py23_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)
python22_executable = StringProperty(
name="Python 2.2",
description="Path to the Python 2.2 executable",
options=set(),
subtype="FILE_PATH",
update=_check_py22_exe,
)
python23_executable = StringProperty(
name="Python 2.3",
description="Path to the Python 2.3 executable",
options=set(),
subtype="FILE_PATH",
update=_check_py23_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):
if not self.is_property_set("python22_valid"):
@ -96,7 +120,9 @@ class KormanAddonPreferences(bpy.types.AddonPreferences):
python22_valid = BoolProperty(options={"HIDDEN", "SKIP_SAVE"})
python23_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):
py_exe = getattr(self, "python{}{}_executable".format(*py_version))
@ -121,8 +147,15 @@ class KormanAddonPreferences(bpy.types.AddonPreferences):
main_col.label("Plasma Games:")
row = main_col.row()
row.template_list("PlasmaGameListRW", "games", self, "games", self,
"active_game_index", rows=3)
row.template_list(
"PlasmaGameListRW",
"games",
self,
"games",
self,
"active_game_index",
rows=3,
)
col = row.column(align=True)
col.operator("world.plasma_game_add", icon="ZOOMIN", 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
# operator. What fun. I guess....
from .properties.prop_world import PlasmaGames
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 utils
class CameraConverter:
def __init__(self, exporter):
self._exporter = weakref.ref(exporter)
@ -31,7 +32,9 @@ class CameraConverter:
brain.poaOffset = hsVector3(*camera_props.poa_offset)
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.zPanLimit = camera_props.y_pan_angle / 2.0
@ -72,7 +75,9 @@ class CameraConverter:
brain.setFlags(plCameraBrain1.kIgnoreSubworldMovement, True)
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.brain = brain.key
@ -97,7 +102,9 @@ class CameraConverter:
continue
cam_trans = plCameraModifier.CamTrans()
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"
trans_info = manual_trans.transition
@ -121,9 +128,15 @@ class CameraConverter:
if props.poa_type == "avatar":
brain.circleFlags |= plCameraBrain1_Circle.kCircleLocalAvatar
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:
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":
brain.circleFlags |= plCameraBrain1_Circle.kFarthest
@ -134,7 +147,9 @@ class CameraConverter:
if props.circle_center is None:
brain.center = hsVector3(*bo.matrix_world.translation)
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
brain.circleFlags |= plCameraBrain1_Circle.kHasCenterObject
@ -172,7 +187,11 @@ class CameraConverter:
def _export_fixed_camera(self, so, bo, props):
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)
brain = self._mgr.find_create_object(plCameraBrain1_Fixed, so=so)
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
# path object, but it makes more sense to me to just animate the camera with
# the details of the path...
pos_fcurves = tuple(i for i in helpers.fetch_fcurves(bo, False) if i.data_path == "location")
pos_ctrl = self._exporter().animation.convert_transform_controller(pos_fcurves, bo.rotation_mode,
bo.matrix_local, bo.matrix_parent_inverse)
pos_fcurves = tuple(
i for i in helpers.fetch_fcurves(bo, False) if i.data_path == "location"
)
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:
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.controller = pos_ctrl
path.affineParts = utils.affine_parts(bo.matrix_local)
@ -217,8 +241,16 @@ class CameraConverter:
if abs(f1 - f2) > 0.001:
break
# 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)):
raise ExportError("'{}': Rail Camera must have more than one keyframe", bo.name)
if any(
(
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:
# The animation is a loop
path.flags |= plAnimPath.kWrap

88
korman/exporter/convert.py

@ -40,17 +40,24 @@ from . import physics
from . import rtlight
from . import utils
class Exporter:
def __init__(self, op):
self._op = op # Blender export operator
self._op = op # Blender export operator
self._objects = []
self.actors = set()
self.want_node_trees = defaultdict(set)
self.exported_nodes = {}
def run(self):
log = 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:
log = (
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
self.mgr = manager.ExportManager(self)
self.mesh = mesh.MeshConverter(self)
@ -153,7 +160,13 @@ class Exporter:
# Grab a naive listing of enabled pages
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))
# 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
if parent is not None:
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.addChild(so.key)
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(
bo.name, parent.name))
bo.name, parent.name
)
)
def _export_coordinate_interface(self, so, bl):
"""Ensures that the SceneObject has a CoordinateInterface"""
@ -260,7 +278,10 @@ class Exporter:
self.report.msg("\nExporting localization...")
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)
inc_progress()
@ -279,10 +300,17 @@ class Exporter:
try:
export_fn = getattr(self, export_fn)
except AttributeError:
self.report.warn("""'{}' 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))
self.report.warn(
"""'{}' 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
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.
# 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):
# Hey, guess what? Blender's camera data is utter crap!
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):
pass
@ -319,7 +349,9 @@ class Exporter:
if bo.data.materials:
self.mesh.export_object(meshObj, so)
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):
self.report.progress_advance()
@ -398,7 +430,12 @@ class Exporter:
for mod in bl_obj.plasma_modifiers.modifiers:
proc = getattr(mod, "post_export", 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)
inc_progress()
@ -413,13 +450,24 @@ class Exporter:
@functools.singledispatch
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)
def _(temporary, parent):
self.exit_stack.enter_context(TemporaryObject(temporary, bpy.data.objects.remove))
self.report.msg("'{}': generated Object '{}' (Plasma Object: {})", parent.name,
temporary.name, temporary.plasma_object.enabled, indent=1)
self.exit_stack.enter_context(
TemporaryObject(temporary, bpy.data.objects.remove)
)
self.report.msg(
"'{}': generated Object '{}' (Plasma Object: {})",
parent.name,
temporary.name,
temporary.plasma_object.enabled,
indent=1,
)
if temporary.plasma_object.enabled:
new_objects.append(temporary)
@ -435,7 +483,9 @@ class Exporter:
@handle_temporary.register(bpy.types.NodeTree)
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)
if temporary.bl_idname == "PlasmaNodeTree":
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
def _get_puddle_class(exporter, name, vs):
if vs:
# 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 plDynaPuddleMgr
def _get_footprint_class(exporter, name, vs):
if vs:
raise ExportError("'{}': Footprints cannot be attached to wavesets", name)
return plDynaFootMgr
class DecalConverter:
_decal_lookup = {
"footprint_dry": _get_footprint_class,
@ -53,7 +59,9 @@ class DecalConverter:
# We don't care about: DynaDecalMgrs in another page.
decal_mgrs, so_key = self._decal_managers.get(decal_name), so.key
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...
waveset_id = plFactory.ClassIndex("plWaveSet7")
@ -61,12 +69,17 @@ class DecalConverter:
so_loc = so_key.location
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)
# HACKAGE: Add the wet/dirty notifes now that we know about all the decal managers.
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 i in (i.object for i in decal_mgrs):
i.addNotify(notify_key)
@ -76,7 +89,9 @@ class DecalConverter:
def export_active_print_shape(self, print_shape, decal_name):
decal_mgrs = self._decal_managers.get(decal_name)
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:
print_shape.addDecalMgr(i)
@ -84,7 +99,9 @@ class DecalConverter:
mat_mgr = self._exporter().mesh.material
mat_keys = mat_mgr.get_materials(bo)
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
for material in (i.object for i in mat_keys):
@ -97,7 +114,14 @@ class DecalConverter:
layer.state.ZFlags |= zFlags
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:
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
decal_mgr = exporter.mgr.find_object(pClass, bl=bo, name=name)
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)
self._decal_managers[decal_name].append(decal_mgr.key)
@ -126,7 +152,9 @@ class DecalConverter:
image = decal.image
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)
mats = exporter.mesh.material.export_print_materials(bo, image, name, blend)
@ -159,8 +187,13 @@ class DecalConverter:
decal_mgr.waitOnEnable = decal_type == "footprint_wet"
if decal_type in {"puddle", "ripple"}:
decal_mgr.wetLength = decal.wet_time
self._notifies[decal_name].update((i.name for i in decal.wet_managers
if i.enabled and i.name != decal_name))
self._notifies[decal_name].update(
(
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?
# 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
class LightBaker:
"""ExportTime Lighting"""
@ -132,7 +133,9 @@ class LightBaker:
# Lightmap passes are expensive, so we will warn about any passes that seem
# particularly wasteful.
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:
largest_pass = 0
@ -146,19 +149,25 @@ class LightBaker:
self._report.msg("Preparing to bake...", indent=1)
for key, value in bake.items():
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]
if not self._prep_for_lightmap(obj, toggle):
self._report.msg("Lightmap '{}' will not be baked -- no applicable lights",
obj.name, indent=2)
self._report.msg(
"Lightmap '{}' will not be baked -- no applicable lights",
obj.name,
indent=2,
)
value.pop(i)
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]
if not self._prep_for_vcols(obj, toggle):
if self._has_valid_material(obj):
self._report.msg("VCols '{}' will not be baked -- no applicable lights",
obj.name, indent=2)
self._report.msg(
"VCols '{}' will not be baked -- no applicable lights",
obj.name,
indent=2,
)
value.pop(i)
else:
raise RuntimeError(key[0])
@ -172,14 +181,28 @@ class LightBaker:
if value:
if key[0] == "lightmap":
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):
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)
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:])
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._fix_vertex_colors(value)
else:
@ -232,8 +255,10 @@ class LightBaker:
if len(edge.link_faces) != 2:
# Either a border edge, or an abomination.
continue
if mesh.use_auto_smooth and (not edge.smooth
or edge.calc_face_angle() > mesh.auto_smooth_angle):
if mesh.use_auto_smooth and (
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
# whose angle is above the theshold. Auto smooth must be on in both cases.
continue
@ -241,10 +266,16 @@ class LightBaker:
# Alright, this edge is connected to our loop AND our face.
# Now for the Fun Stuff(c)... First, actually get ahold of the other
# 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
# 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]
# Phew ! Good, now just pick whichever color has the highest average value
if sum(max_color) / 3 < sum(other_color) / 3:
@ -256,7 +287,7 @@ class LightBaker:
def _generate_lightgroup(self, bo, user_lg=None):
"""Makes a new light group for the baking process that excludes all Plasma RT lamps"""
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
for material in mesh.materials:
@ -274,7 +305,9 @@ class LightBaker:
source = [i for i in bpy.context.scene.objects if i.type == "LAMP"]
else:
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:
# 1) No animated lights, period.
@ -321,9 +354,17 @@ class LightBaker:
if mod.image is not None:
uv_texture_names = frozenset((i.name for i in obj.data.uv_textures))
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:
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)
return False
return True
@ -332,15 +373,27 @@ class LightBaker:
def vcol_bake_required(obj) -> bool:
if obj.plasma_modifiers.lightmap.bake_lightmap:
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
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
if self.force:
return True
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 True
@ -351,7 +404,11 @@ class LightBaker:
if lightmap_mod.bake_pass_name:
bake_pass = bake_passes.get(lightmap_mod.bake_pass_name, 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)
else:
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
# layers this object is on must be selected. We will sanity check this now.
obj_layers = tuple(i.layers)
lm_active_layers = set((i for i, value in enumerate(lm_layers) if value))
obj_active_layers = set((i for i, value in enumerate(obj_layers) if value))
lm_active_layers = set(
(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:
raise ExportError("Bake Lighting '{}': At least one layer the object is on must be selected".format(i.name))
if lightmap_bake_required(i) is False and vcol_bake_required(i) is False:
raise ExportError(
"Bake Lighting '{}': At least one layer the object is on must be selected".format(
i.name
)
)
if (
lightmap_bake_required(i) is False
and vcol_bake_required(i) is False
):
continue
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.
for material in (i for i in mesh.materials if i is not None):
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):
toggle.track(slot, "use", False)
@ -485,8 +557,12 @@ class LightBaker:
# from sharing UVs. Sigh.
if self._mesh.is_collapsed(bo):
# Danger: uv_base.name -> UnicodeDecodeError (wtf? another blender bug?)
self._report.warn("'{}': packing islands in UV Texture '{}' due to modifier collapse",
bo.name, modifier.uv_map, indent=2)
self._report.warn(
"'{}': packing islands in UV Texture '{}' due to modifier collapse",
bo.name,
modifier.uv_map,
indent=2,
)
with self._set_mode("EDIT"):
bpy.ops.mesh.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
# autocolor layer (gulp).
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
return True
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:
uv_textures = bo.data.uv_textures
uvtex = uv_textures.get(self.lightmap_uvtex_name, None)
@ -571,7 +653,9 @@ class LightBaker:
else:
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)
else:
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):
toggle.track(mat, "use_vertex_color_paint", 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)
i.select = value

28
korman/exporter/explosions.py

@ -13,6 +13,7 @@
# You should have received a copy of the GNU General Public License
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
class NonfatalExportError(Exception):
def __init__(self, *args, **kwargs):
assert args
@ -34,12 +35,16 @@ class ExportError(Exception):
class BlendNotSupported(ExportError):
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):
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):
@ -60,14 +65,15 @@ class PlasmaLaunchError(ExportError):
class TooManyUVChannelsError(ExportError):
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(
mat.name, obj.name, maxUVTexCount, numUVTexs)
mat.name, obj.name, maxUVTexCount, numUVTexs
)
super(ExportError, self).__init__(msg)
class TooManyVerticesError(ExportError):
def __init__(self, mesh, matname, vertcount):
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)
@ -76,11 +82,15 @@ class UndefinedPageError(ExportError):
mistakes = {}
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):
if page not in self.mistakes:
self.mistakes[page] = [obj,]
self.mistakes[page] = [
obj,
]
else:
self.mistakes[page].append(obj)
@ -93,4 +103,8 @@ class UndefinedPageError(ExportError):
class UnsupportedTextureError(ExportError):
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"
_MIP_MAGICK = b"KTM\x00"
@enum.unique
class _HeaderBits(enum.IntEnum):
last_export = 0
@ -79,7 +80,10 @@ class ImageCache:
image, tag = texture.image, texture.tag
image_name = str(texture)
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))
if texture.ephemeral or "skip" in method:
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"
# 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.
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))
if method != {"use"} or texture.ephemeral:
return None
@ -150,7 +157,10 @@ class ImageCache:
finally:
if exists:
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
else:
cached_image.modify_time = 0
@ -158,9 +168,15 @@ class ImageCache:
# ensure the data has been loaded from the cache
if cached_image.image_data is None:
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:
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)
return None
return cached_image
@ -229,7 +245,6 @@ class ImageCache:
stream.seek(pos)
yield tuple(_read_image_mips())
def _read_index(self, index_pos, stream):
stream.seek(index_pos)
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.
_ESHTML_REGEX = re.compile("<.+>")
class LocalizationConverter:
def __init__(self, exporter=None, **kwargs):
if exporter is not None:
@ -49,18 +50,26 @@ class LocalizationConverter:
self._strings = defaultdict(lambda: defaultdict(dict))
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 value.is_modified:
self._report.warn("'{}' translation for '{}' is modified on the disk but not reloaded in Blender.",
element_name, language, indent=indent)
self._report.warn(
"'{}' translation for '{}' is modified on the disk but not reloaded in Blender.",
element_name,
language,
indent=indent,
)
value = value.as_string()
self._strings[set_name][element_name][language] = value
@contextmanager
def _generate_file(self, filename, **kwargs):
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
else:
dirname = kwargs.get("dirname", "dat")
@ -77,42 +86,68 @@ class LocalizationConverter:
age_name = self._age_name
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:
stream.write(contents.encode("windows-1252"))
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.",
language, indent=2)
self._report.warn(
"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
# replacement characters ("?") just so it'll work dammit.
stream.write(contents.encode("windows-1252", "replace"))
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:
self._report.msg("Copying localization '{}'", journal_name, indent=1)
for language_name, value in translations.items():
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.",
language_name, indent=2)
self._report.warn(
"Translation '{}' will not be used because it is not supported in this version of Plasma.",
language_name,
indent=2,
)
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)
write_text_file(language_name, file_name, value)
# Ensure that default (read: "English") journal is available
if "English" not in translations:
language_name, value = next(((language_name, value) for language_name, value in translations.items()
if language_name in _SP_LANGUAGES), (None, None))
language_name, value = next(
(
(language_name, value)
for language_name, value in translations.items()
if language_name in _SP_LANGUAGES
),
(None, None),
)
if language_name is not None:
file_name = "{}--{}.txt".format(age_name, journal_name)
# If you manage to screw up this badly... Well, I am very sorry.
if write_text_file(language_name, file_name, value):
self._report.warn("No 'English' translation available, so '{}' will be used as the default",
language_name, indent=2)
self._report.warn(
"No 'English' translation available, so '{}' will be used as the default",
language_name,
indent=2,
)
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):
if not self._strings:
@ -131,7 +166,11 @@ class LocalizationConverter:
database[language_name][set_name][element_name] = value
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)
if method == "database_back_compat":
@ -156,21 +195,25 @@ class LocalizationConverter:
enc = plEncryptedStream.kEncAes if self._version == pvEoa else None
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("<age name=\"{}\">", self._age_name, indent=1)
write_line('<age name="{}">', self._age_name, indent=1)
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():
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):
if _ESHTML_REGEX.search(translation_value):
encoded_value = "<![CDATA[{}]]>".format(translation_value)
else:
encoded_value = xml_escape(translation_value)
write_line("<translation language=\"{language}\">{translation}</translation>",
language=translation_language, translation=encoded_value, indent=4)
write_line(
'<translation language="{language}">{translation}</translation>',
language=translation_language,
translation=encoded_value,
indent=4,
)
write_line("</element>", indent=3)
write_line("</set>", indent=2)
@ -182,8 +225,14 @@ class LocalizationConverter:
def run(self):
age_props = bpy.context.scene.world.plasma_age
loc_path = str(Path(self._path) / "dat" / "{}.loc".format(self._age_name))
log = logger.ExportVerboseLogger if age_props.verbose else logger.ExportProgressLogger
with korlib.ConsoleToggler(age_props.show_console), log(loc_path) as self._report:
log = (
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("Generating Localization")
self._report.progress_start("Exporting Localization Data")
@ -204,15 +253,29 @@ class LocalizationConverter:
inc_progress = self._report.progress_increment
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)
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:
self._report.error("'{}': No content translations available. The localization will not be exported.",
i.name, indent=2)
self._report.error(
"'{}': No content translations available. The localization will not be exported.",
i.name,
indent=2,
)
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()
def _run_generate(self):

73
korman/exporter/logger.py

@ -23,6 +23,7 @@ _HEADING_SIZE = 60
_MAX_ELIPSES = 3
_MAX_TIME_UNTIL_ELIPSES = 2.0
class _ExportLogger:
def __init__(self, print_logs, age_path=None):
self._errors = []
@ -37,7 +38,9 @@ class _ExportLogger:
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
# 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")
return self
@ -85,7 +88,6 @@ class _ExportLogger:
cache = args[0] if len(args) == 1 else args[0].format(*args[1:])
self._porting.append(cache)
def progress_add_step(self, name):
pass
@ -113,8 +115,12 @@ class _ExportLogger:
if num_errors == 1:
raise NonfatalExportError(self._errors[0])
elif num_errors:
raise NonfatalExportError("""{} errors were encountered during export. Check the export log for more details:
{}""", num_errors, self._file.name)
raise NonfatalExportError(
"""{} errors were encountered during export. Check the export log for more details:
{}""",
num_errors,
self._file.name,
)
def save(self):
# TODO
@ -160,7 +166,9 @@ class ExportProgressLogger(_ExportLogger):
if value is not None:
export_time = time.perf_counter() - self._time_start_overall
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_heading("ERROR")
self._progress_print_line(str(value))
@ -191,13 +199,17 @@ class ExportProgressLogger(_ExportLogger):
def progress_end(self):
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
with self._print_condition:
if self._age_path is not None:
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:
self._progress_print_line("\nCOMPLETED IN {:.2f}s".format(export_time))
self._progress_print_heading()
@ -230,7 +242,9 @@ class ExportProgressLogger(_ExportLogger):
num_chars = len(text)
border = "-" * int((_HEADING_SIZE - (num_chars + 2)) / 2)
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)
else:
self._progress_print_line("-" * _HEADING_SIZE)
@ -238,7 +252,9 @@ class ExportProgressLogger(_ExportLogger):
def _progress_print_step(self, done=False, error=False):
with self._print_condition:
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
self._progress_print_volatile("")
else:
@ -246,28 +262,39 @@ class ExportProgressLogger(_ExportLogger):
stage = "{} of {}".format(self._step_progress, self._step_max)
else:
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
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)
step_id = self._step_id + 1
stage_max_whitespace = len(str(num_steps)) * 2
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...
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,
stage_whitespace=stage_whitespace, stage=stage)
step_name=step_name,
step_whitespace=whitespace,
step_id=step_id,
num_steps=num_steps,
stage_whitespace=stage_whitespace,
stage=stage,
)
print_func(line)
def _progress_get_max(self):
return self._step_max
def _progress_set_max(self, value):
assert self._step_id != -1
self._step_max = value
self._progress_print_step()
progress_range = property(_progress_get_max, _progress_set_max)
def progress_start(self, action):
@ -289,13 +316,13 @@ class ExportProgressLogger(_ExportLogger):
while self._progress_alive:
with self._print_condition:
signalled = self._print_condition.wait(timeout=1.0)
print(end='\r')
print(end="\r")
# First, we need to print out any queued whole lines.
# NOTE: no need to lock anything here as Blender uses CPython (GIL)
with self._cursor:
if self._queued_lines:
print(*self._queued_lines, sep='\n')
print(*self._queued_lines, sep="\n")
self._queued_lines.clear()
# 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 self._time_start_step != 0:
if (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
if (
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:
num_dots = 0
print('.' * num_dots, end=" " * (_MAX_ELIPSES - num_dots))
print("." * num_dots, end=" " * (_MAX_ELIPSES - num_dots))
self._cursor.update()
def _progress_get_current(self):
return self._step_progress
def _progress_set_current(self, value):
assert self._step_id != -1
self._step_progress = value
if self._step_max != 0:
self._progress_print_step()
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):
"""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:
location = loc
elif so is not None:
@ -98,7 +98,7 @@ class ExportManager:
self.AddObject(location, pl)
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):
node.addSceneObject(pl.key)
pl.sceneNode = node.key
@ -145,7 +145,9 @@ class ExportManager:
if want_pysdl:
self._pack_agesdl_hook(age)
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)
# Textures.prp
@ -191,7 +193,7 @@ class ExportManager:
else:
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)
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
# for now.
if issubclass(pClass, plSingleModifier):
self._exporter().report.warn("Adding SingleModifier '{}' (type: '{}'') to another SceneObject '{}'",
key.name, pClass.__name__[2:], so.key.name)
self._exporter().report.warn(
"Adding SingleModifier '{}' (type: '{}'') to another SceneObject '{}'",
key.name,
pClass.__name__[2:],
so.key.name,
)
so.addModifier(key)
return key
@ -281,7 +287,10 @@ class ExportManager:
generator = (i for i in bpy.data.texts if i.name.lower() == namei)
result, collision = next(generator, None), next(generator, 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
# AgeSDL Hook Python
@ -321,24 +330,44 @@ class ExportManager:
with output.generate_dat_file(f, enc=self._encryption) as stream:
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))
if fni.fog_method == "none":
stream.writeLine("Graphics.Renderer.Fog.SetDefLinear 0 0 0")
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":
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":
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":
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):
age_name = self._age_info.name
output = self._exporter().output
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 "_"
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"}
class _GeoSpan:
def __init__(self, bo, bm, geospan, pass_index=None):
self.geospan = geospan
@ -42,7 +43,9 @@ class _GeoSpan:
"""Determines the color all vertex colors should be multipled by in this span."""
if self.geospan.props & plGeometrySpan.kDiffuseFoldedIn:
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)
if not bo.plasma_modifiers.lighting.preshade:
return (0.0, 0.0, 0.0, 0.0)
@ -57,7 +60,7 @@ class _RenderLevel:
MAJOR_LATE = 8
_MAJOR_SHIFT = 28
_MINOR_MASK = ((1 << _MAJOR_SHIFT) - 1)
_MINOR_MASK = (1 << _MAJOR_SHIFT) - 1
def __init__(self, bo, pass_index, blend_span=False):
if blend_span:
@ -75,20 +78,24 @@ class _RenderLevel:
def _get_major(self):
return self.level >> self._MAJOR_SHIFT
def _set_major(self, value):
self.level = self._calc_level(value, self.minor)
major = property(_get_major, _set_major)
def _get_minor(self):
return self.level & self._MINOR_MASK
def _set_minor(self, value):
self.level = self._calc_level(self.major, value)
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
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
if mods.test_property("draw_framebuf"):
return self._calc_level(self.MAJOR_FRAMEBUF)
@ -175,7 +182,11 @@ class _MeshManager:
ident = i.identifier
if ident == "rna_type":
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
def __enter__(self):
@ -197,7 +208,7 @@ class _MeshManager:
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
# 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)
# 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)
# 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"]:
mod = bo.modifiers.new(cached_mod["name"], cached_mod["type"])
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
setattr(mod, key, value)
self._entered = False
@ -275,8 +291,13 @@ class MeshConverter(_MeshManager):
alpha_layer = self._find_vtx_alpha_layer(mesh.vertex_colors)
if alpha_layer is None:
return False
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))
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)
return has_alpha
@ -341,7 +362,9 @@ class MeshConverter(_MeshManager):
geospan.props |= plGeometrySpan.kPropNoShadow
# 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:
geospan.addPermaLight(i)
for i in permaProjs:
@ -375,12 +398,16 @@ class MeshConverter(_MeshManager):
# Recall that materials is a mapping of exported materials to blender material indices.
# Therefore, geodata maps blender material indices to working geometry data.
# 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)
# Locate relevant vertex color layers now...
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)
# Convert Blender faces into things we can stuff into libHSPlasma
@ -400,7 +427,12 @@ class MeshConverter(_MeshManager):
# Unpack colors
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:
src = color[i]
tessface_colors = (src.color1, src.color2, src.color3, src.color4)
@ -411,31 +443,63 @@ class MeshConverter(_MeshManager):
else:
src = alpha[i]
# average color becomes the alpha value
tessface_alphas = ((sum(src.color1) / 3), (sum(src.color2) / 3),
(sum(src.color3) / 3), (sum(src.color4) / 3))
tessface_alphas = (
(sum(src.color1) / 3),
(sum(src.color2) / 3),
(sum(src.color3) / 3),
(sum(src.color4) / 3),
)
if bumpmap is not None:
gradPass = []
gradUVWs = []
if len(tessface.vertices) != 3:
gradPass.append([tessface.vertices[0], tessface.vertices[1], tessface.vertices[2]])
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))))
gradPass.append(
[
tessface.vertices[0],
tessface.vertices[1],
tessface.vertices[2],
]
)
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:
gradPass.append(tessface.vertices)
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[1] for uvw in tessface_uvws)),
tuple((uvw[2] for uvw in tessface_uvws)),
)
)
for p, vids in enumerate(gradPass):
dPosDu += self._get_bump_gradient(bumpmap[1], gradUVWs[p], mesh, vids, bumpmap[0], 0)
dPosDv += self._get_bump_gradient(bumpmap[1], gradUVWs[p], mesh, vids, bumpmap[0], 1)
dPosDu += self._get_bump_gradient(
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
# Convert to per-material indices
@ -444,14 +508,18 @@ class MeshConverter(_MeshManager):
# Calculate vertex colors.
if mat2span_LUT:
mult_color = geospans[mat2span_LUT[tessface.material_index]].mult_color
mult_color = geospans[
mat2span_LUT[tessface.material_index]
].mult_color
else:
mult_color = (1.0, 1.0, 1.0, 1.0)
tessface_color, tessface_alpha = tessface_colors[j], tessface_alphas[j]
vertex_color = (int(tessface_color[0] * mult_color[0] * 255),
int(tessface_color[1] * mult_color[1] * 255),
int(tessface_color[2] * mult_color[2] * 255),
int(tessface_alpha * mult_color[0] * 255))
vertex_color = (
int(tessface_color[0] * mult_color[0] * 255),
int(tessface_color[1] * mult_color[1] * 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 :(
# 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
# 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.normalize()
geoVertex.normal = normal
@ -495,7 +565,7 @@ class MeshConverter(_MeshManager):
# PyHSPlasma now returns tuples to indicate this.
geoUVs = list(geoVertex.uvs)
geoUVs[num_user_uvs] += dPosDu
geoUVs[num_user_uvs+1] += dPosDv
geoUVs[num_user_uvs + 1] += dPosDv
geoVertex.uvs = geoUVs
face_verts.append(data.blender2gs[vertex][coluv])
@ -520,7 +590,9 @@ class MeshConverter(_MeshManager):
# TODO: consider busting up the mesh into multiple geospans?
# or hack plDrawableSpans::composeGeometry to do it for us?
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 bumpmap is not None:
@ -534,7 +606,6 @@ class MeshConverter(_MeshManager):
geospan.indices = data.triangles
geospan.vertices = data.vertices
def _get_bump_gradient(self, xform, uvws, mesh, vIds, uvIdx, iUV):
v0 = hsVector3(*mesh.vertices[vIds[0]].co)
v1 = hsVector3(*mesh.vertices[vIds[1]].co)
@ -576,11 +647,19 @@ class MeshConverter(_MeshManager):
def _enumerate_materials(self, bo, mesh):
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)
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
# Otherwise, let's *try* to share meshes as best we can...
if bo.modifiers:
@ -628,8 +707,12 @@ class MeshConverter(_MeshManager):
_diindices = {}
for i in geospans:
dspan = self._find_create_dspan(bo, i.geospan, i.pass_index)
self._report.msg("Exported hsGMaterial '{}' geometry into '{}'",
i.geospan.material.name, dspan.key.name, indent=1)
self._report.msg(
"Exported hsGMaterial '{}' geometry into '{}'",
i.geospan.material.name,
dspan.key.name,
indent=1,
)
idx = dspan.addSourceSpan(i.geospan)
diidx = _diindices.setdefault(dspan, [])
diidx.append(idx)
@ -648,7 +731,9 @@ class MeshConverter(_MeshManager):
waveset_mod = bo.plasma_modifiers.water_basic
if waveset_mod.enabled:
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)
blmat = materials[0][1]
self._check_vtx_nonpreshaded(bo, mesh, 0, blmat)
@ -656,8 +741,12 @@ class MeshConverter(_MeshManager):
geospan = self._create_geospan(bo, mesh, None, blmat, matKey)
# FIXME: Can some of this be generalized?
geospan.props |= (plGeometrySpan.kWaterHeight | plGeometrySpan.kLiteVtxNonPreshaded |
plGeometrySpan.kPropReverseSort | plGeometrySpan.kPropNoShadow)
geospan.props |= (
plGeometrySpan.kWaterHeight
| plGeometrySpan.kLiteVtxNonPreshaded
| plGeometrySpan.kPropReverseSort
| plGeometrySpan.kPropNoShadow
)
geospan.waterHeight = bo.matrix_world.translation[2]
return [_GeoSpan(bo, blmat, geospan)], None
else:
@ -666,9 +755,12 @@ class MeshConverter(_MeshManager):
for i, (blmat_idx, blmat) in enumerate(materials):
self._check_vtx_nonpreshaded(bo, mesh, blmat_idx, blmat)
matKey = self.material.export_material(bo, blmat)
geospans[i] = _GeoSpan(bo, blmat,
self._create_geospan(bo, mesh, blmat_idx, blmat, matKey),
blmat.pass_index)
geospans[i] = _GeoSpan(
bo,
blmat,
self._create_geospan(bo, mesh, blmat_idx, blmat, matKey),
blmat.pass_index,
)
mat2span_LUT[blmat_idx] = i
return geospans, mat2span_LUT
@ -689,7 +781,9 @@ class MeshConverter(_MeshManager):
# AgeName_[District_]_Page_RenderLevel_Crit[Blend]Spans
# Just because it's nice to be consistent
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)
criteria = crit.criteria
@ -699,7 +793,7 @@ class MeshConverter(_MeshManager):
if criteria & plDrawable.kCritSortSpans:
dspan.props |= plDrawable.kPropSortSpans
dspan.renderLevel = crit.render_level.level
dspan.sceneNode = node # AddViaNotify
dspan.sceneNode = node # AddViaNotify
self._dspans[location][crit] = dspan
return dspan
@ -707,13 +801,18 @@ class MeshConverter(_MeshManager):
return self._dspans[location][crit]
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:
return alpha_layer.data
return None
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:
return manual_layer.data
baked_layer = color_collection.get("autocolor")

136
korman/exporter/outfile.py

@ -31,6 +31,7 @@ import zipfile
_CHUNK_SIZE = 0xA00000
_encoding = locale.getpreferredencoding(False)
def _hashfile(filename, hasher, block=0xFFFF):
with open(str(filename), "rb") as handle:
h = hasher()
@ -40,6 +41,7 @@ def _hashfile(filename, hasher, block=0xFFFF):
data = handle.read(block)
return h.digest()
@enum.unique
class _FileType(enum.Enum):
generated_dat = 0
@ -58,6 +60,7 @@ _GATHER_BUILD = {
_FileType.video: "avi",
}
class _OutputFile:
def __init__(self, **kwargs):
self.file_type = kwargs.get("file_type")
@ -71,7 +74,9 @@ class _OutputFile:
if self.file_type in (_FileType.generated_dat, _FileType.generated_ancillary):
self.file_data = kwargs.get("file_data", 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
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:
if 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)
else:
with hsFileStream().open(self.file_path, fmRead) as dec_stream:
@ -188,45 +195,63 @@ class OutputFiles:
self._time = time.time()
def add_ancillary(self, filename, dirname="", text_id=None, str_data=None):
of = _OutputFile(file_type=_FileType.generated_ancillary,
dirname=dirname, filename=filename,
id_data=text_id, file_data=str_data)
of = _OutputFile(
file_type=_FileType.generated_ancillary,
dirname=dirname,
filename=filename,
id_data=text_id,
file_data=str_data,
)
self._files.add(of)
def add_python_code(self, filename, text_id=None, str_data=None):
assert filename not in self._py_files
of = _OutputFile(file_type=_FileType.python_code,
dirname="Python", filename=filename,
id_data=text_id, file_data=str_data,
skip_hash=True,
internal=(self._version != pvMoul),
needs_glue=False)
of = _OutputFile(
file_type=_FileType.python_code,
dirname="Python",
filename=filename,
id_data=text_id,
file_data=str_data,
skip_hash=True,
internal=(self._version != pvMoul),
needs_glue=False,
)
self._files.add(of)
self._py_files.add(filename)
def add_python_mod(self, filename, text_id=None, str_data=None):
assert filename not in self._py_files
of = _OutputFile(file_type=_FileType.python_code,
dirname="Python", filename=filename,
id_data=text_id, file_data=str_data,
skip_hash=True,
internal=(self._version != pvMoul),
needs_glue=True)
of = _OutputFile(
file_type=_FileType.python_code,
dirname="Python",
filename=filename,
id_data=text_id,
file_data=str_data,
skip_hash=True,
internal=(self._version != pvMoul),
needs_glue=True,
)
self._files.add(of)
self._py_files.add(filename)
def add_sdl(self, filename, text_id=None, str_data=None):
of = _OutputFile(file_type=_FileType.sdl,
dirname="SDL", filename=filename,
id_data=text_id, file_data=str_data,
enc=self.super_secure_encryption)
of = _OutputFile(
file_type=_FileType.sdl,
dirname="SDL",
filename=filename,
id_data=text_id,
file_data=str_data,
enc=self.super_secure_encryption,
)
self._files.add(of)
def add_sfx(self, sound_id):
of = _OutputFile(file_type=_FileType.sfx,
dirname="sfx", filename=sound_id.name,
id_data=sound_id)
of = _OutputFile(
file_type=_FileType.sfx,
dirname="sfx",
filename=sound_id.name,
id_data=sound_id,
)
self._files.add(of)
@contextmanager
@ -243,7 +268,7 @@ class OutputFiles:
else:
file_path = self._export_path.joinpath(dirname, filename)
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.open(file_path, fmCreate)
backing_stream = stream
@ -274,8 +299,9 @@ class OutputFiles:
# instead of doing lots of buffer copying to encrypt as a post step.
if not bogus:
kwargs = {
"file_type": _FileType.generated_dat if dirname == "dat" else
_FileType.generated_ancillary,
"file_type": _FileType.generated_dat
if dirname == "dat"
else _FileType.generated_ancillary,
"dirname": dirname,
"filename": filename,
"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)
else:
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:
pyc_objects.append((i.filename, pyc))
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:
if pyc_objects:
with self.generate_dat_file("{}.pak".format(self._exporter().age_name),
dirname="Python", enc=self.super_secure_encryption) as stream:
with self.generate_dat_file(
"{}.pak".format(self._exporter().age_name),
dirname="Python",
enc=self.super_secure_encryption,
) as stream:
korlib.package_python(stream, pyc_objects)
def save(self):
@ -376,7 +411,10 @@ class OutputFiles:
def _write_deps(self):
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
for i in self._generate_files(func):
@ -391,8 +429,11 @@ class OutputFiles:
if i.file_path != dst_path:
shutil.copy2(i.file_path, dst_path)
else:
report.warn("No data found for dependency file '{}'. It will not be copied into the export directory.",
str(i.dirname / i.filename), indent=1)
report.warn(
"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):
report = self._exporter().report
@ -400,17 +441,26 @@ class OutputFiles:
for i in self._generate_files():
key = _GATHER_BUILD.get(i.file_type)
if key is None:
report.warn("Output file '{}' of type '{}' is not supported by MOULa's GatherBuild format.",
i.file_type, i.filename)
report.warn(
"Output file '{}' of type '{}' is not supported by MOULa's GatherBuild format.",
i.file_type,
i.filename,
)
else:
path_str = str(PureWindowsPath(i.dirname, i.filename))
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):
version = self._version
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)
if dat_only:
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
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):
arcpath = i.filename if dat_only else str(Path(i.dirname, i.filename))
if i.file_data:
@ -458,7 +508,11 @@ class OutputFiles:
elif i.file_path:
zf.write(i.file_path, arcpath)
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
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 . import utils
def _set_phys_prop(prop, sim, phys, value=True):
"""Sets properties on plGenericPhysical and plSimulationInterface (seeing as how they are duped)"""
sim.setProperty(prop, value)
phys.setProperty(prop, value)
class PhysicsConverter:
def __init__(self, exporter):
self._exporter = weakref.ref(exporter)
@ -56,8 +58,16 @@ class PhysicsConverter:
if len(v) == 3:
indices += v
elif len(v) == 4:
indices += (v[0], v[1], v[2],)
indices += (v[0], v[2], v[3],)
indices += (
v[0],
v[1],
v[2],
)
indices += (
v[0],
v[2],
v[3],
)
return indices
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]
else:
# 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:
# apply the transform to the physical itself
utils.transform_mesh(mesh, mat)
@ -114,7 +127,9 @@ class PhysicsConverter:
vertices = [hsVector3(*i.co) for i in mesh.vertices]
else:
# 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.indices = self._convert_indices(mesh)
physical.boundsType = plSimDefs.kProxyBounds
@ -126,23 +141,28 @@ class PhysicsConverter:
simIface = so.sim.object
physical = simIface.physical.object
member_group = getattr(plSimDefs, kwargs.get("member_group", "kGroupLOSOnly"))
if physical.memberGroup != member_group and member_group != plSimDefs.kGroupLOSOnly:
member_group = getattr(
plSimDefs, kwargs.get("member_group", "kGroupLOSOnly")
)
if (
physical.memberGroup != member_group
and member_group != plSimDefs.kGroupLOSOnly
):
self._report.warn("{}: Physical memberGroup overwritten!", bo.name)
physical.memberGroup = member_group
self._apply_props(simIface, physical, kwargs)
def generate_physical(self, bo, so, **kwargs):
"""Generates a physical object for the given object pair.
The following optional arguments are allowed:
- bounds: (defaults to collision modifier setting)
- member_group: str attribute of plSimDefs, defaults to kGroupStatic
NOTE that kGroupLOSOnly generation will only succeed if no one else
has generated this physical in another group
- properties: sequence of str bit names from plSimulationInterface
- losdbs: 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
The following optional arguments are allowed:
- bounds: (defaults to collision modifier setting)
- member_group: str attribute of plSimDefs, defaults to kGroupStatic
NOTE that kGroupLOSOnly generation will only succeed if no one else
has generated this physical in another group
- properties: sequence of str bit names from plSimulationInterface
- losdbs: 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
"""
if so.sim is None:
simIface = self._mgr.add_object(pl=plSimulationInterface, bl=bo)
@ -167,12 +187,17 @@ class PhysicsConverter:
if mod.dynamic:
if ver <= pvPots:
physical.collideGroup = (1 << plSimDefs.kGroupDynamic) | \
(1 << plSimDefs.kGroupStatic)
physical.collideGroup = (1 << plSimDefs.kGroupDynamic) | (
1 << plSimDefs.kGroupStatic
)
physical.memberGroup = plSimDefs.kGroupDynamic
physical.mass = mod.mass
_set_phys_prop(plSimulationInterface.kStartInactive, simIface, physical,
value=mod.start_asleep)
_set_phys_prop(
plSimulationInterface.kStartInactive,
simIface,
physical,
value=mod.start_asleep,
)
elif not mod.avatar_blocker:
physical.memberGroup = plSimDefs.kGroupLOSOnly
else:
@ -181,7 +206,9 @@ class PhysicsConverter:
# Line of Sight DB
if mod.camera_blocker:
physical.LOSDBs |= plSimDefs.kLOSDBCameraBlockers
_set_phys_prop(plSimulationInterface.kCameraAvoidObject, simIface, physical)
_set_phys_prop(
plSimulationInterface.kCameraAvoidObject, simIface, physical
)
if mod.terrain:
physical.LOSDBs |= plSimDefs.kLOSDBAvatarWalkable
@ -189,7 +216,11 @@ class PhysicsConverter:
# This could result in a few orphaned PhysicalSndGroups, but I think that's preferable
# to having a bunch of empty objects...?
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)
physical.soundGroup = sndgroup.key
else:
@ -202,7 +233,9 @@ class PhysicsConverter:
# would miss cases where we have animated detectors (subworlds!!!)
def _iter_object_tree(bo, stop_at_subworld):
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
yield bo
bo = bo.parent
@ -229,7 +262,9 @@ class PhysicsConverter:
# 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
# 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)
# If the mass is zero, then we will fail to animate. Fix that.
@ -246,7 +281,11 @@ class PhysicsConverter:
elif ver == pvMoul:
if self._exporter().has_coordiface(bo):
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:
local_space, mat = False, bo.matrix_world
else:
@ -256,9 +295,16 @@ class PhysicsConverter:
simIface = so.sim.object
physical = simIface.physical.object
member_group = getattr(plSimDefs, kwargs.get("member_group", "kGroupLOSOnly"))
if physical.memberGroup != member_group and member_group != plSimDefs.kGroupLOSOnly:
self._report.warn("{}: Physical memberGroup overwritten!", bo.name, indent=2)
member_group = getattr(
plSimDefs, kwargs.get("member_group", "kGroupLOSOnly")
)
if (
physical.memberGroup != member_group
and member_group != plSimDefs.kGroupLOSOnly
):
self._report.warn(
"{}: Physical memberGroup overwritten!", bo.name, indent=2
)
physical.memberGroup = member_group
self._apply_props(simIface, physical, kwargs)
@ -267,7 +313,9 @@ class PhysicsConverter:
"""Exports box bounds based on the object"""
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)
def _export_hull(self, bo, physical, local_space, mat):
@ -285,7 +333,9 @@ class PhysicsConverter:
else:
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
verts = itertools.takewhile(lambda x: isinstance(x, BMVert), result["geom"])
physical.verts = [hsVector3(*i.co) for i in verts]
@ -294,7 +344,9 @@ class PhysicsConverter:
"""Exports sphere bounds based on the object"""
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)
def _export_trimesh(self, bo, physical, local_space, mat):
@ -304,7 +356,9 @@ class PhysicsConverter:
mod = bo.plasma_modifiers.collision
if mod.enabled and mod.proxy_object is not None:
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:
physical.boundsType = plSimDefs.kExplicitBounds
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 ..plasma_magic import plasma_python_glue, very_very_special_python
class PythonPackageExporter:
def __init__(self, filepath, version):
self._filepath = filepath
@ -52,9 +53,13 @@ class PythonPackageExporter:
code = source
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:
raise ExportError("Failed to compyle '{}':\n{}".format(filename, result))
raise ExportError(
"Failed to compyle '{}':\n{}".format(filename, result)
)
py_code.append((filename, result))
inc_progress()
@ -68,9 +73,13 @@ class PythonPackageExporter:
code = source
# 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:
raise ExportError("Failed to compyle '{}':\n{}".format(filename, result))
raise ExportError(
"Failed to compyle '{}':\n{}".format(filename, result)
)
py_code.append((filename, result))
inc_progress()
@ -88,12 +97,19 @@ class PythonPackageExporter:
if age_py.plasma_text.package or age.python_method == "all":
self._pfms[py_filename] = age_py
else:
report.warn("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)
report.warn(
"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:
report.msg("Packing default AgeSDL Python", indent=1)
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):
objects = bpy.context.scene.objects
@ -131,8 +147,14 @@ class PythonPackageExporter:
def run(self):
"""Runs a stripped-down version of the Exporter that only handles Python files"""
age_props = bpy.context.scene.world.plasma_age
log = logger.ExportVerboseLogger if age_props.verbose else logger.ExportProgressLogger
with korlib.ConsoleToggler(age_props.show_console), log(self._filepath) as report:
log = (
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 Helper Python Modules")
report.progress_add_step("Compyling Python Code")
@ -170,7 +192,9 @@ class PythonPackageExporter:
if enc is None:
stream = hsFileStream(self._version).open(self._filepath, fmCreate)
else:
stream = plEncryptedStream(self._version).open(self._filepath, fmCreate, enc)
stream = plEncryptedStream(self._version).open(
self._filepath, fmCreate, enc
)
try:
korlib.package_python(stream, py_code)
finally:

52
korman/exporter/rtlight.py

@ -36,6 +36,7 @@ SHADOW_RESOLUTION = {
"HIGH": 512,
}
class LightConverter:
def __init__(self, exporter):
self._exporter = weakref.ref(exporter)
@ -101,7 +102,7 @@ class LightConverter:
pl.spotOuter = spot_size
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:
pl.falloff = bl.halo_intensity
@ -182,7 +183,9 @@ class LightConverter:
sv_bo = rtlamp.lamp_region
sv_mod = sv_bo.plasma_modifiers.softvolume
if not sv_mod.enabled:
raise ExportError("'{}': '{}' is not a SoftVolume".format(bo.name, sv_bo.name))
raise ExportError(
"'{}': '{}' is not a SoftVolume".format(bo.name, sv_bo.name)
)
sv_key = sv_mod.get_key(self._exporter())
pl_light.softVolume = sv_key
@ -207,7 +210,12 @@ class LightConverter:
# 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.
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)
state = layer.state
@ -230,13 +238,21 @@ class LightConverter:
pl_light.setProperty(plLightInfo.kLPOverAll, True)
elif slot.blend_type == "MULTIPLY":
# 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.projection = layer.key
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.attenDist = rtlamp.shadow_falloff
@ -249,7 +265,7 @@ class LightConverter:
def find_material_light_keys(self, bo, bm):
"""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)
permaLights = []
permaProjs = []
@ -279,21 +295,31 @@ class LightConverter:
break
else:
# didn't find a layer where both lamp and object were, skip it.
self._report.msg("[{}] '{}': not in same layer, skipping...",
lamp.type, obj.name, indent=2)
self._report.msg(
"[{}] '{}': not in same layer, skipping...",
lamp.type,
obj.name,
indent=2,
)
continue
# This is probably where PermaLight vs PermaProj should be sorted out...
pl_light = self.get_light_key(obj, lamp, None)
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)
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)
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)
@ -302,7 +328,9 @@ class LightConverter:
xlate = _BL2PL[bl_light.type]
return self.mgr.find_create_key(xlate, bl=bo, so=so)
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):
for tex in bl_light.texture_slots:

21
korman/exporter/utils.py

@ -22,6 +22,7 @@ from contextlib import contextmanager
from PyHSPlasma import *
def affine_parts(xform):
# 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...
@ -35,10 +36,12 @@ def affine_parts(xform):
affine.U = quaternion(rot)
return affine
def color(blcolor, alpha=1.0):
"""Converts a Blender Color into an hsColorRGBA"""
return hsColorRGBA(blcolor.r, blcolor.g, blcolor.b, alpha)
def matrix44(blmat):
"""Converts a mathutils.Matrix to an hsMatrix44"""
hsmat = hsMatrix44()
@ -49,15 +52,16 @@ def matrix44(blmat):
hsmat[i, 3] = blmat[i][3]
return hsmat
def quaternion(blquat):
"""Converts a mathutils.Quaternion to an hsQuat"""
return hsQuat(blquat.x, blquat.y, blquat.z, blquat.w)
@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
the context"""
the context"""
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
obj.draw_type = "WIRE"
@ -74,10 +78,11 @@ def bmesh_temporary_object(name : str, factory : Callable, page_name : str=None)
bm.free()
bpy.context.scene.objects.unlink(obj)
@contextmanager
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
due to an error"""
due to an error"""
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
obj.draw_type = "WIRE"
@ -95,13 +100,16 @@ def bmesh_object(name: str) -> Iterator[Tuple[bpy.types.Object, bmesh.types.BMes
finally:
bm.free()
@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
of the context."""
of the context."""
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.parent = source.parent
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:
bpy.data.objects.remove(obj)
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
# 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
import math
@contextmanager
def bmesh_from_object(bl):
"""Converts a Blender Object to a BMesh with modifiers applied."""
@ -29,6 +30,7 @@ def bmesh_from_object(bl):
finally:
mesh.free()
class GoodNeighbor:
"""Leave Things the Way You Found Them! (TM)"""
@ -63,6 +65,7 @@ class TemporaryObject:
class UiHelper:
"""This fun little helper makes sure that we don't wreck the UI"""
def __init__(self, context):
self.active_object = context.active_object
self.selected_objects = context.selected_objects
@ -82,7 +85,7 @@ class UiHelper:
def __exit__(self, type, value, traceback):
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.objects.active = self.active_object
@ -94,8 +97,10 @@ class UiHelper:
def ensure_power_of_two(value):
return pow(2, math.floor(math.log(value, 2)))
def fetch_fcurves(id_data, data_fcurves=True):
"""Given a Blender ID, yields its FCurves"""
def _fetch(source):
if source is not None and source.action is not None:
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):
yield i
def find_modifier(bo, modid):
"""Given a Blender Object, finds a given modifier and returns it or None"""
if bo is not None:

42
korman/idprops.py

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

60
korman/korlib/__init__.py

@ -17,6 +17,7 @@ _KORLIB_API_VERSION = 2
try:
from _korlib import _KORLIB_API_VERSION as _C_API_VERSION
if _KORLIB_API_VERSION != _C_API_VERSION:
raise ImportError()
@ -24,10 +25,12 @@ except ImportError as ex:
from .texture import *
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:
msg = "Korlib C Module did not load correctly."
print(msg, "Using PyKorlib :(", sep=' ')
print(msg, "Using PyKorlib :(", sep=" ")
def create_bump_LUT(mipmap):
kLUTHeight = 16
@ -41,33 +44,52 @@ except ImportError as ex:
doneH = 0
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
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
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
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
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
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))
def inspect_voribsfile(stream, header):
raise NotImplementedError("Ogg Vorbis not supported unless _korlib is compiled")
else:
from _korlib import *
from .texture import TextureAlpha
@ -77,8 +99,13 @@ finally:
from .python import *
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
_KEYWORDS = set(_kwlist)
# Python 2.x keywords
_KEYWORDS.add("exec")
@ -128,10 +155,19 @@ finally:
def process(identifier):
# No leading digits in identifiers, so skip the first range element (0...9)
yield next((identifier[0] for low, high in _IDENTIFIER_RANGES[1:]
if low <= ord(identifier[0]) <= high), '_')
yield next(
(
identifier[0]
for low, high in _IDENTIFIER_RANGES[1:]
if low <= ord(identifier[0]) <= high
),
"_",
)
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:
return "".join(process(identifier))

51
korman/korlib/console.py

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

54
korman/korlib/python.py

@ -13,13 +13,14 @@
# You should have received a copy of the GNU General Public License
# 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 os.path
import sys
_python_executables = {}
class PythonNotAvailableError(Exception):
pass
@ -30,11 +31,11 @@ def compyle(file_name, py_code, py_version, report=None, indent=0):
assert my_version == (2, 7) or my_version[0] > 2
# Remember: Python 2.2 file, so no single line if statements...
idx = file_name.find('.')
idx = file_name.find(".")
if idx == -1:
module_name = file_name
else:
module_name = file_name[:idx]
module_name = file_name[:idx]
if report is not None:
report.msg("Compyling {}", file_name, indent=indent)
@ -48,26 +49,31 @@ def compyle(file_name, py_code, py_version, report=None, indent=0):
py_code = py_code.encode("utf-8")
except UnicodeError:
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")
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:
try:
error = result.stdout.decode("utf-8").replace('\r\n', '\n')
error = result.stdout.decode("utf-8").replace("\r\n", "\n")
except UnicodeError:
error = result.stdout
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)
else:
raise NotImplementedError()
def _compyle(module_name, py_code):
# Old python versions have major issues with Windows style newlines.
# Also, bad things happen if there is no newline at the end.
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')
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")
code_object = compile(py_code, module_name, "exec")
# 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.
return marshal.dumps(code_object)
def _find_python(py_version):
def find_executable(py_version):
# First, try to use Blender to find the Python executable
@ -88,7 +95,9 @@ def _find_python(py_version):
pass
else:
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):
return py_executable
@ -108,14 +117,18 @@ def _find_python(py_version):
# I give up, you win.
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:
return py_executable
else:
raise PythonNotAvailableError("{}.{}".format(*py_version))
def _find_python_reg(reg_key, py_version):
import winreg
subkey_name = "Software\\Python\\PythonCore\\{}.{}\\InstallPath".format(*py_version)
try:
python_dir = winreg.QueryValue(reg_key, subkey_name)
@ -124,6 +137,7 @@ def _find_python_reg(reg_key, py_version):
else:
return os.path.join(python_dir, "python.exe")
def package_python(stream, pyc_objects):
# Python.pak format:
# 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.
# 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
base_offset = 4 # uint32_t numFiles
base_offset = 4 # uint32_t numFiles
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:
pyc_info.append((module_name, data_offset, compyled_code))
# index offset overall
base_offset += 2 # writeSafeStr length
base_offset += 2 # writeSafeStr length
# NOTE: This assumes that libHSPlasma's hsStream::writeSafeStr converts
# 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
# current file data offset
@ -165,14 +179,18 @@ def package_python(stream, pyc_objects):
stream.writeInt(len(compyled_code))
stream.write(compyled_code)
def verify_python(py_version, py_exe):
if not py_exe:
return False
import subprocess
try:
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:
return False
else:
@ -186,11 +204,13 @@ def verify_python(py_version, py_exe):
return False
return "{}.{}".format(*py_version) == py_check
if __name__ == "__main__":
# Python tries to be "helpful" on Windows by converting \n to \r\n.
# Therefore we must change the mode of stdout.
if sys.platform == "win32":
import os, msvcrt
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
try:

82
korman/korlib/texture.py

@ -28,6 +28,7 @@ TEX_DETAIL_ALPHA = 0
TEX_DETAIL_ADD = 1
TEX_DETAIL_MULTIPLY = 2
def scale_image(buf, srcW, srcH, dstW, dstH):
"""Scales an RGBA image using the algorithm from CWE's plMipmap::ScaleNicely"""
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_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(16):
idx = i + srcY_start
@ -57,7 +58,7 @@ def scale_image(buf, srcW, srcH, dstW, dstH):
srcX_start = int(max(srcX - filterW, 0))
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(16):
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]
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 = 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
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 = 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
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
# to avoid all the extra allocations.
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
src_idx += 4
weight_total = max(weight_total, 0.0001)
for i in range(4):
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
return bytes(dst)
@ -123,7 +132,7 @@ class GLTexture:
if self._blimg.gl_load() != 0:
raise RuntimeError("failed to load image")
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:
bgl.glBindTexture(bgl.GL_TEXTURE_2D, self._blimg.bindcode[0])
@ -155,14 +164,18 @@ class GLTexture:
@property
def _detail_falloff(self):
num_levels = self.num_levels
return ((self._texkey.detail_fade_start / 100.0) * num_levels,
(self._texkey.detail_fade_stop / 100.0) * num_levels,
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):
return (
(self._texkey.detail_fade_start / 100.0) * num_levels,
(self._texkey.detail_fade_stop / 100.0) * num_levels,
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
):
"""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
@ -190,7 +203,6 @@ class GLTexture:
else:
buf = bytearray(self._image_data)
if self._image_inverted:
buf = self._invert_image(eWidth, eHeight, buf)
@ -207,11 +219,15 @@ class GLTexture:
# Do we need to calculate the alpha component?
if calc_alpha:
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)
def _get_detail_alpha(self, level, dropoff_start, dropoff_stop, detail_max, detail_min):
alpha = (level - dropoff_start) * (detail_min - detail_max) / (dropoff_stop - dropoff_start) + detail_max
def _get_detail_alpha(
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:
return min(detail_max, max(detail_min, alpha))
else:
@ -242,8 +258,10 @@ class GLTexture:
def _get_image_data(self):
return (self._width, self._height, self._image_data)
def _set_image_data(self, value):
self._width, self._height, self._image_data = value
image_data = property(_get_image_data, _set_image_data)
def _invert_image(self, width, height, buf):
@ -251,30 +269,36 @@ class GLTexture:
finalBuf = bytearray(size)
row_stride = width * 4
for i in range(height):
src, dst = i * row_stride, (height - (i+1)) * row_stride
finalBuf[dst:dst+row_stride] = buf[src:src+row_stride]
src, dst = i * row_stride, (height - (i + 1)) * row_stride
finalBuf[dst : dst + row_stride] = buf[src : src + row_stride]
return bytes(finalBuf)
def _make_detail_map_add(self, data, level):
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):
data[i] = int(data[i] * alpha)
data[i+1] = int(data[i+1] * alpha)
data[i+2] = int(data[i+2] * alpha)
data[i + 1] = int(data[i + 1] * alpha)
data[i + 2] = int(data[i + 2] * alpha)
def _make_detail_map_alpha(self, data, level):
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):
data[i+3] = int(data[i+3] * alpha)
data[i + 3] = int(data[i + 3] * alpha)
def _make_detail_map_mult(self, data, level):
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
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
def num_levels(self):

15
korman/nodes/__init__.py

@ -29,12 +29,14 @@ from .node_python import *
from .node_responder import *
from .node_softvolume import *
class PlasmaNodeCategory(NodeCategory):
"""Plasma Node Category"""
@classmethod
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...
# If you add a new category, put the pretty name here!
@ -50,6 +52,7 @@ _kategory_names = {
"SV": "Soft Volume",
}
class PlasmaNodeItem(NodeItem):
def __init__(self, **kwargs):
self._poll_add = kwargs.pop("poll_add", None)
@ -91,11 +94,17 @@ for cls in dict(globals()).values():
_actual_kategories = []
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...
_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))
_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():
nodeitems_utils.register_node_categories("PLASMA_NODES", _actual_kategories)
def unregister():
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 ..exporter.explosions import ExportError
class PlasmaSittingBehaviorNode(PlasmaNodeBase, bpy.types.Node):
bl_category = "AVATAR"
bl_idname = "PlasmaSittingBehaviorNode"
@ -30,26 +31,41 @@ class PlasmaSittingBehaviorNode(PlasmaNodeBase, bpy.types.Node):
pl_attrib = {"ptAttribActivator", "ptAttribActivatorList", "ptAttribNamedActivator"}
approach = EnumProperty(name="Approach",
description="Directions an avatar can approach the seat from",
items=sitting_approach_flags,
default={"kApproachFront", "kApproachLeft", "kApproachRight"},
options={"ENUM_FLAG"})
input_sockets = OrderedDict([
("condition", {
"text": "Condition",
"type": "PlasmaConditionSocket",
}),
])
output_sockets = OrderedDict([
("satisfies", {
"text": "Satisfies",
"type": "PlasmaConditionSocket",
"valid_link_sockets": {"PlasmaConditionSocket", "PlasmaPythonFileNodeSocket"},
}),
])
approach = EnumProperty(
name="Approach",
description="Directions an avatar can approach the seat from",
items=sitting_approach_flags,
default={"kApproachFront", "kApproachLeft", "kApproachRight"},
options={"ENUM_FLAG"},
)
input_sockets = OrderedDict(
[
(
"condition",
{
"text": "Condition",
"type": "PlasmaConditionSocket",
},
),
]
)
output_sockets = OrderedDict(
[
(
"satisfies",
{
"text": "Satisfies",
"type": "PlasmaConditionSocket",
"valid_link_sockets": {
"PlasmaConditionSocket",
"PlasmaPythonFileNodeSocket",
},
},
),
]
)
def draw_buttons(self, context, layout):
col = layout.column()
@ -70,7 +86,12 @@ class PlasmaSittingBehaviorNode(PlasmaNodeBase, bpy.types.Node):
if i is not None:
sitmod.addNotifyKey(i.get_key(exporter, so))
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
def requires_actor(self):
@ -80,9 +101,11 @@ class PlasmaSittingBehaviorNode(PlasmaNodeBase, bpy.types.Node):
class PlasmaAnimStageAdvanceSocketIn(PlasmaNodeSocketBase, bpy.types.NodeSocket):
bl_color = (0.412, 0.2, 0.055, 1.0)
auto_advance = BoolProperty(name="Advance to Next Stage",
description="Automatically advance to the next stage when the animation completes instead of halting",
default=True)
auto_advance = BoolProperty(
name="Advance to Next Stage",
description="Automatically advance to the next stage when the animation completes instead of halting",
default=True,
)
def draw_content(self, context, layout, node, text):
if not self.is_linked:
@ -97,9 +120,11 @@ class PlasmaAnimStageAdvanceSocketIn(PlasmaNodeSocketBase, bpy.types.NodeSocket)
class PlasmaAnimStageRegressSocketIn(PlasmaNodeSocketBase, bpy.types.NodeSocket):
bl_color = (0.412, 0.2, 0.055, 1.0)
auto_regress = BoolProperty(name="Regress to Previous Stage",
description="Automatically regress to the previous stage when the animation completes instead of halting",
default=True)
auto_regress = BoolProperty(
name="Regress to Previous Stage",
description="Automatically regress to the previous stage when the animation completes instead of halting",
default=True,
)
def draw_content(self, context, layout, node, text):
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)
anim_play_flags = [("kPlayNone", "None", "Play stage only when directed by a message"),
("kPlayKey", "Keyboard", "Play stage when the user presses the forward/backward key"),
("kPlayAuto", "Automatic", "Play stage automatically")]
anim_stage_adv_flags = [("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")]
anim_play_flags = [
("kPlayNone", "None", "Play stage only when directed by a message"),
(
"kPlayKey",
"Keyboard",
"Play stage when the user presses the forward/backward key",
),
("kPlayAuto", "Automatic", "Play stage automatically"),
]
anim_stage_adv_flags = [
(
"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):
@ -138,63 +205,94 @@ class PlasmaAnimStageSettingsNode(PlasmaNodeBase, bpy.types.Node):
bl_label = "Animation Stage Settings"
bl_width_default = 325
forward = EnumProperty(name="Forward",
description="Selects which events cause this stage to play forward",
items=anim_play_flags,
default="kPlayNone")
backward = EnumProperty(name="Backward",
description="Selects which events cause this stage to play backward",
items=anim_play_flags,
default="kPlayNone")
stage_advance = EnumProperty(name="Stage Advance",
description="Selects which events cause this stage to advance to the next stage",
items=anim_stage_adv_flags,
default="kAdvanceNone")
stage_regress = EnumProperty(name="Stage Regress",
description="Selects which events cause this stage to regress to the previous stage",
items=anim_stage_rgr_flags,
default="kAdvanceNone")
notify_on = EnumProperty(name="Notify",
description="Which events should send notifications",
items=[
("kNotifyEnter", "Enter",
"Send notification when animation first begins to play"),
("kNotifyLoop", "Loop",
"Send notification when animation starts a loop"),
("kNotifyAdvance", "Advance",
"Send notification when animation is advanced"),
("kNotifyRegress", "Regress",
"Send notification when animation is regressed")
],
default={"kNotifyEnter"},
options={"ENUM_FLAG"})
input_sockets = OrderedDict([
("advance_to", {
"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",
}),
])
forward = EnumProperty(
name="Forward",
description="Selects which events cause this stage to play forward",
items=anim_play_flags,
default="kPlayNone",
)
backward = EnumProperty(
name="Backward",
description="Selects which events cause this stage to play backward",
items=anim_play_flags,
default="kPlayNone",
)
stage_advance = EnumProperty(
name="Stage Advance",
description="Selects which events cause this stage to advance to the next stage",
items=anim_stage_adv_flags,
default="kAdvanceNone",
)
stage_regress = EnumProperty(
name="Stage Regress",
description="Selects which events cause this stage to regress to the previous stage",
items=anim_stage_rgr_flags,
default="kAdvanceNone",
)
notify_on = EnumProperty(
name="Notify",
description="Which events should send notifications",
items=[
(
"kNotifyEnter",
"Enter",
"Send notification when animation first begins to play",
),
("kNotifyLoop", "Loop", "Send notification when animation starts a loop"),
(
"kNotifyAdvance",
"Advance",
"Send notification when animation is advanced",
),
(
"kNotifyRegress",
"Regress",
"Send notification when animation is regressed",
),
],
default={"kNotifyEnter"},
options={"ENUM_FLAG"},
)
input_sockets = OrderedDict(
[
(
"advance_to",
{
"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):
layout.prop(self, "forward")
@ -215,45 +313,66 @@ class PlasmaAnimStageNode(PlasmaNodeBase, bpy.types.Node):
bl_label = "Animation Stage"
bl_width_default = 325
pl_attrib = ("ptAttribAnimation")
anim_name = StringProperty(name="Animation Name",
description="Name of animation to play")
loop_option = EnumProperty(name="Looping",
description="Loop options for animation playback",
items=[("kDontLoop", "Don't Loop", "Don't loop the animation"),
("kLoop", "Loop", "Loop the animation a finite number of times"),
("kLoopForever", "Loop Forever", "Continue playing animation indefinitely")],
default="kDontLoop")
num_loops = IntProperty(name="Num Loops",
description="Number of times to loop animation",
default=0)
input_sockets = OrderedDict([
("stage_settings", {
"text": "Stage Settings",
"type": "PlasmaAnimStageSettingsSocket",
"valid_link_nodes": "PlasmaAnimStageSettingsNode",
"valid_link_sockets": "PlasmaAnimStageSettingsSocket",
"link_limit": 1,
}),
])
output_sockets = OrderedDict([
("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"} ,
}),
])
pl_attrib = "ptAttribAnimation"
anim_name = StringProperty(
name="Animation Name", description="Name of animation to play"
)
loop_option = EnumProperty(
name="Looping",
description="Loop options for animation playback",
items=[
("kDontLoop", "Don't Loop", "Don't loop the animation"),
("kLoop", "Loop", "Loop the animation a finite number of times"),
("kLoopForever", "Loop Forever", "Continue playing animation indefinitely"),
],
default="kDontLoop",
)
num_loops = IntProperty(
name="Num Loops", description="Number of times to loop animation", default=0
)
input_sockets = OrderedDict(
[
(
"stage_settings",
{
"text": "Stage Settings",
"type": "PlasmaAnimStageSettingsSocket",
"valid_link_nodes": "PlasmaAnimStageSettingsNode",
"valid_link_sockets": "PlasmaAnimStageSettingsSocket",
"link_limit": 1,
},
),
]
)
output_sockets = OrderedDict(
[
(
"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):
layout.prop(self, "anim_name")
@ -270,7 +389,15 @@ class PlasmaAnimStageNode(PlasmaNodeBase, bpy.types.Node):
stage_socket = self.find_output_socket("stage")
if stage_socket.is_linked:
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
@ -284,48 +411,69 @@ class PlasmaMultiStageBehaviorNode(PlasmaNodeBase, bpy.types.Node):
bl_label = "Multistage Behavior"
bl_width_default = 200
pl_attrib = ("ptAttribBehavior")
freeze_phys = BoolProperty(name="Freeze Physical",
description="Freeze physical at end",
default=False)
reverse_control = BoolProperty(name="Reverse Controls",
description="Reverse forward/back controls at end",
default=False)
input_sockets = OrderedDict([
("seek_target", {
"text": "Seek Target",
"type": "PlasmaSeekTargetSocketIn",
"valid_link_sockets": "PlasmaSeekTargetSocketOut",
}),
("stage_refs", {
"text": "Stage",
"type": "PlasmaAnimStageRefSocket",
"valid_link_nodes": "PlasmaAnimStageNode",
"valid_link_sockets": "PlasmaAnimStageRefSocket",
"link_limit": 1,
"spawn_empty": True,
}),
("condition", {
"text": "Triggered By",
"type": "PlasmaConditionSocket",
"spawn_empty": True,
}),
])
output_sockets = OrderedDict([
("hosts", {
"text": "Host Script",
"type": "PlasmaBehaviorSocket",
"valid_link_nodes": {"PlasmaPythonFileNode"},
"spawn_empty": True,
}),
("satisfies", {
"text": "Trigger",
"type": "PlasmaConditionSocket",
})
])
pl_attrib = "ptAttribBehavior"
freeze_phys = BoolProperty(
name="Freeze Physical", description="Freeze physical at end", default=False
)
reverse_control = BoolProperty(
name="Reverse Controls",
description="Reverse forward/back controls at end",
default=False,
)
input_sockets = OrderedDict(
[
(
"seek_target",
{
"text": "Seek Target",
"type": "PlasmaSeekTargetSocketIn",
"valid_link_sockets": "PlasmaSeekTargetSocketOut",
},
),
(
"stage_refs",
{
"text": "Stage",
"type": "PlasmaAnimStageRefSocket",
"valid_link_nodes": "PlasmaAnimStageNode",
"valid_link_sockets": "PlasmaAnimStageRefSocket",
"link_limit": 1,
"spawn_empty": True,
},
),
(
"condition",
{
"text": "Triggered By",
"type": "PlasmaConditionSocket",
"spawn_empty": True,
},
),
]
)
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):
layout.prop(self, "freeze_phys")
@ -337,7 +485,9 @@ class PlasmaMultiStageBehaviorNode(PlasmaNodeBase, bpy.types.Node):
if seek_socket.is_linked:
seek_target = seek_socket.links[0].from_node.target
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:
self.raise_error("MultiStage Behavior's seek point object is invalid")
else:
@ -349,7 +499,9 @@ class PlasmaMultiStageBehaviorNode(PlasmaNodeBase, bpy.types.Node):
seek_socket = self.find_input_socket("seek_target")
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.reverseFBControlsOnRelease = self.reverse_control
@ -365,7 +517,7 @@ class PlasmaMultiStageBehaviorNode(PlasmaNodeBase, bpy.types.Node):
settings = stage.find_input("stage_settings")
if settings:
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.regressType = getattr(plAnimStage, settings.stage_regress)
for flag in settings.notify_on:
@ -395,13 +547,20 @@ class PlasmaMultiStageBehaviorNode(PlasmaNodeBase, bpy.types.Node):
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:
if key is not None:
msbmod.addReceiver(key)
else:
exporter.report.warn("'{}' Node '{}' doesn't expose a key. It won't be triggered by '{}'!",
node.bl_idname, node.name, self.name, indent=3)
exporter.report.warn(
"'{}' Node '{}' doesn't expose a key. It won't be triggered by '{}'!",
node.bl_idname,
node.name,
self.name,
indent=3,
)
@property
def requires_actor(self):
@ -423,7 +582,15 @@ class PlasmaAnimStageRefSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
def draw_content(self, context, layout, node, text):
if isinstance(node, PlasmaMultiStageBehaviorNode):
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:
layout.label(text)
else:
@ -434,9 +601,11 @@ class PlasmaAnimStageRefSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
class PlasmaSeekTargetSocketIn(PlasmaNodeSocketBase, bpy.types.NodeSocket):
bl_color = (0.180, 0.350, 0.180, 1.0)
auto_target = BoolProperty(name="Auto Smart Seek",
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)
auto_target = BoolProperty(
name="Auto Smart Seek",
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):
if not self.is_linked:
@ -459,18 +628,28 @@ class PlasmaSeekTargetNode(PlasmaNodeBase, bpy.types.Node):
bl_label = "Seek Target"
bl_width_default = 200
target = PointerProperty(name="Position",
description="Object defining the Seek Point's position",
type=bpy.types.Object)
output_sockets = OrderedDict([
("seekers", {
"text": "Seekers",
"type": "PlasmaSeekTargetSocketOut",
"valid_link_nodes": {"PlasmaMultiStageBehaviorNode", "PlasmaOneShotMsgNode"},
"valid_link_sockets": {"PlasmaSeekTargetSocketIn"},
})
])
target = PointerProperty(
name="Position",
description="Object defining the Seek Point's position",
type=bpy.types.Object,
)
output_sockets = OrderedDict(
[
(
"seekers",
{
"text": "Seekers",
"type": "PlasmaSeekTargetSocketOut",
"valid_link_nodes": {
"PlasmaMultiStageBehaviorNode",
"PlasmaOneShotMsgNode",
},
"valid_link_sockets": {"PlasmaSeekTargetSocketIn"},
},
)
]
)
def draw_buttons(self, context, layout):
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 .. import idprops
class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node):
bl_category = "CONDITIONS"
bl_idname = "PlasmaClickableNode"
@ -32,38 +33,61 @@ class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.N
# These are the Python attributes we can fill in
pl_attrib = {"ptAttribActivator", "ptAttribActivatorList", "ptAttribNamedActivator"}
clickable_object = PointerProperty(name="Clickable",
description="Mesh object that is clickable",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects)
bounds = EnumProperty(name="Bounds",
description="Clickable's bounds (NOTE: only used if your clickable is not a collider)",
items=bounds_types,
default="hull")
input_sockets = OrderedDict([
("region", {
"text": "Avatar Inside Region",
"type": "PlasmaClickableRegionSocket",
}),
("facing", {
"text": "Avatar Facing Target",
"type": "PlasmaFacingTargetSocket",
}),
("message", {
"text": "Message",
"type": "PlasmaEnableMessageSocket",
"spawn_empty": True,
}),
])
output_sockets = OrderedDict([
("satisfies", {
"text": "Satisfies",
"type": "PlasmaConditionSocket",
"valid_link_sockets": {"PlasmaConditionSocket", "PlasmaPythonFileNodeSocket"},
}),
])
clickable_object = PointerProperty(
name="Clickable",
description="Mesh object that is clickable",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects,
)
bounds = EnumProperty(
name="Bounds",
description="Clickable's bounds (NOTE: only used if your clickable is not a collider)",
items=bounds_types,
default="hull",
)
input_sockets = OrderedDict(
[
(
"region",
{
"text": "Avatar Inside Region",
"type": "PlasmaClickableRegionSocket",
},
),
(
"facing",
{
"text": "Avatar Facing Target",
"type": "PlasmaFacingTargetSocket",
},
),
(
"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):
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:
clickable_bo = parent_bo
interface = self._find_create_object(plInterfaceInfoModifier, exporter, bl=clickable_bo, so=clickable_so)
logicmod = self._find_create_key(plLogicModifier, exporter, bl=clickable_bo, so=clickable_so)
interface = self._find_create_object(
plInterfaceInfoModifier, exporter, bl=clickable_bo, so=clickable_so
)
logicmod = self._find_create_key(
plLogicModifier, exporter, bl=clickable_bo, so=clickable_so
)
interface.addIntfKey(logicmod)
# Matches data seen in Cyan's PRPs...
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
# The actual physical object that does the cursor LOS
exporter.physics.generate_physical(clickable_bo, clickable_so, bounds=bounds,
member_group="kGroupLOSOnly",
properties=["kPinned"],
losdbs=["kLOSDBUIItems"])
exporter.physics.generate_physical(
clickable_bo,
clickable_so,
bounds=bounds,
member_group="kGroupLOSOnly",
properties=["kPinned"],
losdbs=["kLOSDBUIItems"],
)
# 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)
# 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)
logicmod.addCondition(activator.key)
logicmod.setLogicFlag(plLogicModifier.kLocalElement, True)
@ -124,7 +160,9 @@ class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.N
def get_key(self, exporter, parent_so):
# careful... we really make lots of keys...
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
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.
# Case: sitting modifier (exports from sit position empty)
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)
else:
return (None, parent_so)
@ -150,27 +190,38 @@ class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.N
return {"clickable_object": "clickable"}
class PlasmaClickableRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node):
class PlasmaClickableRegionNode(
idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node
):
bl_category = "CONDITIONS"
bl_idname = "PlasmaClickableRegionNode"
bl_label = "Clickable Region Settings"
bl_width_default = 200
region_object = PointerProperty(name="Region",
description="Object that defines the region mesh",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects)
bounds = EnumProperty(name="Bounds",
description="Physical object's bounds (NOTE: only used if your clickable is not a collider)",
items=bounds_types,
default="hull")
output_sockets = OrderedDict([
("satisfies", {
"text": "Satisfies",
"type": "PlasmaClickableRegionSocket",
}),
])
region_object = PointerProperty(
name="Region",
description="Object that defines the region mesh",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects,
)
bounds = EnumProperty(
name="Bounds",
description="Physical object's bounds (NOTE: only used if your clickable is not a collider)",
items=bounds_types,
default="hull",
)
output_sockets = OrderedDict(
[
(
"satisfies",
{
"text": "Satisfies",
"type": "PlasmaClickableRegionSocket",
},
),
]
)
def draw_buttons(self, context, layout):
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
# Our physical is a detector and it only detects avatars...
exporter.physics.generate_physical(region_bo, region_so, bounds=bounds,
member_group="kGroupDetector",
report_groups=["kGroupAvatar"])
exporter.physics.generate_physical(
region_bo,
region_so,
bounds=bounds,
member_group="kGroupDetector",
report_groups=["kGroupAvatar"],
)
# I'm glad this crazy mess made sense to someone at Cyan...
# 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
# 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.
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.type = plObjectInVolumeDetector.kTypeAny
# 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...
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
logicmod.addCondition(objinbox_key)
@ -225,19 +284,26 @@ class PlasmaFacingTargetNode(PlasmaNodeBase, bpy.types.Node):
bl_idname = "PlasmaFacingTargetNode"
bl_label = "Facing Target"
directional = BoolProperty(name="Directional",
description="TODO",
default=True)
tolerance = IntProperty(name="Degrees",
description="How far away from the target the avatar can turn (in degrees)",
min=-180, max=180, default=45)
output_sockets = OrderedDict([
("satisfies", {
"text": "Satisfies",
"type": "PlasmaFacingTargetSocket",
}),
])
directional = BoolProperty(name="Directional", description="TODO", default=True)
tolerance = IntProperty(
name="Degrees",
description="How far away from the target the avatar can turn (in degrees)",
min=-180,
max=180,
default=45,
)
output_sockets = OrderedDict(
[
(
"satisfies",
{
"text": "Satisfies",
"type": "PlasmaFacingTargetSocket",
},
),
]
)
def draw_buttons(self, context, layout):
layout.prop(self, "directional")
@ -247,9 +313,11 @@ class PlasmaFacingTargetNode(PlasmaNodeBase, bpy.types.Node):
class PlasmaFacingTargetSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
bl_color = (0.0, 0.267, 0.247, 1.0)
allow_simple = BoolProperty(name="Facing Target",
description="Avatar must be facing the target object",
default=True)
allow_simple = BoolProperty(
name="Facing Target",
description="Avatar must be facing the target object",
default=True,
)
def draw_content(self, context, layout, node, text):
if self.simple_mode:
@ -274,7 +342,9 @@ class PlasmaFacingTargetSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
# This is a programmer failure, so we need a traceback.
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.directional = directional
facing.satisfied = True
@ -283,13 +353,13 @@ class PlasmaFacingTargetSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
@property
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
def simple_mode(self):
"""Simple mode allows a user to click a button on input sockets to automatically generate a
Facing Target condition"""
return (not self.is_linked and not self.is_output)
Facing Target condition"""
return not self.is_linked and not self.is_output
class PlasmaVolumeReportNode(PlasmaNodeBase, bpy.types.Node):
@ -297,22 +367,37 @@ class PlasmaVolumeReportNode(PlasmaNodeBase, bpy.types.Node):
bl_idname = "PlasmaVolumeReportNode"
bl_label = "Region Trigger Settings"
report_when = EnumProperty(name="When",
description="When the region should trigger",
items=[("each", "Each Event", "The region will trigger on every enter/exit"),
("first", "First Event", "The region will trigger on the first event only"),
("count", "Population", "When the region has a certain number of objects inside it")])
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"},
}),
])
report_when = EnumProperty(
name="When",
description="When the region should trigger",
items=[
("each", "Each Event", "The region will trigger on every enter/exit"),
("first", "First Event", "The region will trigger on the first event only"),
(
"count",
"Population",
"When the region has a certain number of objects inside it",
),
],
)
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):
layout.prop(self, "report_when")
@ -332,47 +417,76 @@ class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.type
pl_attrib = {"ptAttribActivator", "ptAttribActivatorList", "ptAttribNamedActivator"}
# Region Mesh
region_object = PointerProperty(name="Region",
description="Object that defines the region mesh",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects)
bounds = EnumProperty(name="Bounds",
description="Physical object's bounds",
items=bounds_types)
region_object = PointerProperty(
name="Region",
description="Object that defines the region mesh",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects,
)
bounds = EnumProperty(
name="Bounds", description="Physical object's bounds", items=bounds_types
)
# Detector Properties
report_on = EnumProperty(name="Triggerers",
description="What triggers this region?",
options={"ANIMATABLE", "ENUM_FLAG"},
items=[("kGroupAvatar", "Avatars", "Avatars trigger this region"),
("kGroupDynamic", "Dynamics", "Any non-avatar dynamic physical object (eg kickables)")],
default={"kGroupAvatar"})
input_sockets = OrderedDict([
("enter", {
"text": "Trigger on Enter",
"type": "PlasmaVolumeSettingsSocketIn",
"valid_link_sockets": {"PlasmaVolumeSettingsSocketOut"},
}),
("exit", {
"text": "Trigger on Exit",
"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"},
}),
])
report_on = EnumProperty(
name="Triggerers",
description="What triggers this region?",
options={"ANIMATABLE", "ENUM_FLAG"},
items=[
("kGroupAvatar", "Avatars", "Avatars trigger this region"),
(
"kGroupDynamic",
"Dynamics",
"Any non-avatar dynamic physical object (eg kickables)",
),
],
default={"kGroupAvatar"},
)
input_sockets = OrderedDict(
[
(
"enter",
{
"text": "Trigger on Enter",
"type": "PlasmaVolumeSettingsSocketIn",
"valid_link_sockets": {"PlasmaVolumeSettingsSocketOut"},
},
),
(
"exit",
{
"text": "Trigger on Exit",
"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):
layout.prop(self, "report_on")
@ -390,9 +504,13 @@ class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.type
parent_key = parent_so.key
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:
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:
return rgn_exit
@ -411,42 +529,78 @@ class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.type
self.raise_error("Region cannot be empty")
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
enter_simple = self.find_input_socket("enter").allow
enter_settings = self.find_input("enter", "PlasmaVolumeReportNode")
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)
# Region Exits
exit_simple = self.find_input_socket("exit").allow
exit_settings = self.find_input("exit", "PlasmaVolumeReportNode")
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)
# Don't forget to export the physical object itself!
exporter.physics.generate_physical(region_bo, region_so, bounds=self.bounds,
member_group="kGroupDetector",
report_groups=self.report_on)
def _export_volume_event(self, exporter, region_bo, region_so, parent_so, event, settings):
exporter.physics.generate_physical(
region_bo,
region_so,
bounds=self.bounds,
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:
suffix = "Enter"
else:
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.setLogicFlag(plLogicModifier.kMultiTrigger, True)
logicmod.notify = self.generate_notify_msg(exporter, parent_so, "satisfies")
# Now, the detector objects
det = self._find_create_object(plObjectInVolumeDetector, exporter, suffix=suffix, bl=region_bo, so=region_so)
volKey = self._find_create_key(plVolumeSensorConditionalObject, exporter, suffix=suffix, bl=region_bo, so=region_so)
det = self._find_create_object(
plObjectInVolumeDetector,
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.type = event
@ -474,13 +628,17 @@ class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.type
@property
def report_enters(self):
return (self.find_input_socket("enter").allow or
self.find_input("enter", "PlasmaVolumeReportNode") is not None)
return (
self.find_input_socket("enter").allow
or self.find_input("enter", "PlasmaVolumeReportNode") is not None
)
@property
def report_exits(self):
return (self.find_input_socket("exit").allow or
self.find_input("exit", "PlasmaVolumeReportNode") is not None)
return (
self.find_input_socket("exit").allow
or self.find_input("exit", "PlasmaVolumeReportNode") is not None
)
class PlasmaVolumeSettingsSocket(PlasmaNodeSocketBase):

152
korman/nodes/node_core.py

@ -21,14 +21,20 @@ import time
from ..exporter import ExportError
class PlasmaNodeBase:
def generate_notify_msg(self, exporter, so, socket_id, idname=None):
notify = plNotifyMsg()
notify.BCastFlags = (plMessage.kNetPropagate | plMessage.kLocalPropagate)
notify.BCastFlags = plMessage.kNetPropagate | plMessage.kLocalPropagate
for i in self.find_outputs(socket_id, idname):
key = i.get_key(exporter, so)
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):
for i in key:
notify.addReceiver(key)
@ -44,7 +50,9 @@ class PlasmaNodeBase:
if single:
name = bl.name if bl is not None else so.key.name
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:
working_name = "{}_{}_{}".format(name, self.id_data.name, self.name)
else:
@ -70,17 +78,23 @@ class PlasmaNodeBase:
def _find_create_object(self, pClass, exporter, **kwargs):
"""Finds or creates an hsKeyedObject specific to this node."""
assert "name" not in kwargs
kwargs["name"] = self.get_key_name(issubclass(pClass, (plObjInterface, plSingleModifier)),
kwargs.pop("suffix", ""), kwargs.get("bl"),
kwargs.get("so"))
kwargs["name"] = self.get_key_name(
issubclass(pClass, (plObjInterface, plSingleModifier)),
kwargs.pop("suffix", ""),
kwargs.get("bl"),
kwargs.get("so"),
)
return exporter.mgr.find_create_object(pClass, **kwargs)
def _find_create_key(self, pClass, exporter, **kwargs):
"""Finds or creates a plKey specific to this node."""
assert "name" not in kwargs
kwargs["name"] = self.get_key_name(issubclass(pClass, (plObjInterface, plSingleModifier)),
kwargs.pop("suffix", ""), kwargs.get("bl"),
kwargs.get("so"))
kwargs["name"] = self.get_key_name(
issubclass(pClass, (plObjInterface, plSingleModifier)),
kwargs.pop("suffix", ""),
kwargs.get("bl"),
kwargs.get("so"),
)
return exporter.mgr.find_create_key(pClass, **kwargs)
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."""
from .node_deprecated import PlasmaDeprecatedNode
source_socket_props = getattr(self.__class__, "output_sockets", {}) if is_output else \
getattr(self.__class__, "input_sockets", {})
source_socket_props = (
getattr(self.__class__, "output_sockets", {})
if is_output
else getattr(self.__class__, "input_sockets", {})
)
source_socket_def = source_socket_props.get(socket.alias, {})
valid_dest_sockets = source_socket_def.get("valid_link_sockets")
valid_dest_nodes = source_socket_def.get("valid_link_nodes")
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
# Korman standard node socket definitions
socket_defs = getattr(dest_node_cls, "input_sockets", {}) if is_output else \
getattr(dest_node_cls, "output_sockets", {})
socket_defs = (
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():
if socket_def.get("can_link") is False:
continue
@ -201,17 +223,29 @@ class PlasmaNodeBase:
continue
# 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
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
# Can the socket_def on the destination node link to this socket?
valid_source_nodes = socket_def.get("valid_link_nodes")
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
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
if valid_source_sockets is None and valid_source_nodes is None:
if socket.bl_idname != socket_def["type"]:
@ -222,10 +256,12 @@ class PlasmaNodeBase:
if poll_add is not None and not poll_add(context):
continue
yield { "node_idname": dest_node_cls.bl_idname,
"node_text": dest_node_cls.bl_label,
"socket_name": socket_name,
"socket_text": socket_def["text"] }
yield {
"node_idname": dest_node_cls.bl_idname,
"node_text": dest_node_cls.bl_label,
"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.
for i in dest_node_cls.generate_valid_links_to(context, socket, is_output):
@ -273,10 +309,12 @@ class PlasmaNodeBase:
@classmethod
def poll(cls, context):
return (context.bl_idname == "PlasmaNodeTree")
return context.bl_idname == "PlasmaNodeTree"
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)
@property
@ -285,8 +323,10 @@ class PlasmaNodeBase:
@property
def _socket_defs(self):
return (getattr(self.__class__, "input_sockets", {}),
getattr(self.__class__, "output_sockets", {}))
return (
getattr(self.__class__, "input_sockets", {}),
getattr(self.__class__, "output_sockets", {}),
)
def _spawn_socket(self, key, options, sockets):
socket = sockets.new(options["type"], options["text"], key)
@ -299,7 +339,11 @@ class PlasmaNodeBase:
def _tattle(self, socket, link, reason):
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):
links = self.id_data.links
@ -320,7 +364,9 @@ class PlasmaNodeBase:
def _update_init_sockets(self, defs, sockets):
# Create any missing sockets and spawn any required empties.
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:
self._spawn_socket(alias, options, sockets)
elif options.get("spawn_empty", False):
@ -382,7 +428,9 @@ class PlasmaNodeBase:
if allowed_sockets or allowed_nodes:
for link in socket.links:
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:
try:
self._tattle(socket, link, "(bad node)")
@ -392,8 +440,13 @@ class PlasmaNodeBase:
pass
continue
if allowed_sockets:
to_from_socket = 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:
to_from_socket = (
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:
self._tattle(socket, link, "(bad socket)")
self.id_data.links.remove(link)
@ -407,16 +460,21 @@ class PlasmaNodeBase:
def _whine(self, msg, *args):
if 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):
"""Represents the final output of a node tree"""
@classmethod
def poll_add(cls, context):
# 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.
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:
@ -424,9 +482,9 @@ class PlasmaNodeSocketBase:
def alias(self):
"""Blender appends .000 stuff if it's a dupe. We don't care about dupe identifiers..."""
ident = self.identifier
if ident.find('.') == -1:
if ident.find(".") == -1:
return ident
return ident.rsplit('.', 1)[0]
return ident.rsplit(".", 1)[0]
def draw(self, context, layout, node, text):
if not self.is_output:
@ -462,9 +520,11 @@ class PlasmaNodeSocketBase:
# loaded. So, only check in that case.
hval = str(hash((i for i in bpy.data.texts)))
if hval != self.possible_links_texts_hash:
self.has_possible_links_value = any(self.node.generate_valid_links_for(bpy.context,
self,
self.is_output))
self.has_possible_links_value = any(
self.node.generate_valid_links_for(
bpy.context, self, self.is_output
)
)
self.possible_links_texts_hash = hval
self.possible_links_update_time = tval
return self.has_possible_links_value
@ -475,8 +535,9 @@ class PlasmaNodeSocketBase:
@classmethod
def register(cls):
cls.has_possible_links = BoolProperty(options={"HIDDEN", "SKIP_SAVE"},
get=cls._has_possible_links)
cls.has_possible_links = BoolProperty(
options={"HIDDEN", "SKIP_SAVE"}, get=cls._has_possible_links
)
cls.has_possible_links_value = BoolProperty(options={"HIDDEN", "SKIP_SAVE"})
cls.possible_links_update_time = FloatProperty(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):
"""A general input socket that will steal the output's color"""
def draw_color(self, context, node):
if self.is_linked:
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:
actors.update(harvest_method())
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
@classmethod
def poll(cls, context):
return (context.scene.render.engine == "PLASMA_GAME")
return context.scene.render.engine == "PLASMA_GAME"
@property
def requires_actor(self):
@ -539,4 +605,6 @@ def _nuke_plasma_nodes(dummy):
for i in bpy.data.node_groups:
if isinstance(i, PlasmaNodeTree):
i.nodes.clear()
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 *
class PlasmaDeprecatedNode(PlasmaNodeBase):
@abc.abstractmethod
def upgrade(self):
@ -53,28 +54,44 @@ class PlasmaResponderCommandNode(PlasmaDeprecatedNode, bpy.types.Node):
bl_idname = "PlasmaResponderCommandNode"
bl_label = "Responder Command"
input_sockets = OrderedDict([
("whodoneit", {
"text": "Condition",
"type": "PlasmaRespCommandSocket",
}),
])
output_sockets = OrderedDict([
("msg", {
"link_limit": 1,
"text": "Message",
"type": "PlasmaMessageSocket",
}),
("trigger", {
"text": "Trigger",
"type": "PlasmaRespCommandSocket",
}),
("reenable", {
"text": "Local Reenable",
"type": "PlasmaEnableMessageSocket",
}),
])
input_sockets = OrderedDict(
[
(
"whodoneit",
{
"text": "Condition",
"type": "PlasmaRespCommandSocket",
},
),
]
)
output_sockets = OrderedDict(
[
(
"msg",
{
"link_limit": 1,
"text": "Message",
"type": "PlasmaMessageSocket",
},
),
(
"trigger",
{
"text": "Trigger",
"type": "PlasmaRespCommandSocket",
},
),
(
"reenable",
{
"text": "Local Reenable",
"type": "PlasmaEnableMessageSocket",
},
),
]
)
def _find_message_sender_node(self, parentCmdNode=None):
if parentCmdNode is None:
@ -103,7 +120,6 @@ class PlasmaResponderCommandNode(PlasmaDeprecatedNode, bpy.types.Node):
self._whine("unexpected command node type '{}'", parentCmdNode.bl_idname)
return None
def upgrade(self):
senderNode = self._find_message_sender_node()
if senderNode is None:
@ -130,6 +146,7 @@ class PlasmaResponderCommandNode(PlasmaDeprecatedNode, bpy.types.Node):
continue
tree.links.new(link.to_socket, fromSocket)
@bpy.app.handlers.persistent
def _upgrade_node_trees(dummy):
for tree in bpy.data.node_groups:
@ -150,4 +167,6 @@ def _upgrade_node_trees(dummy):
# toss deprecated nodes
for node in nuke:
tree.nodes.remove(node)
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 .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
class PlasmaExcludeRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node):
class PlasmaExcludeRegionNode(
idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node
):
bl_category = "LOGIC"
bl_idname = "PlasmaExcludeRegionNode"
bl_label = "Exclude Region"
@ -33,46 +40,70 @@ class PlasmaExcludeRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.typ
def _get_bounds(self):
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")
def _set_bounds(self, value):
if self.region_object is not None:
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,
poll=idprops.poll_mesh_objects)
bounds = EnumProperty(name="Bounds",
description="Region bounds",
items=bounds_types,
get=_get_bounds,
set=_set_bounds)
block_cameras = BoolProperty(name="Block Cameras",
description="The region blocks cameras when it has been cleared")
input_sockets = OrderedDict([
("safe_point", {
"type": "PlasmaExcludeSafePointSocket",
"text": "Safe Point",
"spawn_empty": True,
# This never links to anything...
"valid_link_sockets": frozenset(),
}),
("msg", {
"type": "PlasmaExcludeMessageSocket",
"text": "Message",
"spawn_empty": True,
}),
])
output_sockets = OrderedDict([
("keyref", {
"text": "References",
"type": "PlasmaPythonReferenceNodeSocket",
"valid_link_nodes": {"PlasmaPythonFileNode"},
}),
])
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,
poll=idprops.poll_mesh_objects,
)
bounds = EnumProperty(
name="Bounds",
description="Region bounds",
items=bounds_types,
get=_get_bounds,
set=_set_bounds,
)
block_cameras = BoolProperty(
name="Block Cameras",
description="The region blocks cameras when it has been cleared",
)
input_sockets = OrderedDict(
[
(
"safe_point",
{
"type": "PlasmaExcludeSafePointSocket",
"text": "Safe Point",
"spawn_empty": True,
# This never links to anything...
"valid_link_sockets": frozenset(),
},
),
(
"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):
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):
if self.region_object is None:
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):
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):
excludergn = self.get_key(exporter, parent_so).object
excludergn.setFlag(plExcludeRegionModifier.kBlockCameras, self.block_cameras)
region_so = exporter.mgr.find_create_object(plSceneObject, bl=self.region_object)
region_so = exporter.mgr.find_create_object(
plSceneObject, bl=self.region_object
)
# Safe points
for i in self.find_input_sockets("safe_point"):
safept = i.safepoint_object
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
if exporter.mgr.getVer() <= pvPots:
@ -105,11 +146,15 @@ class PlasmaExcludeRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.typ
else:
member_group = "kGroupStatic"
collide_groups = []
exporter.physics.generate_physical(self.region_object, region_so, bounds=self.bounds,
properties=["kPinned"],
losdbs=["kLOSDBUIBlockers"],
member_group=member_group,
collide_groups=collide_groups)
exporter.physics.generate_physical(
self.region_object,
region_so,
bounds=self.bounds,
properties=["kPinned"],
losdbs=["kLOSDBUIBlockers"],
member_group=member_group,
collide_groups=collide_groups,
)
@property
def export_once(self):
@ -120,12 +165,16 @@ class PlasmaExcludeRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.typ
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)
safepoint_object = PointerProperty(name="Safe Point",
description="A point outside of this exclude region to move the avatar to",
type=bpy.types.Object)
safepoint_object = PointerProperty(
name="Safe Point",
description="A point outside of this exclude region to move the avatar to",
type=bpy.types.Object,
)
def draw(self, context, layout, node, text):
layout.prop(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
_single_user_attribs = {
"ptAttribBoolean", "ptAttribInt", "ptAttribFloat", "ptAttribString", "ptAttribDropDownList",
"ptAttribSceneobject", "ptAttribDynamicMap", "ptAttribGUIDialog", "ptAttribExcludeRegion",
"ptAttribWaveSet", "ptAttribSwimCurrent", "ptAttribAnimation", "ptAttribBehavior",
"ptAttribMaterial", "ptAttribMaterialAnimation", "ptAttribGUIPopUpMenu", "ptAttribGUISkin",
"ptAttribBoolean",
"ptAttribInt",
"ptAttribFloat",
"ptAttribString",
"ptAttribDropDownList",
"ptAttribSceneobject",
"ptAttribDynamicMap",
"ptAttribGUIDialog",
"ptAttribExcludeRegion",
"ptAttribWaveSet",
"ptAttribSwimCurrent",
"ptAttribAnimation",
"ptAttribBehavior",
"ptAttribMaterial",
"ptAttribMaterialAnimation",
"ptAttribGUIPopUpMenu",
"ptAttribGUISkin",
"ptAttribGrassShader",
}
@ -83,9 +96,11 @@ _attrib_key_types = {
"ptAttribGUIPopUpMenu": plFactory.ClassIndex("pfGUIPopUpMenu"),
"ptAttribGUISkin": plFactory.ClassIndex("pfGUISkin"),
"ptAttribWaveSet": plFactory.ClassIndex("plWaveSet7"),
"ptAttribSwimCurrent": (plFactory.ClassIndex("plSwimRegionInterface"),
plFactory.ClassIndex("plSwimCircularCurrentRegion"),
plFactory.ClassIndex("plSwimStraightCurrentRegion")),
"ptAttribSwimCurrent": (
plFactory.ClassIndex("plSwimRegionInterface"),
plFactory.ClassIndex("plSwimCircularCurrentRegion"),
plFactory.ClassIndex("plSwimStraightCurrentRegion"),
),
"ptAttribClusterList": plFactory.ClassIndex("plClusterGroup"),
"ptAttribMaterialAnimation": plFactory.ClassIndex("plLayerAnimation"),
"ptAttribGrassShader": plFactory.ClassIndex("plGrassShaderMod"),
@ -174,8 +189,10 @@ class PlasmaAttribute(bpy.types.PropertyGroup):
def _get_simple_value(self):
return getattr(self, self._simple_attrs[self.attribute_type])
def _set_simple_value(self, value):
setattr(self, self._simple_attrs[self.attribute_type], value)
simple_value = property(_get_simple_value, _set_simple_value)
@ -203,17 +220,21 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
self.attributes.clear()
self.inputs.clear()
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",
description="Python Filename",
update=_update_pyfile)
filename = StringProperty(
name="File Name", description="Python Filename", update=_update_pyfile
)
filepath = StringProperty(options={"HIDDEN"})
text_id = PointerProperty(name="Script File",
description="Script file datablock",
type=bpy.types.Text,
poll=_poll_pytext,
update=_update_pytext)
text_id = PointerProperty(
name="Script File",
description="Script file datablock",
type=bpy.types.Text,
poll=_poll_pytext,
update=_update_pytext,
)
# This property exists for UI purposes ONLY
package = BoolProperty(options={"HIDDEN", "SKIP_SAVE"})
@ -223,7 +244,7 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
@property
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):
main_row = layout.row(align=True)
@ -233,7 +254,9 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
# open operator
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.data_path = self.node_path
operator.filename_property = "filename"
@ -251,14 +274,19 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
# rescan operator
row = main_row.row(align=True)
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:
operator.text_path = self.text_id.name
operator.node_path = self.node_path
# This could happen on an upgrade
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):
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
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)
# PFMs can have their own SDL...
sdl_text = bpy.data.texts.get("{}.sdl".format(py_name), 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)
# Handle exporting the Python Parameters
@ -292,7 +324,11 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
for socket in attrib_sockets:
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):
value = (value,)
for i in value:
@ -312,14 +348,23 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
# an animated lamp.
if not bool(bo.users_group):
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",
so.key.name, indent=3)
exporter.report.msg(
"Marking RT light '{}' as animated due to usage in a Python File node",
so.key.name,
indent=3,
)
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:
exporter.report.warn("Attribute '{}' didn't return a key and therefore will be unavailable to Python",
self.id_data.name, socket.links[0].name, indent=3)
exporter.report.warn(
"Attribute '{}' didn't return a key and therefore will be unavailable to Python",
self.id_data.name,
socket.links[0].name,
indent=3,
)
return
key_type = _attrib_key_types[socket.attribute_type]
@ -328,9 +373,13 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
else:
good_key = key.type == key_type
if not good_key:
exporter.report.warn("'{}' Node '{}' returned an unexpected key type '{}'",
self.id_data.name, socket.links[0].from_node.name,
plFactory.ClassName(key.type), indent=3)
exporter.report.warn(
"'{}' Node '{}' returned an unexpected key type '{}'",
self.id_data.name,
socket.links[0].from_node.name,
plFactory.ClassName(key.type),
indent=3,
)
if isinstance(key.object, plSceneObject):
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 issubclass(i, PlasmaAttribNodeBase):
yield { "node_idname": i.bl_idname,
"node_text": i.bl_label,
"socket_name": "pfm",
"socket_text": "Python File" }
yield {
"node_idname": i.bl_idname,
"node_text": i.bl_label,
"socket_name": "pfm",
"socket_text": "Python File",
}
else:
for socket_name, socket_def in i.output_sockets.items():
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_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
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
yield { "node_idname": i.bl_idname,
"node_text": i.bl_label,
"socket_name": socket_name,
"socket_text": socket_def["text"] }
yield {
"node_idname": i.bl_idname,
"node_text": i.bl_label,
"socket_name": socket_name,
"socket_text": socket_def["text"],
}
@classmethod
def generate_valid_links_to(cls, context, socket, is_output):
@ -394,9 +453,15 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
return
valid_link_sockets = socket_def.get("valid_link_sockets")
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
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
# 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
# *gulp*
yield { "node_idname": "PlasmaPythonFileNode",
"node_text": text_id.name,
"node_settings": { "filename": text_id.name },
"socket_name": attrib["name"],
"socket_text": attrib["name"] }
yield {
"node_idname": "PlasmaPythonFileNode",
"node_text": text_id.name,
"node_settings": {"filename": text_id.name},
"socket_name": attrib["name"],
"socket_text": attrib["name"],
}
def harvest_actors(self):
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
node = i.links[0].from_node
if node.target_object is not None:
@ -486,7 +556,11 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
if not inputs:
self._make_attrib_socket(attrib, empty)
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:
self._make_attrib_socket(attrib, empty)
while len(unconnected) > 1:
@ -643,9 +717,13 @@ class PlasmaAttribDropDownListNode(PlasmaAttribNodeBase, bpy.types.Node):
def _list_items(self, context):
attrib = self.to_socket
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:
return []
value = EnumProperty(items=_list_items)
def draw_buttons(self, context, layout):
@ -665,8 +743,10 @@ class PlasmaAttribIntNode(PlasmaAttribNodeBase, bpy.types.Node):
def _get_int(self):
return round(self.value_float)
def _set_int(self, value):
self.value_float = float(value)
def _on_update_float(self, context):
self.inited = True
@ -710,35 +790,59 @@ class PlasmaAttribIntNode(PlasmaAttribNodeBase, bpy.types.Node):
return self.value_int
else:
return self.value_float
def _set_value(self, value):
self.value_float = value
value = property(_get_value, _set_value)
def _range_label(self, layout):
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):
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
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 True
class PlasmaAttribObjectNode(idprops.IDPropObjectMixin, PlasmaAttribNodeBase, bpy.types.Node):
class PlasmaAttribObjectNode(
idprops.IDPropObjectMixin, PlasmaAttribNodeBase, bpy.types.Node
):
bl_category = "PYTHON"
bl_idname = "PlasmaAttribObjectNode"
bl_label = "Object Attribute"
pl_attrib = ("ptAttribSceneobject", "ptAttribSceneobjectList", "ptAttribAnimation",
"ptAttribSwimCurrent", "ptAttribWaveSet", "ptAttribGrassShader")
target_object = PointerProperty(name="Object",
description="Object containing the required data",
type=bpy.types.Object)
pl_attrib = (
"ptAttribSceneobject",
"ptAttribSceneobjectList",
"ptAttribAnimation",
"ptAttribSwimCurrent",
"ptAttribWaveSet",
"ptAttribGrassShader",
)
target_object = PointerProperty(
name="Object",
description="Object containing the required data",
type=bpy.types.Object,
)
def init(self, context):
super().init(context)
@ -765,8 +869,12 @@ class PlasmaAttribObjectNode(idprops.IDPropObjectMixin, PlasmaAttribNodeBase, bp
return ref_so_key
elif attrib == "ptAttribAnimation":
anim = bo.plasma_modifiers.animation
agmod = exporter.mgr.find_create_key(plAGModifier, so=ref_so, name=anim.key_name)
agmaster = exporter.mgr.find_create_key(plAGMasterModifier, so=ref_so, name=anim.key_name)
agmod = exporter.mgr.find_create_key(
plAGModifier, so=ref_so, name=anim.key_name
)
agmaster = exporter.mgr.find_create_key(
plAGMasterModifier, so=ref_so, name=anim.key_name
)
return agmaster
elif attrib == "ptAttribSwimCurrent":
swimregion = bo.plasma_modifiers.swimregion
@ -774,17 +882,22 @@ class PlasmaAttribObjectNode(idprops.IDPropObjectMixin, PlasmaAttribNodeBase, bp
elif attrib == "ptAttribWaveSet":
waveset = bo.plasma_modifiers.water_basic
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)
elif attrib == "ptAttribGrassShader":
grass_shader = bo.plasma_modifiers.grass_shader
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:
return None
return [exporter.mgr.find_create_key(plGrassShaderMod, so=ref_so, name=i.name)
for i in exporter.mesh.material.get_materials(bo)]
return [
exporter.mgr.find_create_key(plGrassShaderMod, so=ref_so, name=i.name)
for i in exporter.mesh.material.get_materials(bo)
]
@classmethod
def _idprop_mapping(cls):
@ -810,21 +923,31 @@ class PlasmaAttribStringNode(PlasmaAttribNodeBase, bpy.types.Node):
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_idname = "PlasmaAttribTextureNode"
bl_label = "Texture Attribute"
bl_width_default = 175
pl_attrib = ("ptAttribMaterial", "ptAttribMaterialList",
"ptAttribDynamicMap", "ptAttribMaterialAnimation")
pl_attrib = (
"ptAttribMaterial",
"ptAttribMaterialList",
"ptAttribDynamicMap",
"ptAttribMaterialAnimation",
)
def _poll_material(self, value: bpy.types.Material) -> bool:
# 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
# certain materials.
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 True
@ -845,25 +968,37 @@ class PlasmaAttribTextureNode(idprops.IDPropMixin, PlasmaAttribNodeBase, bpy.typ
if self.material is not None:
return value.name in self.material.texture_slots
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):
if value in (slot.texture for slot in i.texture_slots if slot and slot.texture):
for i in (
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 False
else:
return True
target_object = PointerProperty(name="Object",
description="",
type=bpy.types.Object,
poll=idprops.poll_drawable_objects)
material = PointerProperty(name="Material",
description="Material the texture is attached to",
type=bpy.types.Material,
poll=_poll_material)
texture = PointerProperty(name="Texture",
description="Texture to expose to Python",
type=bpy.types.Texture,
poll=_poll_texture)
target_object = PointerProperty(
name="Object",
description="",
type=bpy.types.Object,
poll=idprops.poll_drawable_objects,
)
material = PointerProperty(
name="Material",
description="Material the texture is attached to",
type=bpy.types.Material,
poll=_poll_material,
)
texture = PointerProperty(
name="Texture",
description="Texture to expose to Python",
type=bpy.types.Texture,
poll=_poll_texture,
)
def init(self, context):
super().init(context)
@ -872,14 +1007,26 @@ class PlasmaAttribTextureNode(idprops.IDPropMixin, PlasmaAttribNodeBase, bpy.typ
def draw_buttons(self, context, layout):
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 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
if self.texture is not None:
if not frozenset(self.texture.users_material) & frozenset(iter_materials()):
layout.label("The selected texture is not on a material linked to the target object.", icon="ERROR")
if not frozenset(self.texture.users_material) & frozenset(
iter_materials()
):
layout.label(
"The selected texture is not on a material linked to the target object.",
icon="ERROR",
)
layout.alert = True
layout.prop(self, "target_object")
@ -888,31 +1035,51 @@ class PlasmaAttribTextureNode(idprops.IDPropMixin, PlasmaAttribNodeBase, bpy.typ
def get_key(self, exporter, so):
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
if attrib is None:
self.raise_error("must be connected to a Python File node!")
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)
if attrib == "ptAttribDynamicMap":
yield from filter(lambda x: x and isinstance(x.object, plDynamicTextMap),
(i.object.texture for i in layer_generator))
yield from filter(
lambda x: x and isinstance(x.object, plDynamicTextMap),
(i.object.texture for i in layer_generator),
)
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":
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":
# 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)
remainder = sum((1 for i in result))
if remainder > 1:
exporter.report.warn("'{}.{}': Expected a single layer, but mapped to {}. Make the settings more specific.",
self.id_data.name, self.path_from_id(), remainder + 1, indent=2)
exporter.report.warn(
"'{}.{}': 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:
yield result
else:
@ -920,16 +1087,19 @@ class PlasmaAttribTextureNode(idprops.IDPropMixin, PlasmaAttribNodeBase, bpy.typ
@classmethod
def _idprop_mapping(cls):
return {"material": "material_name",
"texture": "texture_name"}
return {"material": "material_name", "texture": "texture_name"}
def _idprop_sources(self):
return {"material_name": bpy.data.materials,
"texture_name": bpy.data.textures}
return {"material_name": bpy.data.materials, "texture_name": bpy.data.textures}
def _is_animated(self, material, texture):
return ((material.animation_data is not None and material.animation_data.action is not None)
or (texture.animation_data is not None and texture.animation_data.action is not None))
return (
material.animation_data is not None
and material.animation_data.action is not None
) or (
texture.animation_data is not None
and texture.animation_data.action is not None
)
def _is_dyntext(self, texture):
return texture.type == "IMAGE" and texture.image is None
@ -947,7 +1117,6 @@ _attrib_colors = {
"ptAttribResponder": (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),
PlasmaAttribIntNode.pl_attrib: (0.443, 0.439, 0.392, 1.0),
PlasmaAttribObjectNode.pl_attrib: (0.565, 0.267, 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_deprecated import PlasmaVersionedNode
class PlasmaResponderNode(PlasmaVersionedNode, bpy.types.Node):
bl_category = "LOGIC"
bl_idname = "PlasmaResponderNode"
@ -32,50 +33,70 @@ class PlasmaResponderNode(PlasmaVersionedNode, bpy.types.Node):
# These are the Python attributes we can fill in
pl_attrib = {"ptAttribResponder", "ptAttribResponderList", "ptAttribNamedResponder"}
detect_trigger = BoolProperty(name="Detect Trigger",
description="When notified, trigger the Responder",
default=True)
detect_untrigger = BoolProperty(name="Detect UnTrigger",
description="When notified, untrigger the Responder",
default=False)
no_ff_sounds = BoolProperty(name="Don't F-Fwd Sounds",
description="When fast-forwarding, play sound effects",
default=False)
default_state = IntProperty(name="Default State Index",
options=set())
input_sockets = OrderedDict([
("condition", {
"text": "Condition",
"type": "PlasmaConditionSocket",
"spawn_empty": True,
}),
])
output_sockets = OrderedDict([
("keyref", {
"text": "References",
"type": "PlasmaPythonReferenceNodeSocket",
"valid_link_nodes": {"PlasmaPythonFileNode"},
}),
("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,
}),
])
detect_trigger = BoolProperty(
name="Detect Trigger",
description="When notified, trigger the Responder",
default=True,
)
detect_untrigger = BoolProperty(
name="Detect UnTrigger",
description="When notified, untrigger the Responder",
default=False,
)
no_ff_sounds = BoolProperty(
name="Don't F-Fwd Sounds",
description="When fast-forwarding, play sound effects",
default=False,
)
default_state = IntProperty(name="Default State Index", options=set())
input_sockets = OrderedDict(
[
(
"condition",
{
"text": "Condition",
"type": "PlasmaConditionSocket",
"spawn_empty": True,
},
),
]
)
output_sockets = OrderedDict(
[
(
"keyref",
{
"text": "References",
"type": "PlasmaPythonReferenceNodeSocket",
"valid_link_nodes": {"PlasmaPythonFileNode"},
},
),
(
"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):
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.
if self.version == 1:
states = set()
def _link_states(state):
if state in states:
return
@ -162,6 +184,7 @@ class PlasmaResponderNode(PlasmaVersionedNode, bpy.types.Node):
goto = state.find_output("gotostate")
if goto is not None:
_link_states(goto)
for i in self.find_outputs("states"):
_link_states(i)
self.unlink_outputs("states", "socket deprecated (upgrade complete)")
@ -177,63 +200,98 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node):
resp_node = self.find_input("resp")
if resp_node is not None:
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:
return False
else:
return resp_node.default_state == state_idx
return False
def _set_default_state(self, value):
if value:
resp_node = self.find_input("resp")
if resp_node is not None:
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:
self._whine("unable to set default state on responder")
else:
resp_node.default_state = state_idx
default_state = BoolProperty(name="Default State",
description="This state is the responder's default",
get=_get_default_state,
set=_set_default_state,
options=set())
input_sockets = OrderedDict([
("condition", {
"text": "Triggers State",
"type": "PlasmaRespStateSocket",
"spawn_empty": True,
}),
("resp", {
"text": "Responder",
"type": "PlasmaRespStateRefSocket",
"valid_link_nodes": "PlasmaResponderNode",
"valid_link_sockets": "PlasmaRespStateRefSocket",
}),
])
output_sockets = OrderedDict([
# This socket has been deprecated.
("cmds", {
"text": "Commands",
"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",
}),
])
default_state = BoolProperty(
name="Default State",
description="This state is the responder's default",
get=_get_default_state,
set=_set_default_state,
options=set(),
)
input_sockets = OrderedDict(
[
(
"condition",
{
"text": "Triggers State",
"type": "PlasmaRespStateSocket",
"spawn_empty": True,
},
),
(
"resp",
{
"text": "Responder",
"type": "PlasmaRespStateRefSocket",
"valid_link_nodes": "PlasmaResponderNode",
"valid_link_sockets": "PlasmaRespStateRefSocket",
},
),
]
)
output_sockets = OrderedDict(
[
# This socket has been deprecated.
(
"cmds",
{
"text": "Commands",
"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):
layout.active = self.find_input("resp") is not None
@ -274,11 +332,17 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node):
return -1
def find_create_wait(self, exporter, so, node):
i, cmd = next(((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)
i, cmd = next(
((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:
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
def save(self, state):
@ -317,7 +381,9 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node):
pfmNotify.addEvent(proCallbackEventData())
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):
idx, command = commandMgr.add_command(msgNode, waitOn)
if msg.sender is None:
@ -342,7 +408,9 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node):
commandMgr.add_waitable_node(msgNode)
if msgNode.has_linked_callbacks:
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:
childWaitOn = waitOn
@ -352,11 +420,14 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node):
def _get_child_messages(self, node=None):
"""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:
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):
@ -369,7 +440,15 @@ class PlasmaRespStateRefSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
def draw_content(self, context, layout, node, text):
if isinstance(node, PlasmaResponderNode):
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:
layout.label(text)
else:

165
korman/nodes/node_softvolume.py

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

389
korman/operators/op_export.py

@ -27,6 +27,7 @@ from ..helpers import UiHelper
from .. import korlib, plasma_launcher
from ..properties.prop_world import PlasmaAge
class ExportOperator:
def _get_default_path(self, context):
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...
# If you want a volatile property, register it directly on this operator!
_properties = {
"verbose": (BoolProperty, {"name": "Display Verbose Log",
"description": "Shows the verbose export log in the console",
"default": False}),
"show_console": (BoolProperty, {"name": "Display Log Console",
"description": "Forces the Blender System Console open during the export",
"default": True}),
"texcache_path": (StringProperty, {"name": "Texture Cache Path",
"description": "Texture Cache Filepath"}),
"texcache_method": (EnumProperty, {"name": "Texture Cache",
"description": "Texture Cache Settings",
"items": [("skip", "Don't Use Texture Cache", "The texture cache is neither used nor updated."),
("use", "Use Texture Cache", "Use (and update, if needed) cached textures."),
("rebuild", "Rebuild Texture Cache", "Rebuilds the texture cache from scratch.")],
"default": "use"}),
"lighting_method": (EnumProperty, {"name": "Static Lighting",
"description": "Static Lighting Settings",
"items": [("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"}}),
"verbose": (
BoolProperty,
{
"name": "Display Verbose Log",
"description": "Shows the verbose export log in the console",
"default": False,
},
),
"show_console": (
BoolProperty,
{
"name": "Display Log Console",
"description": "Forces the Blender System Console open during the export",
"default": True,
},
),
"texcache_path": (
StringProperty,
{"name": "Texture Cache Path", "description": "Texture Cache Filepath"},
),
"texcache_method": (
EnumProperty,
{
"name": "Texture Cache",
"description": "Texture Cache Settings",
"items": [
(
"skip",
"Don't Use Texture Cache",
"The texture cache is neither used nor updated.",
),
(
"use",
"Use Texture Cache",
"Use (and update, if needed) cached textures.",
),
(
"rebuild",
"Rebuild Texture Cache",
"Rebuilds the texture cache from scratch.",
),
],
"default": "use",
},
),
"lighting_method": (
EnumProperty,
{
"name": "Static Lighting",
"description": "Static Lighting Settings",
"items": [
(
"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...
filepath = StringProperty(subtype="FILE_PATH")
filter_glob = StringProperty(default="*.age;*.zip", options={'HIDDEN'})
version = EnumProperty(name="Version",
description="Plasma version to export this age for",
items=game_versions,
default="pvPots",
options=set())
dat_only = BoolProperty(name="Export Only PRPs",
description="Only the Age PRPs should be exported",
default=True,
options={"HIDDEN"})
actions = EnumProperty(name="Actions",
description="Actions for the exporter to perform",
default={"EXPORT"},
items=[("EXPORT", "Export", "Export the age data"),
("PROFILE", "Profile", "Profile the exporter"),
("LAUNCH", "Launch Age", "Launch the age in Plasma")],
options={"ENUM_FLAG"})
ki = IntProperty(name="KI",
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())
filter_glob = StringProperty(default="*.age;*.zip", options={"HIDDEN"})
version = EnumProperty(
name="Version",
description="Plasma version to export this age for",
items=game_versions,
default="pvPots",
options=set(),
)
dat_only = BoolProperty(
name="Export Only PRPs",
description="Only the Age PRPs should be exported",
default=True,
options={"HIDDEN"},
)
actions = EnumProperty(
name="Actions",
description="Actions for the exporter to perform",
default={"EXPORT"},
items=[
("EXPORT", "Export", "Export the age data"),
("PROFILE", "Profile", "Profile the exporter"),
("LAUNCH", "Launch Age", "Launch the age in Plasma"),
],
options={"ENUM_FLAG"},
)
ki = IntProperty(
name="KI",
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):
layout = self.layout
@ -204,7 +323,10 @@ class PlasmaAgeExportOperator(ExportOperator, bpy.types.Operator):
ageName = path.stem
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"}
# 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:
self.export_active = True
if "PROFILE" in self.actions:
profile_path = str(path.with_name("{}_cProfile".format(ageName)))
profile = cProfile.runctx("e.run()", globals(), locals(), profile_path)
profile_path = str(
path.with_name("{}_cProfile".format(ageName))
)
profile = cProfile.runctx(
"e.run()", globals(), locals(), profile_path
)
else:
e.run()
except exporter.ExportError as error:
@ -274,13 +400,19 @@ class PlasmaAgeExportOperator(ExportOperator, bpy.types.Operator):
def _sanity_check_run_plasma(self):
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 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:
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):
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
# 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.
args = [bpy.app.binary_path_python, plasma_launcher.__file__,
str(client_dir), path.stem, self.version]
args = [
bpy.app.binary_path_python,
plasma_launcher.__file__,
str(client_dir),
path.stem,
self.version,
]
if self.version == "pvMoul":
if self.serverini:
args.append("--serverini")
@ -300,8 +437,13 @@ class PlasmaAgeExportOperator(ExportOperator, bpy.types.Operator):
args.append(self.player)
with exporter.ExportVerboseLogger() as log:
proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
cwd=str(client_dir), universal_newlines=True)
proc = subprocess.Popen(
args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=str(client_dir),
universal_newlines=True,
)
while True:
line = proc.stdout.readline().strip()
if line == "DIE":
@ -320,13 +462,15 @@ class PlasmaLocalizationExportOperator(ExportOperator, bpy.types.Operator):
bl_description = "Export Age localization data"
filepath = StringProperty(subtype="DIR_PATH")
filter_glob = StringProperty(default="*.pak", options={'HIDDEN'})
filter_glob = StringProperty(default="*.pak", options={"HIDDEN"})
version = EnumProperty(name="Version",
description="Plasma version to export this age for",
items=game_versions,
default="pvPots",
options=set())
version = EnumProperty(
name="Version",
description="Plasma version to export this age for",
items=game_versions,
default="pvPots",
options=set(),
)
def execute(self, context):
path = Path(self.filepath)
@ -344,12 +488,16 @@ class PlasmaLocalizationExportOperator(ExportOperator, bpy.types.Operator):
# Age names cannot be python keywords
age_name = context.scene.world.plasma_age.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"}
# Bonus Fun: Implement Profile-mode here (later...)
e = exporter.LocalizationConverter(age_name=age_name, path=self.filepath,
version=globals()[self.version])
e = exporter.LocalizationConverter(
age_name=age_name, path=self.filepath, version=globals()[self.version]
)
try:
e.run()
except exporter.ExportError as error:
@ -368,13 +516,15 @@ class PlasmaPythonExportOperator(ExportOperator, bpy.types.Operator):
bl_description = "Export Age python script package"
filepath = StringProperty(subtype="FILE_PATH")
filter_glob = StringProperty(default="*.pak", options={'HIDDEN'})
filter_glob = StringProperty(default="*.pak", options={"HIDDEN"})
version = EnumProperty(name="Version",
description="Plasma version to export this age for",
items=game_versions,
default="pvPots",
options=set())
version = EnumProperty(
name="Version",
description="Plasma version to export this age for",
items=game_versions,
default="pvPots",
options=set(),
)
def draw(self, context):
layout = self.layout
@ -388,7 +538,7 @@ class PlasmaPythonExportOperator(ExportOperator, bpy.types.Operator):
row = layout.row()
row.enabled = korlib.ConsoleToggler.is_platform_supported()
row.prop(age, "show_console")
layout.prop(age, "verbose")
layout.prop(age, "verbose")
def execute(self, context):
path = Path(self.filepath)
@ -407,12 +557,16 @@ class PlasmaPythonExportOperator(ExportOperator, bpy.types.Operator):
# Age names cannot be python keywords
age_name = context.scene.world.plasma_age.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"}
# Bonus Fun: Implement Profile-mode here (later...)
e = exporter.PythonPackageExporter(filepath=self.filepath,
version=globals()[self.version])
e = exporter.PythonPackageExporter(
filepath=self.filepath, version=globals()[self.version]
)
try:
e.run()
except exporter.ExportError as error:
@ -439,12 +593,17 @@ class PlasmaPythonExportOperator(ExportOperator, bpy.types.Operator):
def menu_cb(self, context):
if context.scene.render.engine == "PLASMA_GAME":
self.layout.operator_context = "INVOKE_DEFAULT"
self.layout.operator(PlasmaAgeExportOperator.bl_idname, text="Plasma Age (.age)")
self.layout.operator(PlasmaPythonExportOperator.bl_idname, text="Plasma Scripts (.pak)")
self.layout.operator(
PlasmaAgeExportOperator.bl_idname, text="Plasma Age (.age)"
)
self.layout.operator(
PlasmaPythonExportOperator.bl_idname, text="Plasma Scripts (.pak)"
)
def register():
bpy.types.INFO_MT_file_export.append(menu_cb)
def unregister():
bpy.types.INFO_MT_file_export.remove(menu_cb)

83
korman/operators/op_image.py

@ -33,6 +33,7 @@ _CUBE_FACES = {
"frontFace": "FR",
}
class ImageOperator:
@classmethod
def poll(cls, context):
@ -44,19 +45,25 @@ class PlasmaBuildCubeMapOperator(ImageOperator, bpy.types.Operator):
bl_label = "Build Cubemap"
bl_description = "Builds a Blender cubemap from six images"
overwrite_existing = BoolProperty(name="Check Existing",
description="Checks for an existing image and overwrites it",
default=True,
options=set())
overwrite_existing = BoolProperty(
name="Check Existing",
description="Checks for an existing image and overwrites it",
default=True,
options=set(),
)
filepath = StringProperty(subtype="FILE_PATH")
require_cube = BoolProperty(name="Require Square Faces",
description="Resize cubemap faces to be square if they are not",
default=True,
options=set())
texture_name = StringProperty(name="Texture",
description="Environment Map Texture to stuff this into",
default="",
options={"HIDDEN"})
require_cube = BoolProperty(
name="Require Square Faces",
description="Resize cubemap faces to be square if they are not",
default=True,
options=set(),
)
texture_name = StringProperty(
name="Texture",
description="Environment Map Texture to stuff this into",
default="",
options={"HIDDEN"},
)
def __init__(self):
self._report = ExportProgressLogger()
@ -91,15 +98,17 @@ class PlasmaBuildCubeMapOperator(ImageOperator, bpy.types.Operator):
face_widths, face_heights, face_data = zip(*face_data)
# 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
image_name = Path(self.filepath).name
idx = image_name.rfind('_')
idx = image_name.rfind("_")
if idx != -1:
suffix = image_name[idx+1:idx+3]
suffix = image_name[idx + 1 : idx + 3]
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)
# 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.msg("Searching for cubemap faces...")
idx = filepath.rfind('_')
idx = filepath.rfind("_")
if idx != -1:
files = []
for key in BLENDER_CUBE_MAP:
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()
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)
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)
self._report.progress_increment()
return tuple(files)
@ -138,7 +151,9 @@ class PlasmaBuildCubeMapOperator(ImageOperator, bpy.types.Operator):
self._report.msg("Generating cubemap image...")
# 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
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):
src_start_idx = (row_current - row_start) * 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)
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
pixels = [None] * image_datasz
@ -183,7 +202,6 @@ class PlasmaBuildCubeMapOperator(ImageOperator, bpy.types.Operator):
image.plasma_image.texcache_method = "rebuild"
return image
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
@ -230,10 +248,17 @@ class PlasmaBuildCubeMapOperator(ImageOperator, bpy.types.Operator):
face_width, face_height = face_widths[i], face_heights[i]
if face_width != min_width or face_height != min_height:
face_name = BLENDER_CUBE_MAP[i][:-4].upper()
self._report.msg("Resizing face '{}' from {}x{} to {}x{}", face_name,
face_width, 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.msg(
"Resizing face '{}' from {}x{} to {}x{}",
face_name,
face_width,
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()
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 ..korlib import ConsoleToggler
class _LightingOperator:
@contextmanager
def _oven(self, context):
@ -34,7 +35,9 @@ class _LightingOperator:
else:
verbose = False
console = True
with UiHelper(context), ConsoleToggler(console), LightBaker(verbose=verbose) as oven:
with UiHelper(context), ConsoleToggler(console), LightBaker(
verbose=verbose
) as oven:
yield oven
@classmethod
@ -63,7 +66,11 @@ class LightmapAutobakePreviewOperator(_LightingOperator, bpy.types.Operator):
bake.lightmap_uvtex_name = "LIGHTMAPGEN_PREVIEW"
bake.force = True
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.")
return {"FINISHED"}
@ -89,9 +96,11 @@ class LightmapBakeMultiOperator(_LightingOperator, bpy.types.Operator):
bl_label = "Bake Lighting"
bl_description = "Bake scene lighting to object(s)"
bake_selection = BoolProperty(name="Bake Selection",
description="Bake only the selected objects (else all objects)",
options=set())
bake_selection = BoolProperty(
name="Bake Selection",
description="Bake only the selected objects (else all objects)",
options=set(),
)
def __init__(self):
super().__init__()
@ -101,7 +110,9 @@ class LightmapBakeMultiOperator(_LightingOperator, bpy.types.Operator):
try:
if profile_me:
cProfile.runctx("self._run(context)", globals(), locals(), "bake_cProfile")
cProfile.runctx(
"self._run(context)", globals(), locals(), "bake_cProfile"
)
else:
self._run(context)
except ExportError as error:
@ -116,8 +127,12 @@ class LightmapBakeMultiOperator(_LightingOperator, bpy.types.Operator):
return {"FINISHED"}
def _run(self, context):
all_objects = 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]
all_objects = (
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:
bake.force = True
@ -136,21 +151,32 @@ class LightmapClearMultiOperator(_LightingOperator, bpy.types.Operator):
bl_label = "Clear Lighting"
bl_description = "Clear baked lighting"
clear_selection = BoolProperty(name="Clear Selection",
description="Clear only the selected objects (else all objects)",
options=set())
clear_selection = BoolProperty(
name="Clear Selection",
description="Clear only the selected objects (else all objects)",
options=set(),
)
def __init__(self):
super().__init__()
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):
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):
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):
i.plasma_modifiers.lightmap.image = None
@ -179,5 +205,6 @@ def _toss_garbage(scene):
if uvtex is not None:
i.uv_textures.remove(uvtex)
# collects light baking 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
class PlasmaMeshOperator:
@classmethod
def poll(cls, context):
@ -37,18 +38,25 @@ class PlasmaAddFlareOperator(PlasmaMeshOperator, bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
# Allows user to specify their own name stem
flare_name = bpy.props.StringProperty(name="Name",
description="Flare name stem",
default="Flare",
options=set())
flare_distance = bpy.props.FloatProperty(name="Distance",
description="Flare's distance from the illuminating object",
min=0.1, max=2.0, step=10, precision=1, default=1.0,
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())
flare_name = bpy.props.StringProperty(
name="Name", description="Flare name stem", default="Flare", options=set()
)
flare_distance = bpy.props.FloatProperty(
name="Distance",
description="Flare's distance from the illuminating object",
min=0.1,
max=2.0,
step=10,
precision=1,
default=1.0,
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
def poll(cls, context):
@ -100,14 +108,26 @@ class PlasmaAddFlareOperator(PlasmaMeshOperator, bpy.types.Operator):
flare_root.plasma_modifiers.viewfacemod.preset_options = "Sprite"
# 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.plasma_object.enabled = True
bpyscene.objects.active = flare_plane
# 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.transform(bm, matrix=mathutils.Matrix.Translation((0.0, 0.0, -self.flare_distance)), space=flare_plane.matrix_world, verts=bm.verts)
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.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")
# 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
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.plasma_layer.skip_depth_write = True
auto_tex.plasma_layer.skip_depth_test = True
@ -166,60 +188,104 @@ class PlasmaAddLadderMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
# Allows user to specify their own name stem
ladder_name = bpy.props.StringProperty(name="Name",
description="Ladder name stem",
default="Ladder",
options=set())
ladder_name = bpy.props.StringProperty(
name="Name", description="Ladder name stem", default="Ladder", options=set()
)
# Basic stats
ladder_height = bpy.props.FloatProperty(name="Height",
description="Height of ladder in feet",
min=6, max=1000, step=200, precision=0, default=6,
unit="LENGTH", subtype="DISTANCE",
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())
ladder_height = bpy.props.FloatProperty(
name="Height",
description="Height of ladder in feet",
min=6,
max=1000,
step=200,
precision=0,
default=6,
unit="LENGTH",
subtype="DISTANCE",
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
gen_back_guide = bpy.props.BoolProperty(name="Ladder",
description="Generates helper object where ladder back should be placed",
default=True,
options=set())
gen_ground_guides = bpy.props.BoolProperty(name="Ground",
description="Generates helper objects where ground should be placed",
default=True,
options=set())
gen_rung_guides = bpy.props.BoolProperty(name="Rungs",
description="Generates helper objects where rungs should be placed",
default=True,
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())
gen_back_guide = bpy.props.BoolProperty(
name="Ladder",
description="Generates helper object where ladder back should be placed",
default=True,
options=set(),
)
gen_ground_guides = bpy.props.BoolProperty(
name="Ground",
description="Generates helper objects where ground should be placed",
default=True,
options=set(),
)
gen_rung_guides = bpy.props.BoolProperty(
name="Rungs",
description="Generates helper objects where rungs should be placed",
default=True,
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
has_upper_entry = bpy.props.BoolProperty(name="Has Upper Entry Point",
description="Specifies whether the ladder has an upper entry",
default=True,
options=set())
upper_entry_enabled = bpy.props.BoolProperty(name="Upper Entry Enabled",
description="Specifies whether the ladder's upper entry is enabled by default at Age start",
default=True,
options=set())
has_lower_entry = bpy.props.BoolProperty(name="Has Lower Entry Point",
description="Specifies whether the ladder has a lower entry",
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())
has_upper_entry = bpy.props.BoolProperty(
name="Has Upper Entry Point",
description="Specifies whether the ladder has an upper entry",
default=True,
options=set(),
)
upper_entry_enabled = bpy.props.BoolProperty(
name="Upper Entry Enabled",
description="Specifies whether the ladder's upper entry is enabled by default at Age start",
default=True,
options=set(),
)
has_lower_entry = bpy.props.BoolProperty(
name="Has Lower Entry Point",
description="Specifies whether the ladder has a lower entry",
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):
layout = self.layout
@ -269,7 +335,9 @@ class PlasmaAddLadderMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
else:
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):
if context.space_data.local_view:
@ -292,15 +360,18 @@ class PlasmaAddLadderMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
rung_yoffset = rung_width_ft / 4
rungs_scale = mathutils.Matrix(
((0.5, 0.0, 0.0),
(0.0, rung_width, 0.0),
(0.0, 0.0, rung_height_ft)))
((0.5, 0.0, 0.0), (0.0, rung_width, 0.0), (0.0, 0.0, rung_height_ft))
)
for rung_num in range(0, int(self.ladder_height)):
side = "L" if (rung_num % 2) == 0 else "R"
mesh = bpy.data.meshes.new("{}_Rung_{}_{}".format(self.name_stem, side, rung_num))
rungs = bpy.data.objects.new("{}_Rung_{}_{}".format(self.name_stem, side, rung_num), mesh)
mesh = bpy.data.meshes.new(
"{}_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.draw_type = "BOUNDS"
@ -314,11 +385,27 @@ class PlasmaAddLadderMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
# 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
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:
rung_pos = mathutils.Matrix.Translation((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)
rung_pos = mathutils.Matrix.Translation(
(
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.free()
@ -341,15 +428,22 @@ class PlasmaAddLadderMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
# Construct the bmesh and assign it to the blender mesh.
bm = bmesh.new()
ladder_scale = mathutils.Matrix(
((0.5, 0.0, 0.0),
(0.0, self.ladder_width / 12, 0.0),
(0.0, 0.0, self.ladder_height)))
(
(0.5, 0.0, 0.0),
(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)
# 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))
bmesh.ops.transform(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)
bmesh.ops.transform(
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.free()
@ -374,17 +468,28 @@ class PlasmaAddLadderMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
bm = bmesh.new()
ground_depth = 3.0
ground_scale = mathutils.Matrix(
((ground_depth, 0.0, 0.0),
(0.0, self.ladder_width / 12, 0.0),
(0.0, 0.0, 0.5)))
(
(ground_depth, 0.0, 0.0),
(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)
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:
ground_pos = mathutils.Matrix.Translation(((ground_depth / 2) + 0.25, 0.0, 0.25))
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)
ground_pos = mathutils.Matrix.Translation(
((ground_depth / 2) + 0.25, 0.0, 0.25)
)
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.free()
@ -408,14 +513,17 @@ class PlasmaAddLadderMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
# Construct the bmesh and assign it to the blender mesh.
bm = bmesh.new()
rgn_scale = mathutils.Matrix(
((self.ladder_width / 12, 0.0, 0.0),
(0.0, 2.5, 0.0),
(0.0, 0.0, 2.0)))
((self.ladder_width / 12, 0.0, 0.0), (0.0, 2.5, 0.0), (0.0, 0.0, 2.0))
)
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))
bmesh.ops.transform(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)
bmesh.ops.transform(
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.free()
@ -449,14 +557,17 @@ class PlasmaAddLadderMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
# Construct the bmesh and assign it to the blender mesh.
bm = bmesh.new()
rgn_scale = mathutils.Matrix(
((self.ladder_width / 12, 0.0, 0.0),
(0.0, 2.5, 0.0),
(0.0, 0.0, 2.0)))
((self.ladder_width / 12, 0.0, 0.0), (0.0, 2.5, 0.0), (0.0, 0.0, 2.0))
)
bmesh.ops.create_cube(bm, size=(1.0), matrix=rgn_scale)
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(bm, matrix=rgn_pos, space=lower_rgn.matrix_world, verts=bm.verts)
bmesh.ops.transform(
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.free()
@ -495,6 +606,7 @@ class PlasmaAddLadderMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
def name_stem(self):
return self.ladder_name if self.ladder_name else "Ladder"
def origin_to_bottom(obj):
# Modified from https://blender.stackexchange.com/a/42110/3055
mw = obj.matrix_world
@ -532,16 +644,30 @@ class PlasmaAddLinkingBookMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
}
# Allows user to specify their own name stem
panel_name = bpy.props.StringProperty(name="Name",
description="Linking Book name stem",
default="LinkingBook",
options=set())
link_anim_type = bpy.props.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())
panel_name = bpy.props.StringProperty(
name="Name",
description="Linking Book name stem",
default="LinkingBook",
options=set(),
)
link_anim_type = bpy.props.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(),
)
def draw(self, context):
layout = self.layout
@ -558,7 +684,9 @@ class PlasmaAddLinkingBookMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
row.prop(self, "link_anim_type", text="Type")
else:
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):
if context.space_data.local_view:
@ -587,7 +715,9 @@ class PlasmaAddLinkingBookMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
bpy.context.scene.objects.link(seek_point)
seek_point.show_name = True
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.plasma_object.enabled = True
@ -595,7 +725,9 @@ class PlasmaAddLinkingBookMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
clk_rgn_name = "{}_ClkRegion".format(self.name_stem)
clk_rgn_size = 6.0
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.plasma_object.enabled = True
@ -625,5 +757,6 @@ class PlasmaAddLinkingBookMeshOperator(PlasmaMeshOperator, bpy.types.Operator):
def register():
bpy.utils.register_module(__name__)
def unregister():
bpy.utils.unregister_module(__name__)

74
korman/operators/op_modifier.py

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

134
korman/operators/op_nodes.py

@ -18,6 +18,7 @@ from bpy.props import *
import itertools
import pickle
class NodeOperator:
@classmethod
def poll(cls, context):
@ -40,7 +41,9 @@ class CreateLinkNodeOperator(NodeOperator, bpy.types.Operator):
_hack = []
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
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_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
for i, link in enumerate(links):
# Pickle protocol 0 uses only ASCII bytes, so we can pretend it's a string easily...
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)),
sock=link["socket_text"])
sock=link["socket_text"],
)
yield (id_string, desc_string, "", i)
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_socket = self._find_source_socket(src_node)
# 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)
if self.is_output:
@ -113,7 +124,9 @@ class CreateLinkNodeOperator(NodeOperator, bpy.types.Operator):
def modal(self, context, event):
# 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:
self._create_link_node(context, self._hack[0][0])
self._hack.clear()
@ -132,9 +145,12 @@ class CreateLinkNodeOperator(NodeOperator, bpy.types.Operator):
def poll(cls, context):
space = context.space_data
# needs active node editor and a tree to add nodes to
return (space.type == 'NODE_EDITOR' and
space.edit_tree and not space.edit_tree.library and
context.scene.render.engine == "PLASMA_GAME")
return (
space.type == "NODE_EDITOR"
and space.edit_tree
and not space.edit_tree.library
and context.scene.render.engine == "PLASMA_GAME"
)
class SelectFileOperator(NodeOperator, bpy.types.Operator):
@ -147,14 +163,23 @@ class SelectFileOperator(NodeOperator, bpy.types.Operator):
filename = StringProperty(options={"HIDDEN"})
data_path = StringProperty(options={"HIDDEN"})
filepath_property = StringProperty(description="Name of property to store filepath in", options={"HIDDEN"})
filename_property = StringProperty(description="Name of property to store filename in", options={"HIDDEN"})
filepath_property = StringProperty(
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):
if bpy.data.texts.get(self.filename, None) is None:
bpy.data.texts.load(self.filepath)
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)
if self.filepath_property:
@ -167,46 +192,28 @@ class SelectFileOperator(NodeOperator, bpy.types.Operator):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
pyAttribArgMap= {
"ptAttribute":
["vislistid", "visliststates"],
"ptAttribBoolean":
["default"],
"ptAttribInt":
["default", "rang"],
"ptAttribFloat":
["default", "rang"],
"ptAttribString":
["default"],
"ptAttribDropDownList":
["options"],
"ptAttribSceneobject":
["netForce"],
"ptAttribSceneobjectList":
["byObject", "netForce"],
"ptAttributeKeyList":
["byObject", "netForce"],
"ptAttribActivator":
["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"],
}
pyAttribArgMap = {
"ptAttribute": ["vislistid", "visliststates"],
"ptAttribBoolean": ["default"],
"ptAttribInt": ["default", "rang"],
"ptAttribFloat": ["default", "rang"],
"ptAttribString": ["default"],
"ptAttribDropDownList": ["options"],
"ptAttribSceneobject": ["netForce"],
"ptAttribSceneobjectList": ["byObject", "netForce"],
"ptAttributeKeyList": ["byObject", "netForce"],
"ptAttribActivator": ["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):
@ -220,6 +227,7 @@ class PlPyAttributeNodeOperator(NodeOperator, bpy.types.Operator):
def execute(self, context):
from ..plasma_attributes import get_attributes_from_str
text_id = bpy.data.texts[self.text_path]
attribs = get_attributes_from_str(text_id.as_string())
@ -250,12 +258,26 @@ class PlPyAttributeNodeOperator(NodeOperator, bpy.types.Operator):
# Load our default argument mapping
if args is not None:
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:
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
if cached.attribute_type in pyAttribArgMap.keys() and not set(pyAttribArgMap[cached.attribute_type]).isdisjoint(attrib.keys()):
argmap.update({key: attrib[key] for key in attrib if key in pyAttribArgMap[cached.attribute_type]})
if cached.attribute_type in pyAttribArgMap.keys() and not set(
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
if argmap:
cached.attribute_arguments.set_arguments(argmap)

20
korman/operators/op_sound.py

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

63
korman/operators/op_toolbox.py

@ -18,6 +18,7 @@ from bpy.props import *
import pickle
import itertools
class ToolboxOperator:
@classmethod
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]
# 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:
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
# a page property. Unfortunately, unless we start bundling some YAML interpreter, we cannot
# 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:
pageid = i.game.properties.get("page_num", 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,
# so don't warn about that.
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:
i.plasma_object.enabled = True
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_property = "page"
page = EnumProperty(name="Page",
description= "Page whose objects should be selected",
items=PageSearchOperator._get_pages,
options=set())
page = EnumProperty(
name="Page",
description="Page whose objects should be selected",
items=PageSearchOperator._get_pages,
options=set(),
)
def execute(self, context):
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_property = "page"
page = EnumProperty(name="Page",
description= "Page whose objects should be selected",
items=PageSearchOperator._get_pages,
options=set())
page = EnumProperty(
name="Page",
description="Page whose objects should be selected",
items=PageSearchOperator._get_pages,
options=set(),
)
def execute(self, context):
desired_page = self.desired_page
@ -172,14 +183,14 @@ class PlasmaToggleAllPlasmaObjectsOperator(ToolboxOperator, bpy.types.Operator):
i.plasma_object.enabled = self.enable
return {"FINISHED"}
class PlasmaToggleDoubleSidedOperator(ToolboxOperator, bpy.types.Operator):
bl_idname = "mesh.plasma_toggle_double_sided"
bl_label = "Toggle All Double Sided"
bl_description = "Toggles all meshes to be double sided"
enable = BoolProperty(name="Enable", description="Enable Double Sided")
def execute(self, context):
enable = self.enable
for mesh in bpy.data.meshes:
@ -191,7 +202,7 @@ class PlasmaToggleDoubleSidedSelectOperator(ToolboxOperator, bpy.types.Operator)
bl_idname = "mesh.plasma_toggle_double_sided_selected"
bl_label = "Toggle Selected Double Sided"
bl_description = "Toggles selected meshes double sided value"
@classmethod
def poll(cls, context):
return super().poll(context) and hasattr(bpy.context, "selected_objects")
@ -232,7 +243,9 @@ class PlasmaTogglePlasmaObjectsOperator(ToolboxOperator, bpy.types.Operator):
return super().poll(context) and hasattr(bpy.context, "selected_objects")
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:
i.plasma_object.enabled = enable
return {"FINISHED"}
@ -242,9 +255,9 @@ class PlasmaToggleSoundExportOperator(ToolboxOperator, bpy.types.Operator):
bl_idname = "object.plasma_toggle_sound_export"
bl_label = "Toggle Sound Export"
bl_description = "Toggles the Export function of all sound emitters' files"
enable = BoolProperty(name="Enable", description="Sound Export Enable")
def execute(self, context):
enable = self.enable
for i in bpy.data.objects:
@ -259,13 +272,21 @@ class PlasmaToggleSoundExportSelectedOperator(ToolboxOperator, bpy.types.Operato
bl_idname = "object.plasma_toggle_sound_export_selected"
bl_label = "Toggle Selected Sound Export"
bl_description = "Toggles the Export function of selected sound emitters' files."
@classmethod
def poll(cls, context):
return super().poll(context) and hasattr(bpy.context, "selected_objects")
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:
if i.plasma_modifiers.soundemit is None:
continue

89
korman/operators/op_ui.py

@ -17,6 +17,7 @@ import addon_utils
import bpy
from bpy.props import *
class UIOperator:
@classmethod
def poll(cls, context):
@ -28,25 +29,37 @@ class CollectionAddOperator(UIOperator, bpy.types.Operator):
bl_label = "Add Item"
bl_description = "Adds an item to the collection"
context = StringProperty(name="ID Path",
description="Path to the relevant datablock from the current context",
options=set())
group_path = StringProperty(name="Property Group Path",
description="Path to the property group from the ID",
options=set())
collection_prop = StringProperty(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())
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())
context = StringProperty(
name="ID Path",
description="Path to the relevant datablock from the current context",
options=set(),
)
group_path = StringProperty(
name="Property Group Path",
description="Path to the property group from the ID",
options=set(),
)
collection_prop = StringProperty(
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(),
)
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):
props = getattr(context, self.context).path_resolve(self.group_path)
@ -54,7 +67,11 @@ class CollectionAddOperator(UIOperator, bpy.types.Operator):
idx = len(collection)
collection.add()
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:
setattr(props, self.index_prop, idx)
return {"FINISHED"}
@ -65,18 +82,26 @@ class CollectionRemoveOperator(UIOperator, bpy.types.Operator):
bl_label = "Remove Item"
bl_description = "Removes an item from the collection"
context = StringProperty(name="ID Path",
description="Path to the relevant datablock from the current context",
options=set())
group_path = StringProperty(name="Property Group Path",
description="Path to the property group from the ID",
options=set())
collection_prop = StringProperty(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())
context = StringProperty(
name="ID Path",
description="Path to the relevant datablock from the current context",
options=set(),
)
group_path = StringProperty(
name="Property Group Path",
description="Path to the property group from the ID",
options=set(),
)
collection_prop = StringProperty(
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):
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 pathlib import Path
class AgeOperator:
@classmethod
def poll(cls, context):
@ -40,7 +41,9 @@ class GameAddOperator(AgeOperator, bpy.types.Operator):
# Blendsucks likes to tack filenames onto our doggone directories...
if not path.is_dir():
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.")
return {"CANCELLED"}
@ -62,7 +65,6 @@ class GameAddOperator(AgeOperator, bpy.types.Operator):
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}

26
korman/ordered_set.py

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

22
korman/plasma_attributes.py

@ -37,10 +37,12 @@ class PlasmaAttributeVisitor(ast.NodeVisitor):
# - assignments with targets
# - that are taking a function call (the ptAttrib Constructor)
# - whose name starts with ptAttrib
if (len(assign.targets) == 1
if (
len(assign.targets) == 1
and isinstance(assign.value, ast.Call)
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
ptVar = assign.targets[0].id
ptType = assign.value.func.id
@ -53,7 +55,11 @@ class PlasmaAttributeVisitor(ast.NodeVisitor):
# which only have an index. We don't want those.
if len(ptArgs) > 1:
# 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.
if ptArgs[2:]:
self._attributes[ptArgs[0]]["args"] = ptArgs[2:]
@ -61,13 +67,15 @@ class PlasmaAttributeVisitor(ast.NodeVisitor):
# Add the keyword arguments, if any.
if 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)
def visit_Name(self, node):
# Workaround for old Cyan scripts: replace variables named "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 node.id
@ -104,10 +112,11 @@ class PlasmaAttributeVisitor(ast.NodeVisitor):
def get_attributes_from_file(filepath):
"""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:
return get_attributes_from_str(script.read())
def get_attributes_from_str(code):
results = funcregex.findall(code)
if results:
@ -119,6 +128,7 @@ def get_attributes_from_str(code):
return v._attributes
return {}
if __name__ == "__main__":
import json
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("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.add_argument("ki", type=int, help="KI Number of the desired player")
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"
if sys.platform == "win32":
client_executables = {
"pvMoul": "plClient.exe",
"pvPots": "UruExplorer.exe"
}
client_executables = {"pvMoul": "plClient.exe", "pvPots": "UruExplorer.exe"}
else:
client_executables = {
"pvMoul": "plClient",
"pvPots": "UruExplorer"
}
client_executables = {"pvMoul": "plClient", "pvPots": "UruExplorer"}
def die(*args, **kwargs):
assert args
@ -55,6 +53,7 @@ def die(*args, **kwargs):
sys.stdout.write("DIE\n")
sys.exit(1)
def write(*args, **kwargs):
assert args
if len(args) == 1 and not kwargs:
@ -65,18 +64,32 @@ def write(*args, **kwargs):
# And this is why we aren't using print()...
sys.stdout.flush()
def backup_vault_dat(path):
backup_path = path.with_suffix(".dat.korman_backup")
shutil.copy2(str(path), str(backup_path))
write("DBG: Copied vault backup: {}", backup_path)
def set_link_chronicle(store, new_value, cond_value=None):
chron_folder = next((i for i in store.getChildren(store.firstNodeID)
if getattr(i, "folderType", None) == plVault.kChronicleFolder), None)
chron_folder = next(
(
i
for i in store.getChildren(store.firstNodeID)
if getattr(i, "folderType", None) == plVault.kChronicleFolder
),
None,
)
if chron_folder is None:
die("Could not locate vault chronicle folder.")
autolink_chron = next((i for i in store.getChildren(chron_folder.nodeID)
if getattr(i, "entryName", None) == autolink_chron_name), None)
autolink_chron = next(
(
i
for i in store.getChildren(chron_folder.nodeID)
if getattr(i, "entryName", None) == autolink_chron_name
),
None,
)
if autolink_chron is None:
write("DBG: Creating AutoLink chronicle...")
autolink_chron = plVaultChronicleNode()
@ -93,10 +106,15 @@ def set_link_chronicle(store, new_value, cond_value=None):
autolink_chron.entryValue = new_value
store.addNode(autolink_chron)
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
def find_player_vault(cwd, name):
sav_dir = cwd.joinpath("sav")
if not sav_dir.is_dir():
@ -121,6 +139,7 @@ def find_player_vault(cwd, name):
return vault_dat, store
die("Could not locate the requested player vault.")
def main():
print("DBG: alive")
args = main_parser.parse_args()
@ -139,11 +158,13 @@ def main():
# Update init file for this schtuff...
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("Nav.PageInHoldList GlobalAnimations")
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...
# 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
@ -161,8 +182,14 @@ def main():
plasma_args = [str(executable), "-iinit", "To_Dni"]
else:
write("DBG: Using a superior client :) :) :)")
plasma_args = [str(executable), "-LocalData", "-SkipLoginDialog", "-ServerIni={}".format(args.serverini),
"-PlayerId={}".format(args.ki), "-Age={}".format(args.age)]
plasma_args = [
str(executable),
"-LocalData",
"-SkipLoginDialog",
"-ServerIni={}".format(args.serverini),
"-PlayerId={}".format(args.ki),
"-Age={}".format(args.age),
]
try:
proc = subprocess.Popen(plasma_args, cwd=str(args.cwd), shell=True)
@ -180,7 +207,9 @@ def main():
vault_store = plVaultStore()
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:
write("DBG: ***Not*** resaving the vault!")
else:
@ -191,6 +220,7 @@ def main():
write("DONE")
sys.exit(0)
if __name__ == "__main__":
try:
main()

20
korman/properties/modifiers/__init__.py

@ -26,6 +26,7 @@ from .render import *
from .sound import *
from .water import *
class PlasmaModifiers(bpy.types.PropertyGroup):
def determine_next_id(self):
"""Gets the ID for the next modifier in the UI"""
@ -40,7 +41,7 @@ class PlasmaModifiers(bpy.types.PropertyGroup):
@property
def modifiers(self):
"""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):
attr = getattr(self, i, None)
@ -66,7 +67,7 @@ class PlasmaModifiers(bpy.types.PropertyGroup):
setattr(cls, i.pl_id, bpy.props.PointerProperty(type=i))
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"""
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"""
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):
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", "")
# The modifier might include the cateogry name in its name, so we'll strip that.
if label != category:
if label.startswith(category):
label = label[len(category)+1:]
label = label[len(category) + 1 :]
if label.endswith(category):
label = label[:-len(category)-1]
label = label[: -len(category) - 1]
tup = (pl_id, label, description, icon, i)
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 ... import idprops
def _convert_frame_time(frame_num):
fps = bpy.context.scene.render.fps
return frame_num / fps
class ActionModifier:
@property
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:
return bo.animation_data.action
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
# datablock for simplicity's sake.
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:
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 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):
@ -67,7 +80,9 @@ class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties):
so = exporter.mgr.find_create_object(plSceneObject, bl=bo)
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:
anims = [self.subanimations.entire_animation]
aganims = list(self._export_ag_anims(exporter, bo, so, anims))
@ -98,16 +113,25 @@ class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties):
else:
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:
exporter.report.warn("Animation '{}' generated no applicators. Nothing will be exported.",
anim_name, indent=2)
exporter.report.warn(
"Animation '{}' generated no applicators. Nothing will be exported.",
anim_name,
indent=2,
)
continue
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.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:
aganim.addApplicator(i)
@ -119,18 +143,24 @@ class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties):
markers = action.pose_markers
initial_marker = markers.get(anim.initial_marker)
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:
aganim.initial = -1.0
if anim.loop:
loop_start = markers.get(anim.loop_start)
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:
aganim.loopStart = aganim.start
loop_end = markers.get(anim.loop_end)
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:
aganim.loopEnd = aganim.end
else:
@ -157,10 +187,12 @@ class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties):
class AnimGroupObject(idprops.IDPropObjectMixin, bpy.types.PropertyGroup):
child_anim = PointerProperty(name="Child Animation",
description="Object whose action is a child animation",
type=bpy.types.Object,
poll=idprops.poll_animated_objects)
child_anim = PointerProperty(
name="Child Animation",
description="Object whose action is a child animation",
type=bpy.types.Object,
poll=idprops.poll_animated_objects,
)
@classmethod
def _idprop_mapping(cls):
@ -175,19 +207,25 @@ class PlasmaAnimationFilterModifier(PlasmaModifierProperties):
bl_description = "Filter animation components"
bl_icon = "UNLINKED"
no_rotation = BoolProperty(name="Filter Rotation",
description="Filter rotations",
options=set())
no_transX = BoolProperty(name="Filter X Translation",
description="Filter the X component of translations",
options=set())
no_transY = BoolProperty(name="Filter Y Translation",
description="Filter the Y component of translations",
options=set())
no_transZ = BoolProperty(name="Filter Z Translation",
description="Filter the Z component of translations",
options=set())
no_rotation = BoolProperty(
name="Filter Rotation", description="Filter rotations", options=set()
)
no_transX = BoolProperty(
name="Filter X Translation",
description="Filter the X component of translations",
options=set(),
)
no_transY = BoolProperty(
name="Filter Y Translation",
description="Filter the Y component of translations",
options=set(),
)
no_transZ = BoolProperty(
name="Filter Z Translation",
description="Filter the Z component of translations",
options=set(),
)
def export(self, exporter, bo, so):
# By this point, the object should already have a plFilterCoordInterface
@ -219,9 +257,11 @@ class PlasmaAnimationGroupModifier(ActionModifier, PlasmaModifierProperties):
bl_description = "Defines related animations"
bl_icon = "GROUP"
children = CollectionProperty(name="Child Animations",
description="Animations that will execute the same commands as this one",
type=AnimGroupObject)
children = CollectionProperty(
name="Child Animations",
description="Animations that will execute the same commands as this one",
type=AnimGroupObject,
)
active_child_index = IntProperty(options={"HIDDEN"})
def export(self, exporter, bo, so):
@ -229,7 +269,9 @@ class PlasmaAnimationGroupModifier(ActionModifier, PlasmaModifierProperties):
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
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...
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..."
exporter.report.warn(msg, self.key_name, child_bo.name, indent=2)
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(agmaster.key)
@ -260,12 +304,13 @@ class PlasmaAnimationGroupModifier(ActionModifier, PlasmaModifierProperties):
class LoopMarker(bpy.types.PropertyGroup):
loop_name = StringProperty(name="Loop Name",
description="Name of this loop")
loop_start = StringProperty(name="Loop Start",
description="Marker name from whence the loop begins")
loop_end = StringProperty(name="Loop End",
description="Marker name from whence the loop ends")
loop_name = StringProperty(name="Loop Name", description="Name of this loop")
loop_start = StringProperty(
name="Loop Start", description="Marker name from whence the loop begins"
)
loop_end = StringProperty(
name="Loop End", description="Marker name from whence the loop ends"
)
class PlasmaAnimationLoopModifier(ActionModifier, PlasmaModifierProperties):
@ -277,9 +322,9 @@ class PlasmaAnimationLoopModifier(ActionModifier, PlasmaModifierProperties):
bl_description = "Animation loop settings"
bl_icon = "PMARKER_SEL"
loops = CollectionProperty(name="Loops",
description="Loop points within the animation",
type=LoopMarker)
loops = CollectionProperty(
name="Loops", description="Loop points within the animation", type=LoopMarker
)
active_loop_index = IntProperty(options={"HIDDEN"})
def export(self, exporter, bo, so):
@ -293,11 +338,23 @@ class PlasmaAnimationLoopModifier(ActionModifier, PlasmaModifierProperties):
start = markers.get(loop.loop_start)
end = markers.get(loop.loop_end)
if start is None:
exporter.report.warn("Animation '{}' Loop '{}': Marker '{}' not found. This loop will not be exported".format(
action.name, loop.loop_name, loop.loop_start), indent=2)
exporter.report.warn(
"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:
exporter.report.warn("Animation '{}' Loop '{}': Marker '{}' not found. This loop will not be exported".format(
action.name, loop.loop_name, loop.loop_end), indent=2)
exporter.report.warn(
"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:
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_icon = "COLLAPSEMENU"
is_enabled = BoolProperty(name="Enabled",
description="Ladder enabled by default at Age start",
default=True)
direction = EnumProperty(name="Direction",
description="Direction of climb",
items=[("UP", "Up", "The avatar will mount the ladder and climb upward"),
("DOWN", "Down", "The avatar will mount the ladder and climb downward"),],
default="DOWN")
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)
is_enabled = BoolProperty(
name="Enabled",
description="Ladder enabled by default at Age start",
default=True,
)
direction = EnumProperty(
name="Direction",
description="Direction of climb",
items=[
("UP", "Up", "The avatar will mount the ladder and climb upward"),
("DOWN", "Down", "The avatar will mount the ladder and climb downward"),
],
default="DOWN",
)
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):
# Create the ladder modifier
@ -61,7 +72,10 @@ class PlasmaLadderModifier(PlasmaModifierProperties):
# engine-defined (45 degree) tolerance
if self.facing_object is not None:
# 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:
# Make our own artificial target -1.0 units back on the local Y axis.
ladderVec = mathutils.Vector((0, -1, 0)) * bo.matrix_world.inverted()
@ -69,48 +83,75 @@ class PlasmaLadderModifier(PlasmaModifierProperties):
mod.ladderView.normalize()
# Generate the detector's physical bounds
bounds = "hull" 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"])
bounds = (
"hull"
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
def requires_actor(self):
return True
sitting_approach_flags = [("kApproachFront", "Front", "Approach from the font"),
("kApproachLeft", "Left", "Approach from the left"),
("kApproachRight", "Right", "Approach from the right"),
("kApproachRear", "Rear", "Approach from the rear guard")]
sitting_approach_flags = [
("kApproachFront", "Front", "Approach from the font"),
("kApproachLeft", "Left", "Approach from the left"),
("kApproachRight", "Right", "Approach from the right"),
("kApproachRear", "Rear", "Approach from the rear guard"),
]
class PlasmaSittingBehavior(idprops.IDPropObjectMixin, PlasmaModifierProperties, PlasmaModifierLogicWiz):
class PlasmaSittingBehavior(
idprops.IDPropObjectMixin, PlasmaModifierProperties, PlasmaModifierLogicWiz
):
pl_id = "sittingmod"
bl_category = "Avatar"
bl_label = "Sitting Behavior"
bl_description = "Avatar sitting position"
approach = EnumProperty(name="Approach",
description="Directions an avatar can approach the seat from",
items=sitting_approach_flags,
default={"kApproachFront", "kApproachLeft", "kApproachRight"},
options={"ENUM_FLAG"})
clickable_object = PointerProperty(name="Clickable",
description="Object that defines the clickable area",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects)
region_object = PointerProperty(name="Region",
description="Object that defines the region mesh",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects)
facing_enabled = BoolProperty(name="Avatar Facing",
description="The avatar must be facing the clickable's Y-axis",
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)
approach = EnumProperty(
name="Approach",
description="Directions an avatar can approach the seat from",
items=sitting_approach_flags,
default={"kApproachFront", "kApproachLeft", "kApproachRight"},
options={"ENUM_FLAG"},
)
clickable_object = PointerProperty(
name="Clickable",
description="Object that defines the clickable area",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects,
)
region_object = PointerProperty(
name="Region",
description="Object that defines the region mesh",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects,
)
facing_enabled = BoolProperty(
name="Avatar Facing",
description="The avatar must be facing the clickable's Y-axis",
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):
if self.facing_enabled:
@ -153,8 +194,7 @@ class PlasmaSittingBehavior(idprops.IDPropObjectMixin, PlasmaModifierProperties,
@classmethod
def _idprop_mapping(cls):
return {"clickable_object": "clickable_obj",
"region_object": "region_obj"}
return {"clickable_object": "clickable_obj", "region_object": "region_obj"}
@property
def key_name(self):
@ -168,4 +208,8 @@ class PlasmaSittingBehavior(idprops.IDPropObjectMixin, PlasmaModifierProperties,
def sanity_check(self):
# The user absolutely MUST specify a clickable or this won't export worth crap.
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
from typing import Any, Dict, Generator, Optional
class PlasmaModifierProperties(bpy.types.PropertyGroup):
@property
def copy_material(self):
@ -52,8 +53,8 @@ class PlasmaModifierProperties(bpy.types.PropertyGroup):
def export(self, exporter, bo, so):
"""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
to the duration of this method.
be generated. No new Blender objects should be created unless their lifespan is constrained
to the duration of this method.
"""
pass
@ -86,7 +87,7 @@ class PlasmaModifierProperties(bpy.types.PropertyGroup):
@property
def no_span_sort(self):
"""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
# 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
# at runtime. What joy. Python FTW. See register() in __init__.py
_subprops = {
"display_order": (IntProperty, {"name": "INTERNAL: Display Ordering",
"description": "Position in the list of buttons",
"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"}}),
"display_order": (
IntProperty,
{
"name": "INTERNAL: Display Ordering",
"description": "Position in the list of buttons",
"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:
def convert_logic(self, bo, **kwargs):
"""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
are responsible for removing the tree from Blender, if applicable."""
fails during conversion, the temporary tree is deleted for you. However, on success, you
are responsible for removing the tree from Blender, if applicable."""
name = kwargs.pop("name", self.key_name)
assert not "tree" in kwargs
tree = bpy.data.node_groups.new(name, "PlasmaNodeTree")
@ -140,7 +152,9 @@ class PlasmaModifierLogicWiz:
else:
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")
with pfm_node.NoUpdate():
pfm_node.filename = filename
@ -152,19 +166,36 @@ class PlasmaModifierLogicWiz:
pfm_node.update()
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`.
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
attribute type is found. For attribute nodes that require multiple values, the `value` may
be set to None and handled in your code."""
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
attribute type is found. For attribute nodes that require multiple values, the `value` may
be set to None and handled in your code."""
from ...nodes.node_python import PlasmaAttribute, PlasmaAttribNodeBase
if attribute_type is None:
assert 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`"
assert (
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__)
node_cls = next((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(
node_cls = next(
(
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
)
@ -180,7 +211,7 @@ class PlasmaModifierLogicWiz:
def pre_export(self, exporter, bo):
"""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)
@ -213,10 +244,13 @@ def _restore_properties(dummy):
# Unregistered propertes are a sequence of (property function,
# property keyword arguments). Interesting design decision :)
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))
bpy.app.handlers.load_pre.append(_restore_properties)
@bpy.app.handlers.persistent
def _upgrade_modifiers(dummy):
# First, run all the upgrades
@ -231,4 +265,6 @@ def _upgrade_modifiers(dummy):
for mod_cls in PlasmaModifierUpgradable.__subclasses__():
for prop in mod_cls.deprecated_properties:
RemoveProperty(mod_cls, attr=prop)
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 ...exporter import ExportError, utils
from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz, PlasmaModifierUpgradable
from .base import (
PlasmaModifierProperties,
PlasmaModifierLogicWiz,
PlasmaModifierUpgradable,
)
from ... import idprops
journal_pfms = {
pvPots : {
pvPots: {
# Supplied by the OfflineKI script:
# https://gitlab.com/diafero/offline-ki/blob/master/offlineki/xSimpleJournal.py
"filename": "xSimpleJournal.py",
"attribs": (
{ 'id': 1, 'type': "ptAttribActivator", "name": "bookClickable" },
{ 'id': 2, 'type': "ptAttribString", "name": "journalFileName" },
{ 'id': 3, 'type': "ptAttribBoolean", "name": "isNotebook" },
{ 'id': 4, 'type': "ptAttribFloat", "name": "BookWidth" },
{ 'id': 5, 'type': "ptAttribFloat", "name": "BookHeight" },
)
{"id": 1, "type": "ptAttribActivator", "name": "bookClickable"},
{"id": 2, "type": "ptAttribString", "name": "journalFileName"},
{"id": 3, "type": "ptAttribBoolean", "name": "isNotebook"},
{"id": 4, "type": "ptAttribFloat", "name": "BookWidth"},
{"id": 5, "type": "ptAttribFloat", "name": "BookHeight"},
),
},
pvMoul : {
pvMoul: {
"filename": "xJournalBookGUIPopup.py",
"attribs": (
{ 'id': 1, 'type': "ptAttribActivator", 'name': "actClickableBook" },
{ 'id': 10, 'type': "ptAttribBoolean", 'name': "StartOpen" },
{ 'id': 11, 'type': "ptAttribFloat", 'name': "BookWidth" },
{ 'id': 12, 'type': "ptAttribFloat", 'name': "BookHeight" },
{ 'id': 13, 'type': "ptAttribString", 'name': "LocPath" },
{ 'id': 14, 'type': "ptAttribString", 'name': "GUIType" },
)
{"id": 1, "type": "ptAttribActivator", "name": "actClickableBook"},
{"id": 10, "type": "ptAttribBoolean", "name": "StartOpen"},
{"id": 11, "type": "ptAttribFloat", "name": "BookWidth"},
{"id": 12, "type": "ptAttribFloat", "name": "BookHeight"},
{"id": 13, "type": "ptAttribString", "name": "LocPath"},
{"id": 14, "type": "ptAttribString", "name": "GUIType"},
),
},
}
# Do not change the numeric IDs. They allow the list to be rearranged.
_languages = [("Dutch", "Nederlands", "Dutch", 0),
("English", "English", "", 1),
("Finnish", "Suomi", "Finnish", 2),
("French", "Français", "French", 3),
("German", "Deutsch", "German", 4),
("Hungarian", "Magyar", "Hungarian", 5),
("Italian", "Italiano ", "Italian", 6),
# Blender 2.79b can't render 日本語 by default
("Japanese", "Nihongo", "Japanese", 7),
("Norwegian", "Norsk", "Norwegian", 8),
("Polish", "Polski", "Polish", 9),
("Romanian", "Română", "Romanian", 10),
("Russian", "Pyccĸий", "Russian", 11),
("Spanish", "Español", "Spanish", 12),
("Swedish", "Svenska", "Swedish", 13)]
_languages = [
("Dutch", "Nederlands", "Dutch", 0),
("English", "English", "", 1),
("Finnish", "Suomi", "Finnish", 2),
("French", "Français", "French", 3),
("German", "Deutsch", "German", 4),
("Hungarian", "Magyar", "Hungarian", 5),
("Italian", "Italiano ", "Italian", 6),
# Blender 2.79b can't render 日本語 by default
("Japanese", "Nihongo", "Japanese", 7),
("Norwegian", "Norsk", "Norwegian", 8),
("Polish", "Polski", "Polish", 9),
("Romanian", "Română", "Romanian", 10),
("Russian", "Pyccĸий", "Russian", 11),
("Spanish", "Español", "Spanish", 12),
("Swedish", "Svenska", "Swedish", 13),
]
languages = sorted(_languages, key=lambda x: x[1])
_DEFAULT_LANGUAGE_NAME = "English"
_DEFAULT_LANGUAGE_ID = 1
class ImageLibraryItem(bpy.types.PropertyGroup):
image = bpy.props.PointerProperty(name="Image Item",
description="Image stored for export.",
type=bpy.types.Image,
options=set())
enabled = bpy.props.BoolProperty(name="Enabled",
description="Specifies whether this image will be stored during export.",
default=True,
options=set())
image = bpy.props.PointerProperty(
name="Image Item",
description="Image stored for export.",
type=bpy.types.Image,
options=set(),
)
enabled = bpy.props.BoolProperty(
name="Enabled",
description="Specifies whether this image will be stored during export.",
default=True,
options=set(),
)
class PlasmaImageLibraryModifier(PlasmaModifierProperties):
@ -99,43 +109,66 @@ class PlasmaImageLibraryModifier(PlasmaModifierProperties):
def export(self, exporter, bo, so):
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:
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):
def _poll_nonpytext(self, value):
return not value.name.endswith(".py")
language = EnumProperty(name="Language",
description="Language of this translation",
items=languages,
default=_DEFAULT_LANGUAGE_NAME,
options=set())
text_id = PointerProperty(name="Contents",
description="Text data block containing the text for this language",
type=bpy.types.Text,
poll=_poll_nonpytext,
options=set())
language = EnumProperty(
name="Language",
description="Language of this translation",
items=languages,
default=_DEFAULT_LANGUAGE_NAME,
options=set(),
)
text_id = PointerProperty(
name="Contents",
description="Text data block containing the text for this language",
type=bpy.types.Text,
poll=_poll_nonpytext,
options=set(),
)
class TranslationMixin:
def export_localization(self, exporter):
translations = [i for i in self.translations if i.text_id is not None]
if not translations:
exporter.report.error("'{}': '{}' No content translations available. The localization will not be exported.",
self.id_data.name, self.bl_label, indent=1)
exporter.report.error(
"'{}': '{}' No content translations available. The localization will not be exported.",
self.id_data.name,
self.bl_label,
indent=1,
)
return
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):
# Ensure there is always a default (read: English) translation available.
default_idx, default = next(((idx, translation) for idx, translation in enumerate(self.translations)
if translation.language == _DEFAULT_LANGUAGE_NAME), (None, None))
default_idx, default = next(
(
(idx, translation)
for idx, translation in enumerate(self.translations)
if translation.language == _DEFAULT_LANGUAGE_NAME
),
(None, None),
)
if default is None:
default_idx = len(self.translations)
default = self.translations.add()
@ -153,8 +186,14 @@ class TranslationMixin:
def _set_translation(self, value):
# We were given an int here, must change to a string
language_name = next((key for key, _, _, i in languages if i == value))
idx = next((idx for idx, translation in enumerate(self.translations)
if translation.language == language_name), None)
idx = next(
(
idx
for idx, translation in enumerate(self.translations)
if translation.language == language_name
),
None,
)
if idx is None:
self.active_translation_index = len(self.translations)
translation = self.translations.add()
@ -171,7 +210,9 @@ class TranslationMixin:
raise RuntimeError("TranslationMixin subclass needs a translation getter!")
class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz, TranslationMixin):
class PlasmaJournalBookModifier(
PlasmaModifierProperties, PlasmaModifierLogicWiz, TranslationMixin
):
pl_id = "journalbookmod"
bl_category = "GUI"
@ -179,59 +220,94 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
bl_description = "Journal Book"
bl_icon = "WORDWRAP_ON"
versions = EnumProperty(name="Export Targets",
description="Plasma versions for which this journal exports",
items=game_versions,
options={"ENUM_FLAG"},
default={"pvMoul"})
start_state = EnumProperty(name="Start",
description="State of journal when activated",
items=[("OPEN", "Open", "Journal will start opened to first page"),
("CLOSED", "Closed", "Journal will start closed showing cover")],
default="CLOSED")
book_type = EnumProperty(name="Book Type",
description="GUI type to be used for the journal",
items=[("bkBook", "Book", "A journal written on worn, yellowed paper"),
("bkNotebook", "Notebook", "A journal written on white, lined paper")],
default="bkBook")
book_scale_w = IntProperty(name="Book Width Scale",
description="Width scale",
default=100, min=0, 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())
versions = EnumProperty(
name="Export Targets",
description="Plasma versions for which this journal exports",
items=game_versions,
options={"ENUM_FLAG"},
default={"pvMoul"},
)
start_state = EnumProperty(
name="Start",
description="State of journal when activated",
items=[
("OPEN", "Open", "Journal will start opened to first page"),
("CLOSED", "Closed", "Journal will start closed showing cover"),
],
default="CLOSED",
)
book_type = EnumProperty(
name="Book Type",
description="GUI type to be used for the journal",
items=[
("bkBook", "Book", "A journal written on worn, yellowed paper"),
("bkNotebook", "Notebook", "A journal written on white, lined paper"),
],
default="bkBook",
)
book_scale_w = IntProperty(
name="Book Width Scale",
description="Width scale",
default=100,
min=0,
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 = EnumProperty(name="Language",
description="Language of this translation",
items=languages,
get=TranslationMixin._get_translation,
set=TranslationMixin._set_translation,
options=set())
active_translation = EnumProperty(
name="Language",
description="Language of this translation",
items=languages,
get=TranslationMixin._get_translation,
set=TranslationMixin._set_translation,
options=set(),
)
def pre_export(self, exporter, bo):
our_versions = (globals()[j] for j in self.versions)
version = exporter.mgr.getVer()
if version not in our_versions:
# We aren't needed here
exporter.report.port("Object '{}' has a JournalMod not enabled for export to the selected engine. Skipping.",
bo.name, version, indent=2)
exporter.report.port(
"Object '{}' has a JournalMod not enabled for export to the selected engine. Skipping.",
bo.name,
version,
indent=2,
)
return
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.transform(bm, matrix=mathutils.Matrix.Translation(bo.matrix_world.translation - rgn_obj.matrix_world.translation),
space=rgn_obj.matrix_world, verts=bm.verts)
bmesh.ops.transform(
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.hide_render = True
yield rgn_obj
@ -240,18 +316,24 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
rgn_obj = self.clickable_region
# 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):
# Assign journal script based on target 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:
self._create_pots_nodes(bo, tree.nodes, journalnode, age_name, rgn_obj)
else:
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.region_object = rgn_obj
@ -281,7 +363,9 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
height.link_output(journalnode, "pfm", "BookHeight")
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.region_object = rgn_obj
@ -309,7 +393,9 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
locpath = nodes.new("PlasmaAttribStringNode")
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.link_output(journalnode, "pfm", "GUIType")
@ -331,38 +417,38 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
linking_pfms = {
pvPots : {
pvPots: {
# Supplied by the OfflineKI script:
# https://gitlab.com/diafero/offline-ki/blob/master/offlineki/xSimpleLinkingBook.py
"filename": "xSimpleLinkingBook.py",
"attribs": (
{ 'id': 1, 'type': "ptAttribActivator", "name": "bookClickable" },
{ 'id': 2, 'type': "ptAttribString", "name": "destinationAge" },
{ 'id': 3, 'type': "ptAttribString", "name": "spawnPoint" },
{ 'id': 4, 'type': "ptAttribString", "name": "linkPanel" },
{ 'id': 5, 'type': "ptAttribString", "name": "bookCover" },
{ 'id': 6, 'type': "ptAttribString", "name": "stampTexture" },
{ 'id': 7, 'type': "ptAttribFloat", "name": "stampX" },
{ 'id': 8, 'type': "ptAttribFloat", "name": "stampY" },
{ 'id': 9, 'type': "ptAttribFloat", "name": "bookWidth" },
{ 'id': 10, 'type': "ptAttribFloat", "name": "BookHeight" },
{ 'id': 11, 'type': "ptAttribBehavior", "name": "msbSeekBeforeUI" },
{ 'id': 12, 'type': "ptAttribResponder", "name": "respOneShot" },
)
{"id": 1, "type": "ptAttribActivator", "name": "bookClickable"},
{"id": 2, "type": "ptAttribString", "name": "destinationAge"},
{"id": 3, "type": "ptAttribString", "name": "spawnPoint"},
{"id": 4, "type": "ptAttribString", "name": "linkPanel"},
{"id": 5, "type": "ptAttribString", "name": "bookCover"},
{"id": 6, "type": "ptAttribString", "name": "stampTexture"},
{"id": 7, "type": "ptAttribFloat", "name": "stampX"},
{"id": 8, "type": "ptAttribFloat", "name": "stampY"},
{"id": 9, "type": "ptAttribFloat", "name": "bookWidth"},
{"id": 10, "type": "ptAttribFloat", "name": "BookHeight"},
{"id": 11, "type": "ptAttribBehavior", "name": "msbSeekBeforeUI"},
{"id": 12, "type": "ptAttribResponder", "name": "respOneShot"},
),
},
pvMoul : {
pvMoul: {
"filename": "xLinkingBookGUIPopup.py",
"attribs": (
{ 'id': 1, 'type': "ptAttribActivator", 'name': "actClickableBook" },
{ 'id': 2, 'type': "ptAttribBehavior", 'name': "SeekBehavior" },
{ 'id': 3, 'type': "ptAttribResponder", 'name': "respLinkResponder" },
{ 'id': 4, 'type': "ptAttribString", 'name': "TargetAge" },
{ 'id': 5, 'type': "ptAttribActivator", 'name': "actBookshelf" },
{ 'id': 6, 'type': "ptAttribActivator", 'name': "shareRegion" },
{ 'id': 7, 'type': "ptAttribBehavior", 'name': "shareBookSeek" },
{ 'id': 10, 'type': "ptAttribBoolean", 'name': "IsDRCStamped" },
{ 'id': 11, 'type': "ptAttribBoolean", 'name': "ForceThirdPerson" },
)
{"id": 1, "type": "ptAttribActivator", "name": "actClickableBook"},
{"id": 2, "type": "ptAttribBehavior", "name": "SeekBehavior"},
{"id": 3, "type": "ptAttribResponder", "name": "respLinkResponder"},
{"id": 4, "type": "ptAttribString", "name": "TargetAge"},
{"id": 5, "type": "ptAttribActivator", "name": "actBookshelf"},
{"id": 6, "type": "ptAttribActivator", "name": "shareRegion"},
{"id": 7, "type": "ptAttribBehavior", "name": "shareBookSeek"},
{"id": 10, "type": "ptAttribBoolean", "name": "IsDRCStamped"},
{"id": 11, "type": "ptAttribBoolean", "name": "ForceThirdPerson"},
),
},
}
@ -375,81 +461,138 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
bl_description = "Linking Book"
bl_icon = "FILE_IMAGE"
versions = EnumProperty(name="Export Targets",
description="Plasma versions for which this journal exports",
items=game_versions,
options={"ENUM_FLAG"},
default={"pvMoul"})
versions = EnumProperty(
name="Export Targets",
description="Plasma versions for which this journal exports",
items=game_versions,
options={"ENUM_FLAG"},
default={"pvMoul"},
)
# Link Info
link_type = EnumProperty(name="Linking Type",
description="The type of Link this Linking Book will use",
items=[
("kBasicLink", "Public Link", "Links to a public instance of the specified Age"),
("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"),],
options=set(),
default="kOriginalBook")
age_name = StringProperty(name="Age Name",
description="Filename of the Age to link to (e.g. Garrison)",)
age_instance = StringProperty(name="Age Instance",
description="Friendly name of the Age to link to (e.g. Gahreesen)",)
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",)
link_type = EnumProperty(
name="Linking Type",
description="The type of Link this Linking Book will use",
items=[
(
"kBasicLink",
"Public Link",
"Links to a public instance of the specified Age",
),
(
"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",
),
],
options=set(),
default="kOriginalBook",
)
age_name = StringProperty(
name="Age Name",
description="Filename of the Age to link to (e.g. Garrison)",
)
age_instance = StringProperty(
name="Age Instance",
description="Friendly name of the Age to link to (e.g. Gahreesen)",
)
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
seek_point = PointerProperty(name="Seek Point",
description="The point the avatar will seek to before opening the Linking Book GUI",
type=bpy.types.Object,
poll=idprops.poll_empty_objects)
clickable_region = PointerProperty(name="Clickable Region",
description="The region in which the avatar must be standing before they can click on the Linking Book",
type=bpy.types.Object,
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)
seek_point = PointerProperty(
name="Seek Point",
description="The point the avatar will seek to before opening the Linking Book GUI",
type=bpy.types.Object,
poll=idprops.poll_empty_objects,
)
clickable_region = PointerProperty(
name="Clickable Region",
description="The region in which the avatar must be standing before they can click on the Linking Book",
type=bpy.types.Object,
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 --
# Popup Appearance
book_cover_image = PointerProperty(name="Book Cover",
description="Image to use for the Linking Book's cover (Optional: book starts open if left blank)",
type=bpy.types.Image,
options=set())
link_panel_image = PointerProperty(name="Linking Panel",
description="Image to use for the Linking Panel",
type=bpy.types.Image,
options=set())
stamp_image = PointerProperty(name="Stamp Image",
description="Image to use for the stamp on the page opposite the book's linking panel, if any",
type=bpy.types.Image,
options=set())
stamp_x = IntProperty(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")
book_cover_image = PointerProperty(
name="Book Cover",
description="Image to use for the Linking Book's cover (Optional: book starts open if left blank)",
type=bpy.types.Image,
options=set(),
)
link_panel_image = PointerProperty(
name="Linking Panel",
description="Image to use for the Linking Panel",
type=bpy.types.Image,
options=set(),
)
stamp_image = PointerProperty(
name="Stamp Image",
description="Image to use for the stamp on the page opposite the book's linking panel, if any",
type=bpy.types.Image,
options=set(),
)
stamp_x = IntProperty(
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:
our_versions = frozenset((globals()[j] for j in self.versions))
@ -458,34 +601,63 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
def pre_export(self, exporter, bo):
if not self._check_version(exporter.mgr.getVer()):
# We aren't needed here
exporter.report.port("Object '{}' has a LinkingBookMod not enabled for export to the selected engine. Skipping.",
self.id_data.name, indent=2)
exporter.report.port(
"Object '{}' has a LinkingBookMod not enabled for export to the selected engine. Skipping.",
self.id_data.name,
indent=2,
)
return
# Auto-generate a six-foot cube region around the clickable if none was provided.
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))
rgn_offset = mathutils.Matrix.Translation(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_offset = mathutils.Matrix.Translation(
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.hide_render = True
yield rgn_obj
else:
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):
if self._check_version(pvPrime, pvPots):
# 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)
user_images = (i for i in (self.book_cover_image, self.link_panel_image, self.stamp_image)
if i is not None)
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
)
for image in user_images:
exporter.mesh.material.export_prepared_image(owner=ilmod, image=image,
allowed_formats={"JPG", "PNG"}, extension="hsm")
exporter.mesh.material.export_prepared_image(
owner=ilmod,
image=image,
allowed_formats={"JPG", "PNG"},
extension="hsm",
)
def harvest_actors(self):
if self.seek_point is not None:
@ -494,13 +666,17 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
def logicwiz(self, bo, tree, age_name, version, region):
# Assign linking book script based on target 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:
self._create_pots_nodes(bo, tree.nodes, linkingnode, age_name, region)
else:
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_region = nodes.new("PlasmaClickableRegionNode")
clickable_region.region_object = clk_region
@ -524,19 +700,25 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
# Book Cover Image
if self.book_cover_image:
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")
# Linking Panel Image
if self.link_panel_image:
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")
# Stamp Image
if self.stamp_image:
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 X Position
@ -578,7 +760,9 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
responder.link_output(responder_state, "state_refs", "resp")
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_region = nodes.new("PlasmaClickableRegionNode")
clickable_region.region_object = clk_region
@ -630,11 +814,17 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
# Linking Panel Name
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")
def sanity_check(self):
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:
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 ... import idprops
class PlasmaVersionedNodeTree(idprops.IDPropMixin, bpy.types.PropertyGroup):
version = EnumProperty(name="Version",
description="Plasma versions this node tree exports under",
items=game_versions,
options={"ENUM_FLAG"},
default=set(list(zip(*game_versions))[0]))
node_tree = PointerProperty(name="Node Tree",
description="Node Tree to export",
type=bpy.types.NodeTree)
version = EnumProperty(
name="Version",
description="Plasma versions this node tree exports under",
items=game_versions,
options={"ENUM_FLAG"},
default=set(list(zip(*game_versions))[0]),
)
node_tree = PointerProperty(
name="Node Tree", description="Node Tree to export", type=bpy.types.NodeTree
)
@classmethod
def _idprop_mapping(cls):
@ -57,7 +60,11 @@ class PlasmaAdvancedLogic(PlasmaModifierProperties):
our_versions = [globals()[j] for j in i.version]
if version in our_versions:
if i.node_tree is None:
raise ExportError("'{}': Advanced Logic is missing a node tree for '{}'".format(bo.name, i.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.
exporter.want_node_trees[i.node_tree.name].add((bo, so))
@ -71,7 +78,9 @@ class PlasmaAdvancedLogic(PlasmaModifierProperties):
@property
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):
@ -96,22 +105,37 @@ class PlasmaMaintainersMarker(PlasmaModifierProperties):
bl_category = "Logic"
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"
calibration = EnumProperty(name="Calibration",
description="State of repair for the Marker",
items=[
("kBroken", "Broken",
"A marker which reports scrambled 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.")
])
calibration = EnumProperty(
name="Calibration",
description="State of repair for the Marker",
items=[
(
"kBroken",
"Broken",
"A marker which reports scrambled 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):
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)
@property

130
korman/properties/modifiers/physics.py

@ -27,7 +27,7 @@ bounds_types = (
("box", "Bounding Box", "Use a perfect bounding box"),
("sphere", "Bounding Sphere", "Use a perfect bounding sphere"),
("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
@ -49,12 +49,15 @@ surface_types = (
("kUser3", "User 3", ""),
)
def bounds_type_index(key):
return list(zip(*bounds_types))[0].index(key)
def bounds_type_str(idx):
return bounds_types[idx][0]
def _set_phys_prop(prop, sim, phys, value=True):
"""Sets properties on plGenericPhysical and plSimulationInterface (seeing as how they are duped)"""
sim.setProperty(prop, value)
@ -69,29 +72,58 @@ class PlasmaCollider(PlasmaModifierProperties):
bl_icon = "MOD_PHYSICS"
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)
camera_blocker = BoolProperty(name="Blocks Camera LOS", description="Object blocks camera line-of-sight", default=True)
avatar_blocker = BoolProperty(
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)
restitution = FloatProperty(name="Restitution", description="Coefficient of collision elasticity", min=0.0, max=1.0)
terrain = BoolProperty(name="Terrain", description="Object represents the ground", default=False)
dynamic = BoolProperty(name="Dynamic", description="Object can be influenced by other objects (ie is kickable)", default=False)
mass = FloatProperty(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())
restitution = FloatProperty(
name="Restitution",
description="Coefficient of collision elasticity",
min=0.0,
max=1.0,
)
terrain = BoolProperty(
name="Terrain", description="Object represents the ground", default=False
)
dynamic = BoolProperty(
name="Dynamic",
description="Object can be influenced by other objects (ie is kickable)",
default=False,
)
mass = FloatProperty(
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):
# All modifier properties are examined by this little stinker...
@ -110,17 +142,34 @@ class PlasmaSubworld(PlasmaModifierProperties):
bl_description = "Subworld definition"
bl_icon = "WORLD"
sub_type = EnumProperty(name="Subworld Type",
description="Specifies the physics strategy to use for this subworld",
items=[("auto", "Auto", "Korman will decide which physics strategy to use"),
("dynamicav", "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",
sub_type = EnumProperty(
name="Subworld Type",
description="Specifies the physics strategy to use for this subworld",
items=[
("auto", "Auto", "Korman will decide which physics strategy to use"),
(
"dynamicav",
"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",
size=3, default=(0.0, 0.0, -32.174), precision=3,
subtype="ACCELERATION", unit="ACCELERATION")
size=3,
default=(0.0, 0.0, -32.174),
precision=3,
subtype="ACCELERATION",
unit="ACCELERATION",
)
def export(self, exporter, bo, so):
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),
# but we definitely don't want it to happen.
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 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:
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
)
)

236
korman/properties/modifiers/region.py

@ -48,17 +48,20 @@ footstep_surface_ids = {
# 18 = swimming (why would you want this?)
}
footstep_surfaces = [("dirt", "Dirt", "Dirt"),
("grass", "Grass", "Grass"),
("metal", "Metal", "Metal Catwalk"),
("puddle", "Puddle", "Shallow Water"),
("rope", "Rope", "Rope Ladder"),
("rug", "Rug", "Carpet Rug"),
("stone", "Stone", "Stone Tile"),
("water", "Water", "Deep Water"),
("woodbridge", "Wood Bridge", "Wood Bridge"),
("woodfloor", "Wood Floor", "Wood Floor"),
("woodladder", "Wood Ladder", "Wood Ladder")]
footstep_surfaces = [
("dirt", "Dirt", "Dirt"),
("grass", "Grass", "Grass"),
("metal", "Metal", "Metal Catwalk"),
("puddle", "Puddle", "Shallow Water"),
("rope", "Rope", "Rope Ladder"),
("rug", "Rug", "Carpet Rug"),
("stone", "Stone", "Stone Tile"),
("water", "Water", "Deep Water"),
("woodbridge", "Wood Bridge", "Wood Bridge"),
("woodfloor", "Wood Floor", "Wood Floor"),
("woodladder", "Wood Ladder", "Wood Ladder"),
]
class PlasmaCameraRegion(PlasmaModifierProperties):
pl_id = "camera_rgn"
@ -68,24 +71,40 @@ class PlasmaCameraRegion(PlasmaModifierProperties):
bl_description = "Camera Region"
bl_icon = "CAMERA_DATA"
camera_type = EnumProperty(name="Camera Type",
description="What kind of camera should be used?",
items=[("auto_follow", "Auto Follow Camera", "Automatically generated follow camera"),
("manual", "Manual Camera", "User specified camera object")],
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())
camera_type = EnumProperty(
name="Camera Type",
description="What kind of camera should be used?",
items=[
(
"auto_follow",
"Auto Follow Camera",
"Automatically generated follow camera",
),
("manual", "Manual Camera", "User specified camera object"),
],
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())
def export(self, exporter, bo, so):
if self.camera_type == "manual":
if self.camera_object is None:
raise ExportError("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)
raise ExportError(
"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
else:
assert self.camera_type[:4] == "auto"
@ -98,9 +117,13 @@ class PlasmaCameraRegion(PlasmaModifierProperties):
# Setup physical stuff
phys_mod = bo.plasma_modifiers.collision
exporter.physics.generate_physical(bo, so, member_group="kGroupDetector",
report_groups=["kGroupAvatar"],
properties=["kPinned"])
exporter.physics.generate_physical(
bo,
so,
member_group="kGroupDetector",
report_groups=["kGroupAvatar"],
properties=["kPinned"],
)
# I don't feel evil enough to make this generate a logic tree...
msg = plCameraMsg()
@ -116,8 +139,14 @@ class PlasmaCameraRegion(PlasmaModifierProperties):
actors = set()
if self.camera_type == "manual":
if self.camera_object is None:
raise ExportError("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())
raise ExportError(
"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:
actors.update(self.auto_camera.harvest_actors())
return actors
@ -134,14 +163,18 @@ class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierLogicWiz):
bl_label = "Footstep"
bl_description = "Footstep Region"
surface = EnumProperty(name="Surface",
description="What kind of surface are we walking on?",
items=footstep_surfaces,
default="stone")
bounds = EnumProperty(name="Region Bounds",
description="Physical object's bounds",
items=bounds_types,
default="hull")
surface = EnumProperty(
name="Surface",
description="What kind of surface are we walking on?",
items=footstep_surfaces,
default="stone",
)
bounds = EnumProperty(
name="Region Bounds",
description="Physical object's bounds",
items=bounds_types,
default="hull",
)
def logicwiz(self, bo, tree):
nodes = tree.nodes
@ -178,13 +211,16 @@ class PlasmaPanicLinkRegion(PlasmaModifierProperties):
bl_label = "Panic Link"
bl_description = "Panic Link Region"
play_anim = BoolProperty(name="Play Animation",
description="Play the link-out animation when panic linking",
default=True)
play_anim = BoolProperty(
name="Play Animation",
description="Play the link-out animation when panic linking",
default=True,
)
def export(self, exporter, bo, so):
exporter.physics.generate_physical(bo, so, member_group="kGroupDetector",
report_groups=["kGroupAvatar"])
exporter.physics.generate_physical(
bo, so, member_group="kGroupDetector", report_groups=["kGroupAvatar"]
)
# Finally, the panic link region proper
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"
# Advanced
use_nodes = BoolProperty(name="Use Nodes",
description="Make this a node-based Soft Volume",
default=False)
node_tree = PointerProperty(name="Node Tree",
description="Node Tree detailing soft volume logic",
type=bpy.types.NodeTree)
use_nodes = BoolProperty(
name="Use Nodes",
description="Make this a node-based Soft Volume",
default=False,
)
node_tree = PointerProperty(
name="Node Tree",
description="Node Tree detailing soft volume logic",
type=bpy.types.NodeTree,
)
# Basic
invert = BoolProperty(name="Invert",
description="Invert the soft region")
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",
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)
invert = BoolProperty(name="Invert", description="Invert the soft region")
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",
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):
sv.insideStrength = self.inside_strength / 100.0
@ -237,7 +289,11 @@ class PlasmaSoftVolume(idprops.IDPropMixin, PlasmaModifierProperties):
tree = self.get_node_tree()
output = tree.find_output("PlasmaSoftVolumeOutputNode")
if output is None:
raise ExportError("SoftVolume '{}' Node Tree '{}' has no output node!".format(self.key_name, tree.name))
raise ExportError(
"SoftVolume '{}' Node Tree '{}' has no output node!".format(
self.key_name, tree.name
)
)
return output.get_key(exporter, so)
else:
pClass = plSoftVolumeInvert if self.invert else plSoftVolumeSimple
@ -251,7 +307,11 @@ class PlasmaSoftVolume(idprops.IDPropMixin, PlasmaModifierProperties):
def _export_convex_region(self, exporter, bo, so):
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
sv = self.get_key(exporter, so).object
@ -296,7 +356,11 @@ class PlasmaSoftVolume(idprops.IDPropMixin, PlasmaModifierProperties):
def get_node_tree(self):
if self.node_tree is None:
raise ExportError("SoftVolume '{}' does not specify a valid Node Tree!".format(self.key_name))
raise ExportError(
"SoftVolume '{}' does not specify a valid Node Tree!".format(
self.key_name
)
)
return self.node_tree
@classmethod
@ -314,34 +378,47 @@ class PlasmaSubworldRegion(PlasmaModifierProperties):
bl_label = "Subworld Region"
bl_description = "Subworld transition region"
subworld = PointerProperty(name="Subworld",
description="Subworld to transition into",
type=bpy.types.Object,
poll=idprops.poll_subworld_objects)
transition = EnumProperty(name="Transition",
description="When to transition to the new subworld",
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())
subworld = PointerProperty(
name="Subworld",
description="Subworld to transition into",
type=bpy.types.Object,
poll=idprops.poll_subworld_objects,
)
transition = EnumProperty(
name="Transition",
description="When to transition to the new subworld",
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):
# Due to the fact that our subworld modifier can produce both RidingAnimatedPhysical
# and [HK|PX]Subworlds depending on the situation, this could get hairy, fast.
# and [HK|PX]Subworlds depending on the situation, this could get hairy, fast.
# Start by surveying the lay of the land.
from_sub, to_sub = bo.plasma_object.subworld, self.subworld
from_isded = exporter.physics.is_dedicated_subworld(from_sub)
to_isded = exporter.physics.is_dedicated_subworld(to_sub)
if 1:
def get_log_text(bo, isded):
main = "[Main World]" if bo is None else bo.name
sub = "Subworld" if isded or bo is None else "RidingAnimatedPhysical"
return main, sub
from_name, from_type = get_log_text(from_sub, from_isded)
to_name, to_type = get_log_text(to_sub, to_isded)
exporter.report.msg("Transition from '{}' ({}) to '{}' ({})",
from_name, from_type, to_name, to_type,
indent=2)
exporter.report.msg(
"Transition from '{}' ({}) to '{}' ({})",
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.
# 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"
else:
msg = plRideAnimatedPhysMsg()
msg.BCastFlags |= plMessage.kLocalPropagate | plMessage.kPropagateToModifiers
msg.BCastFlags |= (
plMessage.kLocalPropagate | plMessage.kPropagateToModifiers
)
msg.sender = so.key
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
# with subworlds, so our enter/exit regions are separate. Here, enter/exit message
# 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":
region.enterMsg = msg
elif self.transition == "exit":
@ -371,5 +452,6 @@ class PlasmaSubworldRegion(PlasmaModifierProperties):
raise ExportAssertionError()
# Fancy pants region collider type shit
exporter.physics.generate_physical(bo, so, member_group="kGroupDetector",
report_groups=["kGroupAvatar"])
exporter.physics.generate_physical(
bo, so, member_group="kGroupDetector", report_groups=["kGroupAvatar"]
)

948
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,
"norepeat": plRandomSoundMod.kNoRepeats,
"coverall": plRandomSoundMod.kCoverall | plRandomSoundMod.kNoRepeats,
"sequential": plRandomSoundMod.kSequential
"sequential": plRandomSoundMod.kSequential,
}
class PlasmaRandomSound(PlasmaModifierProperties):
pl_id = "random_sound"
pl_depends = {"soundemit"}
@ -41,55 +42,101 @@ class PlasmaRandomSound(PlasmaModifierProperties):
bl_label = "Random Sound"
bl_description = ""
mode = EnumProperty(name="Mode",
description="Playback Type",
items=[("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())
mode = EnumProperty(
name="Mode",
description="Playback Type",
items=[
(
"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
play_on = EnumProperty(name="Play On",
description="Play sounds on this collision event",
items=[("slide", "Slide", "Plays a random sound on object slide"),
("impact", "Impact", "Plays a random sound on object slide")],
options=set())
surfaces = EnumProperty(name="Play Against",
description="Sounds are played on collision against these surfaces",
items=surface_types[1:],
options={"ENUM_FLAG"})
play_on = EnumProperty(
name="Play On",
description="Play sounds on this collision event",
items=[
("slide", "Slide", "Plays a random sound on object slide"),
("impact", "Impact", "Plays a random sound on object slide"),
],
options=set(),
)
surfaces = EnumProperty(
name="Play Against",
description="Sounds are played on collision against these surfaces",
items=surface_types[1:],
options={"ENUM_FLAG"},
)
# Timed random sounds
auto_start = BoolProperty(name="Auto Start",
description="Start playing when the Age loads",
default=True,
options=set())
play_mode = EnumProperty(name="Play Mode",
description="",
items=[("normal", "Any", "Plays any attached sound"),
("norepeat", "No Repeats", "Do not replay a sound immediately after itself"),
("coverall", "Full Set", "Once a sound is played, do not replay it until after all sounds are played"),
("sequential", "Sequential", "Play sounds in the order they appear in the emitter")],
default="norepeat",
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())
auto_start = BoolProperty(
name="Auto Start",
description="Start playing when the Age loads",
default=True,
options=set(),
)
play_mode = EnumProperty(
name="Play Mode",
description="",
items=[
("normal", "Any", "Plays any attached sound"),
(
"norepeat",
"No Repeats",
"Do not replay a sound immediately after itself",
),
(
"coverall",
"Full Set",
"Once a sound is played, do not replay it until after all sounds are played",
),
(
"sequential",
"Sequential",
"Play sounds in the order they appear in the emitter",
),
],
default="norepeat",
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):
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:
parent_bo = bo.parent
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)
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"
# 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.group = getattr(plPhysicalSndGroup, parent_bo.plasma_modifiers.collision.surface)
sndgroup.group = getattr(
plPhysicalSndGroup, parent_bo.plasma_modifiers.collision.surface
)
phys.soundGroup = sndgroup.key
rndmod = exporter.mgr.find_key(plRandomSoundMod, bl=bo, so=so)
@ -135,32 +190,55 @@ class PlasmaRandomSound(PlasmaModifierProperties):
else:
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:
surface_id = getattr(plPhysicalSndGroup, surface_name)
if surface_id in sounds:
exporter.report.warn("Overwriting physical {} surface '{}' ID:{}",
groupattr, surface_name, surface_id, indent=2)
exporter.report.warn(
"Overwriting physical {} surface '{}' ID:{}",
groupattr,
surface_name,
surface_id,
indent=2,
)
else:
exporter.report.msg("Got physical {} surface '{}' ID:{}",
groupattr, surface_name, surface_id, indent=2)
exporter.report.msg(
"Got physical {} surface '{}' ID:{}",
groupattr,
surface_name,
surface_id,
indent=2,
)
sounds[surface_id] = rndmod
# 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):
fade_type = EnumProperty(name="Type",
description="Fade Type",
items=[("NONE", "[Disable]", "Don't fade"),
("kLinear", "Linear", "Linear fade"),
("kLogarithmic", "Logarithmic", "Log fade"),
("kExponential", "Exponential", "Exponential fade")],
options=set())
length = FloatProperty(name="Length",
description="Seconds to spend fading",
default=1.0, min=0.0,
options=set(), subtype="TIME", unit="TIME")
fade_type = EnumProperty(
name="Type",
description="Fade Type",
items=[
("NONE", "[Disable]", "Don't fade"),
("kLinear", "Linear", "Linear fade"),
("kLogarithmic", "Logarithmic", "Log fade"),
("kExponential", "Exponential", "Exponential fade"),
],
options=set(),
)
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):
@ -195,86 +273,125 @@ class PlasmaSound(idprops.IDPropMixin, bpy.types.PropertyGroup):
def _update_name(self, context=None):
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:
self.name = self._sound_name
enabled = BoolProperty(name="Enabled", default=True, options=set())
sound = PointerProperty(name="Sound",
description="Sound Datablock",
type=bpy.types.Sound,
update=_update_sound)
updating_sound = BoolProperty(default=False,
options={"HIDDEN", "SKIP_SAVE"})
sound = PointerProperty(
name="Sound",
description="Sound Datablock",
type=bpy.types.Sound,
update=_update_sound,
)
updating_sound = BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"})
is_stereo = BoolProperty(default=True, options={"HIDDEN"})
is_valid = BoolProperty(default=False, options={"HIDDEN"})
sfx_region = PointerProperty(name="Soft Volume",
description="Soft region this sound can be heard in",
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"),
("kAmbience", "Ambience", "Ambient Sounds"),
("kBackgroundMusic", "Music", "Background Music"),
("kGUISound", "GUI", "GUI Effect"),
("kNPCVoices", "NPC", "NPC Speech")],
options=set())
channel = EnumProperty(name="Channel",
description="Which channel(s) to play",
items=[("L", "Left", "Left Channel"),
("R", "Right", "Right Channel")],
options={"ENUM_FLAG"},
default={"L", "R"},
update=_update_name)
auto_start = BoolProperty(name="Auto Start",
description="Start playing when the age is loaded",
default=False,
options=set())
incidental = BoolProperty(name="Incidental",
description="Sound is a low-priority incident and the engine may forgo playback",
default=False,
options=set())
loop = BoolProperty(name="Loop",
description="Loop the sound",
default=False,
options=set())
inner_cone = FloatProperty(name="Inner Angle",
description="Angle of the inner cone from the negative Z-axis",
min=0, max=math.radians(360), default=0, step=100,
options=set(),
subtype="ANGLE")
outer_cone = FloatProperty(name="Outer Angle",
description="Angle of the outer cone from the negative Z-axis",
min=0, max=math.radians(360), default=math.radians(360), step=100,
options=set(),
subtype="ANGLE")
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")
sfx_region = PointerProperty(
name="Soft Volume",
description="Soft region this sound can be heard in",
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"),
("kAmbience", "Ambience", "Ambient Sounds"),
("kBackgroundMusic", "Music", "Background Music"),
("kGUISound", "GUI", "GUI Effect"),
("kNPCVoices", "NPC", "NPC Speech"),
],
options=set(),
)
channel = EnumProperty(
name="Channel",
description="Which channel(s) to play",
items=[("L", "Left", "Left Channel"), ("R", "Right", "Right Channel")],
options={"ENUM_FLAG"},
default={"L", "R"},
update=_update_name,
)
auto_start = BoolProperty(
name="Auto Start",
description="Start playing when the age is loaded",
default=False,
options=set(),
)
incidental = BoolProperty(
name="Incidental",
description="Sound is a low-priority incident and the engine may forgo playback",
default=False,
options=set(),
)
loop = BoolProperty(
name="Loop", description="Loop the sound", default=False, options=set()
)
inner_cone = FloatProperty(
name="Inner Angle",
description="Angle of the inner cone from the negative Z-axis",
min=0,
max=math.radians(360),
default=0,
step=100,
options=set(),
subtype="ANGLE",
)
outer_cone = FloatProperty(
name="Outer Angle",
description="Angle of the outer cone from the negative Z-axis",
min=0,
max=math.radians(360),
default=math.radians(360),
step=100,
options=set(),
subtype="ANGLE",
)
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_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.
# 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.
package = BoolProperty(name="Export",
description="Package this file in the age export",
get=_get_package_value, set=_set_package_value,
options=set())
package = BoolProperty(
name="Export",
description="Package this file in the age export",
get=_get_package_value,
set=_set_package_value,
options=set(),
)
package_value = BoolProperty(options={"HIDDEN", "SKIP_SAVE"})
@property
@ -331,10 +451,23 @@ class PlasmaSound(idprops.IDPropMixin, bpy.types.PropertyGroup):
header.blockAlign = int(header.blockAlign / 2)
dataSize = int(dataSize / 2)
if self.is_3d_stereo:
audible.addSound(self._convert_sound(exporter, so, pClass, header, dataSize, channel="L"))
audible.addSound(self._convert_sound(exporter, so, pClass, header, dataSize, channel="R"))
audible.addSound(
self._convert_sound(exporter, so, pClass, header, dataSize, channel="L")
)
audible.addSound(
self._convert_sound(exporter, so, pClass, header, dataSize, channel="R")
)
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):
if channel is None:
@ -352,10 +485,18 @@ class PlasmaSound(idprops.IDPropMixin, bpy.types.PropertyGroup):
elif self.sfx_region:
sv_mod = self.sfx_region.plasma_modifiers.softvolume
if not sv_mod.enabled:
raise ExportError("'{}': SoundEmit '{}', '{}' is not a SoftVolume".format(self.id_data.name, self._sound_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)
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
@ -368,7 +509,9 @@ class PlasmaSound(idprops.IDPropMixin, bpy.types.PropertyGroup):
sound.properties |= plSound.kPropLooping
if self.incidental:
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
# 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
def _idprop_mapping(cls):
return {"sound": "sound_data",
"sfx_region": "soft_region"}
return {"sound": "sound_data", "sfx_region": "soft_region"}
def _idprop_sources(self):
return {"sound_data": bpy.data.sounds,
"soft_region": bpy.data.objects}
return {"sound_data": bpy.data.sounds, "soft_region": bpy.data.objects}
@property
def is_3d_stereo(self):
return self.sfx_type == "kSoundFX" and self.channel == {"L", "R"} and self.is_stereo
return (
self.sfx_type == "kSoundFX"
and self.channel == {"L", "R"}
and self.is_stereo
)
def _raise_error(self, msg):
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:
raise ExportError("SoundEmitter '{}': {}".format(self.id_data.name, msg))
@ -515,9 +664,13 @@ class PlasmaSoundEmitter(PlasmaModifierProperties):
active_sound_index = IntProperty(options={"HIDDEN"})
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)
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
# 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):
"""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
idx = 0

498
korman/properties/modifiers/water.py

@ -22,7 +22,10 @@ from .base import PlasmaModifierProperties
from ...exporter import ExportError, ExportAssertionError
from ... import idprops
class PlasmaSwimRegion(idprops.IDPropObjectMixin, PlasmaModifierProperties, bpy.types.PropertyGroup):
class PlasmaSwimRegion(
idprops.IDPropObjectMixin, PlasmaModifierProperties, bpy.types.PropertyGroup
):
pl_id = "swimregion"
bl_category = "Water"
@ -36,54 +39,94 @@ class PlasmaSwimRegion(idprops.IDPropObjectMixin, PlasmaModifierProperties, bpy.
"STRAIGHT": plSwimStraightCurrentRegion,
}
region = PointerProperty(name="Region",
description="Swimming detector region",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects)
down_buoyancy = FloatProperty(name="Downward Buoyancy",
description="Distance the avatar sinks into the water",
min=0.0, max=100.0, default=3.0,
options=set())
up_buoyancy = FloatProperty(name="Up Buoyancy",
description="Distance the avatar rises up after sinking",
min=0.0, max=100.0, default=0.05,
options=set())
up_velocity = FloatProperty(name="Up Velcocity",
description="Rate at which the avatar rises",
min=0.0, max=100.0, default=3.0,
options=set())
current_type = EnumProperty(name="Water Current",
description="",
items=[("NONE", "None", "No current"),
("CIRCULAR", "Circular", "Circular current"),
("STRAIGHT", "Straight", "Straight current")],
options=set())
rotation = FloatProperty(name="Rotation",
description="Rate of rotation about the current object",
min=-100.0, max=100.0, 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)
region = PointerProperty(
name="Region",
description="Swimming detector region",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects,
)
down_buoyancy = FloatProperty(
name="Downward Buoyancy",
description="Distance the avatar sinks into the water",
min=0.0,
max=100.0,
default=3.0,
options=set(),
)
up_buoyancy = FloatProperty(
name="Up Buoyancy",
description="Distance the avatar rises up after sinking",
min=0.0,
max=100.0,
default=0.05,
options=set(),
)
up_velocity = FloatProperty(
name="Up Velcocity",
description="Rate at which the avatar rises",
min=0.0,
max=100.0,
default=3.0,
options=set(),
)
current_type = EnumProperty(
name="Water Current",
description="",
items=[
("NONE", "None", "No current"),
("CIRCULAR", "Circular", "Circular current"),
("STRAIGHT", "Straight", "Straight current"),
],
options=set(),
)
rotation = FloatProperty(
name="Rotation",
description="Rate of rotation about the current object",
min=-100.0,
max=100.0,
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):
swimIface = self.get_key(exporter, so).object
@ -101,10 +144,18 @@ class PlasmaSwimRegion(idprops.IDPropObjectMixin, PlasmaModifierProperties, bpy.
swimIface.farDist = self.far_distance
swimIface.nearVel = self.near_velocity
swimIface.farVel = self.far_velocity
if isinstance(swimIface, (plSwimCircularCurrentRegion, plSwimStraightCurrentRegion)):
if isinstance(
swimIface, (plSwimCircularCurrentRegion, plSwimStraightCurrentRegion)
):
if self.current is None:
raise ExportError("Swimming Surface '{}' does not specify a current object".format(bo.name))
swimIface.currentObj = exporter.mgr.find_create_key(plSceneObject, bl=self.current)
raise ExportError(
"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...
# 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.
# Ohey! CWE doesn't let you swim at all if the surface isn't flat...
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:
exporter.physics.generate_flat_proxy(bo, so, z_coord=bo.matrix_world.translation[2],
member_group=member_group,
losdbs=losdbs)
exporter.physics.generate_flat_proxy(
bo,
so,
z_coord=bo.matrix_world.translation[2],
member_group=member_group,
losdbs=losdbs,
)
else:
exporter.physics.generate_physical(bo, so, bounds="trimesh",
member_group=member_group,
losdbs=losdbs)
exporter.physics.generate_physical(
bo, so, bounds="trimesh", member_group=member_group, losdbs=losdbs
)
# Detector region bounds
if self.region is not None:
region_so = exporter.mgr.find_create_object(plSceneObject, bl=self.region)
# Good news: if this phys has already been exported, this is basically a noop
member_group = "kGroupDetector" if exporter.mgr.getVer() == "pvMoul" else "kGroupLOSOnly"
exporter.physics.generate_physical(self.region, region_so,
member_group=member_group,
report_groups=["kGroupAvatar"])
member_group = (
"kGroupDetector"
if exporter.mgr.getVer() == "pvMoul"
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
# 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:
enter_msg, exit_msg = plSwimMsg(), plSwimMsg()
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.swimRegion = swimIface.key
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
# of this sitation. Just in case someone is like "WTF! Why am I not swimming?!?!1111111"
# 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):
pClass = self._CURRENTS[self.current_type]
@ -169,72 +240,66 @@ class PlasmaSwimRegion(idprops.IDPropObjectMixin, PlasmaModifierProperties, bpy.
@classmethod
def _idprop_mapping(cls):
return {"current": "current_object",
"region": "region_name"}
return {"current": "current_object", "region": "region_name"}
class PlasmaWaterModifier(idprops.IDPropMixin, PlasmaModifierProperties, bpy.types.PropertyGroup):
class PlasmaWaterModifier(
idprops.IDPropMixin, PlasmaModifierProperties, bpy.types.PropertyGroup
):
pl_id = "water_basic"
bl_category = "Water"
bl_label = "Basic Water"
bl_description = "Basic water properties"
wind_object = PointerProperty(name="Wind Object",
description="Object whose Y axis represents the wind direction",
type=bpy.types.Object,
poll=idprops.poll_empty_objects)
wind_speed = FloatProperty(name="Wind Speed",
description="Magnitude of the wind",
default=1.0)
envmap = PointerProperty(name="EnvMap",
description="Texture defining an environment map for this water object",
type=bpy.types.Texture,
poll=idprops.poll_envmap_textures)
envmap_radius = FloatProperty(name="Environment Sphere Radius",
description="How far away the first object you want to see is",
min=5.0, max=10000.0,
default=500.0)
specular_tint = FloatVectorProperty(name="Specular Tint",
subtype="COLOR",
min=0.0, max=1.0,
default=(1.0, 1.0, 1.0))
specular_alpha = FloatProperty(name="Specular Alpha",
min=0.0, max=1.0,
default=0.3)
noise = IntProperty(name="Noise",
subtype="PERCENTAGE",
min=0, max=300,
default=50)
specular_start = FloatProperty(name="Specular Start",
min=0.0, max=1000.0,
default=50.0)
specular_end = FloatProperty(name="Specular End",
min=0.0, max=10000.0,
default=1000.0)
ripple_scale = FloatProperty(name="Ripple Scale",
min=5.0, max=1000.0,
default=25.0)
depth_opacity = FloatProperty(name="Opacity End",
min=0.5, max=20.0,
default=3.0)
depth_reflection = FloatProperty(name="Reflection End",
min=0.5, max=20.0,
default=3.0)
depth_wave = FloatProperty(name="Wave End",
min=0.5, max=20.0,
default=4.0)
zero_opacity = FloatProperty(name="Opacity Start",
min=-10.0, max=10.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)
wind_object = PointerProperty(
name="Wind Object",
description="Object whose Y axis represents the wind direction",
type=bpy.types.Object,
poll=idprops.poll_empty_objects,
)
wind_speed = FloatProperty(
name="Wind Speed", description="Magnitude of the wind", default=1.0
)
envmap = PointerProperty(
name="EnvMap",
description="Texture defining an environment map for this water object",
type=bpy.types.Texture,
poll=idprops.poll_envmap_textures,
)
envmap_radius = FloatProperty(
name="Environment Sphere Radius",
description="How far away the first object you want to see is",
min=5.0,
max=10000.0,
default=500.0,
)
specular_tint = FloatVectorProperty(
name="Specular Tint", subtype="COLOR", min=0.0, max=1.0, default=(1.0, 1.0, 1.0)
)
specular_alpha = FloatProperty(name="Specular Alpha", min=0.0, max=1.0, default=0.3)
noise = IntProperty(name="Noise", subtype="PERCENTAGE", min=0, max=300, default=50)
specular_start = FloatProperty(
name="Specular Start", min=0.0, max=1000.0, default=50.0
)
specular_end = FloatProperty(
name="Specular End", min=0.0, max=10000.0, default=1000.0
)
ripple_scale = FloatProperty(name="Ripple Scale", min=5.0, max=1000.0, default=25.0)
depth_opacity = FloatProperty(name="Opacity End", min=0.5, max=20.0, default=3.0)
depth_reflection = FloatProperty(
name="Reflection End", min=0.5, max=20.0, default=3.0
)
depth_wave = FloatProperty(name="Wave End", min=0.5, max=20.0, default=4.0)
zero_opacity = FloatProperty(
name="Opacity Start", min=-10.0, max=10.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
def copy_material(self):
@ -243,14 +308,21 @@ class PlasmaWaterModifier(idprops.IDPropMixin, PlasmaModifierProperties, bpy.typ
def export(self, exporter, bo, so):
waveset = exporter.mgr.find_create_object(plWaveSet7, name=bo.name, so=so)
if self.wind_object:
if self.wind_object.plasma_object.enabled and self.wind_object.plasma_modifiers.animation.enabled:
waveset.refObj = exporter.mgr.find_create_key(plSceneObject, bl=self.wind_object)
if (
self.wind_object.plasma_object.enabled
and self.wind_object.plasma_modifiers.animation.enabled
):
waveset.refObj = exporter.mgr.find_create_key(
plSceneObject, bl=self.wind_object
)
waveset.setFlag(plWaveSet7.kHasRefObject, True)
# This is much like what happened in PyPRP
speed = self.wind_speed
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:
# Stolen shamelessly from PyPRP
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.waterHeight = bo.matrix_world.translation[2]
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.waterOffset = hsVector3(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)
state.waterOffset = hsVector3(
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
if self.envmap:
# 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
state.envCenter = dem.position
state.envRefresh = dem.refreshRate
@ -296,12 +376,10 @@ class PlasmaWaterModifier(idprops.IDPropMixin, PlasmaModifierProperties, bpy.typ
@classmethod
def _idprop_mapping(cls):
return {"wind_object": "wind_object_name",
"envmap": "envmap_name"}
return {"wind_object": "wind_object_name", "envmap": "envmap_name"}
def _idprop_sources(self):
return {"wind_object_name": bpy.data.objects,
"envmap_name": bpy.data.textures}
return {"wind_object_name": bpy.data.objects, "envmap_name": bpy.data.textures}
@property
def key_name(self):
@ -310,10 +388,12 @@ class PlasmaWaterModifier(idprops.IDPropMixin, PlasmaModifierProperties, bpy.typ
class PlasmaShoreObject(idprops.IDPropObjectMixin, bpy.types.PropertyGroup):
display_name = StringProperty(name="Display Name")
shore_object = PointerProperty(name="Shore Object",
description="Object that waves crash upon",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects)
shore_object = PointerProperty(
name="Shore Object",
description="Object that waves crash upon",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects,
)
@classmethod
def _idprop_mapping(cls):
@ -340,37 +420,50 @@ class PlasmaWaterShoreModifier(PlasmaModifierProperties):
shores = CollectionProperty(type=PlasmaShoreObject)
active_shore_index = IntProperty(options={"HIDDEN"})
shore_tint = FloatVectorProperty(name="Shore Tint",
subtype="COLOR",
min=0.0, max=1.0,
default=_shore_tint_default)
shore_opacity = IntProperty(name="Shore Opacity",
subtype="PERCENTAGE",
min=0, max=100,
default=_shore_opacity_default)
wispiness = IntProperty(name="Wispiness",
subtype="PERCENTAGE",
min=0, max=200,
default=_wispiness_default)
period = FloatProperty(name="Period",
min=0.0, max=200.0,
default=_period_default)
finger = FloatProperty(name="Finger",
min=50.0, max=300.0,
default=_finger_default)
edge_opacity = IntProperty(name="Edge Opacity",
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)
shore_tint = FloatVectorProperty(
name="Shore Tint",
subtype="COLOR",
min=0.0,
max=1.0,
default=_shore_tint_default,
)
shore_opacity = IntProperty(
name="Shore Opacity",
subtype="PERCENTAGE",
min=0,
max=100,
default=_shore_opacity_default,
)
wispiness = IntProperty(
name="Wispiness",
subtype="PERCENTAGE",
min=0,
max=200,
default=_wispiness_default,
)
period = FloatProperty(name="Period", min=0.0, max=200.0, default=_period_default)
finger = FloatProperty(name="Finger", min=50.0, max=300.0, default=_finger_default)
edge_opacity = IntProperty(
name="Edge Opacity",
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):
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.edgeRadius = self._edge_radius_default / 100.0
wavestate.period = self._period_default / 100.0
@ -382,11 +475,19 @@ class PlasmaWaterShoreModifier(PlasmaModifierProperties):
for i in self.shores:
if i.shore_object is None:
raise ExportError("'{}': Shore Object for '{}' is invalid".format(self.key_name, i.display_name))
waveset.addShore(exporter.mgr.find_create_key(plSceneObject, bl=i.shore_object))
raise ExportError(
"'{}': Shore Object for '{}' is invalid".format(
self.key_name, i.display_name
)
)
waveset.addShore(
exporter.mgr.find_create_key(plSceneObject, bl=i.shore_object)
)
wavestate.wispiness = self.wispiness / 100.0
wavestate.minColor = hsColorRGBA(*self.shore_tint, alpha=(self.shore_opacity / 100.0))
wavestate.minColor = hsColorRGBA(
*self.shore_tint, alpha=(self.shore_opacity / 100.0)
)
wavestate.edgeOpacity = self.edge_opacity / 100.0
wavestate.edgeRadius = self.edge_radius / 100.0
wavestate.period = self.period / 100.0
@ -413,28 +514,43 @@ class PlasmaWaveState:
@classmethod
def register(cls):
cls.min_length = FloatProperty(name="Min Length",
description="Smallest wave length",
min=0.1, max=50.0,
default=cls._min_length_default)
cls.max_length = FloatProperty(name="Max Length",
description="Largest wave length",
min=0.1, max=50.0,
default=cls._max_length_default)
cls.amplitude = IntProperty(name="Amplitude",
description="Multiplier for wave height",
subtype="PERCENTAGE",
min=0, max=100,
default=cls._amplitude_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)
cls.min_length = FloatProperty(
name="Min Length",
description="Smallest wave length",
min=0.1,
max=50.0,
default=cls._min_length_default,
)
cls.max_length = FloatProperty(
name="Max Length",
description="Largest wave length",
min=0.1,
max=50.0,
default=cls._max_length_default,
)
cls.amplitude = IntProperty(
name="Amplitude",
description="Multiplier for wave height",
subtype="PERCENTAGE",
min=0,
max=100,
default=cls._amplitude_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):

62
korman/properties/prop_anim.py

@ -23,6 +23,7 @@ import functools
import itertools
from typing import Iterable, Iterator
class PlasmaAnimation(bpy.types.PropertyGroup):
ENTIRE_ANIMATION = "(Entire Animation)"
@ -103,7 +104,7 @@ class PlasmaAnimation(bpy.types.PropertyGroup):
"entire_animation": {
bpy.types.Object: "plasma_modifiers.animation.initial_marker",
bpy.types.Texture: "plasma_layer.anim_initial_marker",
}
},
},
"loop_start": {
"type": StringProperty,
@ -144,10 +145,16 @@ class PlasmaAnimation(bpy.types.PropertyGroup):
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
# is not actually of type PlasmaAnimation. Meaning that self is some other object (great).
fcurves = itertools.chain.from_iterable((id.animation_data.action.fcurves
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)
fcurves = itertools.chain.from_iterable(
(
id.animation_data.action.fcurves
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
@classmethod
@ -203,13 +210,14 @@ class PlasmaAnimation(bpy.types.PropertyGroup):
if self.is_entire_animation:
attr_path = cls._get_from_class_lut(self.id_data, lut)
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])
return getattr(prop_group, attr_path[prop_delim+1:])
return getattr(prop_group, attr_path[prop_delim + 1 :])
else:
return default
else:
return getattr(self, "{}_value".format(prop_name))
return proc
@classmethod
@ -218,11 +226,12 @@ class PlasmaAnimation(bpy.types.PropertyGroup):
if self.is_entire_animation:
attr_path = cls._get_from_class_lut(self.id_data, lut)
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])
setattr(prop_group, attr_path[prop_delim+1:], value)
setattr(prop_group, attr_path[prop_delim + 1 :], value)
else:
setattr(self, "{}_value".format(prop_name), value)
return proc
@classmethod
@ -238,27 +247,39 @@ class PlasmaAnimation(bpy.types.PropertyGroup):
value_kwargs = deepcopy(kwargs)
value_kwargs["options"].add("HIDDEN")
value_props = { 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))
value_props = {
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
if needs_accessors:
# We have to use these weirdo wrappers because Blender only accepts function objects
# 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["set"] = cls._make_prop_setter(prop_name, definitions["entire_animation"])
kwargs["get"] = cls._make_prop_getter(
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))
@classmethod
def register_entire_animation(cls, id_type, rna_type):
"""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
now abstracted away to serve as the backing store for the new "entire animation" method."""
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."""
for prop_name, definitions in cls._PROPERTIES.items():
lut = definitions.get("entire_animation", {})
path_from_id = lut.get(id_type)
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.update(cls._ENTIRE_ANIMATION_PROPERTIES.get(prop_name, {}))
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:
self.active_animation_index_value = value
active_animation_index = IntProperty(get=_get_active_index, set=_set_active_index,
options={"HIDDEN"})
active_animation_index = IntProperty(
get=_get_active_index, set=_set_active_index, 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.
@ -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
# 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.
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
def animations(self) -> Iterable[PlasmaAnimation]:

529
korman/properties/prop_camera.py

@ -19,213 +19,395 @@ import math
from .. import idprops
camera_types = [("circle", "Circle Camera", "The camera circles a fixed point"),
("follow", "Follow Camera", "The camera follows an object"),
("fixed", "Fixed Camera", "The camera is fixed in one location"),
("rail", "Rail Camera", "The camera follows an object by moving along a line"),
("firstperson", "First Person", "Simulates first person view and disappears avatar")]
camera_types = [
("circle", "Circle Camera", "The camera circles a fixed point"),
("follow", "Follow Camera", "The camera follows an object"),
("fixed", "Fixed Camera", "The camera is fixed in one location"),
("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):
poa_acceleration = FloatProperty(name="PoA Acceleration",
description="Rate the camera's Point of Attention tracking velocity increases in feet per second squared",
min=-100.0, max=100.0, precision=0, default=60.0,
unit="ACCELERATION", options=set())
poa_deceleration = FloatProperty(name="PoA Deceleration",
description="Rate the camera's Point of Attention tracking velocity decreases in feet per second squared",
min=-100.0, 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())
poa_acceleration = FloatProperty(
name="PoA Acceleration",
description="Rate the camera's Point of Attention tracking velocity increases in feet per second squared",
min=-100.0,
max=100.0,
precision=0,
default=60.0,
unit="ACCELERATION",
options=set(),
)
poa_deceleration = FloatProperty(
name="PoA Deceleration",
description="Rate the camera's Point of Attention tracking velocity decreases in feet per second squared",
min=-100.0,
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",
description="Rate the camera's positional velocity increases in feet per second squared",
min=-100.0, max=100.0, precision=0, default=60.0,
unit="ACCELERATION", options=set())
pos_deceleration = FloatProperty(name="Position Deceleration",
description="Rate the camera's positional velocity decreases in feet per second squared",
min=-100.0, 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())
pos_acceleration = FloatProperty(
name="Position Acceleration",
description="Rate the camera's positional velocity increases in feet per second squared",
min=-100.0,
max=100.0,
precision=0,
default=60.0,
unit="ACCELERATION",
options=set(),
)
pos_deceleration = FloatProperty(
name="Position Deceleration",
description="Rate the camera's positional velocity decreases in feet per second squared",
min=-100.0,
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):
camera = PointerProperty(name="Camera",
description="The camera from which this transition is intended",
type=bpy.types.Object,
poll=idprops.poll_camera_objects,
options=set())
camera = PointerProperty(
name="Camera",
description="The camera from which this transition is intended",
type=bpy.types.Object,
poll=idprops.poll_camera_objects,
options=set(),
)
transition = PointerProperty(type=PlasmaTransition, options=set())
mode = EnumProperty(name="Transition Mode",
description="Type of transition that should occur between the two cameras",
items=[("ignore", "Ignore Camera", "Ignore this camera and do not transition"),
("auto", "Auto", "Auto transition as defined by the two cameras' properies"),
("manual", "Manual", "Manually defined transition")],
default="auto",
options=set())
enabled = BoolProperty(name="Enabled",
description="Export this transition",
default=True,
options=set())
mode = EnumProperty(
name="Transition Mode",
description="Type of transition that should occur between the two cameras",
items=[
("ignore", "Ignore Camera", "Ignore this camera and do not transition"),
(
"auto",
"Auto",
"Auto transition as defined by the two cameras' properies",
),
("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):
# Point of Attention
poa_type = EnumProperty(name="Point of Attention",
description="The point of attention that this camera tracks",
items=[("avatar", "Track Local Player", "Camera tracks the player's avatar"),
("object", "Track Object", "Camera tracks an object in the scene"),
("none", "Don't Track", "Camera does not track anything")],
options=set())
poa_object = PointerProperty(name="PoA Object",
description="Object the camera should track as its Point of Attention",
type=bpy.types.Object,
options=set())
poa_offset = FloatVectorProperty(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())
poa_type = EnumProperty(
name="Point of Attention",
description="The point of attention that this camera tracks",
items=[
("avatar", "Track Local Player", "Camera tracks the player's avatar"),
("object", "Track Object", "Camera tracks an object in the scene"),
("none", "Don't Track", "Camera does not track anything"),
],
options=set(),
)
poa_object = PointerProperty(
name="PoA Object",
description="Object the camera should track as its Point of Attention",
type=bpy.types.Object,
options=set(),
)
poa_offset = FloatVectorProperty(
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
pos_offset = FloatVectorProperty(name="Position Offset",
description="Offset the camera's position",
soft_min=-50.0, soft_max=50.0,
size=3, default=(0.0, 10.0, 3.0),
options=set())
pos_worldspace = BoolProperty(name="Worldspace Offset",
description="Position offset is in worldspace coordinates",
options=set())
pos_offset = FloatVectorProperty(
name="Position Offset",
description="Offset the camera's position",
soft_min=-50.0,
soft_max=50.0,
size=3,
default=(0.0, 10.0, 3.0),
options=set(),
)
pos_worldspace = BoolProperty(
name="Worldspace Offset",
description="Position offset is in worldspace coordinates",
options=set(),
)
# Default Transition
transition = PointerProperty(type=PlasmaTransition, options=set())
# Limit Panning
x_pan_angle = FloatProperty(name="X Degrees",
description="Maximum camera pan angle in the X direction",
min=0.0, max=math.radians(180.0), precision=0, default=math.radians(90.0),
subtype="ANGLE", options=set())
y_pan_angle = FloatProperty(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())
x_pan_angle = FloatProperty(
name="X Degrees",
description="Maximum camera pan angle in the X direction",
min=0.0,
max=math.radians(180.0),
precision=0,
default=math.radians(90.0),
subtype="ANGLE",
options=set(),
)
y_pan_angle = FloatProperty(
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
fov = FloatProperty(name="Default FOV",
description="Horizontal Field of View angle",
min=0.0, max=math.radians(180.0), precision=0, default=math.radians(70.0),
subtype="ANGLE")
limit_zoom = BoolProperty(name="Limit Zoom",
description="The camera allows zooming per artist limitations",
options=set())
zoom_max = FloatProperty(name="Max FOV",
description="Maximum camera FOV when zooming",
min=0.0, 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())
fov = FloatProperty(
name="Default FOV",
description="Horizontal Field of View angle",
min=0.0,
max=math.radians(180.0),
precision=0,
default=math.radians(70.0),
subtype="ANGLE",
)
limit_zoom = BoolProperty(
name="Limit Zoom",
description="The camera allows zooming per artist limitations",
options=set(),
)
zoom_max = FloatProperty(
name="Max FOV",
description="Maximum camera FOV when zooming",
min=0.0,
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
maintain_los = BoolProperty(name="Maintain LOS",
description="The camera should move to maintain line-of-sight with the object it's tracking",
default=True,
options=set())
fall_vertical = BoolProperty(name="Fall Camera",
description="The camera will orient itself vertically when the local player begins falling",
options=set())
fast_run = BoolProperty(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())
maintain_los = BoolProperty(
name="Maintain LOS",
description="The camera should move to maintain line-of-sight with the object it's tracking",
default=True,
options=set(),
)
fall_vertical = BoolProperty(
name="Fall Camera",
description="The camera will orient itself vertically when the local player begins falling",
options=set(),
)
fast_run = BoolProperty(
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
primary_camera = BoolProperty(name="Primary Camera",
description="The camera should be considered the Age's primary camera.",
options=set())
primary_camera = BoolProperty(
name="Primary Camera",
description="The camera should be considered the Age's primary camera.",
options=set(),
)
# Cricle Camera
def _get_circle_radius(self):
# 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.
return self.get_circle_radius(bpy.context.object)
def _set_circle_radius(self, value):
# Don't really care about error checking...
self.circle_radius_value = value
circle_center = PointerProperty(name="Center",
description="Center of the circle camera's orbit",
type=bpy.types.Object,
options=set())
circle_pos = EnumProperty(name="Position on Circle",
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"),
("farthest", "Farthest Point", "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"})
circle_center = PointerProperty(
name="Center",
description="Center of the circle camera's orbit",
type=bpy.types.Object,
options=set(),
)
circle_pos = EnumProperty(
name="Position on Circle",
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",
),
(
"farthest",
"Farthest Point",
"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
anim_enabled = BoolProperty(name="Animation Enabled",
description="Export the camera's animation",
default=True,
options=set())
start_on_push = BoolProperty(name="Start on Push",
description="Start playing the camera's animation when the camera is activated",
default=True,
options=set())
stop_on_pop = BoolProperty(name="Pause on Pop",
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())
anim_enabled = BoolProperty(
name="Animation Enabled",
description="Export the camera's animation",
default=True,
options=set(),
)
start_on_push = BoolProperty(
name="Start on Push",
description="Start playing the camera's animation when the camera is activated",
default=True,
options=set(),
)
stop_on_pop = BoolProperty(
name="Pause on Pop",
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_pos = EnumProperty(name="Position on Rail",
description="The point on the rail the camera moves to",
items=[("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())
rail_pos = EnumProperty(
name="Position on Rail",
description="The point on the rail the camera moves to",
items=[
(
"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):
"""Gets the circle camera radius for this camera when it is attached to the given Object"""
assert bo 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 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...
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:
actors.add(self.circle_center.name)
return actors
class PlasmaCamera(bpy.types.PropertyGroup):
camera_type = EnumProperty(name="Camera Type",
description="",
items=camera_types,
options=set())
camera_type = EnumProperty(
name="Camera Type", description="", items=camera_types, options=set()
)
settings = PointerProperty(type=PlasmaCameraProperties, options=set())
transitions = CollectionProperty(type=PlasmaManualTransition,
name="Transitions",
description="",
options=set())
transitions = CollectionProperty(
type=PlasmaManualTransition, name="Transitions", description="", options=set()
)
active_transition_index = IntProperty(options={"HIDDEN"})
@property

23
korman/properties/prop_image.py

@ -16,11 +16,20 @@
import bpy
from bpy.props import *
class PlasmaImage(bpy.types.PropertyGroup):
texcache_method = EnumProperty(name="Texture Cache",
description="Texture Cache Settings",
items=[("skip", "Don't Cache Image", "This image is never cached."),
("use", "Use Image Cache", "This image should be cached."),
("rebuild", "Refresh Image Cache", "Forces this image to be recached on the next export.")],
default="use",
options=set())
texcache_method = EnumProperty(
name="Texture Cache",
description="Texture Cache Settings",
items=[
("skip", "Don't Cache Image", "This image is never cached."),
("use", "Use Image Cache", "This image should be cached."),
(
"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
class PlasmaLamp(idprops.IDPropObjectMixin, bpy.types.PropertyGroup):
affect_characters = BoolProperty(name="Affect Avatars",
description="This lamp affects avatars",
options=set(),
default=True)
affect_characters = BoolProperty(
name="Affect Avatars",
description="This lamp affects avatars",
options=set(),
default=True,
)
# Shadow settings
cast_shadows = BoolProperty(name="Cast",
description="This lamp casts runtime shadows",
default=True)
shadow_falloff = FloatProperty(name="Falloff",
description="Distance from the Lamp past which we don't cast shadows",
min=5.0, max=50.0, default=10.0,
options=set())
shadow_distance = FloatProperty(name="Fade Distance",
description="Distance at which the shadow has completely faded out",
min=0.0, max=500.0, default=0.0,
options=set())
shadow_power = IntProperty(name="Power",
description="Multiplier for the shadow's intensity",
min=0, max=200, default=100,
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())
cast_shadows = BoolProperty(
name="Cast", description="This lamp casts runtime shadows", default=True
)
shadow_falloff = FloatProperty(
name="Falloff",
description="Distance from the Lamp past which we don't cast shadows",
min=5.0,
max=50.0,
default=10.0,
options=set(),
)
shadow_distance = FloatProperty(
name="Fade Distance",
description="Distance at which the shadow has completely faded out",
min=0.0,
max=500.0,
default=0.0,
options=set(),
)
shadow_power = IntProperty(
name="Power",
description="Multiplier for the shadow's intensity",
min=0,
max=200,
default=100,
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",
description="Soft region this light is active inside",
type=bpy.types.Object,
poll=idprops.poll_softvolume_objects)
lamp_region = PointerProperty(
name="Soft Volume",
description="Soft region this light is active inside",
type=bpy.types.Object,
poll=idprops.poll_softvolume_objects,
)
# For LimitedDirLights
size_height = FloatProperty(name="Height",
description="Size of the area for the Area Lamp in the Z direction",
min=0.0, default=200.0,
options=set())
size_height = FloatProperty(
name="Height",
description="Size of the area for the Area Lamp in the Z direction",
min=0.0,
default=200.0,
options=set(),
)
def has_light_group(self, bo):
return bool(bo.users_group)

76
korman/properties/prop_object.py

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

182
korman/properties/prop_scene.py

@ -19,9 +19,11 @@ import itertools
from ..exporter.etlight import _NUM_RENDER_LAYERS
class PlasmaBakePass(bpy.types.PropertyGroup):
def _get_display_name(self):
return self.name
def _set_display_name(self, value):
for i in bpy.data.objects:
lm = i.plasma_modifiers.lightmap
@ -29,32 +31,32 @@ class PlasmaBakePass(bpy.types.PropertyGroup):
lm.bake_pass_name = value
self.name = value
display_name = StringProperty(name="Pass Name",
get=_get_display_name,
set=_set_display_name,
options=set())
display_name = StringProperty(
name="Pass Name", get=_get_display_name, set=_set_display_name, options=set()
)
render_layers = BoolVectorProperty(name="Layers to Bake",
description="Render layers to use for baking",
options=set(),
subtype="LAYER",
size=_NUM_RENDER_LAYERS,
default=((True,) * _NUM_RENDER_LAYERS))
render_layers = BoolVectorProperty(
name="Layers to Bake",
description="Render layers to use for baking",
options=set(),
subtype="LAYER",
size=_NUM_RENDER_LAYERS,
default=((True,) * _NUM_RENDER_LAYERS),
)
class PlasmaWetDecalRef(bpy.types.PropertyGroup):
enabled = BoolProperty(name="Enabled",
default=True,
options=set())
enabled = BoolProperty(name="Enabled", default=True, options=set())
name = StringProperty(name="Decal Name",
description="Wet decal manager",
options=set())
name = StringProperty(
name="Decal Name", description="Wet decal manager", options=set()
)
class PlasmaDecalManager(bpy.types.PropertyGroup):
def _get_display_name(self):
return self.name
def _set_display_name(self, value):
prev_value = self.name
for i in bpy.data.objects:
@ -69,59 +71,88 @@ class PlasmaDecalManager(bpy.types.PropertyGroup):
j.name = value
self.name = value
name = StringProperty(name="Decal Name",
options=set())
display_name = StringProperty(name="Display Name",
get=_get_display_name,
set=_set_display_name,
options=set())
decal_type = EnumProperty(name="Decal Type",
description="",
items=[("footprint_dry", "Footprint (Dry)", ""),
("footprint_wet", "Footprint (Wet)", ""),
("puddle", "Water Ripple (Shallow)", ""),
("ripple", "Water Ripple (Deep)", "")],
default="footprint_dry",
options=set())
image = PointerProperty(name="Image",
description="",
type=bpy.types.Image,
options=set())
blend = EnumProperty(name="Blend Mode",
description="",
items=[("kBlendAdd", "Add", ""),
("kBlendAlpha", "Alpha", ""),
("kBlendMADD", "Brighten", ""),
("kBlendMult", "Multiply", "")],
default="kBlendAlpha",
options=set())
length = IntProperty(name="Length",
description="",
subtype="PERCENTAGE",
min=0, soft_min=25, soft_max=400, default=100,
options=set())
width = IntProperty(name="Width",
description="",
subtype="PERCENTAGE",
min=0, soft_min=25, soft_max=400, default=100,
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())
name = StringProperty(name="Decal Name", options=set())
display_name = StringProperty(
name="Display Name", get=_get_display_name, set=_set_display_name, options=set()
)
decal_type = EnumProperty(
name="Decal Type",
description="",
items=[
("footprint_dry", "Footprint (Dry)", ""),
("footprint_wet", "Footprint (Wet)", ""),
("puddle", "Water Ripple (Shallow)", ""),
("ripple", "Water Ripple (Deep)", ""),
],
default="footprint_dry",
options=set(),
)
image = PointerProperty(
name="Image", description="", type=bpy.types.Image, options=set()
)
blend = EnumProperty(
name="Blend Mode",
description="",
items=[
("kBlendAdd", "Add", ""),
("kBlendAlpha", "Alpha", ""),
("kBlendMADD", "Brighten", ""),
("kBlendMult", "Multiply", ""),
],
default="kBlendAlpha",
options=set(),
)
length = IntProperty(
name="Length",
description="",
subtype="PERCENTAGE",
min=0,
soft_min=25,
soft_max=400,
default=100,
options=set(),
)
width = IntProperty(
name="Width",
description="",
subtype="PERCENTAGE",
min=0,
soft_min=25,
soft_max=400,
default=100,
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
wet_managers = CollectionProperty(type=PlasmaWetDecalRef)
@ -135,8 +166,11 @@ class PlasmaScene(bpy.types.PropertyGroup):
decal_managers = CollectionProperty(type=PlasmaDecalManager)
active_decal_index = IntProperty(options={"HIDDEN"})
modifier_copy_object = PointerProperty(name="INTERNAL: Object to copy modifiers from",
options={"HIDDEN", "SKIP_SAVE"},
type=bpy.types.Object)
modifier_copy_id = StringProperty(name="INTERNAL: Modifier to copy from",
options={"HIDDEN", "SKIP_SAVE"})
modifier_copy_object = PointerProperty(
name="INTERNAL: Object to copy modifiers from",
options={"HIDDEN", "SKIP_SAVE"},
type=bpy.types.Object,
)
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
from bpy.props import *
class PlasmaSound(bpy.types.PropertyGroup):
package = BoolProperty(name="Export",
description="Package this file in the age export",
default=True,
options=set())
package = BoolProperty(
name="Export",
description="Package this file in the age export",
default=True,
options=set(),
)

7
korman/properties/prop_text.py

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

179
korman/properties/prop_texture.py

@ -19,12 +19,15 @@ from bpy.props import *
from .. import idprops
from .prop_anim import PlasmaAnimationCollection
class EnvMapVisRegion(idprops.IDPropObjectMixin, bpy.types.PropertyGroup):
enabled = BoolProperty(default=True)
control_region = PointerProperty(name="Control",
description="Object defining a Plasma Visibility Control",
type=bpy.types.Object,
poll=idprops.poll_visregion_objects)
control_region = PointerProperty(
name="Control",
description="Object defining a Plasma Visibility Control",
type=bpy.types.Object,
poll=idprops.poll_visregion_objects,
)
@classmethod
def _idprop_mapping(cls):
@ -34,71 +37,113 @@ class EnvMapVisRegion(idprops.IDPropObjectMixin, bpy.types.PropertyGroup):
class PlasmaLayer(bpy.types.PropertyGroup):
bl_idname = "texture.plasma_layer"
opacity = FloatProperty(name="Layer Opacity",
description="Opacity of the texture",
default=100.0, min=0.0, max=100.0,
precision=0, subtype="PERCENTAGE")
alpha_halo = BoolProperty(name="High Alpha Test",
description="Fixes halos seen around semitransparent objects resulting from sorting errors",
default=False)
envmap_color = FloatVectorProperty(name="Environment Map Color",
description="The default background color rendered onto the Environment Map",
min=0.0,
max=1.0,
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)
opacity = FloatProperty(
name="Layer Opacity",
description="Opacity of the texture",
default=100.0,
min=0.0,
max=100.0,
precision=0,
subtype="PERCENTAGE",
)
alpha_halo = BoolProperty(
name="High Alpha Test",
description="Fixes halos seen around semitransparent objects resulting from sorting errors",
default=False,
)
envmap_color = FloatVectorProperty(
name="Environment Map Color",
description="The default background color rendered onto the Environment Map",
min=0.0,
max=1.0,
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"})
is_detail_map = BoolProperty(name="Detail Fade",
description="Texture fades out as distance from the camera increases",
default=False,
options=set())
detail_fade_start = IntProperty(name="Falloff Start",
description="",
min=0, max=100, default=0,
options=set(), subtype="PERCENTAGE")
detail_fade_stop = IntProperty(name="Falloff Stop",
description="",
min=0, max=100, default=100,
options=set(), subtype="PERCENTAGE")
detail_opacity_start = IntProperty(name="Opacity Start",
description="",
min=0, max=100, default=50,
options=set(), subtype="PERCENTAGE")
detail_opacity_stop = IntProperty(name="Opacity Stop",
description="",
min=0, max=100, default=0,
options=set(), subtype="PERCENTAGE")
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())
is_detail_map = BoolProperty(
name="Detail Fade",
description="Texture fades out as distance from the camera increases",
default=False,
options=set(),
)
detail_fade_start = IntProperty(
name="Falloff Start",
description="",
min=0,
max=100,
default=0,
options=set(),
subtype="PERCENTAGE",
)
detail_fade_stop = IntProperty(
name="Falloff Stop",
description="",
min=0,
max=100,
default=100,
options=set(),
subtype="PERCENTAGE",
)
detail_opacity_start = IntProperty(
name="Opacity Start",
description="",
min=0,
max=100,
default=50,
options=set(),
subtype="PERCENTAGE",
)
detail_opacity_stop = IntProperty(
name="Opacity Stop",
description="",
min=0,
max=100,
default=0,
options=set(),
subtype="PERCENTAGE",
)
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)

222
korman/properties/prop_world.py

@ -19,42 +19,44 @@ from PyHSPlasma import *
from ..addon_prefs import game_versions
class PlasmaFni(bpy.types.PropertyGroup):
bl_idname = "world.plasma_fni"
fog_color = FloatVectorProperty(name="Fog Color",
description="The default fog color used in your age",
default=(0.4, 0.3, 0.1),
min=0.0,
max=1.0,
subtype="COLOR")
fog_method = EnumProperty(name="Fog Type",
items=[
("linear", "Linear", "Fog Based on Linear Distance"),
("exp", "Exponential", "Fog Based on Exponential Distance"),
("exp2", "Exponential 2", "Fog Based on Exponential Distance Squared"),
("none", "None", "No Fog")
])
fog_start = FloatProperty(name="Start",
description="",
default=-1500.0)
fog_end = FloatProperty(name="End",
description="",
default=20000.0)
fog_density = FloatProperty(name="Density",
description="",
default=1.0,
min=0.0)
clear_color = FloatVectorProperty(name="Clear Color",
description="The default background color rendered in your age",
min=0.0,
max=1.0,
subtype="COLOR")
yon = IntProperty(name="Draw Distance",
description="The distance (in feet) Plasma will draw",
default=100000,
soft_min=100,
min=1)
fog_color = FloatVectorProperty(
name="Fog Color",
description="The default fog color used in your age",
default=(0.4, 0.3, 0.1),
min=0.0,
max=1.0,
subtype="COLOR",
)
fog_method = EnumProperty(
name="Fog Type",
items=[
("linear", "Linear", "Fog Based on Linear Distance"),
("exp", "Exponential", "Fog Based on Exponential Distance"),
("exp2", "Exponential 2", "Fog Based on Exponential Distance Squared"),
("none", "None", "No Fog"),
],
)
fog_start = FloatProperty(name="Start", description="", default=-1500.0)
fog_end = FloatProperty(name="End", description="", default=20000.0)
fog_density = FloatProperty(name="Density", description="", default=1.0, min=0.0)
clear_color = FloatVectorProperty(
name="Clear Color",
description="The default background color rendered in your age",
min=0.0,
max=1.0,
subtype="COLOR",
)
yon = IntProperty(
name="Draw Distance",
description="The distance (in feet) Plasma will draw",
default=100000,
soft_min=100,
min=1,
)
class PlasmaGames(bpy.types.PropertyGroup):
@ -114,37 +116,47 @@ class PlasmaPage(bpy.types.PropertyGroup):
self.name = "Page%02i" % suffix
self.check_suffixes = True
name = StringProperty(name="Name",
description="Name of the specified page",
update=_rename_page)
seq_suffix = IntProperty(name="ID",
description="A numerical ID for this page",
soft_min=0, # Negatives indicate global--advanced users only
default=0, # The add operator will autogen a default
update=_check_suffix)
auto_load = BoolProperty(name="Auto Load",
description="Load this page on link-in",
default=True)
local_only = BoolProperty(name="Local Only",
description="This page should not synchronize with the server",
default=False)
enabled = BoolProperty(name="Export Page",
description="Export this page",
default=True)
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]))
name = StringProperty(
name="Name", description="Name of the specified page", update=_rename_page
)
seq_suffix = IntProperty(
name="ID",
description="A numerical ID for this page",
soft_min=0, # Negatives indicate global--advanced users only
default=0, # The add operator will autogen a default
update=_check_suffix,
)
auto_load = BoolProperty(
name="Auto Load", description="Load this page on link-in", default=True
)
local_only = BoolProperty(
name="Local Only",
description="This page should not synchronize with the server",
default=False,
)
enabled = BoolProperty(
name="Export Page", description="Export this page", default=True
)
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...
last_name = StringProperty(description="INTERNAL: Cached page name",
options={"HIDDEN"})
last_seq_suffix = IntProperty(description="INTERNAL: Cached sequence suffix",
options={"HIDDEN"})
check_suffixes = BoolProperty(description="INTERNAL: Should we sanity-check suffixes?",
options={"HIDDEN"},
default=False)
last_name = StringProperty(
description="INTERNAL: Cached page name", options={"HIDDEN"}
)
last_seq_suffix = IntProperty(
description="INTERNAL: Cached sequence suffix", options={"HIDDEN"}
)
check_suffixes = BoolProperty(
description="INTERNAL: Should we sanity-check suffixes?",
options={"HIDDEN"},
default=False,
)
class PlasmaAge(bpy.types.PropertyGroup):
@ -153,14 +165,20 @@ class PlasmaAge(bpy.types.PropertyGroup):
log_func = exporter.report.warn
else:
log_func = exporter.report.port
if self.seq_prefix <= self.MOUL_PREFIX_RANGE[0] 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)
if (
self.seq_prefix <= self.MOUL_PREFIX_RANGE[0]
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.dayLength = self.day_length
_age_info.lingerTime = 180 # this is fairly standard
_age_info.maxCapacity = 50 # the server currently ignores this
_age_info.lingerTime = 180 # this is fairly standard
_age_info.maxCapacity = 50 # the server currently ignores this
_age_info.name = exporter.age_name
_age_info.seqPrefix = self.seq_prefix
_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)
SP_PRFIX_RANGE = ((pow(2, 24) - pow(2, 23)) * -1, pow(2, 23) - 1)
day_length = FloatProperty(name="Day Length",
description="Length of a day (in hours) on this age",
default=30.230000,
soft_min=0.1,
min=0.0)
start_time = IntProperty(name="Start Time",
description="Seconds from 1/1/1970 until the first day on this age",
subtype="UNSIGNED",
default=672211080,
min=0)
seq_prefix = IntProperty(name="Sequence Prefix",
description="A unique numerical ID for this age",
min=SP_PRFIX_RANGE[0],
soft_min=0, # Negative indicates global--advanced users only
soft_max=MOUL_PREFIX_RANGE[1],
max=SP_PRFIX_RANGE[1],
default=100)
pages = CollectionProperty(name="Pages",
description="Registry pages for this age",
type=PlasmaPage)
age_sdl = BoolProperty(name="Age Global SDL",
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())
day_length = FloatProperty(
name="Day Length",
description="Length of a day (in hours) on this age",
default=30.230000,
soft_min=0.1,
min=0.0,
)
start_time = IntProperty(
name="Start Time",
description="Seconds from 1/1/1970 until the first day on this age",
subtype="UNSIGNED",
default=672211080,
min=0,
)
seq_prefix = IntProperty(
name="Sequence Prefix",
description="A unique numerical ID for this age",
min=SP_PRFIX_RANGE[0],
soft_min=0, # Negative indicates global--advanced users only
soft_max=MOUL_PREFIX_RANGE[1],
max=SP_PRFIX_RANGE[1],
default=100,
)
pages = CollectionProperty(
name="Pages", description="Registry pages for this age", type=PlasmaPage
)
age_sdl = BoolProperty(
name="Age Global SDL",
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
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...
from bl_ui import properties_material
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_shading.COMPAT_ENGINES.add("PLASMA_GAME")
@ -37,18 +38,22 @@ properties_material.MATERIAL_PT_shadow.COMPAT_ENGINES.add("PLASMA_GAME")
del properties_material
from bl_ui import properties_data_mesh
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_vertex_colors.COMPAT_ENGINES.add("PLASMA_GAME")
del properties_data_mesh
def _whitelist_all(mod):
for i in dir(mod):
attr = getattr(mod, i)
if hasattr(attr, "COMPAT_ENGINES"):
getattr(attr, "COMPAT_ENGINES").add("PLASMA_GAME")
from bl_ui import properties_data_lamp
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_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
from bl_ui import properties_render
_whitelist_all(properties_render)
del properties_render
from bl_ui import properties_texture
_whitelist_all(properties_texture)
del properties_texture
from bl_ui import properties_world
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_environment_lighting.COMPAT_ENGINES.add("PLASMA_GAME")

1
korman/ui/__init__.py

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

69
korman/ui/modifiers/anim.py

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

2
korman/ui/modifiers/avatar.py

@ -17,6 +17,7 @@ import bpy
from ...helpers import find_modifier
def laddermod(modifier, layout, context):
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")
def sittingmod(modifier, layout, context):
layout.row().prop(modifier, "approach")

35
korman/ui/modifiers/gui.py

@ -18,21 +18,47 @@ from pathlib import Path
from . import ui_list
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:
layout.label("[No Image Specified]", icon="ERROR")
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="")
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:
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):
layout.prop_menu_enum(modifier, "versions")
@ -68,6 +94,7 @@ def journalbookmod(modifier, layout, context):
main_col.label("Clickable Region:")
main_col.prop(modifier, "clickable_region", text="")
def linkingbookmod(modifier, layout, context):
def row_alert(prop_name, **kwargs):
row = layout.row()

27
korman/ui/modifiers/logic.py

@ -17,8 +17,20 @@ import bpy
from .. import ui_list
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:
# Using layout.prop on the pointer prevents clicking on the item O.o
layout.label(item.node_tree.name, icon="NODETREE")
@ -27,8 +39,15 @@ class LogicListUI(bpy.types.UIList):
def advanced_logic(modifier, layout, context):
ui_list.draw_modifier_list(layout, "LogicListUI", modifier, "logic_groups",
"active_group_index", rows=2, maxrows=3)
ui_list.draw_modifier_list(
layout,
"LogicListUI",
modifier,
"logic_groups",
"active_group_index",
rows=2,
maxrows=3,
)
# Modify the logic groups
if modifier.logic_groups:
@ -36,9 +55,11 @@ def advanced_logic(modifier, layout, context):
layout.row().prop_menu_enum(logic, "version")
layout.prop(logic, "node_tree", icon="NODETREE")
def spawnpoint(modifier, layout, context):
layout.label(text="Avatar faces negative Y.")
def maintainersmarker(modifier, layout, context):
layout.label(text="Positive Y is North, positive Z is up.")
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
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
def collision(modifier, layout, context):
layout.prop(modifier, "bounds")
layout.prop(modifier, "surface")
@ -45,6 +46,7 @@ def collision(modifier, layout, context):
row.active = modifier.bounds == "trimesh"
row.prop(modifier, "proxy_object")
def subworld_def(modifier, layout, context):
layout.prop(modifier, "sub_type")
if modifier.sub_type != "dynamicav":

18
korman/ui/modifiers/region.py

@ -16,6 +16,7 @@
import bpy
from .. import ui_camera
def camera_rgn(modifier, layout, context):
layout.prop(modifier, "camera_type")
if modifier.camera_type == "manual":
@ -29,20 +30,28 @@ def camera_rgn(modifier, layout, context):
layout.separator()
i(layout, cam_type, cam_props)
_draw_props(layout, (ui_camera.draw_camera_mode_props,
ui_camera.draw_camera_poa_props,
ui_camera.draw_camera_pos_props,
ui_camera.draw_camera_manipulation_props))
_draw_props(
layout,
(
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):
layout.prop(modifier, "bounds")
layout.prop(modifier, "surface")
def paniclink(modifier, layout, context):
phys_mod = context.object.plasma_modifiers.collision
layout.prop(phys_mod, "bounds")
layout.prop(modifier, "play_anim")
def softvolume(modifier, layout, context):
row = layout.row()
row.prop(modifier, "use_nodes", text="", icon="NODETREE")
@ -59,6 +68,7 @@ def softvolume(modifier, layout, context):
col.prop(modifier, "invert")
col.prop(modifier, "soft_distance")
def subworld_rgn(modifier, layout, context):
layout.prop(modifier, "subworld")
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 ...exporter.mesh import _VERTEX_COLOR_LAYERS
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:
layout.label("[No Object Specified]", icon="ERROR")
else:
@ -37,8 +49,15 @@ def blend(modifier, layout, context):
layout.separator()
layout.label("Render Dependencies:")
ui_list.draw_modifier_list(layout, "BlendOntoListUI", modifier, "dependencies",
"active_dependency_index", rows=2, maxrows=4)
ui_list.draw_modifier_list(
layout,
"BlendOntoListUI",
modifier,
"dependencies",
"active_dependency_index",
rows=2,
maxrows=4,
)
try:
dependency_ref = modifier.dependencies[modifier.active_dependency_index]
except:
@ -49,7 +68,18 @@ def blend(modifier, layout, context):
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:
layout.label(item.name, icon="BRUSH_DATA")
layout.prop(item, "enabled", text="")
@ -69,34 +99,54 @@ def decal_print(modifier, layout, context):
row.prop(modifier, "height")
layout.separator()
ui_list.draw_modifier_list(layout, "DecalMgrListUI", modifier, "managers",
"active_manager_index", rows=2, maxrows=3)
ui_list.draw_modifier_list(
layout,
"DecalMgrListUI",
modifier,
"managers",
"active_manager_index",
rows=2,
maxrows=3,
)
try:
mgr_ref = modifier.managers[modifier.active_manager_index]
except:
pass
else:
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.prop_search(mgr_ref, "name", scene, "decal_managers", icon="BRUSH_DATA")
layout.alert = False
def decal_receive(modifier, layout, context):
ui_list.draw_modifier_list(layout, "DecalMgrListUI", modifier, "managers",
"active_manager_index", rows=2, maxrows=3)
ui_list.draw_modifier_list(
layout,
"DecalMgrListUI",
modifier,
"managers",
"active_manager_index",
rows=2,
maxrows=3,
)
try:
mgr_ref = modifier.managers[modifier.active_manager_index]
except:
pass
else:
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.prop_search(mgr_ref, "name", scene, "decal_managers", icon="BRUSH_DATA")
def dynatext(modifier, layout, context):
col = layout.column()
col.alert = modifier.texture is None
@ -128,12 +178,16 @@ def dynatext(modifier, layout, context):
split = layout.split()
col = split.column(align=True)
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_bottom")
col = split.column(align=True)
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_right")
@ -142,6 +196,7 @@ def dynatext(modifier, layout, context):
flow.prop_menu_enum(modifier, "justify")
flow.prop(modifier, "line_spacing")
def fademod(modifier, layout, context):
layout.prop(modifier, "fader_type")
@ -162,17 +217,23 @@ def fademod(modifier, layout, context):
col.prop(modifier, "far_opaq")
col.prop(modifier, "far_trans")
if (modifier.fader_type in ("SimpleDist", "DistOpacity") and
not (modifier.near_trans <= modifier.near_opaq <= modifier.far_opaq <= modifier.far_trans)):
if modifier.fader_type in ("SimpleDist", "DistOpacity") and not (
modifier.near_trans
<= modifier.near_opaq
<= modifier.far_opaq
<= modifier.far_trans
):
# Warn the user that the values are not recommended.
layout.label("Distance values must be equal or increasing!", icon="ERROR")
def followmod(modifier, layout, context):
layout.row().prop(modifier, "follow_mode", expand=True)
layout.prop(modifier, "leader_type")
if modifier.leader_type == "kFollowObject":
layout.prop(modifier, "leader", icon="OUTLINER_OB_MESH")
def grass_shader(modifier, layout, context):
layout.prop(modifier, "wave_selector", icon="SMOOTHCURVE")
layout.separator()
@ -188,6 +249,7 @@ def grass_shader(modifier, layout, context):
col.prop(wave, "direction", text="")
box.prop(wave, "speed")
def lighting(modifier, layout, context):
split = layout.split()
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("Animated 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("Other Plasma lights {} be cast at runtime.".format("will" if modifier.rt_lights else "will NOT"),
icon="LAYER_USED")
col.label(
"Specular lights will be cast to specular materials at runtime.",
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"
if lightmap.enabled and lightmap.lights:
col.label("All '{}' lights will be baked to {}".format(lightmap.lights.name, map_type),
icon="LAYER_USED")
col.label(
"All '{}' lights will be baked to {}".format(
lightmap.lights.name, map_type
),
icon="LAYER_USED",
)
elif have_static_lights:
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:
col.label("No static lights will be baked.", icon="LAYER_USED")
def lightmap(modifier, layout, context):
pl_scene = context.scene.plasma_scene
is_texture = modifier.bake_type == "lightmap"
layout.prop(modifier, "bake_type")
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:
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.active = is_texture
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")
col = layout.column()
col.active = is_texture
@ -247,15 +340,23 @@ def lightmap(modifier, layout, context):
col.prop(modifier, "image", icon="IMAGE_RGB")
# 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")
else:
row = layout.row(align=True)
if modifier.bake_lightmap:
row.operator("object.plasma_lightmap_preview", "Preview", icon="RENDER_STILL").final = False
row.operator("object.plasma_lightmap_preview", "Bake for Export", icon="RENDER_STILL").final = True
row.operator(
"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:
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...
# 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:
tex = bpy.data.textures.get("LIGHTMAPGEN_PREVIEW")
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:
layout.template_preview(tex, show_buttons=False)
def rtshadow(modifier, layout, context):
split = layout.split()
col = split.column()
@ -279,6 +383,7 @@ def rtshadow(modifier, layout, context):
col.prop(modifier, "limit_resolution")
col.prop(modifier, "self_shadow")
def viewfacemod(modifier, layout, context):
layout.prop(modifier, "preset_options")
@ -302,8 +407,20 @@ def viewfacemod(modifier, layout, context):
col.enabled = modifier.offset
col.prop(modifier, "offset_coord")
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:
layout.label("[No Object Specified]", icon="ERROR")
else:
@ -312,12 +429,20 @@ class VisRegionListUI(bpy.types.UIList):
def visibility(modifier, layout, context):
ui_list.draw_modifier_list(layout, "VisRegionListUI", modifier, "regions",
"active_region_index", rows=2, maxrows=3)
ui_list.draw_modifier_list(
layout,
"VisRegionListUI",
modifier,
"regions",
"active_region_index",
rows=2,
maxrows=3,
)
if modifier.regions:
layout.prop(modifier.regions[modifier.active_region_index], "control_region")
def visregion(modifier, layout, context):
layout.prop(modifier, "mode")

36
korman/ui/modifiers/sound.py

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

32
korman/ui/modifiers/water.py

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

38
korman/ui/ui_anim.py

@ -17,20 +17,41 @@ import bpy
from . import ui_list
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")
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
# is a first class collection property. Fancy.
anims = getattr(prop_base, anims_collection_name)
kwargs.setdefault("rows", 2)
ui_list.draw_list(layout, "AnimListUI", context_attr, anims,
"animation_collection", "active_animation_index",
name_prop="animation_name", name_prefix="Animation",
**kwargs)
ui_list.draw_list(
layout,
"AnimListUI",
context_attr,
anims,
"animation_collection",
"active_animation_index",
name_prop="animation_name",
name_prefix="Animation",
**kwargs
)
try:
anim = anims.animation_collection[anims.active_animation_index]
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
draw_single_animation(sub, anim)
def draw_single_animation(layout, anim):
row = layout.row()
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)
if action:
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.active = anim.loop and not anim.sdl_var
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 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
if can_alert:
value = getattr(props, the_prop)
@ -31,6 +34,7 @@ def _draw_alert_prop(layout, props, the_prop, cam_type, alert_cam="", min=None,
else:
layout.prop(props, the_prop, **kwargs)
def _draw_gated_prop(layout, props, gate_prop, actual_prop):
row = layout.row(align=True)
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.prop(props, actual_prop)
def draw_camera_manipulation_props(layout, cam_type, props):
# Camera Panning
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_rate")
def draw_camera_mode_props(layout, cam_type, props):
# Point of Attention
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.prop(props, "ignore_subworld")
def draw_camera_poa_props(layout, cam_type, props):
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_worldspace")
def draw_camera_pos_props(layout, cam_type, props):
trans = props.transition
@ -111,12 +119,33 @@ def draw_camera_pos_props(layout, cam_type, props):
# Position Transitions
col.active = cam_type != "circle"
col.label("Default Position Transition:")
_draw_alert_prop(col, trans, "pos_acceleration", cam_type,
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")
_draw_alert_prop(
col,
trans,
"pos_acceleration",
cam_type,
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.active = cam_type in {"firstperson", "follow"}
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_worldspace")
def draw_circle_camera_props(layout, props):
# Circle Camera Stuff
layout.prop(props, "circle_center")
@ -137,6 +167,7 @@ def draw_circle_camera_props(layout, props):
row.active = props.circle_center is None
row.prop(props, "circle_radius_ui")
class CameraButtonsPanel:
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
@ -144,7 +175,7 @@ class CameraButtonsPanel:
@classmethod
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):
@ -189,7 +220,10 @@ class PlasmaCameraCirclePanel(CameraButtonsPanel, bpy.types.Panel):
@classmethod
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):
@ -204,7 +238,9 @@ class PlasmaCameraAnimationPanel(CameraButtonsPanel, bpy.types.Panel):
split = layout.split()
col = split.column()
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.prop(props, "start_on_push")
col.prop(props, "stop_on_pop")
@ -212,17 +248,24 @@ class PlasmaCameraAnimationPanel(CameraButtonsPanel, bpy.types.Panel):
col = split.column()
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.label("Rail:")
col.prop(props, "rail_pos", text="")
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):
self.layout.active = context.object.plasma_object.has_animation_data
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):
@ -234,7 +277,18 @@ class PlasmaCameraViewPanel(CameraButtonsPanel, bpy.types.Panel):
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:
layout.label("[Default Transition]")
else:
@ -249,8 +303,16 @@ class PlasmaCameraTransitionPanel(CameraButtonsPanel, bpy.types.Panel):
layout = self.layout
camera = context.camera.plasma_camera
ui_list.draw_list(layout, "TransitionListUI", "camera", camera, "transitions",
"active_transition_index", rows=3, maxrows=4)
ui_list.draw_list(
layout,
"TransitionListUI",
"camera",
camera,
"transitions",
"active_transition_index",
rows=3,
maxrows=4,
)
try:
item = camera.transitions[camera.active_transition_index]

1
korman/ui/ui_image.py

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

10
korman/ui/ui_lamp.py

@ -15,6 +15,7 @@
import bpy
class LampButtonsPanel:
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
@ -22,8 +23,11 @@ class LampButtonsPanel:
@classmethod
def poll(cls, context):
return (context.object and context.scene.render.engine == "PLASMA_GAME" and
isinstance(context.object.data, bpy.types.Lamp))
return (
context.object
and context.scene.render.engine == "PLASMA_GAME"
and isinstance(context.object.data, bpy.types.Lamp)
)
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(plasma_lamp, "size_height", text="H")
# Swap out the draw functions for the standard Area Shape panel
# TODO: Maybe we should consider standardizing an interface for overriding
# standard Blender panels? This seems like a really useful approach.
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 = _draw_area_lamp
del properties_data_lamp

49
korman/ui/ui_list.py

@ -15,28 +15,38 @@
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,
the parent datablock must be available in the context provided to operators. This should
always be true, but this is Blender...
Arguments:
- layout: required
- listtype: bpy.types.UIList subclass
- context_attr: attribute name to get the properties from in the current context
- prop_base: property group owning the collection
- collection_name: name of the collection property
- index_name: name of the active element index property
- name_prefix: (optional) prefix to apply to display name of new elements
- name_prop: (optional) property for each element's display name
*** any other arguments are passed as keyword arguments to the template_list call
the parent datablock must be available in the context provided to operators. This should
always be true, but this is Blender...
Arguments:
- layout: required
- listtype: bpy.types.UIList subclass
- context_attr: attribute name to get the properties from in the current context
- prop_base: property group owning the collection
- collection_name: name of the collection property
- index_name: name of the active element index property
- name_prefix: (optional) prefix to apply to display name of new elements
- name_prop: (optional) property for each element's display name
*** any other arguments are passed as keyword arguments to the template_list call
"""
prop_path = prop_base.path_from_id()
name_prefix = kwargs.pop("name_prefix", "")
name_prop = kwargs.pop("name_prop", "")
row = layout.row()
row.template_list(listtype, collection_name, prop_base, collection_name,
prop_base, index_name, **kwargs)
row.template_list(
listtype,
collection_name,
prop_base,
collection_name,
prop_base,
index_name,
**kwargs
)
col = row.column(align=True)
op = col.operator("ui.plasma_collection_add", icon="ZOOMIN", text="")
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.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 *
class PlasmaMenu:
@classmethod
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_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):
if context.scene.render.engine != "PLASMA_GAME":
@ -46,17 +49,25 @@ class PlasmaHelpMenu(PlasmaMenu, bpy.types.Menu):
def draw(self, context):
layout = self.layout
layout.operator("wm.url_open", text="About Korman", icon="URL").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"
layout.operator(
"wm.url_open", text="About Korman", icon="URL"
).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):
self.layout.menu("menu.plasma_help", text="Korman", icon="URL")
def register():
bpy.types.INFO_MT_add.append(PlasmaAddMenu.build_menu)
bpy.types.INFO_MT_help.prepend(PlasmaHelpMenu.build_menu)
def unregister():
bpy.types.INFO_MT_add.remove(PlasmaAddMenu.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
class ModifierButtonsPanel:
bl_space_type = "PROPERTIES"
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
# 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
# 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):
"""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()
# 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.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("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
row.operator(
"object.plasma_modifier_move_up", text="", icon="TRIA_UP"
).active_modifier = modifier.display_order
row.operator(
"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
# 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.separator()
layout.operator("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.operator(
"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.operator("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
layout.operator(
"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
class ObjectButtonsPanel:
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"

29
korman/ui/ui_render_layer.py

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

75
korman/ui/ui_scene.py

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

1
korman/ui/ui_text.py

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

30
korman/ui/ui_texture.py

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

118
korman/ui/ui_toolbox.py

@ -16,6 +16,7 @@
import bpy
import itertools
class ToolboxPanel:
bl_category = "Tools"
bl_space_type = "VIEW_3D"
@ -35,40 +36,113 @@ class PlasmaToolboxPanel(ToolboxPanel, bpy.types.Panel):
col = layout.column(align=True)
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
all_plasma_objects = all((i.plasma_object.enabled for i in bpy.context.selected_objects))
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")
all_plasma_objects = all(
(i.plasma_object.enabled for i in bpy.context.selected_objects)
)
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
col.label("Plasma Pages:")
col.operator("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.operator(
"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.operator("object.plasma_lightmap_bake", icon="RENDER_STILL", text="Bake All").bake_selection = False
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.operator(
"object.plasma_lightmap_bake", icon="RENDER_STILL", text="Bake All"
).bake_selection = False
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.operator("object.plasma_toggle_sound_export", icon="MUTE_IPO_OFF", text="Enable All").enable = True
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.operator(
"object.plasma_toggle_sound_export", icon="MUTE_IPO_OFF", text="Enable All"
).enable = True
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.operator("texture.plasma_enable_all_textures", icon="TEXTURE", text="Enable All")
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
col.operator(
"texture.plasma_enable_all_textures", icon="TEXTURE", text="Enable All"
)
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
col.label("Double Sided:")
col.operator("mesh.plasma_toggle_double_sided", icon="MESH_DATA", text="Disable All").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.operator(
"mesh.plasma_toggle_double_sided", icon="MESH_DATA", text="Disable All"
).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.operator("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")
col.operator(
"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"
age_path = self.format_path()
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:
op.actions = {"LAUNCH"}
op.dat_only = False
@ -133,8 +135,15 @@ class PlasmaGamePanel(AgeButtonsPanel, PlasmaGameHelper, bpy.types.Panel):
row = layout.row()
# Remember: game storage moved to addon preferences!
row.template_list("PlasmaGameListRO", "games", prefs, "games", games,
"active_game_index", rows=2)
row.template_list(
"PlasmaGameListRO",
"games",
prefs,
"games",
games,
"active_game_index",
rows=2,
)
row.operator("ui.korman_open_prefs", icon="PREFERENCES", text="")
layout.separator()
@ -168,20 +177,54 @@ class PlasmaGamePanel(AgeButtonsPanel, PlasmaGameHelper, bpy.types.Panel):
# Special Menu
row = row.row(align=True)
row.enabled = True
row.menu("PlasmaGameExportMenu", icon='DOWNARROW_HLT', text="")
row.menu("PlasmaGameExportMenu", icon="DOWNARROW_HLT", text="")
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")
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")
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, "enabled", text="")
@ -195,8 +238,9 @@ class PlasmaAgePanel(AgeButtonsPanel, bpy.types.Panel):
# We want a list of pages and an editor below that
row = layout.row()
row.template_list("PlasmaPageList", "pages", age, "pages", age,
"active_page_index", rows=2)
row.template_list(
"PlasmaPageList", "pages", age, "pages", age, "active_page_index", rows=2
)
col = row.column(align=True)
col.operator("world.plasma_page_add", icon="ZOOMIN", 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
legal_identifier = korlib.is_legal_python2_identifier(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]
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]
)
# Core settings
layout.separator()
@ -242,22 +289,36 @@ class PlasmaAgePanel(AgeButtonsPanel, bpy.types.Panel):
col.prop(age, "age_name", text="")
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]:
# 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
if illegal_age_name:
if not age.age_name:
layout.label(text="Age names cannot be empty", icon="ERROR")
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:
fixed_identifier = korlib.replace_python2_identifier(age.age_name)
layout.label(text="Age's SDL will use the name '{}'".format(fixed_identifier), icon="ERROR")
if '_' in age.age_name:
layout.label(text="Age names should not contain underscores", icon="ERROR")
layout.label(
text="Age's SDL will use the name '{}'".format(fixed_identifier),
icon="ERROR",
)
if "_" in age.age_name:
layout.label(
text="Age names should not contain underscores", icon="ERROR"
)
layout.separator()
split = layout.split()
@ -289,8 +350,14 @@ class PlasmaEnvironmentPanel(AgeButtonsPanel, bpy.types.Panel):
fni = context.world.plasma_fni
# 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:
layout.label(text="Fog Start Value should be less than the End Value", icon="ERROR")
if (
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
split = layout.split()

Loading…
Cancel
Save