From 74278b21baa242b29a3b58ccc7243c452fbe069d Mon Sep 17 00:00:00 2001 From: dhhruv <180320107529.ce.dhruv@gmail.com> Date: Fri, 15 Oct 2021 21:09:00 +0530 Subject: [PATCH] Files Reformatted and Restructured for Better Visibility and Understanding. --- korman/__init__.py | 12 +- korman/addon_prefs.py | 116 ++- korman/exporter/animation.py | 679 +++++++++++++---- korman/exporter/camera.py | 58 +- korman/exporter/convert.py | 88 ++- korman/exporter/decal.py | 55 +- korman/exporter/etlight.py | 156 +++- korman/exporter/explosions.py | 28 +- korman/exporter/image.py | 27 +- korman/exporter/locman.py | 123 ++- korman/exporter/logger.py | 73 +- korman/exporter/manager.py | 55 +- korman/exporter/material.py | 713 +++++++++++++----- korman/exporter/mesh.py | 205 +++-- korman/exporter/outfile.py | 136 +++- korman/exporter/physics.py | 116 ++- korman/exporter/python.py | 44 +- korman/exporter/rtlight.py | 52 +- korman/exporter/utils.py | 21 +- korman/helpers.py | 8 +- korman/idprops.py | 42 +- korman/korlib/__init__.py | 60 +- korman/korlib/console.py | 51 +- korman/korlib/python.py | 54 +- korman/korlib/texture.py | 82 +- korman/nodes/__init__.py | 15 +- korman/nodes/node_avatar.py | 577 +++++++++----- korman/nodes/node_conditions.py | 470 ++++++++---- korman/nodes/node_core.py | 152 ++-- korman/nodes/node_deprecated.py | 65 +- korman/nodes/node_logic.py | 153 ++-- korman/nodes/node_messages.py | 880 ++++++++++++++-------- korman/nodes/node_python.py | 381 +++++++--- korman/nodes/node_responder.py | 267 ++++--- korman/nodes/node_softvolume.py | 165 +++-- korman/operators/__init__.py | 2 + korman/operators/op_export.py | 389 +++++++--- korman/operators/op_image.py | 83 ++- korman/operators/op_lightmap.py | 55 +- korman/operators/op_mesh.py | 357 ++++++--- korman/operators/op_modifier.py | 74 +- korman/operators/op_nodes.py | 134 ++-- korman/operators/op_sound.py | 20 +- korman/operators/op_toolbox.py | 63 +- korman/operators/op_ui.py | 89 ++- korman/operators/op_world.py | 6 +- korman/ordered_set.py | 26 +- korman/plasma_attributes.py | 22 +- korman/plasma_launcher.py | 68 +- korman/properties/modifiers/__init__.py | 20 +- korman/properties/modifiers/anim.py | 155 ++-- korman/properties/modifiers/avatar.py | 140 ++-- korman/properties/modifiers/base.py | 90 ++- korman/properties/modifiers/gui.py | 630 ++++++++++------ korman/properties/modifiers/logic.py | 68 +- korman/properties/modifiers/physics.py | 130 +++- korman/properties/modifiers/region.py | 236 ++++-- korman/properties/modifiers/render.py | 948 +++++++++++++++--------- korman/properties/modifiers/sound.py | 467 ++++++++---- korman/properties/modifiers/water.py | 498 ++++++++----- korman/properties/prop_anim.py | 62 +- korman/properties/prop_camera.py | 529 ++++++++----- korman/properties/prop_image.py | 23 +- korman/properties/prop_lamp.py | 105 ++- korman/properties/prop_object.py | 76 +- korman/properties/prop_scene.py | 182 +++-- korman/properties/prop_sound.py | 11 +- korman/properties/prop_text.py | 7 +- korman/properties/prop_texture.py | 179 +++-- korman/properties/prop_world.py | 222 +++--- korman/render.py | 8 + korman/ui/__init__.py | 1 + korman/ui/modifiers/anim.py | 69 +- korman/ui/modifiers/avatar.py | 2 + korman/ui/modifiers/gui.py | 35 +- korman/ui/modifiers/logic.py | 27 +- korman/ui/modifiers/physics.py | 2 + korman/ui/modifiers/region.py | 18 +- korman/ui/modifiers/render.py | 187 ++++- korman/ui/modifiers/sound.py | 36 +- korman/ui/modifiers/water.py | 32 +- korman/ui/ui_anim.py | 38 +- korman/ui/ui_camera.py | 94 ++- korman/ui/ui_image.py | 1 + korman/ui/ui_lamp.py | 10 +- korman/ui/ui_list.py | 49 +- korman/ui/ui_menus.py | 19 +- korman/ui/ui_modifiers.py | 43 +- korman/ui/ui_object.py | 1 + korman/ui/ui_render_layer.py | 29 +- korman/ui/ui_scene.py | 75 +- korman/ui/ui_text.py | 1 + korman/ui/ui_texture.py | 30 +- korman/ui/ui_toolbox.py | 118 ++- korman/ui/ui_world.py | 105 ++- 95 files changed, 9250 insertions(+), 4125 deletions(-) diff --git a/korman/__init__.py b/korman/__init__.py index 824cfd8..b3d49f7 100644 --- a/korman/__init__.py +++ b/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", } diff --git a/korman/addon_prefs.py b/korman/addon_prefs.py index 5bcc5a5..be136eb 100644 --- a/korman/addon_prefs.py +++ b/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) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index 60b3051..a06b907 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -27,6 +27,7 @@ from PyHSPlasma import * from . import utils + class AnimationConverter: def __init__(self, exporter): self._exporter = weakref.ref(exporter) @@ -35,8 +36,15 @@ class AnimationConverter: def convert_frame_time(self, frame_num: int) -> float: return frame_num / self._bl_fps - def convert_object_animations(self, bo: bpy.types.Object, so: plSceneObject, anim_name: str, *, - start: Optional[int] = None, end: Optional[int] = None) -> Iterable[plAGApplicator]: + def convert_object_animations( + self, + bo: bpy.types.Object, + so: plSceneObject, + anim_name: str, + *, + start: Optional[int] = None, + end: Optional[int] = None + ) -> Iterable[plAGApplicator]: if not bo.plasma_object.has_animation_data: return [] @@ -56,30 +64,74 @@ class AnimationConverter: # things that aren't the typical position, rotation, scale animations. applicators = [] if isinstance(bo.data, bpy.types.Camera): - applicators.append(self._convert_camera_animation(bo, so, obj_fcurves, data_fcurves, anim_name, start, end)) + applicators.append( + self._convert_camera_animation( + bo, so, obj_fcurves, data_fcurves, anim_name, start, end + ) + ) else: - applicators.append(self._convert_transform_animation(bo, obj_fcurves, bo.matrix_local, bo.matrix_parent_inverse, start=start, end=end)) + applicators.append( + self._convert_transform_animation( + bo, + obj_fcurves, + bo.matrix_local, + bo.matrix_parent_inverse, + start=start, + end=end, + ) + ) if bo.plasma_modifiers.soundemit.enabled: - applicators.extend(self._convert_sound_volume_animation(bo.name, obj_fcurves, bo.plasma_modifiers.soundemit, start, end)) + applicators.extend( + self._convert_sound_volume_animation( + bo.name, obj_fcurves, bo.plasma_modifiers.soundemit, start, end + ) + ) if isinstance(bo.data, bpy.types.Lamp): lamp = bo.data - applicators.extend(self._convert_lamp_color_animation(bo.name, data_fcurves, lamp, start, end)) + applicators.extend( + self._convert_lamp_color_animation( + bo.name, data_fcurves, lamp, start, end + ) + ) if isinstance(lamp, bpy.types.SpotLamp): - applicators.extend(self._convert_spot_lamp_animation(bo.name, data_fcurves, lamp, start, end)) + applicators.extend( + self._convert_spot_lamp_animation( + bo.name, data_fcurves, lamp, start, end + ) + ) if isinstance(lamp, bpy.types.PointLamp): - applicators.extend(self._convert_omni_lamp_animation(bo.name, data_fcurves, lamp, start, end)) + applicators.extend( + self._convert_omni_lamp_animation( + bo.name, data_fcurves, lamp, start, end + ) + ) return [i for i in applicators if i is not None] - def _convert_camera_animation(self, bo, so, obj_fcurves, data_fcurves, anim_name: str, - start: Optional[int], end: Optional[int]): + def _convert_camera_animation( + self, + bo, + so, + obj_fcurves, + data_fcurves, + anim_name: str, + start: Optional[int], + end: Optional[int], + ): has_fov_anim = False if data_fcurves: # The hard part about this crap is that FOV animations are not stored in ATC Animations # instead, FOV animation keyframes are held inside of the camera modifier. Cyan's solution # in PlasmaMAX appears to be for any xform keyframe, add two messages to the camera modifier # representing the FOV at that point. Makes more sense to me to use each FOV keyframe instead - fov_fcurve = next((i for i in data_fcurves if i.data_path == "plasma_camera.settings.fov"), None) + fov_fcurve = next( + ( + i + for i in data_fcurves + if i.data_path == "plasma_camera.settings.fov" + ), + None, + ) if fov_fcurve: # NOTE: this is another critically important key ordering in the SceneObject modifier # list. CameraModifier calls into AGMasterMod code that assumes the AGModifier @@ -92,7 +144,6 @@ class AnimationConverter: degrees = math.degrees fov_fcurve.update() - # Well, now that we have multiple animations, we are using our fancier FCurve processing. # Unfortunately, the code still looks like sin. What can you do? keyframes, _ = self._process_fcurve(fov_fcurve, start=start, end=end) @@ -104,7 +155,9 @@ class AnimationConverter: # So remember, these are messages. When we hit a keyframe, we're dispatching a message # representing the NEXT desired FOV. this_keyframe = keyframes[i] - next_keyframe = keyframes[0] if i+1 == num_keyframes else keyframes[i+1] + next_keyframe = ( + keyframes[0] if i + 1 == num_keyframes else keyframes[i + 1] + ) # This message is held on the camera modifier and sent to the animation... It calls # back when the animation reaches the keyframe time, causing the FOV message to be sent. @@ -128,12 +181,17 @@ class AnimationConverter: # export-time and while playing the game, the camera modifier just steals its # parameters and passes them to the brain. Can't make this stuff up. # Be sure to only export each instruction once. - if not any((msg.config.accel == next_keyframe.frame_time for msg in camera.fovInstructions)): + if not any( + ( + msg.config.accel == next_keyframe.frame_time + for msg in camera.fovInstructions + ) + ): cam_msg = plCameraMsg() cam_msg.addReceiver(cam_key) cam_msg.setCmd(plCameraMsg.kAddFOVKeyFrame, True) cam_config = cam_msg.config - cam_config.accel = next_keyframe.frame_time # Yassss... + cam_config.accel = next_keyframe.frame_time # Yassss... cam_config.fovW = degrees(next_keyframe.values[0]) cam_config.fovH = degrees(next_keyframe.values[0] * aspect) camera.addFOVInstruction(cam_msg) @@ -143,8 +201,15 @@ class AnimationConverter: # If we exported any FOV animation at all, then we need to ensure there is an applicator # returned from here... At bare minimum, we'll need the applicator with an empty # CompoundController. This should be sufficient to keep CWE from crashing... - applicator = self._convert_transform_animation(bo, obj_fcurves, bo.matrix_local, bo.matrix_parent_inverse, - allow_empty=has_fov_anim, start=start, end=end) + applicator = self._convert_transform_animation( + bo, + obj_fcurves, + bo.matrix_local, + bo.matrix_parent_inverse, + allow_empty=has_fov_anim, + start=start, + end=end, + ) camera = self._mgr.find_create_object(plCameraModifier, so=so) camera.animated = applicator is not None return applicator @@ -153,15 +218,25 @@ class AnimationConverter: if not fcurves: return None - energy_curve = next((i for i in fcurves if i.data_path == "energy" and i.keyframe_points), None) - color_curves = [i for i in fcurves if i.data_path == "color" and i.keyframe_points] + energy_curve = next( + (i for i in fcurves if i.data_path == "energy" and i.keyframe_points), None + ) + color_curves = [ + i for i in fcurves if i.data_path == "color" and i.keyframe_points + ] if energy_curve is None and color_curves is None: return None elif lamp.use_only_shadow: - self._exporter().report.warn("Cannot animate Lamp color because this lamp only casts shadows", indent=3) + self._exporter().report.warn( + "Cannot animate Lamp color because this lamp only casts shadows", + indent=3, + ) return None elif not lamp.use_specular and not lamp.use_diffuse: - self._exporter().report.warn("Cannot animate Lamp color because neither Diffuse nor Specular are enabled", indent=3) + self._exporter().report.warn( + "Cannot animate Lamp color because neither Diffuse nor Specular are enabled", + indent=3, + ) return None # OK Specular is easy. We just toss out the color as a point3. @@ -170,12 +245,20 @@ class AnimationConverter: return map(lambda x: x * -1.0, color) else: return color - color_keyframes, color_bez = self._process_keyframes(color_curves, 3, lamp.color, - convert=convert_specular_animation, - start=start, end=end) + + color_keyframes, color_bez = self._process_keyframes( + color_curves, + 3, + lamp.color, + convert=convert_specular_animation, + start=start, + end=end, + ) if color_keyframes and lamp.use_specular: channel = plPointControllerChannel() - channel.controller = self._make_point3_controller(color_keyframes, color_bez) + channel.controller = self._make_point3_controller( + color_keyframes, color_bez + ) applicator = plLightSpecularApplicator() applicator.channelName = name applicator.channel = channel @@ -188,12 +271,21 @@ class AnimationConverter: else: proc = lambda x: x * energy[0] return map(proc, color) + diffuse_channels = dict(color=3, energy=1) diffuse_defaults = dict(color=lamp.color, energy=lamp.energy) - diffuse_fcurves = color_curves + [energy_curve,] - diffuse_keyframes = self._process_fcurves(diffuse_fcurves, diffuse_channels, 3, - convert_diffuse_animation, diffuse_defaults, - start=start, end=end) + diffuse_fcurves = color_curves + [ + energy_curve, + ] + diffuse_keyframes = self._process_fcurves( + diffuse_fcurves, + diffuse_channels, + 3, + convert_diffuse_animation, + diffuse_defaults, + start=start, + end=end, + ) if not diffuse_keyframes: return None @@ -227,9 +319,12 @@ class AnimationConverter: # All types allow animating cutoff if distance_fcurve is not None: channel = plScalarControllerChannel() - channel.controller = self.make_scalar_leaf_controller(distance_fcurve, - lambda x: x if lamp.use_sphere else x * 2, - start=start, end=end) + channel.controller = self.make_scalar_leaf_controller( + distance_fcurve, + lambda x: x if lamp.use_sphere else x * 2, + start=start, + end=end, + ) applicator = plOmniCutoffApplicator() applicator.channelName = name applicator.channel = channel @@ -238,10 +333,19 @@ class AnimationConverter: falloff = lamp.falloff_type if falloff == "CONSTANT": if energy_fcurve is not None: - report.warn("Constant attenuation cannot be animated in Plasma", ident=3) + report.warn( + "Constant attenuation cannot be animated in Plasma", ident=3 + ) elif falloff == "INVERSE_LINEAR": - keyframes = self._process_fcurves(omni_fcurves, omni_channels, 1, convert_omni_atten, - omni_defaults, start=start, end=end) + keyframes = self._process_fcurves( + omni_fcurves, + omni_channels, + 1, + convert_omni_atten, + omni_defaults, + start=start, + end=end, + ) if keyframes: channel = plScalarControllerChannel() channel.controller = self._make_scalar_leaf_controller(keyframes, False) @@ -251,30 +355,51 @@ class AnimationConverter: yield applicator elif falloff == "INVERSE_SQUARE": if self._mgr.getVer() >= pvMoul: - report.port("Lamp {} Falloff animations are only supported in Myst Online: Uru Live", falloff, indent=3) - keyframes = self._process_fcurves(omni_fcurves, omni_channels, 1, convert_omni_atten, - omni_defaults, start=start, end=end) + report.port( + "Lamp {} Falloff animations are only supported in Myst Online: Uru Live", + falloff, + indent=3, + ) + keyframes = self._process_fcurves( + omni_fcurves, + omni_channels, + 1, + convert_omni_atten, + omni_defaults, + start=start, + end=end, + ) if keyframes: channel = plScalarControllerChannel() - channel.controller = self._make_scalar_leaf_controller(keyframes, False) + channel.controller = self._make_scalar_leaf_controller( + keyframes, False + ) applicator = plOmniSqApplicator() applicator.channelName = name applicator.channel = channel yield applicator else: - report.warn("Lamp {} Falloff animations are not supported for this version of Plasma", falloff, indent=3) + report.warn( + "Lamp {} Falloff animations are not supported for this version of Plasma", + falloff, + indent=3, + ) else: - report.warn("Lamp Falloff '{}' animations are not supported", falloff, ident=3) + report.warn( + "Lamp Falloff '{}' animations are not supported", falloff, ident=3 + ) def _convert_sound_volume_animation(self, name, fcurves, soundemit, start, end): if not fcurves: return None - convert_volume = lambda x: math.log10(max(.01, x / 100.0)) * 20.0 + convert_volume = lambda x: math.log10(max(0.01, x / 100.0)) * 20.0 for sound in soundemit.sounds: path = "{}.volume".format(sound.path_from_id()) - fcurve = next((i for i in fcurves if i.data_path == path and i.keyframe_points), None) + fcurve = next( + (i for i in fcurves if i.data_path == path and i.keyframe_points), None + ) if fcurve is None: continue @@ -287,15 +412,20 @@ class AnimationConverter: # so yes, we must convert the same animation data again and again. # To make matters worse, the way that these keyframes are stored can cause # the animation to evaluate to a no-op. Be ready for that. - controller = self.make_scalar_leaf_controller(fcurve, convert=convert_volume, start=start, end=end) + controller = self.make_scalar_leaf_controller( + fcurve, convert=convert_volume, start=start, end=end + ) if controller is not None: channel = plScalarControllerChannel() channel.controller = controller applicator.channel = channel yield applicator else: - self._exporter().report.warn("[{}]: Volume animation evaluated to zero keyframes!", - sound.sound.name, indent=2) + self._exporter().report.warn( + "[{}]: Volume animation evaluated to zero keyframes!", + sound.sound.name, + indent=2, + ) break def _convert_spot_lamp_animation(self, name, fcurves, lamp, start, end): @@ -310,8 +440,9 @@ class AnimationConverter: # Spot Outer is just the size keyframes... if size_fcurve is not None: channel = plScalarControllerChannel() - channel.controller = self.make_scalar_leaf_controller(size_fcurve, lambda x: math.degrees(x), - start=start, end=end) + channel.controller = self.make_scalar_leaf_controller( + size_fcurve, lambda x: math.degrees(x), start=start, end=end + ) applicator = plSpotOuterApplicator() applicator.channelName = name applicator.channel = channel @@ -327,8 +458,15 @@ class AnimationConverter: inner_fcurves = [blend_fcurve, size_fcurve] inner_channels = dict(spot_blend=1, spot_size=1) inner_defaults = dict(spot_blend=lamp.spot_blend, spot_size=lamp.spot_size) - keyframes = self._process_fcurves(inner_fcurves, inner_channels, 1, convert_spot_inner, - inner_defaults, start=start, end=end) + keyframes = self._process_fcurves( + inner_fcurves, + inner_channels, + 1, + convert_spot_inner, + inner_defaults, + start=start, + end=end, + ) if keyframes: channel = plScalarControllerChannel() @@ -338,17 +476,38 @@ class AnimationConverter: applicator.channel = channel yield applicator - def _convert_transform_animation(self, bo, fcurves, default_xform, adjust_xform, *, allow_empty: Optional[bool] = False, - start: Optional[int] = None, end: Optional[int] = None) -> Optional[plMatrixChannelApplicator]: + def _convert_transform_animation( + self, + bo, + fcurves, + default_xform, + adjust_xform, + *, + allow_empty: Optional[bool] = False, + start: Optional[int] = None, + end: Optional[int] = None + ) -> Optional[plMatrixChannelApplicator]: if adjust_xform != mathutils.Matrix.Identity(4): - self._exporter().report.warn(("{}: Transform animation is not local and may export incorrectly. " + - "Please use Alt-P -> Clear Parent Inverse before animating objects to avoid issues.").format(bo.name), indent=1) + self._exporter().report.warn( + ( + "{}: Transform animation is not local and may export incorrectly. " + + "Please use Alt-P -> Clear Parent Inverse before animating objects to avoid issues." + ).format(bo.name), + indent=1, + ) else: # Adjustment matrix is identity, just pass None instead... adjust_xform = None - tm = self.convert_transform_controller(fcurves, bo.rotation_mode, default_xform, adjust_xform, allow_empty=allow_empty, - start=start, end=end) + tm = self.convert_transform_controller( + fcurves, + bo.rotation_mode, + default_xform, + adjust_xform, + allow_empty=allow_empty, + start=start, + end=end, + ) if tm is None and not allow_empty: return None @@ -362,10 +521,17 @@ class AnimationConverter: return applicator - def convert_transform_controller(self, fcurves, rotation_mode: str, default_xform, adjust_xform, *, - allow_empty: Optional[bool] = False, - start: Optional[int] = None, - end: Optional[int] = None) -> Union[None, plCompoundController]: + def convert_transform_controller( + self, + fcurves, + rotation_mode: str, + default_xform, + adjust_xform, + *, + allow_empty: Optional[bool] = False, + start: Optional[int] = None, + end: Optional[int] = None + ) -> Union[None, plCompoundController]: if not fcurves and not allow_empty: return None @@ -384,14 +550,16 @@ class AnimationConverter: def convert_rot_keyframe(rot): # Rotation: may cause issues if scale is present. - if isinstance(rot, mathutils.Quaternion): # quaternion from an axis-angle + if isinstance( + rot, mathutils.Quaternion + ): # quaternion from an axis-angle return adjust_rotation * rot elif isinstance(rot, mathutils.Euler): return (adjust_rotation * rot.to_quaternion()).to_euler(rot.order) - else: # tuple - if len(rot) == 4: # quat in a tuple + else: # tuple + if len(rot) == 4: # quat in a tuple return (adjust_rotation * mathutils.Quaternion(rot))[:] - else: # XYZ euler in a tuple + else: # XYZ euler in a tuple rot = mathutils.Euler(rot, "XYZ").to_quaternion() return (adjust_rotation * rot).to_euler("XYZ")[:] @@ -408,12 +576,30 @@ class AnimationConverter: convert_rot = None convert_scale = None - pos = self.make_pos_controller(fcurves, "location", default_xform.to_translation(), - convert=convert_pos, start=start, end=end) - rot = self.make_rot_controller(fcurves, rotation_mode, default_xform, - convert=convert_rot, start=start, end=end) - scale = self.make_scale_controller(fcurves, "scale", default_xform.to_scale(), - convert=convert_scale, start=start, end=end) + pos = self.make_pos_controller( + fcurves, + "location", + default_xform.to_translation(), + convert=convert_pos, + start=start, + end=end, + ) + rot = self.make_rot_controller( + fcurves, + rotation_mode, + default_xform, + convert=convert_rot, + start=start, + end=end, + ) + scale = self.make_scale_controller( + fcurves, + "scale", + default_xform.to_scale(), + convert=convert_scale, + start=start, + end=end, + ) if pos is None and rot is None and scale is None: if not allow_empty: return None @@ -429,7 +615,9 @@ class AnimationConverter: master = self._mgr.find_create_key(plAGMasterMod, so=so, bl=bo) return mod, master - def get_anigraph_objects(self, bo=None, so=None) -> Tuple[plAGModifier, plAGMasterMod]: + def get_anigraph_objects( + self, bo=None, so=None + ) -> Tuple[plAGModifier, plAGMasterMod]: mod = self._mgr.find_create_object(plAGModifier, so=so, bl=bo) master = self._mgr.find_create_object(plAGMasterMod, so=so, bl=bo) return mod, master @@ -440,13 +628,20 @@ class AnimationConverter: # (but obviously this is not wrong...) group_mod = bo.plasma_modifiers.animation_group if group_mod.enabled: - return self._mgr.find_create_key(plMsgForwarder, bl=bo, so=so, name=group_mod.key_name) + return self._mgr.find_create_key( + plMsgForwarder, bl=bo, so=so, name=group_mod.key_name + ) else: return self.get_anigraph_keys(bo, so)[1] - def get_frame_time_range(self, *anims: Iterable[Union[plAGApplicator, plController]], - so: Optional[plSceneObject] = None, name: Optional[str] = None) -> Tuple[int, int]: + def get_frame_time_range( + self, + *anims: Iterable[Union[plAGApplicator, plController]], + so: Optional[plSceneObject] = None, + name: Optional[str] = None + ) -> Tuple[int, int]: """Determines the range of frame numbers in an exported animation.""" + def iter_frame_times(): nonlocal name for anim in anims: @@ -456,7 +651,9 @@ class AnimationConverter: # Maybe a camera FOV thing, or something. continue - def iter_leaves(ctrl: Optional[plController]) -> Iterator[plLeafController]: + def iter_leaves( + ctrl: Optional[plController], + ) -> Iterator[plLeafController]: if ctrl is None: return elif isinstance(ctrl, plCompoundController): @@ -468,7 +665,9 @@ class AnimationConverter: else: raise ValueError(ctrl) - yield from (key.frameTime for leaf in iter_leaves(anim) for key in leaf.keys[0]) + yield from ( + key.frameTime for leaf in iter_leaves(anim) for key in leaf.keys[0] + ) # Special case: camera animations are over on the plCameraModifier. Grr. if so is not None: @@ -476,47 +675,81 @@ class AnimationConverter: if camera is not None: if not name: name = "(Entire Animation)" - yield from (msg.time for msg, _ in camera.messageQueue if isinstance(msg, plAnimCmdMsg) and msg.animName == name) + yield from ( + msg.time + for msg, _ in camera.messageQueue + if isinstance(msg, plAnimCmdMsg) and msg.animName == name + ) try: return min(iter_frame_times()), max(iter_frame_times()) except ValueError: return 0.0, 0.0 - def make_matrix44_controller(self, fcurves, pos_path: str, scale_path: str, pos_default, scale_default, - *, start: Optional[int] = None, end: Optional[int] = None) -> Optional[plLeafController]: + def make_matrix44_controller( + self, + fcurves, + pos_path: str, + scale_path: str, + pos_default, + scale_default, + *, + start: Optional[int] = None, + end: Optional[int] = None + ) -> Optional[plLeafController]: def convert_matrix_keyframe(**kwargs) -> hsMatrix44: pos = kwargs[pos_path] scale = kwargs[scale_path] - translation = hsVector3(pos[0] - (scale[0] - 1.0) / 2.0, - -pos[1] - (scale[1] - 1.0) / 2.0, - pos[2] - (scale[2] - 1.0) / 2.0) + translation = hsVector3( + pos[0] - (scale[0] - 1.0) / 2.0, + -pos[1] - (scale[1] - 1.0) / 2.0, + pos[2] - (scale[2] - 1.0) / 2.0, + ) matrix = hsMatrix44() matrix.setTranslate(translation) matrix.setScale(hsVector3(*scale)) return matrix - fcurves = [i for i in fcurves if i.data_path == pos_path or i.data_path == scale_path] + fcurves = [ + i for i in fcurves if i.data_path == pos_path or i.data_path == scale_path + ] if not fcurves: return None - channels = { pos_path: 3, scale_path: 3 } - default_values = { pos_path: pos_default, scale_path: scale_default } - keyframes = self._process_fcurves(fcurves, channels, 1, convert_matrix_keyframe, - default_values, start=start, end=end) + channels = {pos_path: 3, scale_path: 3} + default_values = {pos_path: pos_default, scale_path: scale_default} + keyframes = self._process_fcurves( + fcurves, + channels, + 1, + convert_matrix_keyframe, + default_values, + start=start, + end=end, + ) if not keyframes: return None # Now we make the controller return self._make_matrix44_controller(keyframes) - def make_pos_controller(self, fcurves, data_path: str, default_xform, - convert: Optional[Callable] = None, *, start: Optional[int] = None, - end: Optional[int] = None) -> Optional[plLeafController]: - pos_curves = [i for i in fcurves if i.data_path == data_path and i.keyframe_points] - keyframes, bez_chans = self._process_keyframes(pos_curves, 3, default_xform, convert, - start=start, end=end) + def make_pos_controller( + self, + fcurves, + data_path: str, + default_xform, + convert: Optional[Callable] = None, + *, + start: Optional[int] = None, + end: Optional[int] = None + ) -> Optional[plLeafController]: + pos_curves = [ + i for i in fcurves if i.data_path == data_path and i.keyframe_points + ] + keyframes, bez_chans = self._process_keyframes( + pos_curves, 3, default_xform, convert, start=start, end=end + ) if not keyframes: return None @@ -525,34 +758,58 @@ class AnimationConverter: ctrl = self._make_point3_controller(keyframes, bez_chans) return ctrl - def make_rot_controller(self, fcurves, rotation_mode: str, default_xform, - convert: Optional[Callable] = None, *, start: Optional[int] = None, - end: Optional[int] = None) -> Union[None, plCompoundController, plLeafController]: + def make_rot_controller( + self, + fcurves, + rotation_mode: str, + default_xform, + convert: Optional[Callable] = None, + *, + start: Optional[int] = None, + end: Optional[int] = None + ) -> Union[None, plCompoundController, plLeafController]: if rotation_mode in {"AXIS_ANGLE", "QUATERNION"}: - rot_curves = [i for i in fcurves if i.data_path == "rotation_{}".format(rotation_mode.lower()) and i.keyframe_points] + rot_curves = [ + i + for i in fcurves + if i.data_path == "rotation_{}".format(rotation_mode.lower()) + and i.keyframe_points + ] if not rot_curves: return None default_xform = default_xform.to_quaternion() if rotation_mode == "AXIS_ANGLE": default_xform = default_xform.to_axis_angle() - default_xform = (default_xform[1], default_xform[0].x, default_xform[0].y, default_xform[0].z) + default_xform = ( + default_xform[1], + default_xform[0].x, + default_xform[0].y, + default_xform[0].z, + ) if convert is not None: convert_original = convert - convert = lambda x: convert_original(mathutils.Quaternion(x[1:4], x[0]))[:] + convert = lambda x: convert_original( + mathutils.Quaternion(x[1:4], x[0]) + )[:] else: convert = lambda x: mathutils.Quaternion(x[1:4], x[0])[:] # Just dropping bezier stuff on the floor because Plasma does not support it, and # I think that opting into quaternion keyframes is a good enough indication that # you're OK with that. - keyframes, bez_chans = self._process_keyframes(rot_curves, 4, default_xform, convert, - start=start, end=end) + keyframes, bez_chans = self._process_keyframes( + rot_curves, 4, default_xform, convert, start=start, end=end + ) if keyframes: return self._make_quat_controller(keyframes) else: - rot_curves = [i for i in fcurves if i.data_path == "rotation_euler" and i.keyframe_points] + rot_curves = [ + i + for i in fcurves + if i.data_path == "rotation_euler" and i.keyframe_points + ] if not rot_curves: return None @@ -567,9 +824,17 @@ class AnimationConverter: result = convert(result) return result[:] - euler_convert = convert_euler_keyframe if rotation_mode != "XYZ" else convert - keyframes, bez_chans = self._process_keyframes(rot_curves, 3, default_xform.to_euler(rotation_mode), - euler_convert, start=start, end=end) + euler_convert = ( + convert_euler_keyframe if rotation_mode != "XYZ" else convert + ) + keyframes, bez_chans = self._process_keyframes( + rot_curves, + 3, + default_xform.to_euler(rotation_mode), + euler_convert, + start=start, + end=end, + ) if keyframes: # Once again, quaternion keyframes do not support bezier interpolation. Ideally, # we would just drop support for rotation beziers entirely to simplify all this @@ -579,12 +844,22 @@ class AnimationConverter: else: return self._make_quat_controller(keyframes) - def make_scale_controller(self, fcurves, data_path: str, default_xform, - convert: Optional[Callable] = None, *, start: Optional[int] = None, - end: Optional[int] = None) -> Optional[plLeafController]: - scale_curves = [i for i in fcurves if i.data_path == data_path and i.keyframe_points] - keyframes, bez_chans = self._process_keyframes(scale_curves, 3, default_xform, convert, - start=start, end=end) + def make_scale_controller( + self, + fcurves, + data_path: str, + default_xform, + convert: Optional[Callable] = None, + *, + start: Optional[int] = None, + end: Optional[int] = None + ) -> Optional[plLeafController]: + scale_curves = [ + i for i in fcurves if i.data_path == data_path and i.keyframe_points + ] + keyframes, bez_chans = self._process_keyframes( + scale_curves, 3, default_xform, convert, start=start, end=end + ) if not keyframes: return None @@ -592,10 +867,14 @@ class AnimationConverter: ctrl = self._make_scale_value_controller(keyframes, bez_chans) return ctrl - def make_scalar_leaf_controller(self, fcurve: bpy.types.FCurve, - convert: Optional[Callable] = None, *, - start: Optional[int] = None, - end: Optional[int] = None) -> Optional[plLeafController]: + def make_scalar_leaf_controller( + self, + fcurve: bpy.types.FCurve, + convert: Optional[Callable] = None, + *, + start: Optional[int] = None, + end: Optional[int] = None + ) -> Optional[plLeafController]: keyframes, bezier = self._process_fcurve(fcurve, convert, start=start, end=end) if not keyframes: return None @@ -620,7 +899,9 @@ class AnimationConverter: def _make_point3_controller(self, keyframes, bezier) -> plLeafController: ctrl = plLeafController() - keyframe_type = hsKeyFrame.kBezPoint3KeyFrame if bezier else hsKeyFrame.kPoint3KeyFrame + keyframe_type = ( + hsKeyFrame.kBezPoint3KeyFrame if bezier else hsKeyFrame.kPoint3KeyFrame + ) exported_frames = [] for keyframe in keyframes: @@ -659,12 +940,18 @@ class AnimationConverter: value.normalize() exported.value = utils.quaternion(value) else: - raise ValueError("Unexpected number of channels in quaternion keyframe {}".format(num_channels)) + raise ValueError( + "Unexpected number of channels in quaternion keyframe {}".format( + num_channels + ) + ) exported_frames.append(exported) ctrl.keys = (exported_frames, keyframe_type) return ctrl - def _make_scalar_compound_controller(self, keyframes, bez_chans) -> plCompoundController: + def _make_scalar_compound_controller( + self, keyframes, bez_chans + ) -> plCompoundController: ctrl = plCompoundController() subctrls = ("X", "Y", "Z") for i in subctrls: @@ -673,7 +960,11 @@ class AnimationConverter: for keyframe in keyframes: for i, subctrl in enumerate(subctrls): - keyframe_type = hsKeyFrame.kBezScalarKeyFrame if i in bez_chans else hsKeyFrame.kScalarKeyFrame + keyframe_type = ( + hsKeyFrame.kBezScalarKeyFrame + if i in bez_chans + else hsKeyFrame.kScalarKeyFrame + ) exported = hsScalarKey() exported.frame = keyframe.frame_num exported.frameTime = keyframe.frame_time @@ -689,7 +980,9 @@ class AnimationConverter: def _make_scalar_leaf_controller(self, keyframes, bezier) -> plLeafController: ctrl = plLeafController() - keyframe_type = hsKeyFrame.kBezScalarKeyFrame if bezier else hsKeyFrame.kScalarKeyFrame + keyframe_type = ( + hsKeyFrame.kBezScalarKeyFrame if bezier else hsKeyFrame.kScalarKeyFrame + ) exported_frames = [] for keyframe in keyframes: @@ -705,7 +998,9 @@ class AnimationConverter: return ctrl def _make_scale_value_controller(self, keyframes, bez_chans) -> plLeafController: - keyframe_type = hsKeyFrame.kBezScaleKeyFrame if bez_chans else hsKeyFrame.kScaleKeyFrame + keyframe_type = ( + hsKeyFrame.kBezScaleKeyFrame if bez_chans else hsKeyFrame.kScaleKeyFrame + ) exported_frames = [] # Hmm... This smells... But it was basically doing this before the rewrite. @@ -727,7 +1022,7 @@ class AnimationConverter: def _sort_and_dedupe_keyframes(self, keyframes: Dict) -> Sequence: """Takes in the final, unsorted keyframe sequence and sorts it. If all keyframes are - equivalent, eg due to a convert function, then they are discarded.""" + equivalent, eg due to a convert function, then they are discarded.""" num_keyframes = len(keyframes) keyframes_sorted = [keyframes[i] for i in sorted(keyframes)] @@ -736,17 +1031,32 @@ class AnimationConverter: def filter_boundaries(i): if i == 0 or i == num_keyframes - 1: return False - left, me, right = keyframes_sorted[i - 1], keyframes_sorted[i], keyframes_sorted[i + 1] + left, me, right = ( + keyframes_sorted[i - 1], + keyframes_sorted[i], + keyframes_sorted[i + 1], + ) return left.values == me.values == right.values - filtered_indices = list(itertools.filterfalse(filter_boundaries, range(num_keyframes))) + filtered_indices = list( + itertools.filterfalse(filter_boundaries, range(num_keyframes)) + ) if len(filtered_indices) == 2: - if keyframes_sorted[filtered_indices[0]].values == keyframes_sorted[filtered_indices[1]].values: + if ( + keyframes_sorted[filtered_indices[0]].values + == keyframes_sorted[filtered_indices[1]].values + ): return [] return [keyframes_sorted[i] for i in filtered_indices] - def _process_fcurve(self, fcurve: bpy.types.FCurve, convert: Optional[Callable] = None, *, - start: Optional[int] = None, end: Optional[int] = None) -> Tuple[Sequence, AbstractSet]: + def _process_fcurve( + self, + fcurve: bpy.types.FCurve, + convert: Optional[Callable] = None, + *, + start: Optional[int] = None, + end: Optional[int] = None + ) -> Tuple[Sequence, AbstractSet]: """Like _process_keyframes, but for one fcurve""" # Adapt from incoming single item sequence to a single argument. @@ -755,32 +1065,52 @@ class AnimationConverter: else: single_convert = None # Can't proxy to _process_fcurves because it only supports linear interoplation. - return self._process_keyframes([fcurve], 1, [0.0], single_convert, start=start, end=end) + return self._process_keyframes( + [fcurve], 1, [0.0], single_convert, start=start, end=end + ) - def _santize_converted_values(self, num_channels: int, raw_values: Union[Dict, Sequence], convert: Callable): + def _santize_converted_values( + self, num_channels: int, raw_values: Union[Dict, Sequence], convert: Callable + ): assert convert is not None if isinstance(raw_values, Dict): values = convert(**raw_values) elif isinstance(raw_values, Sequence): values = convert(raw_values) else: - raise AssertionError("Unexpected type for raw_values: {}".format(raw_values.__class__)) + raise AssertionError( + "Unexpected type for raw_values: {}".format(raw_values.__class__) + ) if not isinstance(values, Sequence) and isinstance(values, Iterable): values = tuple(values) if not isinstance(values, Sequence): - assert num_channels == 1, "Converter returned 1 value but expected {}".format(num_channels) + assert ( + num_channels == 1 + ), "Converter returned 1 value but expected {}".format(num_channels) values = (values,) else: - assert len(values) == num_channels, "Converter returned {} values but expected {}".format(len(values), num_channels) + assert ( + len(values) == num_channels + ), "Converter returned {} values but expected {}".format( + len(values), num_channels + ) return values - def _process_fcurves(self, fcurves: Sequence, channels: Dict[str, int], result_channels: int, - convert: Callable, defaults: Dict[str, Union[float, Sequence]], *, - start: Optional[int] = None, end: Optional[int] = None) -> Sequence: + def _process_fcurves( + self, + fcurves: Sequence, + channels: Dict[str, int], + result_channels: int, + convert: Callable, + defaults: Dict[str, Union[float, Sequence]], + *, + start: Optional[int] = None, + end: Optional[int] = None + ) -> Sequence: """This consumes a sequence of Blender FCurves that map to a single Plasma controller. - Like `_process_keyframes()`, except the converter function is mandatory, and each - Blender `data_path` must have a fixed number of channels. + Like `_process_keyframes()`, except the converter function is mandatory, and each + Blender `data_path` must have a fixed number of channels. """ # TODO: This fxn should probably issue a warning if any keyframes use bezier interpolation. @@ -806,9 +1136,17 @@ class AnimationConverter: fcurve_keyframes = defaultdict(lambda: defaultdict(dict)) for fcurve in (i for i in fcurves if i is not None): for fkey in filter(framenum_filter, fcurve.keyframe_points): - fcurve_keyframes[fkey.co[0]][fcurve.data_path][fcurve.array_index] = fkey - - def iter_channel_values(frame_num : int, fcurves : Dict, fkeys : Dict, num_channels : int, defaults : Union[float, Sequence]): + fcurve_keyframes[fkey.co[0]][fcurve.data_path][ + fcurve.array_index + ] = fkey + + def iter_channel_values( + frame_num: int, + fcurves: Dict, + fkeys: Dict, + num_channels: int, + defaults: Union[float, Sequence], + ): for i in range(num_channels): fkey = fkeys.get(i, None) if fkey is None: @@ -820,7 +1158,9 @@ class AnimationConverter: try: yield defaults[i] except: - assert num_channels == 1, "Got a non-subscriptable default for a multi-channel keyframe." + assert ( + num_channels == 1 + ), "Got a non-subscriptable default for a multi-channel keyframe." yield defaults else: yield fcurve.evaluate(frame_num) @@ -834,9 +1174,21 @@ class AnimationConverter: keyframe.frame_num = int(frame_num * (30.0 / fps)) keyframe.frame_num_blender = frame_num keyframe.frame_time = frame_num / fps - keyframe.values_raw = { data_path: tuple(iter_channel_values(frame_num, grouped_fcurves[data_path], fkeys, num_channels, defaults[data_path])) - for data_path, num_channels in channels.items() } - keyframe.values = self._santize_converted_values(result_channels, keyframe.values_raw, convert) + keyframe.values_raw = { + data_path: tuple( + iter_channel_values( + frame_num, + grouped_fcurves[data_path], + fkeys, + num_channels, + defaults[data_path], + ) + ) + for data_path, num_channels in channels.items() + } + keyframe.values = self._santize_converted_values( + result_channels, keyframe.values_raw, convert + ) # Very gnawty keyframe.in_tans = [0.0] * result_channels @@ -845,9 +1197,16 @@ class AnimationConverter: return self._sort_and_dedupe_keyframes(keyframes) - def _process_keyframes(self, fcurves, num_channels: int, default_values: Sequence, - convert: Optional[Callable] = None, *, start: Optional[int] = None, - end: Optional[int] = None) -> Tuple[Sequence, AbstractSet]: + def _process_keyframes( + self, + fcurves, + num_channels: int, + default_values: Sequence, + convert: Optional[Callable] = None, + *, + start: Optional[int] = None, + end: Optional[int] = None + ) -> Tuple[Sequence, AbstractSet]: """Groups all FCurves for the same frame together""" keyframe_data = type("KeyFrameData", (), {}) fps, pi = self._bl_fps, math.pi @@ -863,7 +1222,9 @@ class AnimationConverter: else: framenum_filter = lambda x: True - indexed_fcurves = { fcurve.array_index: fcurve for fcurve in fcurves if fcurve is not None } + indexed_fcurves = { + fcurve.array_index: fcurve for fcurve in fcurves if fcurve is not None + } for i, fcurve in indexed_fcurves.items(): fcurve.update() for fkey in filter(framenum_filter, fcurve.keyframe_points): @@ -896,12 +1257,26 @@ class AnimationConverter: if convert is None: keyframe.values = keyframe.values_raw else: - keyframe.values = self._santize_converted_values(num_channels, keyframe.values_raw, convert) + keyframe.values = self._santize_converted_values( + num_channels, keyframe.values_raw, convert + ) - for i, fkey in ((i, fkey) for i, fkey in fkeys.items() if fkey.interpolation == "BEZIER"): + for i, fkey in ( + (i, fkey) for i, fkey in fkeys.items() if fkey.interpolation == "BEZIER" + ): value = keyframe.values_raw[i] - keyframe.in_tans[i] = -(value - fkey.handle_left[1]) / (frame_num - fkey.handle_left[0]) / fps / (2 * pi) - keyframe.out_tans[i] = (value - fkey.handle_right[1]) / (frame_num - fkey.handle_right[0]) / fps / (2 * pi) + keyframe.in_tans[i] = ( + -(value - fkey.handle_left[1]) + / (frame_num - fkey.handle_left[0]) + / fps + / (2 * pi) + ) + keyframe.out_tans[i] = ( + (value - fkey.handle_right[1]) + / (frame_num - fkey.handle_right[0]) + / fps + / (2 * pi) + ) bez_chans.add(i) keyframes[frame_num] = keyframe diff --git a/korman/exporter/camera.py b/korman/exporter/camera.py index bed100c..63a1399 100644 --- a/korman/exporter/camera.py +++ b/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 diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index 9761eb3..6facd71 100644 --- a/korman/exporter/convert.py +++ b/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) diff --git a/korman/exporter/decal.py b/korman/exporter/decal.py index d6120b2..9dc707b 100644 --- a/korman/exporter/decal.py +++ b/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 diff --git a/korman/exporter/etlight.py b/korman/exporter/etlight.py index 8b8a67d..d377137 100644 --- a/korman/exporter/etlight.py +++ b/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 diff --git a/korman/exporter/explosions.py b/korman/exporter/explosions.py index f21ca6c..3d08197 100644 --- a/korman/exporter/explosions.py +++ b/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 . + 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 + ) + ) diff --git a/korman/exporter/image.py b/korman/exporter/image.py index cf5ac5c..4ebf8c1 100644 --- a/korman/exporter/image.py +++ b/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 diff --git a/korman/exporter/locman.py b/korman/exporter/locman.py index 13418b7..330c29f 100644 --- a/korman/exporter/locman.py +++ b/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("") + write_line('') write_line("") - write_line("", self._age_name, indent=1) + write_line('', self._age_name, indent=1) for set_name, elements in sets.items(): - write_line("", set_name, indent=2) + write_line('', set_name, indent=2) for element_name, value in elements.items(): - write_line("", element_name, indent=3) + write_line('', element_name, indent=3) for translation_language, translation_value in iter_element(value): if _ESHTML_REGEX.search(translation_value): encoded_value = "".format(translation_value) else: encoded_value = xml_escape(translation_value) - write_line("{translation}", - language=translation_language, translation=encoded_value, indent=4) + write_line( + '{translation}', + language=translation_language, + translation=encoded_value, + indent=4, + ) write_line("", indent=3) write_line("", 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): diff --git a/korman/exporter/logger.py b/korman/exporter/logger.py index 7a91316..faae94c 100644 --- a/korman/exporter/logger.py +++ b/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) diff --git a/korman/exporter/manager.py b/korman/exporter/manager.py index e5cc8e2..3316418 100644 --- a/korman/exporter/manager.py +++ b/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) diff --git a/korman/exporter/material.py b/korman/exporter/material.py index 5d42323..2078d65 100644 --- a/korman/exporter/material.py +++ b/korman/exporter/material.py @@ -35,8 +35,15 @@ _MAX_STENCILS = 6 # Blender cube map mega image to libHSPlasma plCubicEnvironmap faces mapping... # See https://blender.stackexchange.com/questions/46891/how-to-render-an-environment-to-a-cube-map-in-cycles -BLENDER_CUBE_MAP = ("leftFace", "backFace", "rightFace", - "bottomFace", "topFace", "frontFace") +BLENDER_CUBE_MAP = ( + "leftFace", + "backFace", + "rightFace", + "bottomFace", + "topFace", + "frontFace", +) + class _Texture: _DETAIL_BLEND = { @@ -78,8 +85,9 @@ class _Texture: self.alpha_type = TextureAlpha.full else: self.alpha_type = kwargs.get("alpha_type", TextureAlpha.opaque) - self.allowed_formats = kwargs.get("allowed_formats", - {"DDS"} if self.mipmap else {"PNG", "JPG"}) + self.allowed_formats = kwargs.get( + "allowed_formats", {"DDS"} if self.mipmap else {"PNG", "JPG"} + ) self.is_cube_map = kwargs.get("is_cube_map", False) # Basic format sanity @@ -120,10 +128,14 @@ class _Texture: name = "ALPHAGEN_{}".format(self.name) if self.is_detail_map: - name = "DETAILGEN_{}-{}-{}-{}-{}_{}".format(self._DETAIL_BLEND[self.detail_blend], - self.detail_fade_start, self.detail_fade_stop, - self.detail_opacity_start, self.detail_opacity_stop, - self.name) + name = "DETAILGEN_{}-{}-{}-{}-{}_{}".format( + self._DETAIL_BLEND[self.detail_blend], + self.detail_fade_start, + self.detail_fade_stop, + self.detail_opacity_start, + self.detail_opacity_stop, + self.name, + ) return name def _update(self, other): @@ -150,10 +162,19 @@ class MaterialConverter: "NONE": self._export_texture_type_none, } self._animation_exporters = { - "ambientCtl": functools.partial(self._export_layer_diffuse_animation, converter=self.get_material_ambient), + "ambientCtl": functools.partial( + self._export_layer_diffuse_animation, + converter=self.get_material_ambient, + ), "opacityCtl": self._export_layer_opacity_animation, - "preshadeCtl": functools.partial(self._export_layer_diffuse_animation, converter=self.get_material_preshade), - "runtimeCtl": functools.partial(self._export_layer_diffuse_animation, converter=self.get_material_runtime), + "preshadeCtl": functools.partial( + self._export_layer_diffuse_animation, + converter=self.get_material_preshade, + ), + "runtimeCtl": functools.partial( + self._export_layer_diffuse_animation, + converter=self.get_material_runtime, + ), "transformCtl": self._export_layer_transform_animation, } @@ -180,7 +201,9 @@ class MaterialConverter: elif method == "dcm2dem": return True elif method == "perengine": - return (ver >= pvMoul and envmap.mapping == "PLANE") or envmap.mapping == "CUBE" + return ( + ver >= pvMoul and envmap.mapping == "PLANE" + ) or envmap.mapping == "CUBE" else: raise NotImplementedError(method) else: @@ -193,8 +216,17 @@ class MaterialConverter: # being a waveset, doublesided, etc. single_user = self._requires_single_user(bo, bm) if single_user: - mat_name = "{}_AutoSingle".format(bm.name) if bo.name == bm.name else "{}_{}".format(bo.name, bm.name) - self._report.msg("Exporting Material '{}' as single user '{}'", bm.name, mat_name, indent=1) + mat_name = ( + "{}_AutoSingle".format(bm.name) + if bo.name == bm.name + else "{}_{}".format(bo.name, bm.name) + ) + self._report.msg( + "Exporting Material '{}' as single user '{}'", + bm.name, + mat_name, + indent=1, + ) hgmat = None else: # Ensure that RT-lit objects don't infect the static-lit objects. @@ -205,7 +237,9 @@ class MaterialConverter: mat_prefix = "RTLit_" else: mat_prefix = "" - mat_prefix2 = "NonVtxP_" if self._exporter().mesh.is_nonpreshaded(bo, bm) else "" + mat_prefix2 = ( + "NonVtxP_" if self._exporter().mesh.is_nonpreshaded(bo, bm) else "" + ) mat_name = "".join((mat_prefix, mat_prefix2, bm.name)) self._report.msg("Exporting Material '{}'", mat_name, indent=1) hsgmat = self._mgr.find_key(hsGMaterial, name=mat_name, bl=bo) @@ -213,7 +247,11 @@ class MaterialConverter: return hsgmat hsgmat = self._mgr.add_object(hsGMaterial, name=mat_name, bl=bo) - slots = [(idx, slot) for idx, slot in enumerate(bm.texture_slots) if self._can_export_texslot(slot)] + slots = [ + (idx, slot) + for idx, slot in enumerate(bm.texture_slots) + if self._can_export_texslot(slot) + ] # There is a major difference in how Blender and Plasma handle stencils. # In Blender, the stencil is on top and applies to every layer below is. In Plasma, the stencil @@ -223,7 +261,11 @@ class MaterialConverter: # main texture and 1 piggyback. num_stencils = sum((1 for i in slots if i[1].use_stencil)) if num_stencils > _MAX_STENCILS: - raise ExportError("Material '{}' uses too many stencils. The maximum is {}".format(bm.name, _MAX_STENCILS)) + raise ExportError( + "Material '{}' uses too many stencils. The maximum is {}".format( + bm.name, _MAX_STENCILS + ) + ) stencils = [] restart_pass_next = False @@ -232,17 +274,21 @@ class MaterialConverter: # Prepend any BumpMapping magic layers if slot.use_map_normal: if bo in self._bump_mats: - raise ExportError("Material '{}' has more than one bumpmap layer".format(bm.name)) + raise ExportError( + "Material '{}' has more than one bumpmap layer".format(bm.name) + ) du, dw, dv = self.export_bumpmap_slot(bo, bm, hsgmat, slot, idx) - hsgmat.addLayer(du.key) # Du - hsgmat.addLayer(dw.key) # Dw - hsgmat.addLayer(dv.key) # Dv + hsgmat.addLayer(du.key) # Du + hsgmat.addLayer(dw.key) # Dw + hsgmat.addLayer(dv.key) # Dv if slot.use_stencil: stencils.append((idx, slot)) else: tex_name = "{}_{}".format(mat_name, slot.name) - tex_layer = self.export_texture_slot(bo, bm, hsgmat, slot, idx, name=tex_name) + tex_layer = self.export_texture_slot( + bo, bm, hsgmat, slot, idx, name=tex_name + ) if restart_pass_next: tex_layer.state.miscFlags |= hsGMatState.kMiscRestartPassHere restart_pass_next = False @@ -256,20 +302,28 @@ class MaterialConverter: tex_state = tex_layer.state if not tex_state.blendFlags & hsGMatState.kBlendMask: tex_state.blendFlags |= hsGMatState.kBlendAlpha - tex_state.miscFlags |= hsGMatState.kMiscRestartPassHere | hsGMatState.kMiscBindNext + tex_state.miscFlags |= ( + hsGMatState.kMiscRestartPassHere | hsGMatState.kMiscBindNext + ) curr_stencils = len(stencils) for i in range(curr_stencils): stencil_idx, stencil = stencils[i] - stencil_name = "STENCILGEN_{}@{}_{}".format(stencil.name, bm.name, slot.name) - stencil_layer = self.export_texture_slot(bo, bm, hsgmat, stencil, stencil_idx, name=stencil_name) - if i+1 < curr_stencils: + stencil_name = "STENCILGEN_{}@{}_{}".format( + stencil.name, bm.name, slot.name + ) + stencil_layer = self.export_texture_slot( + bo, bm, hsgmat, stencil, stencil_idx, name=stencil_name + ) + if i + 1 < curr_stencils: stencil_layer.state.miscFlags |= hsGMatState.kMiscBindNext hsgmat.addLayer(stencil_layer.key) # Plasma makes several assumptions that every hsGMaterial has at least one layer. If this # material had no Textures, we will need to initialize a default layer if not hsgmat.layers: - layer = self._mgr.find_create_object(plLayer, name="{}_AutoLayer".format(mat_name), bl=bo) + layer = self._mgr.find_create_object( + plLayer, name="{}_AutoLayer".format(mat_name), bl=bo + ) self._obj2layer[bo][bm][None].append(layer.key) self._propagate_material_settings(bo, bm, layer) layer = self._export_layer_animations(bo, bm, None, 0, layer) @@ -292,8 +346,14 @@ class MaterialConverter: layer.ambient = hsColorRGBA(0.0, 0.0, 0.0, 1.0) layer.preshade = hsColorRGBA(0.0, 0.0, 0.0, 1.0) layer.runtime = hsColorRGBA(1.0, 1.0, 1.0, 1.0) - self.export_prepared_image(name=image_name, image=image, alpha_type=image_alpha, - owner=layer, allowed_formats={"DDS"}, indent=4) + self.export_prepared_image( + name=image_name, + image=image, + alpha_type=image_alpha, + owner=layer, + allowed_formats={"DDS"}, + indent=4, + ) material = self._mgr.add_object(hsGMaterial, bl=bo, name=name) material.addLayer(layer.key) return material, layer @@ -302,14 +362,19 @@ class MaterialConverter: image_alpha = self._test_image_alpha(image) if image_alpha == TextureAlpha.opaque and want_preshade: - self._report.warn("Using an opaque texture with alpha blending -- this may look bad") + self._report.warn( + "Using an opaque texture with alpha blending -- this may look bad" + ) # Non-alpha blendmodes absolutely cannot have an alpha channel. Period. Nada. # You can't even filter it out with blend flags. We'll try to mitigate the damage by # exporting a DXT1 version. As of right now, opaque vs on_off does nothing, so we still # get some turd-alpha data. if image_alpha == TextureAlpha.full and not want_preshade: - self._report.warn("Using an alpha texture with a non-alpha blend mode -- this may look bad", indent=3) + self._report.warn( + "Using an alpha texture with a non-alpha blend mode -- this may look bad", + indent=3, + ) image_alpha = TextureAlpha.opaque image_name = "DECALPRINT_{}".format(image.name) else: @@ -336,12 +401,19 @@ class MaterialConverter: self._report.msg("Exporting Print Material '{}'", prename, indent=3) pre_material, pre_layer = make_print_material(prename) pre_material.compFlags |= hsGMaterial.kCompNeedsBlendChannel - pre_layer.state.miscFlags |= hsGMatState.kMiscBindNext | hsGMatState.kMiscRestartPassHere + pre_layer.state.miscFlags |= ( + hsGMatState.kMiscBindNext | hsGMatState.kMiscRestartPassHere + ) pre_layer.preshade = hsColorRGBA(1.0, 1.0, 1.0, 1.0) - blend_layer = self._mgr.add_object(plLayer, bl=bo, name="{}_AlphaBlend".format(rtname)) - blend_layer.state.blendFlags = hsGMatState.kBlendAlpha | hsGMatState.kBlendNoTexColor | \ - hsGMatState.kBlendAlphaMult + blend_layer = self._mgr.add_object( + plLayer, bl=bo, name="{}_AlphaBlend".format(rtname) + ) + blend_layer.state.blendFlags = ( + hsGMatState.kBlendAlpha + | hsGMatState.kBlendNoTexColor + | hsGMatState.kBlendAlphaMult + ) blend_layer.state.clampFlags = hsGMatState.kClampTexture blend_layer.state.ZFlags = hsGMatState.kZNoZWrite blend_layer.ambient = hsColorRGBA(1.0, 1.0, 1.0, 1.0) @@ -374,9 +446,15 @@ class MaterialConverter: self._report.msg("Exporting Plasma Bumpmap Layers for '{}'", name, indent=2) # Okay, now we need to make 3 layers for the Du, Dw, and Dv - du_layer = self._mgr.find_create_object(plLayer, name="{}_DU_BumpLut".format(name), bl=bo) - dw_layer = self._mgr.find_create_object(plLayer, name="{}_DW_BumpLut".format(name), bl=bo) - dv_layer = self._mgr.find_create_object(plLayer, name="{}_DV_BumpLut".format(name), bl=bo) + du_layer = self._mgr.find_create_object( + plLayer, name="{}_DU_BumpLut".format(name), bl=bo + ) + dw_layer = self._mgr.find_create_object( + plLayer, name="{}_DW_BumpLut".format(name), bl=bo + ) + dv_layer = self._mgr.find_create_object( + plLayer, name="{}_DV_BumpLut".format(name), bl=bo + ) for layer in (du_layer, dw_layer, dv_layer): layer.ambient = hsColorRGBA(1.0, 1.0, 1.0, 1.0) @@ -393,7 +471,9 @@ class MaterialConverter: if not slot.use_map_specular: du_layer.state.blendFlags = hsGMatState.kBlendMADD - du_layer.state.miscFlags |= hsGMatState.kMiscBumpDu | hsGMatState.kMiscRestartPassHere + du_layer.state.miscFlags |= ( + hsGMatState.kMiscBumpDu | hsGMatState.kMiscRestartPassHere + ) dw_layer.state.miscFlags |= hsGMatState.kMiscBumpDw dv_layer.state.miscFlags |= hsGMatState.kMiscBumpDv @@ -406,7 +486,9 @@ class MaterialConverter: LUT_key = self._mgr.find_key(plMipmap, loc=page, name="BumpLutTexture") if LUT_key is None: - bumpLUT = plMipmap("BumpLutTexture", 16, 16, 1, plBitmap.kUncompressed, plBitmap.kRGB8888) + bumpLUT = plMipmap( + "BumpLutTexture", 16, 16, 1, plBitmap.kUncompressed, plBitmap.kRGB8888 + ) create_bump_LUT(bumpLUT) self._mgr.AddObject(page, bumpLUT) LUT_key = bumpLUT.key @@ -417,7 +499,9 @@ class MaterialConverter: return (du_layer, dw_layer, dv_layer) - def export_texture_slot(self, bo, bm, hsgmat, slot, idx, name=None, blend_flags=True): + def export_texture_slot( + self, bo, bm, hsgmat, slot, idx, name=None, blend_flags=True + ): if name is None: name = "{}_{}".format(bm.name if bm is not None else bo.name, slot.name) self._report.msg("Exporting Plasma Layer '{}'", name, indent=2) @@ -433,25 +517,39 @@ class MaterialConverter: self._report.msg("Using UV Map #{} '{}'", i, name, indent=3) break else: - self._report.msg("No UVMap specified... Blindly using the first one, maybe it exists :|", indent=3) + self._report.msg( + "No UVMap specified... Blindly using the first one, maybe it exists :|", + indent=3, + ) # Transform xform = hsMatrix44() - translation = hsVector3(slot.offset.x - (slot.scale.x - 1.0) / 2.0, - -slot.offset.y - (slot.scale.y - 1.0) / 2.0, - slot.offset.z - (slot.scale.z - 1.0) / 2.0) + translation = hsVector3( + slot.offset.x - (slot.scale.x - 1.0) / 2.0, + -slot.offset.y - (slot.scale.y - 1.0) / 2.0, + slot.offset.z - (slot.scale.z - 1.0) / 2.0, + ) xform.setTranslate(translation) xform.setScale(hsVector3(*slot.scale)) layer.transform = xform - wantStencil, canStencil = slot.use_stencil, slot.use_stencil and bm is not None and not slot.use_map_normal + wantStencil, canStencil = ( + slot.use_stencil, + slot.use_stencil and bm is not None and not slot.use_map_normal, + ) if wantStencil and not canStencil: - self._exporter().report.warn("{} wants to stencil, but this is not a real Material".format(slot.name)) + self._exporter().report.warn( + "{} wants to stencil, but this is not a real Material".format(slot.name) + ) state = layer.state if canStencil: hsgmat.compFlags |= hsGMaterial.kCompNeedsBlendChannel - state.blendFlags |= hsGMatState.kBlendAlpha | hsGMatState.kBlendAlphaMult | hsGMatState.kBlendNoTexColor + state.blendFlags |= ( + hsGMatState.kBlendAlpha + | hsGMatState.kBlendAlphaMult + | hsGMatState.kBlendNoTexColor + ) state.ZFlags |= hsGMatState.kZNoZWrite layer.ambient = hsColorRGBA(1.0, 1.0, 1.0, 1.0) elif blend_flags: @@ -518,7 +616,9 @@ class MaterialConverter: converter = self._exporter().animation texture = tex_slot.texture if tex_slot is not None else None - def attach_layer(pClass: type, anim_name: str, controllers: Dict[str, plController]): + def attach_layer( + pClass: type, anim_name: str, controllers: Dict[str, plController] + ): nonlocal top_layer name = "{}_{}".format(base_layer.key.name, anim_name) layer_animation = self._mgr.find_create_object(pClass, bl=bo, name=name) @@ -543,15 +643,18 @@ class MaterialConverter: start, end = anim.start, anim.end else: start, end = None, None - controllers = self._export_layer_controllers(bo, bm, tex_slot, idx, base_layer, - start=start, end=end) + controllers = self._export_layer_controllers( + bo, bm, tex_slot, idx, base_layer, start=start, end=end + ) if not controllers: continue pClass = plLayerSDLAnimation if anim.sdl_var else plLayerAnimation attach_layer(pClass, anim.animation_name, controllers) atc = top_layer.timeConvert - atc.begin, atc.end = converter.get_frame_time_range(*controllers.values()) + atc.begin, atc.end = converter.get_frame_time_range( + *controllers.values() + ) atc.loopBegin, atc.loopEnd = atc.begin, atc.end if not anim.auto_start: atc.flags |= plAnimTimeConvert.kStopped @@ -561,21 +664,32 @@ class MaterialConverter: top_layer.varName = anim.sdl_var else: # Crappy automatic entire layer animation. Loop it by default. - controllers = self._export_layer_controllers(bo, bm, tex_slot, idx, base_layer) + controllers = self._export_layer_controllers( + bo, bm, tex_slot, idx, base_layer + ) if controllers: attach_layer(plLayerAnimation, "(Entire Animation)", controllers) atc = top_layer.timeConvert atc.flags |= plAnimTimeConvert.kLoop - atc.begin, atc.end = converter.get_frame_time_range(*controllers.values()) + atc.begin, atc.end = converter.get_frame_time_range( + *controllers.values() + ) atc.loopBegin = atc.begin atc.loopEnd = atc.end return top_layer - - def _export_layer_controllers(self, bo: bpy.types.Object, bm: bpy.types.Material, tex_slot, - idx: int, base_layer, *, start: Optional[int] = None, - end: Optional[int] = None) -> Dict[str, plController]: + def _export_layer_controllers( + self, + bo: bpy.types.Object, + bm: bpy.types.Material, + tex_slot, + idx: int, + base_layer, + *, + start: Optional[int] = None, + end: Optional[int] = None + ) -> Dict[str, plController]: """Convert animations on this material/texture combo in the requested range to Plasma controllers""" def harvest_fcurves(bl_id, collection, data_path=None): @@ -589,7 +703,13 @@ class MaterialConverter: if data_path is None: collection.extend(action.fcurves) else: - collection.extend((i for i in action.fcurves if i.data_path.startswith(data_path))) + collection.extend( + ( + i + for i in action.fcurves + if i.data_path.startswith(data_path) + ) + ) return action return None @@ -609,12 +729,16 @@ class MaterialConverter: # animation controllers. controllers = {} for attr, converter in self._animation_exporters.items(): - ctrl = converter(bo, bm, tex_slot, base_layer, fcurves, start=start, end=end) + ctrl = converter( + bo, bm, tex_slot, base_layer, fcurves, start=start, end=end + ) if ctrl is not None: controllers[attr] = ctrl return controllers - def _export_layer_diffuse_animation(self, bo, bm, tex_slot, base_layer, fcurves, *, start, end, converter): + def _export_layer_diffuse_animation( + self, bo, bm, tex_slot, base_layer, fcurves, *, start, end, converter + ): assert converter is not None # If there's no material, then this is simply impossible. @@ -626,12 +750,19 @@ class MaterialConverter: result = converter(bo, bm, mathutils.Color(color_sequence)) return result.red, result.green, result.blue - ctrl = self._exporter().animation.make_pos_controller(fcurves, "diffuse_color", - bm.diffuse_color, translate_color, - start=start, end=end) + ctrl = self._exporter().animation.make_pos_controller( + fcurves, + "diffuse_color", + bm.diffuse_color, + translate_color, + start=start, + end=end, + ) return ctrl - def _export_layer_opacity_animation(self, bo, bm, tex_slot, base_layer, fcurves, *, start, end): + def _export_layer_opacity_animation( + self, bo, bm, tex_slot, base_layer, fcurves, *, start, end + ): # Dumb function to intercept the opacity values and properly flag the base layer def process_opacity(value): self._handle_layer_opacity(base_layer, value) @@ -639,20 +770,30 @@ class MaterialConverter: for i in fcurves: if i.data_path == "plasma_layer.opacity": - ctrl = self._exporter().animation.make_scalar_leaf_controller(i, process_opacity, start=start, end=end) + ctrl = self._exporter().animation.make_scalar_leaf_controller( + i, process_opacity, start=start, end=end + ) return ctrl return None - def _export_layer_transform_animation(self, bo, bm, tex_slot, base_layer, fcurves, *, start, end): + def _export_layer_transform_animation( + self, bo, bm, tex_slot, base_layer, fcurves, *, start, end + ): if tex_slot is not None: path = tex_slot.path_from_id() pos_path = "{}.offset".format(path) scale_path = "{}.scale".format(path) # Plasma uses the controller to generate a matrix44... so we have to produce a leaf controller - ctrl = self._exporter().animation.make_matrix44_controller(fcurves, pos_path, scale_path, - tex_slot.offset, tex_slot.scale, - start=start, end=end) + ctrl = self._exporter().animation.make_matrix44_controller( + fcurves, + pos_path, + scale_path, + tex_slot.offset, + tex_slot.scale, + start=start, + end=end, + ) return ctrl return None @@ -683,9 +824,13 @@ class MaterialConverter: # Sanity check: the image here should be 3x2 faces, so we should not have any # dam remainder... if width % 3 != 0: - raise ExportError("CubeMap '{}' width must be a multiple of 3".format(texture.image.name)) + raise ExportError( + "CubeMap '{}' width must be a multiple of 3".format(texture.image.name) + ) if height % 2 != 0: - raise ExportError("CubeMap '{}' height must be a multiple of 2".format(texture.image.name)) + raise ExportError( + "CubeMap '{}' height must be a multiple of 2".format(texture.image.name) + ) # According to PlasmaMAX, we don't give a rip about UVs... layer.UVWSrc = plLayerInterface.kUVWReflect @@ -696,10 +841,16 @@ class MaterialConverter: # to a big "finalize" save step to prevent races. The texture cache would # prevent that as well, so we could theoretically slice-and-dice the single # image here... but... meh. Offloading taim. - self.export_prepared_image(texture=texture, owner=layer, indent=3, - alpha_type=TextureAlpha.opaque, mipmap=True, - allowed_formats={"DDS"}, is_cube_map=True, tag="cubemap") - + self.export_prepared_image( + texture=texture, + owner=layer, + indent=3, + alpha_type=TextureAlpha.opaque, + mipmap=True, + allowed_formats={"DDS"}, + is_cube_map=True, + tag="cubemap", + ) def export_dynamic_env(self, bo, layer, texture, pl_class): bl_env = texture.environment_map @@ -713,7 +864,9 @@ class MaterialConverter: oRes = bl_env.resolution eRes = helpers.ensure_power_of_two(oRes) if oRes != eRes: - self._report.msg("Overriding EnvMap size to ({}x{}) -- POT", eRes, eRes, indent=3) + self._report.msg( + "Overriding EnvMap size to ({}x{}) -- POT", eRes, eRes, indent=3 + ) # And now for the general ho'hum-ness pl_env = self._mgr.find_create_object(pl_class, bl=bo, name=name) @@ -733,9 +886,13 @@ class MaterialConverter: for region in texture.plasma_layer.vis_regions: rgn = region.control_region if rgn is None: - raise ExportError("'{}': Has an invalid Visibility Control".format(texture.name)) + raise ExportError( + "'{}': Has an invalid Visibility Control".format(texture.name) + ) if not rgn.plasma_modifiers.visregion.enabled: - raise ExportError("'{}': '{}' is not a VisControl".format(texture.name, rgn.name)) + raise ExportError( + "'{}': '{}' is not a VisControl".format(texture.name, rgn.name) + ) visregions.append(self._mgr.find_create_key(plVisRegion, bl=rgn)) pl_env.visRegions = visregions @@ -756,7 +913,9 @@ class MaterialConverter: # This is really just so we don't raise any eyebrows if anyone is looking at the files. # If you're disabling DCMs, then you're obviuously trolling! # Cyan generates a single color image, but we'll just set the layer colors and go away. - fake_layer = self._mgr.find_create_object(plLayer, bl=bo, name="{}_DisabledDynEnvMap".format(texture.name)) + fake_layer = self._mgr.find_create_object( + plLayer, bl=bo, name="{}_DisabledDynEnvMap".format(texture.name) + ) fake_layer.ambient = layer.ambient fake_layer.preshade = layer.preshade fake_layer.runtime = layer.runtime @@ -765,15 +924,25 @@ class MaterialConverter: if pl_env.camera is None: layer.UVWSrc = plLayerInterface.kUVWPosition - layer.state.miscFlags |= (hsGMatState.kMiscCam2Screen | hsGMatState.kMiscPerspProjection) + layer.state.miscFlags |= ( + hsGMatState.kMiscCam2Screen | hsGMatState.kMiscPerspProjection + ) else: faces = pl_env.faces + (pl_env,) # If the user specifies a camera object, this might be worthy of a notice. if viewpt.type == "CAMERA": - warn = self._report.port if bl_env.mapping == "PLANE" else self._report.warn - warn("Environment Map '{}' is exporting as a cube map. The viewpoint '{}' is a camera, but only its position will be used.", - bl_env.id_data.name, viewpt.name, indent=5) + warn = ( + self._report.port + if bl_env.mapping == "PLANE" + else self._report.warn + ) + warn( + "Environment Map '{}' is exporting as a cube map. The viewpoint '{}' is a camera, but only its position will be used.", + bl_env.id_data.name, + viewpt.name, + indent=5, + ) # DEMs can do just a position vector. We actually prefer this because the WaveSet exporter # will probably want to steal it for diabolical purposes... In MOUL, root objects are @@ -810,9 +979,15 @@ class MaterialConverter: # Does the image have any alpha at all? if texture.image is not None: alpha_type = self._test_image_alpha(texture.image) - has_alpha = texture.use_calculate_alpha or slot.use_stencil or alpha_type != TextureAlpha.opaque + has_alpha = ( + texture.use_calculate_alpha + or slot.use_stencil + or alpha_type != TextureAlpha.opaque + ) if (texture.image.use_alpha and texture.use_alpha) and not has_alpha: - warning = "'{}' wants to use alpha, but '{}' is opaque".format(texture.name, texture.image.name) + warning = "'{}' wants to use alpha, but '{}' is opaque".format( + texture.name, texture.image.name + ) self._exporter().report.warn(warning, indent=3) else: alpha_type, has_alpha = TextureAlpha.opaque, False @@ -845,7 +1020,9 @@ class MaterialConverter: # Otherwise, we toss this layer and some info into our pending texture dict and process it # when the exporter tells us to finalize all our shit if texture.image is None: - dtm = self._mgr.find_create_object(plDynamicTextMap, name="{}_DynText".format(layer.key.name), bl=bo) + dtm = self._mgr.find_create_object( + plDynamicTextMap, name="{}_DynText".format(layer.key.name), bl=bo + ) if texture.use_alpha: dtm.hasAlpha = True if not state.blendFlags & hsGMatState.kBlendMask: @@ -864,20 +1041,28 @@ class MaterialConverter: detail_blend = TEX_DETAIL_MULTIPLY # Herp, derp... Detail blends are all based on alpha - if layer_props.is_detail_map and not state.blendFlags & hsGMatState.kBlendMask: + if ( + layer_props.is_detail_map + and not state.blendFlags & hsGMatState.kBlendMask + ): state.blendFlags |= hsGMatState.kBlendDetail allowed_formats = {"DDS"} if mipmap else {"PNG", "BMP"} - self.export_prepared_image(texture=texture, owner=layer, - alpha_type=alpha_type, force_calc_alpha=slot.use_stencil, - is_detail_map=layer_props.is_detail_map, - detail_blend=detail_blend, - detail_fade_start=layer_props.detail_fade_start, - detail_fade_stop=layer_props.detail_fade_stop, - detail_opacity_start=layer_props.detail_opacity_start, - detail_opacity_stop=layer_props.detail_opacity_stop, - mipmap=mipmap, allowed_formats=allowed_formats, - indent=3) + self.export_prepared_image( + texture=texture, + owner=layer, + alpha_type=alpha_type, + force_calc_alpha=slot.use_stencil, + is_detail_map=layer_props.is_detail_map, + detail_blend=detail_blend, + detail_fade_start=layer_props.detail_fade_start, + detail_fade_stop=layer_props.detail_fade_stop, + detail_opacity_start=layer_props.detail_opacity_start, + detail_opacity_stop=layer_props.detail_opacity_stop, + mipmap=mipmap, + allowed_formats=allowed_formats, + indent=3, + ) def _export_texture_type_none(self, bo, layer, slot, idx): # We'll allow this, just for sanity's sake... @@ -885,7 +1070,11 @@ class MaterialConverter: def _export_texture_type_blend(self, bo, layer, slot, idx): state = layer.state - state.blendFlags |= hsGMatState.kBlendAlpha | hsGMatState.kBlendAlphaMult | hsGMatState.kBlendNoTexColor + state.blendFlags |= ( + hsGMatState.kBlendAlpha + | hsGMatState.kBlendAlphaMult + | hsGMatState.kBlendNoTexColor + ) state.clampFlags |= hsGMatState.kClampTexture state.ZFlags |= hsGMatState.kZNoZWrite @@ -895,12 +1084,12 @@ class MaterialConverter: def export_alpha_blend(self, progression, axis, owner, indent=2): """This exports an alpha blend texture as exposed by bpy.types.BlendTexture. - The following arguments are expected: - - progression: (required) - - axis: (required) - - owner: (required) the Plasma object using this image - - indent: (optional) indentation level for log messages - default: 2 + The following arguments are expected: + - progression: (required) + - axis: (required) + - owner: (required) the Plasma object using this image + - indent: (optional) indentation level for log messages + default: 2 """ # Certain blend types don't use an axis... @@ -913,6 +1102,7 @@ class MaterialConverter: image = bpy.data.images.get(filename) if image is None: + def _calc_diagonal(x, y, width, height): distance = math.sqrt(pow(x, 2) + pow(y, 2)) total = math.sqrt(pow(width, 2) + pow(height, 2)) @@ -960,16 +1150,26 @@ class MaterialConverter: ("QUADRATIC", "VERTICAL"): (4, 64), } funcs = { - ("DIAGONAL", ""): _calc_diagonal, - ("EASING", "HORIZONTAL"): lambda x, y, width, height: 0.5 - math.cos(x / width * math.pi) * 0.5, - ("EASING", "VERTICAL"): lambda x, y, width, height: 0.5 - math.cos(y / height * math.pi) * 0.5, + ("DIAGONAL", ""): _calc_diagonal, + ("EASING", "HORIZONTAL"): lambda x, y, width, height: 0.5 + - math.cos(x / width * math.pi) * 0.5, + ("EASING", "VERTICAL"): lambda x, y, width, height: 0.5 + - math.cos(y / height * math.pi) * 0.5, ("LINEAR", "HORIZONTAL"): lambda x, y, width, height: x / width, ("LINEAR", "VERTICAL"): lambda x, y, width, height: y / height, - ("QUADRATIC", "HORIZONTAL"): lambda x, y, width, height: pow(x / width, 2), - ("QUADRATIC", "VERTICAL"): lambda x, y, width, height: pow(y / height, 2), + ("QUADRATIC", "HORIZONTAL"): lambda x, y, width, height: pow( + x / width, 2 + ), + ("QUADRATIC", "VERTICAL"): lambda x, y, width, height: pow( + y / height, 2 + ), ("QUADRATIC_SPHERE", ""): _calc_quad_sphere, - ("RADIAL", "HORIZONTAL"): functools.partial(_calc_radial, horizontal=True), - ("RADIAL", "VERTICAL"): functools.partial(_calc_radial, horizontal=False), + ("RADIAL", "HORIZONTAL"): functools.partial( + _calc_radial, horizontal=True + ), + ("RADIAL", "VERTICAL"): functools.partial( + _calc_radial, horizontal=False + ), ("SPHERICAL", ""): _calc_lin_sphere, } @@ -987,31 +1187,39 @@ class MaterialConverter: for y in range(height): offset = (y * width * 4) + (x * 4) value = func(x, y, width, height) - pixels[offset:offset+4] = (value,) * 4 - image = bpy.data.images.new(filename, width=width, height=height, alpha=True) + pixels[offset : offset + 4] = (value,) * 4 + image = bpy.data.images.new( + filename, width=width, height=height, alpha=True + ) image.pixels = pixels - self.export_prepared_image(image=image, owner=owner, allowed_formats={"BMP"}, - alpha_type=TextureAlpha.full, indent=indent, ephemeral=True) + self.export_prepared_image( + image=image, + owner=owner, + allowed_formats={"BMP"}, + alpha_type=TextureAlpha.full, + indent=indent, + ephemeral=True, + ) def export_prepared_image(self, **kwargs): """This exports an externally prepared image and an optional owning layer. - The following arguments are typical: - - texture: (co-required) the image texture datablock to export - - image: (co-required) the image datablock to export - - owner: (required) the Plasma object using this image - - mipmap: (optional) should the image be mipmapped? - - allowed_formats: (optional) set of string *hints* for desired image export type - valid options: BMP, DDS, JPG, PNG - - extension: (optional) file extension to use for the image object - to use the image datablock extension, set this to None - - indent: (optional) indentation level for log messages - default: 2 - - ephemeral: (optional) never cache this image - - tag: (optional) an optional identifier hint that allows multiple images with the - same name to coexist in the cache - - is_cube_map: (optional) indicates the provided image contains six cube faces - that must be split into six separate images for Plasma + The following arguments are typical: + - texture: (co-required) the image texture datablock to export + - image: (co-required) the image datablock to export + - owner: (required) the Plasma object using this image + - mipmap: (optional) should the image be mipmapped? + - allowed_formats: (optional) set of string *hints* for desired image export type + valid options: BMP, DDS, JPG, PNG + - extension: (optional) file extension to use for the image object + to use the image datablock extension, set this to None + - indent: (optional) indentation level for log messages + default: 2 + - ephemeral: (optional) never cache this image + - tag: (optional) an optional identifier hint that allows multiple images with the + same name to coexist in the cache + - is_cube_map: (optional) indicates the provided image contains six cube faces + that must be split into six separate images for Plasma """ owner = kwargs.pop("owner", None) indent = kwargs.pop("indent", 2) @@ -1019,8 +1227,12 @@ class MaterialConverter: image = key.image if key not in self._pending: - self._report.msg("Stashing '{}' for conversion as '{}'", image.name, key, indent=indent) - self._pending[key] = [owner.key,] + self._report.msg( + "Stashing '{}' for conversion as '{}'", image.name, key, indent=indent + ) + self._pending[key] = [ + owner.key, + ] else: self._report.msg("Found another user of '{}'", key, indent=indent) self._pending[key].append(owner.key) @@ -1059,14 +1271,30 @@ class MaterialConverter: compression = plBitmap.kUncompressed else: raise RuntimeError(allowed_formats) - dxt = plBitmap.kDXT5 if key.alpha_type == TextureAlpha.full else plBitmap.kDXT1 + dxt = ( + plBitmap.kDXT5 + if key.alpha_type == TextureAlpha.full + else plBitmap.kDXT1 + ) # Mayhaps we have a cached version of this that has already been exported cached_image = texcache.get_from_texture(key, compression) if cached_image is None: - numLevels, width, height, data = self._finalize_cache(texcache, key, image, name, compression, dxt) - self._finalize_bitmap(key, owners, name, numLevels, width, height, compression, dxt, data) + numLevels, width, height, data = self._finalize_cache( + texcache, key, image, name, compression, dxt + ) + self._finalize_bitmap( + key, + owners, + name, + numLevels, + width, + height, + compression, + dxt, + data, + ) else: width, height = cached_image.export_size data = cached_image.image_data @@ -1075,15 +1303,41 @@ class MaterialConverter: # If the cached image data is junk, PyHSPlasma will raise a RuntimeError, # so we'll attempt a recache... try: - self._finalize_bitmap(key, owners, name, numLevels, width, height, compression, dxt, data) + self._finalize_bitmap( + key, + owners, + name, + numLevels, + width, + height, + compression, + dxt, + data, + ) except RuntimeError: - self._report.warn("Cached image is corrupted! Recaching image...", indent=1) - numLevels, width, height, data = self._finalize_cache(texcache, key, image, name, compression, dxt) - self._finalize_bitmap(key, owners, name, numLevels, width, height, compression, dxt, data) + self._report.warn( + "Cached image is corrupted! Recaching image...", indent=1 + ) + numLevels, width, height, data = self._finalize_cache( + texcache, key, image, name, compression, dxt + ) + self._finalize_bitmap( + key, + owners, + name, + numLevels, + width, + height, + compression, + dxt, + data, + ) inc_progress() - def _finalize_bitmap(self, key, owners, name, numLevels, width, height, compression, dxt, data): + def _finalize_bitmap( + self, key, owners, name, numLevels, width, height, compression, dxt, data + ): mgr = self._mgr # Now we poke our new bitmap into the pending layers. Note that we have to do some funny @@ -1093,15 +1347,24 @@ class MaterialConverter: self._report.msg("Adding to...", indent=1) for owner_key in owners: owner = owner_key.object - self._report.msg("[{} '{}']", owner.ClassName()[2:], owner_key.name, indent=2) - page = mgr.get_textures_page(owner_key) # Layer's page or Textures.prp + self._report.msg( + "[{} '{}']", owner.ClassName()[2:], owner_key.name, indent=2 + ) + page = mgr.get_textures_page(owner_key) # Layer's page or Textures.prp # If we haven't created this texture in the page (either layer's page or Textures.prp), # then we need to do that and stuff the level data. This is a little tedious, but we # need to be careful to manage our resources correctly if page not in pages: - mipmap = plMipmap(name=name, width=width, height=height, numLevels=numLevels, - compType=compression, format=plBitmap.kRGB8888, dxtLevel=dxt) + mipmap = plMipmap( + name=name, + width=width, + height=height, + numLevels=numLevels, + compType=compression, + format=plBitmap.kRGB8888, + dxtLevel=dxt, + ) if key.is_cube_map: assert len(data) == 6 texture = plCubicEnvironmap(name) @@ -1132,9 +1395,13 @@ class MaterialConverter: def _finalize_cache(self, texcache, key, image, name, compression, dxt): if key.is_cube_map: - numLevels, width, height, data = self._finalize_cube_map(key, image, name, compression, dxt) + numLevels, width, height, data = self._finalize_cube_map( + key, image, name, compression, dxt + ) else: - numLevels, width, height, data = self._finalize_single_image(key, image, name, compression, dxt) + numLevels, width, height, data = self._finalize_single_image( + key, image, name, compression, dxt + ) texcache.add_texture(key, numLevels, (width, height), compression, data) return numLevels, width, height, data @@ -1155,8 +1422,14 @@ class MaterialConverter: # in the case of POT faces. So, we will scale the image AGAIN, if Blender did # something funky. if oWidth != cWidth or oHeight != cHeight: - self._report.warn("Image was resized by Blender to ({}x{})--resizing the resize to ({}x{})", - cWidth, cHeight, oWidth, oHeight, indent=1) + self._report.warn( + "Image was resized by Blender to ({}x{})--resizing the resize to ({}x{})", + cWidth, + cHeight, + oWidth, + oHeight, + indent=1, + ) data = scale_image(data, cWidth, cHeight, oWidth, oHeight) # Face dimensions @@ -1192,18 +1465,31 @@ class MaterialConverter: name = face_name[:-4].upper() if compression == plBitmap.kDirectXCompression: numLevels = glimage.num_levels - self._report.msg("Generating mip levels for cube face '{}'", name, indent=1) + self._report.msg( + "Generating mip levels for cube face '{}'", name, indent=1 + ) # If we're compressing this mofo, we'll need a temporary mipmap to do that here... - mipmap = plMipmap(name=name, width=eWidth, height=eHeight, numLevels=numLevels, - compType=compression, format=plBitmap.kRGB8888, dxtLevel=dxt) + mipmap = plMipmap( + name=name, + width=eWidth, + height=eHeight, + numLevels=numLevels, + compType=compression, + format=plBitmap.kRGB8888, + dxtLevel=dxt, + ) else: numLevels = 1 - self._report.msg("Compressing single level for cube face '{}'", name, indent=1) + self._report.msg( + "Compressing single level for cube face '{}'", name, indent=1 + ) face_images[i] = [None] * numLevels for j in range(numLevels): - level_data = glimage.get_level_data(j, key.calc_alpha, report=self._report) + level_data = glimage.get_level_data( + j, key.calc_alpha, report=self._report + ) if compression == plBitmap.kDirectXCompression: mipmap.CompressImage(j, level_data) level_data = mipmap.getLevel(j) @@ -1227,8 +1513,15 @@ class MaterialConverter: # If this is a DXT-compressed mipmap, we need to use a temporary mipmap # to do the compression. We'll then steal the data from it. - mipmap = plMipmap(name=name, width=eWidth, height=eHeight, numLevels=numLevels, - compType=compression, format=plBitmap.kRGB8888, dxtLevel=dxt) + mipmap = plMipmap( + name=name, + width=eWidth, + height=eHeight, + numLevels=numLevels, + compType=compression, + format=plBitmap.kRGB8888, + dxtLevel=dxt, + ) else: numLevels = 1 self._report.msg("Compressing single level", indent=1) @@ -1237,30 +1530,47 @@ class MaterialConverter: # this mipmap for per-page textures :( data = [None] * numLevels for i in range(numLevels): - level_data = glimage.get_level_data(i, key.calc_alpha, report=self._report) + level_data = glimage.get_level_data( + i, key.calc_alpha, report=self._report + ) if compression == plBitmap.kDirectXCompression: mipmap.CompressImage(i, level_data) level_data = mipmap.getLevel(i) data[i] = level_data - return numLevels, eWidth, eHeight, [data,] - - def get_materials(self, bo: bpy.types.Object, bm: Optional[bpy.types.Material] = None) -> Iterator[plKey]: + return ( + numLevels, + eWidth, + eHeight, + [ + data, + ], + ) + + def get_materials( + self, bo: bpy.types.Object, bm: Optional[bpy.types.Material] = None + ) -> Iterator[plKey]: material_dict = self._obj2mat.get(bo, {}) if bm is None: return material_dict.values() else: return material_dict.get(bm, []) - def get_layers(self, bo: Optional[bpy.types.Object] = None, - bm: Optional[bpy.types.Material] = None, - tex: Optional[bpy.types.Texture] = None) -> Iterator[plKey]: + def get_layers( + self, + bo: Optional[bpy.types.Object] = None, + bm: Optional[bpy.types.Material] = None, + tex: Optional[bpy.types.Texture] = None, + ) -> Iterator[plKey]: # All three? Simple. if bo is not None and bm is not None and tex is not None: yield from filter(None, self._obj2layer[bo][bm][tex]) return if bo is None and bm is None and tex is None: - self._exporter().report.warn("Asking for all the layers we've ever exported, eh? You like living dangerously.", indent=2) + self._exporter().report.warn( + "Asking for all the layers we've ever exported, eh? You like living dangerously.", + indent=2, + ) # What we want to do is filter _obj2layers: # bo if set, or all objects @@ -1272,7 +1582,12 @@ class MaterialConverter: elif tex is not None: material_seq = tex.users_material else: - _iter = filter(lambda x: x and x.material, itertools.chain.from_iterable((i.material_slots for i in object_iter()))) + _iter = filter( + lambda x: x and x.material, + itertools.chain.from_iterable( + (i.material_slots for i in object_iter()) + ), + ) # Performance turd: this could, in the worst case, block on creating a list of every material # attached to every single Plasma Object in the current scene. This whole algorithm sucks, # though, so whatever. @@ -1281,7 +1596,11 @@ class MaterialConverter: for filtered_obj in object_iter(): for filtered_mat in material_seq: all_texes = self._obj2layer[filtered_obj][filtered_mat] - filtered_texes = all_texes[tex] if tex is not None else itertools.chain.from_iterable(all_texes.values()) + filtered_texes = ( + all_texes[tex] + if tex is not None + else itertools.chain.from_iterable(all_texes.values()) + ) yield from filter(None, filtered_texes) def get_base_layer(self, hsgmat): @@ -1295,20 +1614,23 @@ class MaterialConverter: def get_bump_layer(self, bo): return self._bump_mats.get(bo, None) - def get_material_ambient(self, bo, bm, color: Union[None, mathutils.Color] = None) -> hsColorRGBA: + def get_material_ambient( + self, bo, bm, color: Union[None, mathutils.Color] = None + ) -> hsColorRGBA: # Although Plasma calls this the ambient color, it is actually always used as the emissive color. emit_scale = bm.emit * 0.5 if emit_scale > 0.0: if color is None: color = bm.diffuse_color - return hsColorRGBA(color.r * emit_scale, - color.g * emit_scale, - color.b * emit_scale, - 1.0) + return hsColorRGBA( + color.r * emit_scale, color.g * emit_scale, color.b * emit_scale, 1.0 + ) else: return utils.color(bpy.context.scene.world.ambient_color) - def get_material_preshade(self, bo, bm, color: Union[None, mathutils.Color] = None) -> hsColorRGBA: + def get_material_preshade( + self, bo, bm, color: Union[None, mathutils.Color] = None + ) -> hsColorRGBA: # This color is always used for shading. In all lighting equations, it represents the world # ambient color. Anyway, if we have a manual (read: animated color), just dump that out. if color is not None: @@ -1320,7 +1642,10 @@ class MaterialConverter: # we'll want black if it's ONLY runtime lighting (and white for lightmaps). Otherwise, # just use the material color for now. if self._exporter().mesh.is_nonpreshaded(bo, bm): - if bo.plasma_modifiers.lightmap.bake_lightmap and not bo.plasma_modifiers.lighting.rt_lights: + if ( + bo.plasma_modifiers.lightmap.bake_lightmap + and not bo.plasma_modifiers.lighting.rt_lights + ): return hsColorRGBA.kWhite elif not bo.plasma_modifiers.lighting.preshade: return hsColorRGBA.kBlack @@ -1328,7 +1653,9 @@ class MaterialConverter: # Gulp return utils.color(bm.diffuse_color) - def get_material_runtime(self, bo, bm, color: Union[None, mathutils.Color] = None) -> hsColorRGBA: + def get_material_runtime( + self, bo, bm, color: Union[None, mathutils.Color] = None + ) -> hsColorRGBA: # The layer runstime color has no effect if the lighting equation is kLiteVtxNonPreshaded, # so return black to prevent animations from being exported. if self._exporter().mesh.is_nonpreshaded(bo, bm): @@ -1339,12 +1666,17 @@ class MaterialConverter: color = bm.diffuse_color return utils.color(color) - def get_texture_animation_key(self, bo, bm, texture, anim_name: str) -> Iterator[plKey]: + def get_texture_animation_key( + self, bo, bm, texture, anim_name: str + ) -> Iterator[plKey]: """Finds the appropriate key for sending messages to an animated Texture""" if not anim_name: anim_name = "(Entire Animation)" - for top_layer in filter(lambda x: isinstance(x.object, plLayerAnimationBase), self.get_layers(bo, bm, texture)): + for top_layer in filter( + lambda x: isinstance(x.object, plLayerAnimationBase), + self.get_layers(bo, bm, texture), + ): base_layer = top_layer.object.bottomOfStack needle = top_layer while needle is not None: @@ -1371,13 +1703,15 @@ class MaterialConverter: is_waveset = bo.plasma_modifiers.water_basic.enabled if bo.data.show_double_sided: if is_waveset: - self._report.warn("FORCING single sided--this is a waveset (are you insane?)") + self._report.warn( + "FORCING single sided--this is a waveset (are you insane?)" + ) else: state.miscFlags |= hsGMatState.kMiscTwoSided # Shade Flags if not bm.use_mist: - state.shadeFlags |= hsGMatState.kShadeNoFog # Dead in CWE + state.shadeFlags |= hsGMatState.kShadeNoFog # Dead in CWE state.shadeFlags |= hsGMatState.kShadeReallyNoFog if bm.use_shadeless: @@ -1387,7 +1721,10 @@ class MaterialConverter: # Lightmapped emissive layers seem to cause cascading render issues. Skip flagging it # and just hope that the ambient color bump is good enough. if bo.plasma_modifiers.lightmap.bake_lightmap: - self._report.warn("A lightmapped and emissive material??? You like living dangerously...", indent=3) + self._report.warn( + "A lightmapped and emissive material??? You like living dangerously...", + indent=3, + ) else: state.shadeFlags |= hsGMatState.kShadeEmissive @@ -1402,9 +1739,14 @@ class MaterialConverter: def requires_material_shading(self, bm: bpy.types.Material) -> bool: """Determines if this material requires the lighting equation we all know and love - (kLiteMaterial) in order to display opacity and color animations.""" + (kLiteMaterial) in order to display opacity and color animations.""" if bm.animation_data is not None and bm.animation_data.action is not None: - if any((i.data_path == "diffuse_color" for i in bm.animation_data.action.fcurves)): + if any( + ( + i.data_path == "diffuse_color" + for i in bm.animation_data.action.fcurves + ) + ): return True for slot in filter(lambda x: x and x.use and x.texture, bm.texture_slots): @@ -1416,7 +1758,12 @@ class MaterialConverter: return True if tex.animation_data is not None and tex.animation_data.action is not None: - if any((i.data_path == "plasma_layer.opacity" for i in tex.animation_data.action.fcurves)): + if any( + ( + i.data_path == "plasma_layer.opacity" + for i in tex.animation_data.action.fcurves + ) + ): return True return False diff --git a/korman/exporter/mesh.py b/korman/exporter/mesh.py index 2c24c40..349ea65 100644 --- a/korman/exporter/mesh.py +++ b/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") diff --git a/korman/exporter/outfile.py b/korman/exporter/outfile.py index d3ee7e6..7e012b8 100644 --- a/korman/exporter/outfile.py +++ b/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): diff --git a/korman/exporter/physics.py b/korman/exporter/physics.py index 0a6a0e9..3112204 100644 --- a/korman/exporter/physics.py +++ b/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) diff --git a/korman/exporter/python.py b/korman/exporter/python.py index e397d95..17901e7 100644 --- a/korman/exporter/python.py +++ b/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: diff --git a/korman/exporter/rtlight.py b/korman/exporter/rtlight.py index ca97a77..d3faf50 100644 --- a/korman/exporter/rtlight.py +++ b/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: diff --git a/korman/exporter/utils.py b/korman/exporter/utils.py index 296228a..6a9c0b5 100644 --- a/korman/exporter/utils.py +++ b/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 diff --git a/korman/helpers.py b/korman/helpers.py index 31c1d20..c9ea7c8 100644 --- a/korman/helpers.py +++ b/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: diff --git a/korman/idprops.py b/korman/idprops.py index 40cc474..f83b614 100644 --- a/korman/idprops.py +++ b/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) diff --git a/korman/korlib/__init__.py b/korman/korlib/__init__.py index 1b6270d..f4d2f08 100644 --- a/korman/korlib/__init__.py +++ b/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)) diff --git a/korman/korlib/console.py b/korman/korlib/console.py index 19995b2..560574f 100644 --- a/korman/korlib/console.py +++ b/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. diff --git a/korman/korlib/python.py b/korman/korlib/python.py index 5869539..1e1205a 100644 --- a/korman/korlib/python.py +++ b/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 . -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: diff --git a/korman/korlib/texture.py b/korman/korlib/texture.py index e1d9948..ed4a342 100644 --- a/korman/korlib/texture.py +++ b/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): diff --git a/korman/nodes/__init__.py b/korman/nodes/__init__.py index 57aa758..868f01e 100644 --- a/korman/nodes/__init__.py +++ b/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") diff --git a/korman/nodes/node_avatar.py b/korman/nodes/node_avatar.py index 0049bb4..22f258e 100644 --- a/korman/nodes/node_avatar.py +++ b/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() diff --git a/korman/nodes/node_conditions.py b/korman/nodes/node_conditions.py index 5ea931f..525d894 100644 --- a/korman/nodes/node_conditions.py +++ b/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): diff --git a/korman/nodes/node_core.py b/korman/nodes/node_core.py index 86a2d3f..a784310 100644 --- a/korman/nodes/node_core.py +++ b/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) diff --git a/korman/nodes/node_deprecated.py b/korman/nodes/node_deprecated.py index d3d5bdd..919e8ac 100644 --- a/korman/nodes/node_deprecated.py +++ b/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) diff --git a/korman/nodes/node_logic.py b/korman/nodes/node_logic.py index a9a38e3..ba42f34 100644 --- a/korman/nodes/node_logic.py +++ b/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") diff --git a/korman/nodes/node_messages.py b/korman/nodes/node_messages.py index a42196c..934e00c 100644 --- a/korman/nodes/node_messages.py +++ b/korman/nodes/node_messages.py @@ -23,21 +23,29 @@ from ..properties.modifiers.region import footstep_surfaces, footstep_surface_id from ..exporter import ExportError from .. import idprops + class PlasmaMessageSocketBase(PlasmaNodeSocketBase): bl_color = (0.004, 0.282, 0.349, 1.0) + + class PlasmaMessageSocket(PlasmaMessageSocketBase, bpy.types.NodeSocket): pass class PlasmaMessageNode(PlasmaNodeBase): - input_sockets = OrderedDict([ - ("sender", { - "text": "Sender", - "type": "PlasmaMessageSocket", - "valid_link_sockets": "PlasmaMessageSocket", - "spawn_empty": True, - }), - ]) + input_sockets = OrderedDict( + [ + ( + "sender", + { + "text": "Sender", + "type": "PlasmaMessageSocket", + "valid_link_sockets": "PlasmaMessageSocket", + "spawn_empty": True, + }, + ), + ] + ) @property def has_callbacks(self): @@ -46,14 +54,19 @@ class PlasmaMessageNode(PlasmaNodeBase): class PlasmaMessageWithCallbacksNode(PlasmaMessageNode): - output_sockets = OrderedDict([ - ("msgs", { - "can_link": "can_link_callback", - "text": "Send On Completion", - "type": "PlasmaMessageSocket", - "valid_link_sockets": "PlasmaMessageSocket", - }), - ]) + output_sockets = OrderedDict( + [ + ( + "msgs", + { + "can_link": "can_link_callback", + "text": "Send On Completion", + "type": "PlasmaMessageSocket", + "valid_link_sockets": "PlasmaMessageSocket", + }, + ), + ] + ) @property def can_link_callback(self): @@ -93,17 +106,23 @@ class PlasmaMessageWithCallbacksNode(PlasmaMessageNode): return self.find_output("msgs") is not None -class PlasmaAnimCmdMsgNode(idprops.IDPropMixin, PlasmaMessageWithCallbacksNode, bpy.types.Node): +class PlasmaAnimCmdMsgNode( + idprops.IDPropMixin, PlasmaMessageWithCallbacksNode, bpy.types.Node +): bl_category = "MSG" bl_idname = "PlasmaAnimCmdMsgNode" bl_label = "Animation Command" bl_width_default = 190 - anim_type = EnumProperty(name="Type", - description="Animation type to affect", - items=[("OBJECT", "Object", "Mesh Action"), - ("TEXTURE", "Texture", "Texture Action")], - default="OBJECT") + anim_type = EnumProperty( + name="Type", + description="Animation type to affect", + items=[ + ("OBJECT", "Object", "Mesh Action"), + ("TEXTURE", "Texture", "Texture Action"), + ], + default="OBJECT", + ) def _poll_texture(self, value): # must be a legal option... but is it a member of this material... or, if no material, @@ -111,8 +130,14 @@ class PlasmaAnimCmdMsgNode(idprops.IDPropMixin, PlasmaMessageWithCallbacksNode, if self.target_material is not None: return value.name in self.target_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: @@ -123,115 +148,204 @@ class PlasmaAnimCmdMsgNode(idprops.IDPropMixin, PlasmaMessageWithCallbacksNode, # 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 - target_object = PointerProperty(name="Object", - description="Target object", - type=bpy.types.Object) - target_material = PointerProperty(name="Material", - description="Target material", - type=bpy.types.Material, - poll=_poll_material) - target_texture = PointerProperty(name="Texture", - description="Target texture", - type=bpy.types.Texture, - poll=_poll_texture) - - go_to = EnumProperty(name="Go To", - description="Where should the animation start?", - items=[("kGoToBegin", "Beginning", "The beginning"), - ("kGoToLoopBegin", "Loop Beginning", "The beginning of the active loop"), - ("CURRENT", "(Don't Change)", "The current position"), - ("kGoToEnd", "Ending", "The end"), - ("kGoToLoopEnd", "Loop Ending", "The end of the active loop")], - default="CURRENT") - action = EnumProperty(name="Action", - description="What do you want the animation to do?", - items=[("kContinue", "Play", "Plays the animation"), - ("kPlayToPercent", "Play to Percent", "Plays the animation until a given percent is complete"), - ("kPlayToTime", "Play to Frame", "Plays the animation up to a given frame number"), - ("kStop", "Stop", "Stops the animation",), - ("kToggleState", "Toggle", "Toggles between Play and Stop"), - ("CURRENT", "(Don't Change)", "Don't change the animation's playing state")], - default="CURRENT") - play_direction = EnumProperty(name="Direction", - description="Which direction do you want to play from?", - items=[("kSetForwards", "Forward", "Play forwards"), - ("kSetBackwards", "Backwards", "Play backwards"), - ("CURRENT", "(Don't Change)", "Don't change the play direction")], - default="CURRENT") - play_to_percent = IntProperty(name="Play To", - description="Percentage at which to stop the animation", - subtype="PERCENTAGE", - min=0, max=100, default=50) - play_to_frame = IntProperty(name="Play To", - description="Frame at which to stop the animation", - min=0) + target_object = PointerProperty( + name="Object", description="Target object", type=bpy.types.Object + ) + target_material = PointerProperty( + name="Material", + description="Target material", + type=bpy.types.Material, + poll=_poll_material, + ) + target_texture = PointerProperty( + name="Texture", + description="Target texture", + type=bpy.types.Texture, + poll=_poll_texture, + ) + + go_to = EnumProperty( + name="Go To", + description="Where should the animation start?", + items=[ + ("kGoToBegin", "Beginning", "The beginning"), + ("kGoToLoopBegin", "Loop Beginning", "The beginning of the active loop"), + ("CURRENT", "(Don't Change)", "The current position"), + ("kGoToEnd", "Ending", "The end"), + ("kGoToLoopEnd", "Loop Ending", "The end of the active loop"), + ], + default="CURRENT", + ) + action = EnumProperty( + name="Action", + description="What do you want the animation to do?", + items=[ + ("kContinue", "Play", "Plays the animation"), + ( + "kPlayToPercent", + "Play to Percent", + "Plays the animation until a given percent is complete", + ), + ( + "kPlayToTime", + "Play to Frame", + "Plays the animation up to a given frame number", + ), + ( + "kStop", + "Stop", + "Stops the animation", + ), + ("kToggleState", "Toggle", "Toggles between Play and Stop"), + ("CURRENT", "(Don't Change)", "Don't change the animation's playing state"), + ], + default="CURRENT", + ) + play_direction = EnumProperty( + name="Direction", + description="Which direction do you want to play from?", + items=[ + ("kSetForwards", "Forward", "Play forwards"), + ("kSetBackwards", "Backwards", "Play backwards"), + ("CURRENT", "(Don't Change)", "Don't change the play direction"), + ], + default="CURRENT", + ) + play_to_percent = IntProperty( + name="Play To", + description="Percentage at which to stop the animation", + subtype="PERCENTAGE", + min=0, + max=100, + default=50, + ) + play_to_frame = IntProperty( + name="Play To", description="Frame at which to stop the animation", min=0 + ) def _set_loop_name(self, context): """Updates loop_begin and loop_end when the loop name is changed""" pass - looping = EnumProperty(name="Looping", - description="Is the animation looping?", - items=[("kSetLooping", "Yes", "The animation is looping",), - ("CURRENT", "(Don't Change)", "Don't change the loop status"), - ("kSetUnLooping", "No", "The animation is NOT looping")], - default="CURRENT") - loop_name = StringProperty(name="Active Loop", - description="Name of the active loop", - update=_set_loop_name) - loop_begin = IntProperty(name="Loop Begin", - description="Frame number at which the loop begins", - min=0) - loop_end = IntProperty(name="Loop End", - description="Frame number at which the loop ends", - min=0) - - event = EnumProperty(name="Callback", - description="Event upon which to callback the Responder", - items=[("kEnd", "End", "When the action ends"), - ("NONE", "(None)", "Don't notify the Responder at all"), - ("kStop", "Stop", "When the action is stopped by a message")], - default="kEnd") + looping = EnumProperty( + name="Looping", + description="Is the animation looping?", + items=[ + ( + "kSetLooping", + "Yes", + "The animation is looping", + ), + ("CURRENT", "(Don't Change)", "Don't change the loop status"), + ("kSetUnLooping", "No", "The animation is NOT looping"), + ], + default="CURRENT", + ) + loop_name = StringProperty( + name="Active Loop", description="Name of the active loop", update=_set_loop_name + ) + loop_begin = IntProperty( + name="Loop Begin", description="Frame number at which the loop begins", min=0 + ) + loop_end = IntProperty( + name="Loop End", description="Frame number at which the loop ends", min=0 + ) + + event = EnumProperty( + name="Callback", + description="Event upon which to callback the Responder", + items=[ + ("kEnd", "End", "When the action ends"), + ("NONE", "(None)", "Don't notify the Responder at all"), + ("kStop", "Stop", "When the action is stopped by a message"), + ], + default="kEnd", + ) # Blender memory workaround _ENTIRE_ANIMATION = "(Entire Animation)" + def _get_anim_names(self, context): if self.anim_type == "OBJECT": - items = [(anim.animation_name, anim.animation_name, "") - for anim in self.target_object.plasma_modifiers.animation.subanimations] + items = [ + (anim.animation_name, anim.animation_name, "") + for anim in self.target_object.plasma_modifiers.animation.subanimations + ] elif self.anim_type == "TEXTURE": if self.target_texture is not None: - items = [(anim.animation_name, anim.animation_name, "") - for anim in self.target_texture.plasma_layer.subanimations] + items = [ + (anim.animation_name, anim.animation_name, "") + for anim in self.target_texture.plasma_layer.subanimations + ] elif self.target_material is not None or self.target_object is not None: if self.target_material is None: - materials = (i.material for i in self.target_object.material_slots if i and i.material) + materials = ( + i.material + for i in self.target_object.material_slots + if i and i.material + ) else: materials = (self.target_material,) - layer_props = (i.texture.plasma_layer for mat in materials for i in mat.texture_slots if i and i.texture) - all_anims = frozenset((anim.animation_name for i in layer_props for anim in i.subanimations)) + layer_props = ( + i.texture.plasma_layer + for mat in materials + for i in mat.texture_slots + if i and i.texture + ) + all_anims = frozenset( + ( + anim.animation_name + for i in layer_props + for anim in i.subanimations + ) + ) items = [(i, i, "") for i in all_anims] else: - items = [(PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, "")] + items = [ + ( + PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, + PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, + "", + ) + ] else: raise RuntimeError() # We always want "(Entire Animation)", if it exists, to be the first item. - entire = items.index((PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, "")) + entire = items.index( + ( + PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, + PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, + "", + ) + ) if entire not in (-1, 0): items.pop(entire) - items.insert(0, (PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, "")) + items.insert( + 0, + ( + PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, + PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, + "", + ), + ) return items - anim_name = EnumProperty(name="Animation", - description="Name of the animation to control", - items=_get_anim_names, - options=set()) + anim_name = EnumProperty( + name="Animation", + description="Name of the animation to control", + items=_get_anim_names, + options=set(), + ) def draw_buttons(self, context, layout): layout.prop(self, "anim_type") @@ -240,7 +354,9 @@ class PlasmaAnimCmdMsgNode(idprops.IDPropMixin, PlasmaMessageWithCallbacksNode, if self.anim_type == "OBJECT": col.alert = self.target_object is None else: - col.alert = not any((self.target_object, self.target_material, self.target_texture)) + col.alert = not any( + (self.target_object, self.target_material, self.target_texture) + ) col.prop(self, "target_object") if self.anim_type != "OBJECT": col.prop(self, "target_material") @@ -261,7 +377,11 @@ class PlasmaAnimCmdMsgNode(idprops.IDPropMixin, PlasmaMessageWithCallbacksNode, if self.anim_type != "OBJECT": loops = None else: - loops = None if self.target_object is None else self.target_object.plasma_modifiers.animation_loop + loops = ( + None + if self.target_object is None + else self.target_object.plasma_modifiers.animation_loop + ) if loops is not None and loops.enabled: layout.prop_search(self, "loop_name", loops, "loops", icon="PMARKER_ACT") else: @@ -293,10 +413,18 @@ class PlasmaAnimCmdMsgNode(idprops.IDPropMixin, PlasmaMessageWithCallbacksNode, material = self.target_material texture = self.target_texture if obj is None and material is None and texture is None: - self.raise_error("At least one of: target object, material, texture MUST be specified") - target = exporter.mesh.material.get_texture_animation_key(obj, material, texture, self.anim_name) - - target = [i for i in target if not isinstance(i.object, (plAgeGlobalAnim, plLayerSDLAnimation))] + self.raise_error( + "At least one of: target object, material, texture MUST be specified" + ) + target = exporter.mesh.material.get_texture_animation_key( + obj, material, texture, self.anim_name + ) + + target = [ + i + for i in target + if not isinstance(i.object, (plAgeGlobalAnim, plLayerSDLAnimation)) + ] if not target: self.raise_error("No controllable animations were found.") for i in target: @@ -332,14 +460,18 @@ class PlasmaAnimCmdMsgNode(idprops.IDPropMixin, PlasmaMessageWithCallbacksNode, @classmethod def _idprop_mapping(cls): - return {"target_object": "object_name", - "target_material": "material_name", - "target_texture": "texture_name"} + return { + "target_object": "object_name", + "target_material": "material_name", + "target_texture": "texture_name", + } def _idprop_sources(self): - return {"object_name": bpy.data.objects, - "material_name": bpy.data.materials, - "texture_name": bpy.data.textures} + return { + "object_name": bpy.data.objects, + "material_name": bpy.data.materials, + "texture_name": bpy.data.textures, + } class PlasmaCameraMsgNode(PlasmaMessageNode, bpy.types.Node): @@ -348,20 +480,40 @@ class PlasmaCameraMsgNode(PlasmaMessageNode, bpy.types.Node): bl_label = "Camera" bl_width_default = 200 - cmd = EnumProperty(name="Command", - description="Command to send to the camera system", - items=[("push", "Push Camera", "Pushes a new camera onto the camera stack and transitions to it"), - ("pop", "Pop Camera", "Pops the camera off the camera stack"), - ("disablefp", "Disable First Person", "Forces the camera into third person if it is currently in first person and disables first person mode"), - ("enablefp", "Enable First Person", "Reenables the first person camera and switches back to it if the player was in first person previously")], - options=set()) - camera = PointerProperty(name="Camera", - type=bpy.types.Object, - poll=idprops.poll_camera_objects, - options=set()) - cut = BoolProperty(name="Cut Transition", - description="Immediately swap over to the new camera without a transition animation", - options=set()) + cmd = EnumProperty( + name="Command", + description="Command to send to the camera system", + items=[ + ( + "push", + "Push Camera", + "Pushes a new camera onto the camera stack and transitions to it", + ), + ("pop", "Pop Camera", "Pops the camera off the camera stack"), + ( + "disablefp", + "Disable First Person", + "Forces the camera into third person if it is currently in first person and disables first person mode", + ), + ( + "enablefp", + "Enable First Person", + "Reenables the first person camera and switches back to it if the player was in first person previously", + ), + ], + options=set(), + ) + camera = PointerProperty( + name="Camera", + type=bpy.types.Object, + poll=idprops.poll_camera_objects, + options=set(), + ) + cut = BoolProperty( + name="Cut Transition", + description="Immediately swap over to the new camera without a transition animation", + options=set(), + ) def convert_message(self, exporter, so): msg = plCameraMsg() @@ -373,8 +525,11 @@ class PlasmaCameraMsgNode(PlasmaMessageNode, bpy.types.Node): # the presence of the kResponderTrigger command. msg.setCmd(plCameraMsg.kResponderTrigger, self.cmd == "push") msg.setCmd(plCameraMsg.kRegionPushCamera, True) - msg.setCmd(plCameraMsg.kSetAsPrimary, self.camera is None - or self.camera.data.plasma_camera.settings.primary_camera) + msg.setCmd( + plCameraMsg.kSetAsPrimary, + self.camera is None + or self.camera.data.plasma_camera.settings.primary_camera, + ) msg.setCmd(plCameraMsg.kCut, self.cut) elif self.cmd == "disablefp": msg.setCmd(plCameraMsg.kResponderSetThirdPerson) @@ -396,31 +551,49 @@ class PlasmaEnableMsgNode(PlasmaMessageNode, bpy.types.Node): bl_idname = "PlasmaEnableMsgNode" bl_label = "Enable/Disable" - output_sockets = OrderedDict([ - ("receivers", { - "text": "Send To", - "type": "PlasmaEnableMessageSocket", - "valid_link_sockets": {"PlasmaEnableMessageSocket", "PlasmaNodeSocketInputGeneral"}, - }), - ]) - - cmd = EnumProperty(name="Command", - description="How should we affect the object's state?", - items=[("kDisable", "Disable", "Deactivate the object"), - ("kEnable", "Enable", "Activate the object")], - default="kEnable") - settings = EnumProperty(name="Affects", - description="Which attributes should we change", - items=[("kAudible", "Audio", "Sounds played by this object"), - ("kPhysical", "Physics", "Physical simulation of the object"), - ("kDrawable", "Visibility", "Visible geometry/light of the object"), - ("kModifiers", "Modifiers", "Modifiers attached to the object")], - options={"ENUM_FLAG"}, - default={"kAudible", "kDrawable", "kPhysical", "kModifiers"}) - bcast_to_children = BoolProperty(name="Send to Children", - description="Send the message to objects parented to the object", - default=False, - options=set()) + output_sockets = OrderedDict( + [ + ( + "receivers", + { + "text": "Send To", + "type": "PlasmaEnableMessageSocket", + "valid_link_sockets": { + "PlasmaEnableMessageSocket", + "PlasmaNodeSocketInputGeneral", + }, + }, + ), + ] + ) + + cmd = EnumProperty( + name="Command", + description="How should we affect the object's state?", + items=[ + ("kDisable", "Disable", "Deactivate the object"), + ("kEnable", "Enable", "Activate the object"), + ], + default="kEnable", + ) + settings = EnumProperty( + name="Affects", + description="Which attributes should we change", + items=[ + ("kAudible", "Audio", "Sounds played by this object"), + ("kPhysical", "Physics", "Physical simulation of the object"), + ("kDrawable", "Visibility", "Visible geometry/light of the object"), + ("kModifiers", "Modifiers", "Modifiers attached to the object"), + ], + options={"ENUM_FLAG"}, + default={"kAudible", "kDrawable", "kPhysical", "kModifiers"}, + ) + bcast_to_children = BoolProperty( + name="Send to Children", + description="Send the message to objects parented to the object", + default=False, + options=set(), + ) def convert_message(self, exporter, so): settings = self.settings @@ -491,18 +664,21 @@ class PlasmaExcludeRegionMsg(PlasmaMessageNode, bpy.types.Node): bl_idname = "PlasmaExcludeRegionMsg" bl_label = "Exclude Region" - output_sockets = OrderedDict([ - ("region", { - "text": "Region", - "type": "PlasmaExcludeMessageSocket" - }), - ]) - - cmd = EnumProperty(name="Command", - description="Exclude Region State", - items=[("kClear", "Clear", "Clear all avatars from the region"), - ("kRelease", "Release", "Allow avatars to enter the region")], - default="kClear") + output_sockets = OrderedDict( + [ + ("region", {"text": "Region", "type": "PlasmaExcludeMessageSocket"}), + ] + ) + + cmd = EnumProperty( + name="Command", + description="Exclude Region State", + items=[ + ("kClear", "Clear", "Clear all avatars from the region"), + ("kRelease", "Release", "Allow avatars to enter the region"), + ], + default="kClear", + ) def convert_message(self, exporter, so): msg = plExcludeRegionMsg() @@ -521,29 +697,60 @@ class PlasmaLinkToAgeMsg(PlasmaMessageNode, bpy.types.Node): bl_label = "Link to Age" bl_width_default = 280 - rules = EnumProperty(name="Rules", - description="Rules describing which age instance to link to", - items=[("kOriginalBook", "Original Age", "Links to a personally owned instance, creating if none exists"), - ("kOwnedBook", "Owned Age", "Links to a personally owned instance, fails if none exists"), - ("kChildAgeBook", "Child Age", "Links to an age instance parented to another personal age"), - ("kSubAgeBook", "Sub Age", "Links to an age instance owned by the current age instance"), - ("kBasicLink", "Basic", "Links to a specific age instance")]) - parent_filename = StringProperty(name="Parent Age", - description="Filename of the age that owns the age instance we're linking to") - - age_filename = StringProperty(name="Age Filename", - description="Filename of the age to link to (eg 'Garden'") - age_instance = StringProperty(name="Age Instance", - description="Instance name of the age to link to (eg 'Eder Kemo')") - age_uuid = StringProperty(name="Age Guid", - description="Instance GUID to link to (eg 'ea489821-6c35-4bd0-9dae-bb17c585e680')") - - spawn_title = StringProperty(name="Spawn Title", - description="Title of the Spawn Point to use", - default="Default") - spawn_point = StringProperty(name="Spawn Point", - description="Name of the Spawn Point's Plasma Object", - default="LinkInPointDefault") + rules = EnumProperty( + name="Rules", + description="Rules describing which age instance to link to", + items=[ + ( + "kOriginalBook", + "Original Age", + "Links to a personally owned instance, creating if none exists", + ), + ( + "kOwnedBook", + "Owned Age", + "Links to a personally owned instance, fails if none exists", + ), + ( + "kChildAgeBook", + "Child Age", + "Links to an age instance parented to another personal age", + ), + ( + "kSubAgeBook", + "Sub Age", + "Links to an age instance owned by the current age instance", + ), + ("kBasicLink", "Basic", "Links to a specific age instance"), + ], + ) + parent_filename = StringProperty( + name="Parent Age", + description="Filename of the age that owns the age instance we're linking to", + ) + + age_filename = StringProperty( + name="Age Filename", description="Filename of the age to link to (eg 'Garden'" + ) + age_instance = StringProperty( + name="Age Instance", + description="Instance name of the age to link to (eg 'Eder Kemo')", + ) + age_uuid = StringProperty( + name="Age Guid", + description="Instance GUID to link to (eg 'ea489821-6c35-4bd0-9dae-bb17c585e680')", + ) + + spawn_title = StringProperty( + name="Spawn Title", + description="Title of the Spawn Point to use", + default="Default", + ) + spawn_point = StringProperty( + name="Spawn Point", + description="Name of the Spawn Point's Plasma Object", + default="LinkInPointDefault", + ) def convert_message(self, exporter, so): msg = plLinkToAgeMsg() @@ -554,7 +761,9 @@ class PlasmaLinkToAgeMsg(PlasmaMessageNode, bpy.types.Node): if self.rules == "kChildAgeBook": als.parentAgeFilename = self.parent_filename ais.ageFilename = self.age_filename - ais.ageInstanceName = self.age_instance if self.age_instance else self.age_filename + ais.ageInstanceName = ( + self.age_instance if self.age_instance else self.age_filename + ) if self.rules == "kBasicLink": try: ais.ageInstanceGuid = self.age_uuid @@ -602,32 +811,45 @@ class PlasmaLinkToAgeMsg(PlasmaMessageNode, bpy.types.Node): layout.prop(self, "spawn_point") -class PlasmaOneShotMsgNode(idprops.IDPropObjectMixin, PlasmaMessageWithCallbacksNode, bpy.types.Node): +class PlasmaOneShotMsgNode( + idprops.IDPropObjectMixin, PlasmaMessageWithCallbacksNode, bpy.types.Node +): bl_category = "MSG" bl_idname = "PlasmaOneShotMsgNode" bl_label = "One Shot" bl_width_default = 210 - pos_object = PointerProperty(name="Position", - description="Object defining the OneShot position", - type=bpy.types.Object) - seek = EnumProperty(name="Seek", - description="How the avatar should approach the OneShot position", - items=[("SMART", "Smart Seek", "Let the engine figure out the best path"), - ("DUMB", "Seek", "Shuffle to the OneShot position"), - ("NONE", "Warp", "Warp the avatar to the OneShot position")], - default="SMART") - - animation = StringProperty(name="Animation", - description="Name of the animation the avatar should execute") - marker = StringProperty(name="Marker", - description="Name of the marker specifying when to notify the Responder") - drivable = BoolProperty(name="Drivable", - description="Player retains control of the avatar during the OneShot", - default=False) - reversable = BoolProperty(name="Reversable", - description="Player can reverse the OneShot", - default=False) + pos_object = PointerProperty( + name="Position", + description="Object defining the OneShot position", + type=bpy.types.Object, + ) + seek = EnumProperty( + name="Seek", + description="How the avatar should approach the OneShot position", + items=[ + ("SMART", "Smart Seek", "Let the engine figure out the best path"), + ("DUMB", "Seek", "Shuffle to the OneShot position"), + ("NONE", "Warp", "Warp the avatar to the OneShot position"), + ], + default="SMART", + ) + + animation = StringProperty( + name="Animation", description="Name of the animation the avatar should execute" + ) + marker = StringProperty( + name="Marker", + description="Name of the marker specifying when to notify the Responder", + ) + drivable = BoolProperty( + name="Drivable", + description="Player retains control of the avatar during the OneShot", + default=False, + ) + reversable = BoolProperty( + name="Reversable", description="Player can reverse the OneShot", default=False + ) def convert_callback_message(self, exporter, so, msg, target, wait): msg.addCallback(self.marker, target, wait) @@ -682,31 +904,42 @@ class PlasmaOneShotMsgNode(idprops.IDPropObjectMixin, PlasmaMessageWithCallbacks class PlasmaOneShotCallbackSocket(PlasmaMessageSocketBase, bpy.types.NodeSocket): - marker = StringProperty(name="Marker", - description="Marker specifying the time at which to send a callback to this Responder") + marker = StringProperty( + name="Marker", + description="Marker specifying the time at which to send a callback to this Responder", + ) def draw(self, context, layout, node, text): layout.prop(self, "marker") -class PlasmaSceneObjectMsgRcvrNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node): +class PlasmaSceneObjectMsgRcvrNode( + idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node +): bl_category = "MSG" bl_idname = "PlasmaSceneObjectMsgRcvrNode" bl_label = "Send To Object" bl_width_default = 190 - input_sockets = OrderedDict([ - ("message", { - "text": "Message", - "type": "PlasmaNodeSocketInputGeneral", - "valid_link_sockets": {"PlasmaEnableMessageSocket"}, - "spawn_empty": True, - }), - ]) - - target_object = PointerProperty(name="Object", - description="Object to send the message to", - type=bpy.types.Object) + input_sockets = OrderedDict( + [ + ( + "message", + { + "text": "Message", + "type": "PlasmaNodeSocketInputGeneral", + "valid_link_sockets": {"PlasmaEnableMessageSocket"}, + "spawn_empty": True, + }, + ), + ] + ) + + target_object = PointerProperty( + name="Object", + description="Object to send the message to", + type=bpy.types.Object, + ) def draw_buttons(self, context, layout): layout.prop(self, "target_object") @@ -723,7 +956,9 @@ class PlasmaSceneObjectMsgRcvrNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bp return {"target_object": "object_name"} -class PlasmaSoundMsgNode(idprops.IDPropObjectMixin, PlasmaMessageWithCallbacksNode, bpy.types.Node): +class PlasmaSoundMsgNode( + idprops.IDPropObjectMixin, PlasmaMessageWithCallbacksNode, bpy.types.Node +): bl_category = "MSG" bl_idname = "PlasmaSoundMsgNode" bl_label = "Sound" @@ -732,48 +967,82 @@ class PlasmaSoundMsgNode(idprops.IDPropObjectMixin, PlasmaMessageWithCallbacksNo def _poll_sound_emitters(self, value): return value.plasma_modifiers.soundemit.enabled - emitter_object = PointerProperty(name="Object", - description="Sound emitter object", - type=bpy.types.Object, - poll=_poll_sound_emitters) - sound_name = StringProperty(name="Sound", - description="Sound datablock") - - go_to = EnumProperty(name="Go To", - description="Where should the sound start?", - items=[("BEGIN", "Beginning", "The beginning"), - ("CURRENT", "(Don't Change)", "The current position"), - ("TIME", "Time", "The time specified in seconds")], - default="CURRENT") - looping = EnumProperty(name="Looping", - description="Is the sound looping?", - items=[("kSetLooping", "Yes", "The sound is looping",), - ("CURRENT", "(Don't Change)", "Don't change the loop status"), - ("kUnSetLooping", "No", "The sound is NOT looping")], - default="CURRENT") - action = EnumProperty(name="Action", - description="What do you want the sound to do?", - items=[("kPlay", "Play", "Plays the sound"), - ("kStop", "Stop", "Stops the sound",), - ("kToggleState", "Toggle", "Toggles between Play and Stop"), - ("CURRENT", "(Don't Change)", "Don't change the sound's playing state")], - default="CURRENT") - volume = EnumProperty(name="Volume", - description="What should happen to the volume?", - items=[("MUTE", "Mute", "Mutes the volume"), - ("CURRENT", "(Don't Change)", "Don't change the volume"), - ("CUSTOM", "Custom", "Manually specify the volume")], - default="CURRENT") - - time = FloatProperty(name="Time", - description="Time in seconds to begin playing from", - min=0.0, default=0.0, - options=set(), subtype="TIME", unit="TIME") - volume_pct = IntProperty(name="Volume Level", - description="Volume to play the sound", - min=0, max=100, default=100, - options=set(), - subtype="PERCENTAGE") + emitter_object = PointerProperty( + name="Object", + description="Sound emitter object", + type=bpy.types.Object, + poll=_poll_sound_emitters, + ) + sound_name = StringProperty(name="Sound", description="Sound datablock") + + go_to = EnumProperty( + name="Go To", + description="Where should the sound start?", + items=[ + ("BEGIN", "Beginning", "The beginning"), + ("CURRENT", "(Don't Change)", "The current position"), + ("TIME", "Time", "The time specified in seconds"), + ], + default="CURRENT", + ) + looping = EnumProperty( + name="Looping", + description="Is the sound looping?", + items=[ + ( + "kSetLooping", + "Yes", + "The sound is looping", + ), + ("CURRENT", "(Don't Change)", "Don't change the loop status"), + ("kUnSetLooping", "No", "The sound is NOT looping"), + ], + default="CURRENT", + ) + action = EnumProperty( + name="Action", + description="What do you want the sound to do?", + items=[ + ("kPlay", "Play", "Plays the sound"), + ( + "kStop", + "Stop", + "Stops the sound", + ), + ("kToggleState", "Toggle", "Toggles between Play and Stop"), + ("CURRENT", "(Don't Change)", "Don't change the sound's playing state"), + ], + default="CURRENT", + ) + volume = EnumProperty( + name="Volume", + description="What should happen to the volume?", + items=[ + ("MUTE", "Mute", "Mutes the volume"), + ("CURRENT", "(Don't Change)", "Don't change the volume"), + ("CUSTOM", "Custom", "Manually specify the volume"), + ], + default="CURRENT", + ) + + time = FloatProperty( + name="Time", + description="Time in seconds to begin playing from", + min=0.0, + default=0.0, + options=set(), + subtype="TIME", + unit="TIME", + ) + volume_pct = IntProperty( + name="Volume Level", + description="Volume to play the sound", + min=0, + max=100, + default=100, + options=set(), + subtype="PERCENTAGE", + ) def convert_callback_message(self, exporter, so, msg, target, wait): assert not self.is_random_sound, "Callbacks are not available for random sounds" @@ -789,7 +1058,9 @@ class PlasmaSoundMsgNode(idprops.IDPropObjectMixin, PlasmaMessageWithCallbacksNo self.raise_error("Sound emitter must be set") soundemit = self.emitter_object.plasma_modifiers.soundemit if not soundemit.enabled: - self.raise_error("'{}' is not a valid Sound Emitter".format(self.emitter_object.name)) + self.raise_error( + "'{}' is not a valid Sound Emitter".format(self.emitter_object.name) + ) if self.is_random_sound: yield from self._convert_random_sound_msg(exporter, so) @@ -820,12 +1091,22 @@ class PlasmaSoundMsgNode(idprops.IDPropObjectMixin, PlasmaMessageWithCallbacksNo # Always test the specified audible for validity if self.sound_name and soundemit.sounds.get(self.sound_name, None) is None: - self.raise_error("Invalid Sound '{}' requested from Sound Emitter '{}'".format(self.sound_name, self.emitter_object.name)) + self.raise_error( + "Invalid Sound '{}' requested from Sound Emitter '{}'".format( + self.sound_name, self.emitter_object.name + ) + ) # Remember that 3D stereo sounds are exported as two emitters... # But, if we only have one sound attached, who cares, we can just address the message to all - audible_key = exporter.mgr.find_create_key(plAudioInterface, bl=self.emitter_object) - indices = (-1,) if not self.sound_name or len(soundemit.sounds) == 1 else soundemit.get_sound_indices(self.sound_name) + audible_key = exporter.mgr.find_create_key( + plAudioInterface, bl=self.emitter_object + ) + indices = ( + (-1,) + if not self.sound_name or len(soundemit.sounds) == 1 + else soundemit.get_sound_indices(self.sound_name) + ) for idx in indices: msg = plSoundMsg() msg.addReceiver(audible_key) @@ -866,7 +1147,9 @@ class PlasmaSoundMsgNode(idprops.IDPropObjectMixin, PlasmaMessageWithCallbacksNo if self.emitter_object is not None: soundemit = self.emitter_object.plasma_modifiers.soundemit if soundemit.enabled: - layout.prop_search(self, "sound_name", soundemit, "sounds", icon="SOUND") + layout.prop_search( + self, "sound_name", soundemit, "sounds", icon="SOUND" + ) else: layout.label("Not a Sound Emitter", icon="ERROR") @@ -901,10 +1184,12 @@ class PlasmaTimerCallbackMsgNode(PlasmaMessageWithCallbacksNode, bpy.types.Node) bl_idname = "PlasmaTimerCallbackMsgNode" bl_label = "Timed Callback" - delay = FloatProperty(name="Delay", - description="Time (in seconds) to wait until continuing", - min=0.1, - default=1.0) + delay = FloatProperty( + name="Delay", + description="Time (in seconds) to wait until continuing", + min=0.1, + default=1.0, + ) def draw_buttons(self, context, layout): layout.prop(self, "delay") @@ -924,15 +1209,20 @@ class PlasmaTriggerMultiStageMsgNode(PlasmaMessageNode, bpy.types.Node): bl_idname = "PlasmaTriggerMultiStageMsgNode" bl_label = "Trigger MultiStage" - output_sockets = OrderedDict([ - ("satisfies", { - "text": "Trigger", - "type": "PlasmaConditionSocket", - "valid_link_nodes": "PlasmaMultiStageBehaviorNode", - "valid_link_sockets": "PlasmaConditionSocket", - "link_limit": 1, - }) - ]) + output_sockets = OrderedDict( + [ + ( + "satisfies", + { + "text": "Trigger", + "type": "PlasmaConditionSocket", + "valid_link_nodes": "PlasmaMultiStageBehaviorNode", + "valid_link_sockets": "PlasmaConditionSocket", + "link_limit": 1, + }, + ) + ] + ) def convert_message(self, exporter, so): # Yeah, this is not a REAL Plasma message, but the Korman way is to try to hide these little @@ -952,16 +1242,18 @@ class PlasmaFootstepSoundMsgNode(PlasmaMessageNode, bpy.types.Node): bl_idname = "PlasmaFootstepSoundMsgNode" bl_label = "Footstep Sound" - surface = EnumProperty(name="Surface", - description="What kind of surface are we walking on?", - items=footstep_surfaces, - default="stone") + surface = EnumProperty( + name="Surface", + description="What kind of surface are we walking on?", + items=footstep_surfaces, + default="stone", + ) def draw_buttons(self, context, layout): layout.prop(self, "surface") def convert_message(self, exporter, so): msg = plArmatureEffectStateMsg() - msg.BCastFlags |= (plMessage.kPropagateToModifiers | plMessage.kNetPropagate) + msg.BCastFlags |= plMessage.kPropagateToModifiers | plMessage.kNetPropagate msg.surface = footstep_surface_ids[self.surface] return msg diff --git a/korman/nodes/node_python.py b/korman/nodes/node_python.py index dd0b94a..74a7bc0 100644 --- a/korman/nodes/node_python.py +++ b/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), diff --git a/korman/nodes/node_responder.py b/korman/nodes/node_responder.py index 10ccf69..0fdd675 100644 --- a/korman/nodes/node_responder.py +++ b/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: diff --git a/korman/nodes/node_softvolume.py b/korman/nodes/node_softvolume.py index 06b8144..decca86 100644 --- a/korman/nodes/node_softvolume.py +++ b/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 diff --git a/korman/operators/__init__.py b/korman/operators/__init__.py index 7ff172a..9314448 100644 --- a/korman/operators/__init__.py +++ b/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() diff --git a/korman/operators/op_export.py b/korman/operators/op_export.py index 5f41e18..8473058 100644 --- a/korman/operators/op_export.py +++ b/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) diff --git a/korman/operators/op_image.py b/korman/operators/op_image.py index d7b39de..3cc22f6 100644 --- a/korman/operators/op_image.py +++ b/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) diff --git a/korman/operators/op_lightmap.py b/korman/operators/op_lightmap.py index 7d98f91..9eb5d85 100644 --- a/korman/operators/op_lightmap.py +++ b/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) diff --git a/korman/operators/op_mesh.py b/korman/operators/op_mesh.py index 5a5a98e..31b7a2c 100644 --- a/korman/operators/op_mesh.py +++ b/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__) diff --git a/korman/operators/op_modifier.py b/korman/operators/op_modifier.py index 9fb18cc..faf5fb7 100644 --- a/korman/operators/op_modifier.py +++ b/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"} diff --git a/korman/operators/op_nodes.py b/korman/operators/op_nodes.py index 67e1196..c5b1114 100644 --- a/korman/operators/op_nodes.py +++ b/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) diff --git a/korman/operators/op_sound.py b/korman/operators/op_sound.py index 911a562..0ef97b3 100644 --- a/korman/operators/op_sound.py +++ b/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 diff --git a/korman/operators/op_toolbox.py b/korman/operators/op_toolbox.py index a5ffb64..a82ffe9 100644 --- a/korman/operators/op_toolbox.py +++ b/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 diff --git a/korman/operators/op_ui.py b/korman/operators/op_ui.py index ffa3b17..89378a2 100644 --- a/korman/operators/op_ui.py +++ b/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) diff --git a/korman/operators/op_world.py b/korman/operators/op_world.py index 866a580..3bbdceb 100644 --- a/korman/operators/op_world.py +++ b/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"} diff --git a/korman/ordered_set.py b/korman/ordered_set.py index eb333f6..ccefcab 100644 --- a/korman/ordered_set.py +++ b/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 - diff --git a/korman/plasma_attributes.py b/korman/plasma_attributes.py index 70408fb..71e2fab 100644 --- a/korman/plasma_attributes.py +++ b/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 diff --git a/korman/plasma_launcher.py b/korman/plasma_launcher.py index 4937b87..30c5fc9 100644 --- a/korman/plasma_launcher.py +++ b/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() diff --git a/korman/properties/modifiers/__init__.py b/korman/properties/modifiers/__init__.py index ba179df..259d83f 100644 --- a/korman/properties/modifiers/__init__.py +++ b/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, []) diff --git a/korman/properties/modifiers/anim.py b/korman/properties/modifiers/anim.py index 63b8572..74db6a7 100644 --- a/korman/properties/modifiers/anim.py +++ b/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), + ) diff --git a/korman/properties/modifiers/avatar.py b/korman/properties/modifiers/avatar.py index 00455e6..f5cb029 100644 --- a/korman/properties/modifiers/avatar.py +++ b/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 + ) + ) diff --git a/korman/properties/modifiers/base.py b/korman/properties/modifiers/base.py index 1e4827b..a3ae44b 100644 --- a/korman/properties/modifiers/base.py +++ b/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) diff --git a/korman/properties/modifiers/gui.py b/korman/properties/modifiers/gui.py index f69996a..4cad13e 100644 --- a/korman/properties/modifiers/gui.py +++ b/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 + ) diff --git a/korman/properties/modifiers/logic.py b/korman/properties/modifiers/logic.py index 2fcec98..9761274 100644 --- a/korman/properties/modifiers/logic.py +++ b/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 diff --git a/korman/properties/modifiers/physics.py b/korman/properties/modifiers/physics.py index bf4c7ff..5c8c5fb 100644 --- a/korman/properties/modifiers/physics.py +++ b/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 + ) + ) diff --git a/korman/properties/modifiers/region.py b/korman/properties/modifiers/region.py index 8c887de..b565b04 100644 --- a/korman/properties/modifiers/region.py +++ b/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"] + ) diff --git a/korman/properties/modifiers/render.py b/korman/properties/modifiers/render.py index 7ec6493..eedff33 100644 --- a/korman/properties/modifiers/render.py +++ b/korman/properties/modifiers/render.py @@ -19,22 +19,27 @@ from bpy.props import * import functools from PyHSPlasma import * -from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz, PlasmaModifierUpgradable +from .base import ( + PlasmaModifierProperties, + PlasmaModifierLogicWiz, + PlasmaModifierUpgradable, +) from ...exporter.etlight import _NUM_RENDER_LAYERS from ...exporter import utils from ...exporter.explosions import ExportError from .gui import languages, PlasmaJournalTranslation, TranslationMixin from ... import idprops + class PlasmaBlendOntoObject(bpy.types.PropertyGroup): - blend_onto = PointerProperty(name="Blend Onto", - description="Object to render first", - options=set(), - type=bpy.types.Object, - poll=idprops.poll_drawable_objects) - enabled = BoolProperty(name="Enabled", - default=True, - options=set()) + blend_onto = PointerProperty( + name="Blend Onto", + description="Object to render first", + options=set(), + type=bpy.types.Object, + poll=idprops.poll_drawable_objects, + ) + enabled = BoolProperty(name="Enabled", default=True, options=set()) class PlasmaBlendMod(PlasmaModifierProperties): @@ -44,19 +49,39 @@ class PlasmaBlendMod(PlasmaModifierProperties): bl_label = "Blending" bl_description = "Advanced Blending Options" - render_level = EnumProperty(name="Render Pass", - description="Suggested render pass for this object.", - items=[("AUTO", "(Auto)", "Let Korman decide when to render this object."), - ("OPAQUE", "Before Avatar", "Prefer for the object to draw before the avatar."), - ("FRAMEBUF", "Frame Buffer", "Prefer for the object to draw after the avatar but before other blended objects."), - ("BLEND", "Blended", "Prefer for the object to draw after most other geometry in the blended pass.")], - options=set()) - sort_faces = EnumProperty(name="Sort Faces", - description="", - items=[("AUTO", "(Auto)", "Let Korman decide if faces should be sorted."), - ("ALWAYS", "Always", "Force the object's faces to be sorted."), - ("NEVER", "Never", "Force the object's faces to never be sorted.")], - options=set()) + render_level = EnumProperty( + name="Render Pass", + description="Suggested render pass for this object.", + items=[ + ("AUTO", "(Auto)", "Let Korman decide when to render this object."), + ( + "OPAQUE", + "Before Avatar", + "Prefer for the object to draw before the avatar.", + ), + ( + "FRAMEBUF", + "Frame Buffer", + "Prefer for the object to draw after the avatar but before other blended objects.", + ), + ( + "BLEND", + "Blended", + "Prefer for the object to draw after most other geometry in the blended pass.", + ), + ], + options=set(), + ) + sort_faces = EnumProperty( + name="Sort Faces", + description="", + items=[ + ("AUTO", "(Auto)", "Let Korman decide if faces should be sorted."), + ("ALWAYS", "Always", "Force the object's faces to be sorted."), + ("NEVER", "Never", "Force the object's faces to never be sorted."), + ], + options=set(), + ) dependencies = CollectionProperty(type=PlasmaBlendOntoObject) active_dependency_index = IntProperty(options={"HIDDEN"}) @@ -110,21 +135,24 @@ class PlasmaBlendMod(PlasmaModifierProperties): return False def iter_dependencies(self): - for i in (j.blend_onto for j in self.dependencies if j.blend_onto is not None and j.enabled): + for i in ( + j.blend_onto + for j in self.dependencies + if j.blend_onto is not None and j.enabled + ): yield i def sanity_check(self): if self.has_circular_dependency: - raise ExportError("'{}': Circular Render Dependency detected!".format(self.name)) + raise ExportError( + "'{}': Circular Render Dependency detected!".format(self.name) + ) class PlasmaDecalManagerRef(bpy.types.PropertyGroup): - enabled = BoolProperty(name="Enabled", - default=True, - options=set()) + enabled = BoolProperty(name="Enabled", default=True, options=set()) - name = StringProperty(name="Decal Name", - options=set()) + name = StringProperty(name="Decal Name", options=set()) class PlasmaDecalMod: @@ -146,25 +174,30 @@ class PlasmaDecalPrintMod(PlasmaDecalMod, PlasmaModifierProperties): bl_label = "Print Decal" bl_description = "Prints a decal onto an object" - decal_type = EnumProperty(name="Decal Type", - description="Type of decal to print onto another object", - items=[("DYNAMIC", "Dynamic", "This object prints a decal onto dynamic decal surfaces"), - ("STATIC", "Static", "This object is a decal itself")], - options=set()) + decal_type = EnumProperty( + name="Decal Type", + description="Type of decal to print onto another object", + items=[ + ( + "DYNAMIC", + "Dynamic", + "This object prints a decal onto dynamic decal surfaces", + ), + ("STATIC", "Static", "This object is a decal itself"), + ], + options=set(), + ) # Dynamic Decals - length = FloatProperty(name="Length", - min=0.1, soft_max=30.0, precision=2, - default=0.45, - options=set()) - width = FloatProperty(name="Width", - min=0.1, soft_max=30.0, precision=2, - default=0.9, - options=set()) - height = FloatProperty(name="Height", - min=0.1, soft_max=30.0, precision=2, - default=1.0, - options=set()) + length = FloatProperty( + name="Length", min=0.1, soft_max=30.0, precision=2, default=0.45, options=set() + ) + width = FloatProperty( + name="Width", min=0.1, soft_max=30.0, precision=2, default=0.9, options=set() + ) + height = FloatProperty( + name="Height", min=0.1, soft_max=30.0, precision=2, default=1.0, options=set() + ) @property def copy_material(self): @@ -172,7 +205,11 @@ class PlasmaDecalPrintMod(PlasmaDecalMod, PlasmaModifierProperties): def get_key(self, exporter, so): if self.decal_type == "DYNAMIC": - pClass = plActivePrintShape if any((i.enabled for i in self.managers)) else plPrintShape + pClass = ( + plActivePrintShape + if any((i.enabled for i in self.managers)) + else plPrintShape + ) return exporter.mgr.find_create_key(pClass, so=so) def export(self, exporter, bo, so): @@ -190,6 +227,7 @@ class PlasmaDecalPrintMod(PlasmaDecalMod, PlasmaModifierProperties): f = functools.partial(exporter.decal.export_active_print_shape, print_shape) self._iter_decals(f) + class PlasmaDecalReceiveMod(PlasmaDecalMod, PlasmaModifierProperties): pl_id = "decal_receive" @@ -216,55 +254,99 @@ class PlasmaFadeMod(PlasmaModifierProperties): bl_label = "Opacity Fader" bl_description = "Fades an object based on distance or line-of-sight" - fader_type = EnumProperty(name="Fader Type", - description="Type of opacity fade", - items=[("DistOpacity", "Distance", "Fade based on distance to object"), - ("FadeOpacity", "Line-of-Sight", "Fade based on line-of-sight to object"), - ("SimpleDist", "Simple Distance", "Fade for use as Great Zero Markers")], - default="SimpleDist") - - fade_in_time = FloatProperty(name="Fade In Time", - description="Number of seconds before the object is fully visible", - min=0.0, max=5.0, default=0.5, subtype="TIME", unit="TIME") - fade_out_time = FloatProperty(name="Fade Out Time", - description="Number of seconds before the object is fully invisible", - min=0.0, max=5.0, default=0.5, subtype="TIME", unit="TIME") - bounds_center = BoolProperty(name="Use Mesh Midpoint", - description="Use mesh's midpoint to calculate LOS instead of object origin", - default=False) - - near_trans = FloatProperty(name="Near Transparent", - description="Nearest distance at which the object is fully transparent", - min=0.0, default=0.0, subtype="DISTANCE", unit="LENGTH") - near_opaq = FloatProperty(name="Near Opaque", - description="Nearest distance at which the object is fully opaque", - min=0.0, default=0.0, subtype="DISTANCE", unit="LENGTH") - far_opaq = FloatProperty(name="Far Opaque", - description="Farthest distance at which the object is fully opaque", - min=0.0, default=15.0, subtype="DISTANCE", unit="LENGTH") - far_trans = FloatProperty(name="Far Transparent", - description="Farthest distance at which the object is fully transparent", - min=0.0, default=20.0, subtype="DISTANCE", unit="LENGTH") + fader_type = EnumProperty( + name="Fader Type", + description="Type of opacity fade", + items=[ + ("DistOpacity", "Distance", "Fade based on distance to object"), + ("FadeOpacity", "Line-of-Sight", "Fade based on line-of-sight to object"), + ("SimpleDist", "Simple Distance", "Fade for use as Great Zero Markers"), + ], + default="SimpleDist", + ) + + fade_in_time = FloatProperty( + name="Fade In Time", + description="Number of seconds before the object is fully visible", + min=0.0, + max=5.0, + default=0.5, + subtype="TIME", + unit="TIME", + ) + fade_out_time = FloatProperty( + name="Fade Out Time", + description="Number of seconds before the object is fully invisible", + min=0.0, + max=5.0, + default=0.5, + subtype="TIME", + unit="TIME", + ) + bounds_center = BoolProperty( + name="Use Mesh Midpoint", + description="Use mesh's midpoint to calculate LOS instead of object origin", + default=False, + ) + + near_trans = FloatProperty( + name="Near Transparent", + description="Nearest distance at which the object is fully transparent", + min=0.0, + default=0.0, + subtype="DISTANCE", + unit="LENGTH", + ) + near_opaq = FloatProperty( + name="Near Opaque", + description="Nearest distance at which the object is fully opaque", + min=0.0, + default=0.0, + subtype="DISTANCE", + unit="LENGTH", + ) + far_opaq = FloatProperty( + name="Far Opaque", + description="Farthest distance at which the object is fully opaque", + min=0.0, + default=15.0, + subtype="DISTANCE", + unit="LENGTH", + ) + far_trans = FloatProperty( + name="Far Transparent", + description="Farthest distance at which the object is fully transparent", + min=0.0, + default=20.0, + subtype="DISTANCE", + unit="LENGTH", + ) def export(self, exporter, bo, so): if self.fader_type == "DistOpacity": - mod = exporter.mgr.find_create_object(plDistOpacityMod, so=so, name=self.key_name) + mod = exporter.mgr.find_create_object( + plDistOpacityMod, so=so, name=self.key_name + ) mod.nearTrans = self.near_trans mod.nearOpaq = self.near_opaq mod.farOpaq = self.far_opaq mod.farTrans = self.far_trans elif self.fader_type == "FadeOpacity": - mod = exporter.mgr.find_create_object(plFadeOpacityMod, so=so, name=self.key_name) + mod = exporter.mgr.find_create_object( + plFadeOpacityMod, so=so, name=self.key_name + ) mod.fadeUp = self.fade_in_time mod.fadeDown = self.fade_out_time mod.boundsCenter = self.bounds_center elif self.fader_type == "SimpleDist": - mod = exporter.mgr.find_create_object(plDistOpacityMod, so=so, name=self.key_name) + mod = exporter.mgr.find_create_object( + plDistOpacityMod, so=so, name=self.key_name + ) mod.nearTrans = 0.0 mod.nearOpaq = 0.0 mod.farOpaq = self.far_opaq mod.farTrans = self.far_trans - + @property def requires_actor(self): return self.fader_type == "FadeOpacity" @@ -277,29 +359,33 @@ class PlasmaFollowMod(idprops.IDPropObjectMixin, PlasmaModifierProperties): bl_label = "Follow" bl_description = "Follow the movement of the camera, player, or another object" - follow_mode = EnumProperty(name="Mode", - description="Leader's movement to follow", - items=[ - ("kPositionX", "X Axis", "Follow the leader's X movements"), - ("kPositionY", "Y Axis", "Follow the leader's Y movements"), - ("kPositionZ", "Z Axis", "Follow the leader's Z movements"), - ("kRotate", "Rotation", "Follow the leader's rotation movements"), - ], - default={"kPositionX", "kPositionY", "kPositionZ"}, - options={"ENUM_FLAG"}) - - leader_type = EnumProperty(name="Leader Type", - description="Leader to follow", - items=[ - ("kFollowCamera", "Camera", "Follow the camera"), - ("kFollowListener", "Listener", "Follow listeners"), - ("kFollowPlayer", "Player", "Follow the local player"), - ("kFollowObject", "Object", "Follow an object"), - ]) - - leader = PointerProperty(name="Leader Object", - description="Object to follow", - type=bpy.types.Object) + follow_mode = EnumProperty( + name="Mode", + description="Leader's movement to follow", + items=[ + ("kPositionX", "X Axis", "Follow the leader's X movements"), + ("kPositionY", "Y Axis", "Follow the leader's Y movements"), + ("kPositionZ", "Z Axis", "Follow the leader's Z movements"), + ("kRotate", "Rotation", "Follow the leader's rotation movements"), + ], + default={"kPositionX", "kPositionY", "kPositionZ"}, + options={"ENUM_FLAG"}, + ) + + leader_type = EnumProperty( + name="Leader Type", + description="Leader to follow", + items=[ + ("kFollowCamera", "Camera", "Follow the camera"), + ("kFollowListener", "Listener", "Follow listeners"), + ("kFollowPlayer", "Player", "Follow the local player"), + ("kFollowObject", "Object", "Follow an object"), + ], + ) + + leader = PointerProperty( + name="Leader Object", description="Object to follow", type=bpy.types.Object + ) def export(self, exporter, bo, so): fm = exporter.mgr.find_create_object(plFollowMod, so=so, name=self.key_name) @@ -315,7 +401,11 @@ class PlasmaFollowMod(idprops.IDPropObjectMixin, PlasmaModifierProperties): if self.leader: fm.leader = exporter.mgr.find_create_key(plSceneObject, bl=self.leader) else: - raise ExportError("'{}': Follow's leader object must be selected".format(self.key_name)) + raise ExportError( + "'{}': Follow's leader object must be selected".format( + self.key_name + ) + ) @classmethod def _idprop_mapping(cls): @@ -327,23 +417,25 @@ class PlasmaFollowMod(idprops.IDPropObjectMixin, PlasmaModifierProperties): class PlasmaGrassWave(bpy.types.PropertyGroup): - distance = FloatVectorProperty(name="Distance", - size=3, - default=(0.2, 0.2, 0.1), - subtype="XYZ", - unit="LENGTH", - options=set()) - direction = FloatVectorProperty(name="Direction", - size=2, - default=(0.2, 0.05), - soft_min=0.0, soft_max=1.0, - unit="LENGTH", - subtype="XYZ", - options=set()) - speed = FloatProperty(name="Speed", - default=0.1, - unit="VELOCITY", - options=set()) + distance = FloatVectorProperty( + name="Distance", + size=3, + default=(0.2, 0.2, 0.1), + subtype="XYZ", + unit="LENGTH", + options=set(), + ) + direction = FloatVectorProperty( + name="Direction", + size=2, + default=(0.2, 0.05), + soft_min=0.0, + soft_max=1.0, + unit="LENGTH", + subtype="XYZ", + options=set(), + ) + speed = FloatProperty(name="Speed", default=0.1, unit="VELOCITY", options=set()) class PlasmaGrassShaderMod(PlasmaModifierProperties): @@ -359,12 +451,16 @@ class PlasmaGrassShaderMod(PlasmaModifierProperties): wave4 = PointerProperty(type=PlasmaGrassWave) # UI Accessor - wave_selector = EnumProperty(items=[("wave1", "Wave 1", ""), - ("wave2", "Wave 2", ""), - ("wave3", "Wave 3", ""), - ("wave4", "Wave 4", "")], - name="Waves", - options=set()) + wave_selector = EnumProperty( + items=[ + ("wave1", "Wave 1", ""), + ("wave2", "Wave 2", ""), + ("wave3", "Wave 3", ""), + ("wave4", "Wave 4", ""), + ], + name="Waves", + options=set(), + ) @property def copy_material(self): @@ -379,23 +475,33 @@ class PlasmaGrassShaderMod(PlasmaModifierProperties): materials = exporter.mesh.material.get_materials(bo) if not materials: - exporter.report.warning("No materials are associated with this object, no grass shader exported!", - indent=3) + exporter.report.warning( + "No materials are associated with this object, no grass shader exported!", + indent=3, + ) return elif len(materials) > 1: - exporter.report.warning("Ah, a multiple material grass shader, eh. You like living dangerously...", - indent=3) + exporter.report.warning( + "Ah, a multiple material grass shader, eh. You like living dangerously...", + indent=3, + ) for material in materials: - mod = exporter.mgr.find_create_object(plGrassShaderMod, so=so, name=material.name) + mod = exporter.mgr.find_create_object( + plGrassShaderMod, so=so, name=material.name + ) mod.material = material - for mod_wave, settings in zip(mod.waves, (self.wave1, self.wave2, self.wave3, self.wave4)): + for mod_wave, settings in zip( + mod.waves, (self.wave1, self.wave2, self.wave3, self.wave4) + ): mod_wave.dist = hsVector3(*settings.distance) mod_wave.dirX, mod_wave.dirY = settings.direction mod_wave.speed = settings.speed -class PlasmaLightMapGen(idprops.IDPropMixin, PlasmaModifierProperties, PlasmaModifierUpgradable): +class PlasmaLightMapGen( + idprops.IDPropMixin, PlasmaModifierProperties, PlasmaModifierUpgradable +): pl_id = "lightmap" bl_category = "Render" @@ -404,45 +510,56 @@ class PlasmaLightMapGen(idprops.IDPropMixin, PlasmaModifierProperties, PlasmaMod deprecated_properties = {"render_layers"} - quality = EnumProperty(name="Quality", - description="Resolution of lightmap", - items=[ - ("128", "128px", "128x128 pixels"), - ("256", "256px", "256x256 pixels"), - ("512", "512px", "512x512 pixels"), - ("1024", "1024px", "1024x1024 pixels"), - ("2048", "2048px", "2048x2048 pixels"), - ]) - - bake_type = EnumProperty(name="Bake To", - description="Destination for baked lighting data", - items=[ - ("lightmap", "Lightmap Texture", "Bakes lighting to a lightmap texture"), - ("vcol", "Vertex Colors", "Bakes lighting to vertex colors"), - ], - options=set()) - - render_layers = BoolVectorProperty(name="Layers", - description="DEPRECATED: Render layers to use for baking", - options={"HIDDEN"}, - subtype="LAYER", - size=_NUM_RENDER_LAYERS, - default=((True,) * _NUM_RENDER_LAYERS)) - - bake_pass_name = StringProperty(name="Bake Pass", - description="Pass in which to bake lighting", - options=set()) - - lights = PointerProperty(name="Light Group", - description="Group that defines the collection of lights to bake", - type=bpy.types.Group) - - uv_map = StringProperty(name="UV Texture", - description="UV Texture used as the basis for the lightmap") - - image = PointerProperty(name="Baked Image", - description="Use this image instead of re-baking the lighting each export", - type=bpy.types.Image) + quality = EnumProperty( + name="Quality", + description="Resolution of lightmap", + items=[ + ("128", "128px", "128x128 pixels"), + ("256", "256px", "256x256 pixels"), + ("512", "512px", "512x512 pixels"), + ("1024", "1024px", "1024x1024 pixels"), + ("2048", "2048px", "2048x2048 pixels"), + ], + ) + + bake_type = EnumProperty( + name="Bake To", + description="Destination for baked lighting data", + items=[ + ("lightmap", "Lightmap Texture", "Bakes lighting to a lightmap texture"), + ("vcol", "Vertex Colors", "Bakes lighting to vertex colors"), + ], + options=set(), + ) + + render_layers = BoolVectorProperty( + name="Layers", + description="DEPRECATED: Render layers to use for baking", + options={"HIDDEN"}, + subtype="LAYER", + size=_NUM_RENDER_LAYERS, + default=((True,) * _NUM_RENDER_LAYERS), + ) + + bake_pass_name = StringProperty( + name="Bake Pass", description="Pass in which to bake lighting", options=set() + ) + + lights = PointerProperty( + name="Light Group", + description="Group that defines the collection of lights to bake", + type=bpy.types.Group, + ) + + uv_map = StringProperty( + name="UV Texture", description="UV Texture used as the basis for the lightmap" + ) + + image = PointerProperty( + name="Baked Image", + description="Use this image instead of re-baking the lighting each export", + type=bpy.types.Image, + ) @property def bake_lightmap(self): @@ -470,7 +587,9 @@ class PlasmaLightMapGen(idprops.IDPropMixin, PlasmaModifierProperties, PlasmaMod # If no lightmap image is found, then either lightmap generation failed (error raised by oven) # or baking is turned off. Either way, bail out. - lightmap_im = self.image if self.image is not None else exporter.oven.get_lightmap(bo) + lightmap_im = ( + self.image if self.image is not None else exporter.oven.get_lightmap(bo) + ) if lightmap_im is None: return mat_mgr = exporter.mesh.material @@ -478,12 +597,25 @@ class PlasmaLightMapGen(idprops.IDPropMixin, PlasmaModifierProperties, PlasmaMod # Find the stupid UVTex uvtex_name = exporter.oven.lightmap_uvtex_name - uvw_src = next((i for i, uvtex in enumerate(bo.data.uv_textures) if uvtex.name == uvtex_name), None) + uvw_src = next( + ( + i + for i, uvtex in enumerate(bo.data.uv_textures) + if uvtex.name == uvtex_name + ), + None, + ) if uvw_src is None: - raise ExportError("'{}': Lightmap UV Texture '{}' seems to be missing. Did you delete it?", bo.name, uvtex_name) + raise ExportError( + "'{}': Lightmap UV Texture '{}' seems to be missing. Did you delete it?", + bo.name, + uvtex_name, + ) for matKey in materials: - layer = exporter.mgr.add_object(plLayer, name="{}_LIGHTMAPGEN".format(matKey.name), so=so) + layer = exporter.mgr.add_object( + plLayer, name="{}_LIGHTMAPGEN".format(matKey.name), so=so + ) layer.UVWSrc = uvw_src # Colors science'd from PRPs @@ -494,7 +626,7 @@ class PlasmaLightMapGen(idprops.IDPropMixin, PlasmaModifierProperties, PlasmaMod # GMatState gstate = layer.state gstate.blendFlags |= hsGMatState.kBlendMult - gstate.clampFlags |= (hsGMatState.kClampTextureU | hsGMatState.kClampTextureV) + gstate.clampFlags |= hsGMatState.kClampTextureU | hsGMatState.kClampTextureV gstate.ZFlags |= hsGMatState.kZNoZWrite gstate.miscFlags |= hsGMatState.kMiscLightMap @@ -503,11 +635,14 @@ class PlasmaLightMapGen(idprops.IDPropMixin, PlasmaModifierProperties, PlasmaMod mat.addPiggyBack(layer.key) # Mmm... cheating - mat_mgr.export_prepared_image(owner=layer, image=lightmap_im, - allowed_formats={"PNG", "JPG"}, - extension="hsm", - ephemeral=True, - indent=2) + mat_mgr.export_prepared_image( + owner=layer, + image=lightmap_im, + allowed_formats={"PNG", "JPG"}, + extension="hsm", + ephemeral=True, + indent=2, + ) @classmethod def _idprop_mapping(cls): @@ -537,7 +672,10 @@ class PlasmaLightMapGen(idprops.IDPropMixin, PlasmaModifierProperties, PlasmaMod render_layers = tuple(self.render_layers) # Try to find a render pass matching, if possible... - bake_pass = next((i for i in bake_passes if tuple(i.render_layers) == render_layers), None) + bake_pass = next( + (i for i in bake_passes if tuple(i.render_layers) == render_layers), + None, + ) if bake_pass is None: bake_pass = bake_passes.add() bake_pass.display_name = "Pass {}".format(len(bake_passes)) @@ -554,14 +692,18 @@ class PlasmaLightingMod(PlasmaModifierProperties): bl_label = "Lighting Info" bl_description = "Fine tune Plasma lighting settings" - force_rt_lights = BoolProperty(name="Force RT Lighting", - description="Unleashes satan by forcing the engine to dynamically light this object", - default=False, - options=set()) - force_preshade = BoolProperty(name="Force Vertex Shading", - description="Ensures vertex lights are baked, even if illogical", - default=False, - options=set()) + force_rt_lights = BoolProperty( + name="Force RT Lighting", + description="Unleashes satan by forcing the engine to dynamically light this object", + default=False, + options=set(), + ) + force_preshade = BoolProperty( + name="Force Vertex Shading", + description="Ensures vertex lights are baked, even if illogical", + default=False, + options=set(), + ) @property def allow_preshade(self): @@ -613,29 +755,36 @@ class PlasmaLightingMod(PlasmaModifierProperties): _LOCALIZED_TEXT_PFM = ( - { 'id': 1, 'type': "ptAttribDynamicMap", 'name': "dynTextMap", }, - { 'id': 2, 'type': "ptAttribString", 'name': "locPath" }, - { 'id': 3, 'type': "ptAttribString", 'name': "fontFace" }, - { 'id': 4, 'type': "ptAttribInt", 'name': "fontSize" }, - { 'id': 5, 'type': "ptAttribFloat", 'name': "fontColorR" }, - { 'id': 6, 'type': "ptAttribFloat", 'name': "fontColorG" }, - { 'id': 7, 'type': "ptAttribFloat", 'name': "fontColorB" }, - { 'id': 8, 'type': "ptAttribFloat", 'name': "fontColorA" }, - { 'id': 9, 'type': "ptAttribInt", 'name': "marginTop" }, - { 'id': 10, 'type': "ptAttribInt", 'name': "marginLeft" }, - { 'id': 11, 'type': "ptAttribInt", 'name': "marginBottom" }, - { 'id': 12, 'type': "ptAttribInt", 'name': "marginRight" }, - { 'id': 13, 'type': "ptAttribInt", 'name': "lineSpacing" }, + { + "id": 1, + "type": "ptAttribDynamicMap", + "name": "dynTextMap", + }, + {"id": 2, "type": "ptAttribString", "name": "locPath"}, + {"id": 3, "type": "ptAttribString", "name": "fontFace"}, + {"id": 4, "type": "ptAttribInt", "name": "fontSize"}, + {"id": 5, "type": "ptAttribFloat", "name": "fontColorR"}, + {"id": 6, "type": "ptAttribFloat", "name": "fontColorG"}, + {"id": 7, "type": "ptAttribFloat", "name": "fontColorB"}, + {"id": 8, "type": "ptAttribFloat", "name": "fontColorA"}, + {"id": 9, "type": "ptAttribInt", "name": "marginTop"}, + {"id": 10, "type": "ptAttribInt", "name": "marginLeft"}, + {"id": 11, "type": "ptAttribInt", "name": "marginBottom"}, + {"id": 12, "type": "ptAttribInt", "name": "marginRight"}, + {"id": 13, "type": "ptAttribInt", "name": "lineSpacing"}, # Yes, it's really a ptAttribDropDownList, but those are only for use in # artist generated node trees. - { 'id': 14, 'type': "ptAttribString", 'name': "justify" }, - { 'id': 15, 'type': "ptAttribFloat", 'name': "clearColorR" }, - { 'id': 16, 'type': "ptAttribFloat", 'name': "clearColorG" }, - { 'id': 17, 'type': "ptAttribFloat", 'name': "clearColorB" }, - { 'id': 18, 'type': "ptAttribFloat", 'name': "clearColorA" }, + {"id": 14, "type": "ptAttribString", "name": "justify"}, + {"id": 15, "type": "ptAttribFloat", "name": "clearColorR"}, + {"id": 16, "type": "ptAttribFloat", "name": "clearColorG"}, + {"id": 17, "type": "ptAttribFloat", "name": "clearColorB"}, + {"id": 18, "type": "ptAttribFloat", "name": "clearColorA"}, ) -class PlasmaLocalizedTextModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz, TranslationMixin): + +class PlasmaLocalizedTextModifier( + PlasmaModifierProperties, PlasmaModifierLogicWiz, TranslationMixin +): pl_id = "dynatext" bl_category = "Render" @@ -643,16 +792,18 @@ class PlasmaLocalizedTextModifier(PlasmaModifierProperties, PlasmaModifierLogicW bl_description = "" bl_icon = "TEXT" - translations = CollectionProperty(name="Translations", - type=PlasmaJournalTranslation, - options=set()) + translations = CollectionProperty( + name="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 _poll_dyna_text(self, value: bpy.types.Texture) -> bool: if value.type != "IMAGE": @@ -660,54 +811,60 @@ class PlasmaLocalizedTextModifier(PlasmaModifierProperties, PlasmaModifierLogicW if value.image is not None: return False tex_materials = frozenset(value.users_material) - obj_materials = frozenset(filter(None, (i.material for i in self.id_data.material_slots))) + obj_materials = frozenset( + filter(None, (i.material for i in self.id_data.material_slots)) + ) return bool(tex_materials & obj_materials) - texture = PointerProperty(name="Texture", - description="The texture to write the localized text on", - type=bpy.types.Texture, - poll=_poll_dyna_text) - - font_face = StringProperty(name="Font Face", - default="Arial", - options=set()) - font_size = IntProperty(name="Font Size", - default=12, - min=0, soft_max=72, - options=set()) - font_color = FloatVectorProperty(name="Font Color", - default=(0.0, 0.0, 0.0, 1.0), - min=0.0, max=1.0, - subtype="COLOR", size=4, - options=set()) + texture = PointerProperty( + name="Texture", + description="The texture to write the localized text on", + type=bpy.types.Texture, + poll=_poll_dyna_text, + ) + + font_face = StringProperty(name="Font Face", default="Arial", options=set()) + font_size = IntProperty( + name="Font Size", default=12, min=0, soft_max=72, options=set() + ) + font_color = FloatVectorProperty( + name="Font Color", + default=(0.0, 0.0, 0.0, 1.0), + min=0.0, + max=1.0, + subtype="COLOR", + size=4, + options=set(), + ) # Using individual properties for better UI documentation - margin_top = IntProperty(name="Margin Top", - min=-4096, soft_min=0, max=4096, - options=set()) - margin_left = IntProperty(name="Margin Left", - min=-4096, soft_min=0, max=4096, - options=set()) - margin_bottom = IntProperty(name="Margin Bottom", - min=-4096, soft_min=0, max=4096, - options=set()) - margin_right = IntProperty(name="Margin Right", - min=-4096, soft_min=0, max=4096, - options=set()) - - justify = EnumProperty(name="Justification", - items=[("left", "Left", ""), - ("center", "Center", ""), - ("right", "Right", "")], - default="left", - options=set()) - line_spacing = IntProperty(name="Line Spacing", - default=0, - soft_min=0, soft_max=10, - options=set()) + margin_top = IntProperty( + name="Margin Top", min=-4096, soft_min=0, max=4096, options=set() + ) + margin_left = IntProperty( + name="Margin Left", min=-4096, soft_min=0, max=4096, options=set() + ) + margin_bottom = IntProperty( + name="Margin Bottom", min=-4096, soft_min=0, max=4096, options=set() + ) + margin_right = IntProperty( + name="Margin Right", min=-4096, soft_min=0, max=4096, options=set() + ) + + justify = EnumProperty( + name="Justification", + items=[("left", "Left", ""), ("center", "Center", ""), ("right", "Right", "")], + default="left", + options=set(), + ) + line_spacing = IntProperty( + name="Line Spacing", default=0, soft_min=0, soft_max=10, options=set() + ) def pre_export(self, exporter, bo): - yield self.convert_logic(bo, age_name=exporter.age_name, version=exporter.mgr.getVer()) + yield self.convert_logic( + bo, age_name=exporter.age_name, version=exporter.mgr.getVer() + ) def logicwiz(self, bo, tree, *, age_name, version): # Rough justice. If the dynamic text map texture doesn't request alpha, then we'll want @@ -715,19 +872,44 @@ class PlasmaLocalizedTextModifier(PlasmaModifierProperties, PlasmaModifierLogicW # add text surfaces directly to objects, opposed to where Cyan tends to use a separate # transparent object over the background object. if not self.texture.use_alpha: - material_filter = lambda slot: slot and slot.material and self.texture in (i.texture for i in slot.material.texture_slots if i) + material_filter = ( + lambda slot: slot + and slot.material + and self.texture + in (i.texture for i in slot.material.texture_slots if i) + ) for slot in filter(material_filter, bo.material_slots): - self._create_nodes(bo, tree, age_name=age_name, version=version, - material=slot.material, clear_color=slot.material.diffuse_color) + self._create_nodes( + bo, + tree, + age_name=age_name, + version=version, + material=slot.material, + clear_color=slot.material.diffuse_color, + ) else: self._create_nodes(bo, tree, age_name=age_name, version=version) - def _create_nodes(self, bo, tree, *, age_name, version, material=None, clear_color=None): - pfm_node = self._create_python_file_node(tree, "xDynTextLoc.py", _LOCALIZED_TEXT_PFM) - loc_path = self.key_name if version <= pvPots else "{}.{}.{}".format(age_name, self.localization_set, self.key_name) - - self._create_python_attribute(pfm_node, "dynTextMap", "ptAttribDynamicMap", - target_object=bo, material=material, texture=self.texture) + def _create_nodes( + self, bo, tree, *, age_name, version, material=None, clear_color=None + ): + pfm_node = self._create_python_file_node( + tree, "xDynTextLoc.py", _LOCALIZED_TEXT_PFM + ) + loc_path = ( + self.key_name + if version <= pvPots + else "{}.{}.{}".format(age_name, self.localization_set, self.key_name) + ) + + self._create_python_attribute( + pfm_node, + "dynTextMap", + "ptAttribDynamicMap", + target_object=bo, + material=material, + texture=self.texture, + ) self._create_python_attribute(pfm_node, "locPath", value=loc_path) self._create_python_attribute(pfm_node, "fontFace", value=self.font_face) self._create_python_attribute(pfm_node, "fontSize", value=self.font_size) @@ -737,7 +919,9 @@ class PlasmaLocalizedTextModifier(PlasmaModifierProperties, PlasmaModifierLogicW self._create_python_attribute(pfm_node, "fontColorA", value=self.font_color[3]) self._create_python_attribute(pfm_node, "marginTop", value=self.margin_top) self._create_python_attribute(pfm_node, "marginLeft", value=self.margin_left) - self._create_python_attribute(pfm_node, "marginBottom", value=self.margin_bottom) + self._create_python_attribute( + pfm_node, "marginBottom", value=self.margin_bottom + ) self._create_python_attribute(pfm_node, "marginRight", value=self.margin_right) self._create_python_attribute(pfm_node, "justify", value=self.justify) @@ -753,7 +937,9 @@ class PlasmaLocalizedTextModifier(PlasmaModifierProperties, PlasmaModifierLogicW def sanity_check(self): if self.texture is None: - raise ExportError("'{}': Localized Text modifier requires a texture", self.id_data.name) + raise ExportError( + "'{}': Localized Text modifier requires a texture", self.id_data.name + ) class PlasmaShadowCasterMod(PlasmaModifierProperties): @@ -763,30 +949,51 @@ class PlasmaShadowCasterMod(PlasmaModifierProperties): bl_label = "Cast RT Shadow" bl_description = "Cast runtime shadows" - blur = IntProperty(name="Blur", - description="Blur factor for the shadow map", - min=0, max=100, default=0, - subtype="PERCENTAGE", options=set()) - boost = IntProperty(name="Boost", - description="Multiplies the shadow's power", - min=0, max=5000, default=100, - subtype="PERCENTAGE", options=set()) - falloff = IntProperty(name="Falloff", - description="Multiplier for each lamp's falloff value", - min=10, max=1000, default=100, - subtype="PERCENTAGE", options=set()) - - limit_resolution = BoolProperty(name="Limit Resolution", - description="Increase performance by halving the resolution of the shadow map", - default=False, - options=set()) - self_shadow = BoolProperty(name="Self Shadow", - description="Object can cast shadows on itself", - default=False, - options=set()) + blur = IntProperty( + name="Blur", + description="Blur factor for the shadow map", + min=0, + max=100, + default=0, + subtype="PERCENTAGE", + options=set(), + ) + boost = IntProperty( + name="Boost", + description="Multiplies the shadow's power", + min=0, + max=5000, + default=100, + subtype="PERCENTAGE", + options=set(), + ) + falloff = IntProperty( + name="Falloff", + description="Multiplier for each lamp's falloff value", + min=10, + max=1000, + default=100, + subtype="PERCENTAGE", + options=set(), + ) + + limit_resolution = BoolProperty( + name="Limit Resolution", + description="Increase performance by halving the resolution of the shadow map", + default=False, + options=set(), + ) + self_shadow = BoolProperty( + name="Self Shadow", + description="Object can cast shadows on itself", + default=False, + options=set(), + ) def export(self, exporter, bo, so): - caster = exporter.mgr.find_create_object(plShadowCaster, so=so, name=self.key_name) + caster = exporter.mgr.find_create_object( + plShadowCaster, so=so, name=self.key_name + ) caster.attenScale = self.falloff / 100.0 caster.blurScale = self.blur / 100.0 caster.boost = self.boost / 100.0 @@ -803,39 +1010,49 @@ class PlasmaViewFaceMod(idprops.IDPropObjectMixin, PlasmaModifierProperties): bl_label = "Swivel" bl_description = "Swivel object to face the camera, player, or another object" - preset_options = EnumProperty(name="Type", - description="Type of Facing", - items=[ - ("Billboard", "Billboard", "Face the camera (Y Axis only)"), - ("Sprite", "Sprite", "Face the camera (All Axis)"), - ("Custom", "Custom", "Custom Swivel"), - ]) - - follow_mode = EnumProperty(name="Target Type", - description="Target of the swivel", - items=[ - ("kFaceCam", "Camera", "Face the camera"), - ("kFaceList", "Listener", "Face listeners"), - ("kFacePlay", "Player", "Face the local player"), - ("kFaceObj", "Object", "Face an object"), - ]) - target = PointerProperty(name="Target Object", - description="Object to face", - type=bpy.types.Object) - - pivot_on_y = BoolProperty(name="Pivot on local Y", - description="Swivel only around the local Y axis", - default=False) + preset_options = EnumProperty( + name="Type", + description="Type of Facing", + items=[ + ("Billboard", "Billboard", "Face the camera (Y Axis only)"), + ("Sprite", "Sprite", "Face the camera (All Axis)"), + ("Custom", "Custom", "Custom Swivel"), + ], + ) + + follow_mode = EnumProperty( + name="Target Type", + description="Target of the swivel", + items=[ + ("kFaceCam", "Camera", "Face the camera"), + ("kFaceList", "Listener", "Face listeners"), + ("kFacePlay", "Player", "Face the local player"), + ("kFaceObj", "Object", "Face an object"), + ], + ) + target = PointerProperty( + name="Target Object", description="Object to face", type=bpy.types.Object + ) + + pivot_on_y = BoolProperty( + name="Pivot on local Y", + description="Swivel only around the local Y axis", + default=False, + ) offset = BoolProperty(name="Offset", description="Use offset vector", default=False) - offset_local = BoolProperty(name="Local", description="Use local coordinates", default=False) + offset_local = BoolProperty( + name="Local", description="Use local coordinates", default=False + ) offset_coord = FloatVectorProperty(name="", subtype="XYZ") def export(self, exporter, bo, so): - vfm = exporter.mgr.find_create_object(plViewFaceModifier, so=so, name=self.key_name) + vfm = exporter.mgr.find_create_object( + plViewFaceModifier, so=so, name=self.key_name + ) # Set a default scaling (libHSPlasma will set this to 0 otherwise). - vfm.scale = hsVector3(1,1,1) + vfm.scale = hsVector3(1, 1, 1) l2p = utils.matrix44(bo.matrix_local) vfm.localToParent = l2p vfm.parentToLocal = l2p.inverse() @@ -857,9 +1074,15 @@ class PlasmaViewFaceMod(idprops.IDPropObjectMixin, PlasmaModifierProperties): # If this swivel is following an object, make sure that the # target has been selected and is a valid SO. if self.target: - vfm.faceObj = exporter.mgr.find_create_key(plSceneObject, bl=self.target) + vfm.faceObj = exporter.mgr.find_create_key( + plSceneObject, bl=self.target + ) else: - raise ExportError("'{}': Swivel's target object must be selected".format(self.key_name)) + raise ExportError( + "'{}': Swivel's target object must be selected".format( + self.key_name + ) + ) if self.pivot_on_y: vfm.setFlag(plViewFaceModifier.kPivotY, True) @@ -887,18 +1110,38 @@ class PlasmaVisControl(idprops.IDPropObjectMixin, PlasmaModifierProperties): bl_label = "Visibility Control" bl_description = "Controls object visibility using VisRegions" - mode = EnumProperty(name="Mode", - description="Purpose of the VisRegion", - items=[("normal", "Normal", "Objects are only visible when the camera is inside this region"), - ("exclude", "Exclude", "Objects are only visible when the camera is outside this region"), - ("fx", "Special FX", "This is a list of objects used for special effects only")]) - soft_region = PointerProperty(name="Region", - description="Object defining the SoftVolume for this VisRegion", - type=bpy.types.Object, - poll=idprops.poll_softvolume_objects) - replace_normal = BoolProperty(name="Hide Drawables", - description="Hides drawables attached to this region", - default=True) + mode = EnumProperty( + name="Mode", + description="Purpose of the VisRegion", + items=[ + ( + "normal", + "Normal", + "Objects are only visible when the camera is inside this region", + ), + ( + "exclude", + "Exclude", + "Objects are only visible when the camera is outside this region", + ), + ( + "fx", + "Special FX", + "This is a list of objects used for special effects only", + ), + ], + ) + soft_region = PointerProperty( + name="Region", + description="Object defining the SoftVolume for this VisRegion", + type=bpy.types.Object, + poll=idprops.poll_softvolume_objects, + ) + replace_normal = BoolProperty( + name="Hide Drawables", + description="Hides drawables attached to this region", + default=True, + ) def export(self, exporter, bo, so): rgn = exporter.mgr.find_create_object(plVisRegion, bl=bo, so=so) @@ -913,12 +1156,20 @@ class PlasmaVisControl(idprops.IDPropObjectMixin, PlasmaModifierProperties): rgn.region = this_sv.get_key(exporter, so) else: if not self.soft_region: - raise ExportError("'{}': Visibility Control must have a Soft Volume selected".format(self.key_name)) + raise ExportError( + "'{}': Visibility Control must have a Soft Volume selected".format( + self.key_name + ) + ) sv_bo = self.soft_region sv = sv_bo.plasma_modifiers.softvolume exporter.report.msg("[VisRegion] SoftVolume '{}'", sv_bo.name, indent=1) if not sv.enabled: - raise ExportError("'{}': '{}' is not a SoftVolume".format(self.key_name, sv_bo.name)) + raise ExportError( + "'{}': '{}' is not a SoftVolume".format( + self.key_name, sv_bo.name + ) + ) rgn.region = sv.get_key(exporter) rgn.setProperty(plVisRegion.kIsNot, self.mode == "exclude") @@ -929,10 +1180,12 @@ class PlasmaVisControl(idprops.IDPropObjectMixin, PlasmaModifierProperties): class VisRegion(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): @@ -946,8 +1199,7 @@ class PlasmaVisibilitySet(PlasmaModifierProperties): bl_label = "Visibility Set" bl_description = "Defines areas where this object is visible" - regions = CollectionProperty(name="Visibility Regions", - type=VisRegion) + regions = CollectionProperty(name="Visibility Regions", type=VisRegion) active_region_index = IntProperty(options={"HIDDEN"}) def export(self, exporter, bo, so): @@ -967,5 +1219,11 @@ class PlasmaVisibilitySet(PlasmaModifierProperties): if not region.enabled: continue if not region.control_region: - raise ExportError("{}: Not all Visibility Controls are set up properly in Visibility Set".format(bo.name)) - addRegion(exporter.mgr.find_create_key(plVisRegion, bl=region.control_region)) + raise ExportError( + "{}: Not all Visibility Controls are set up properly in Visibility Set".format( + bo.name + ) + ) + addRegion( + exporter.mgr.find_create_key(plVisRegion, bl=region.control_region) + ) diff --git a/korman/properties/modifiers/sound.py b/korman/properties/modifiers/sound.py index 0559d08..2f3dcbe 100644 --- a/korman/properties/modifiers/sound.py +++ b/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 diff --git a/korman/properties/modifiers/water.py b/korman/properties/modifiers/water.py index 891c7fb..41642e5 100644 --- a/korman/properties/modifiers/water.py +++ b/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): diff --git a/korman/properties/prop_anim.py b/korman/properties/prop_anim.py index 47fb593..cf3aaea 100644 --- a/korman/properties/prop_anim.py +++ b/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]: diff --git a/korman/properties/prop_camera.py b/korman/properties/prop_camera.py index 0be191f..7ef0434 100644 --- a/korman/properties/prop_camera.py +++ b/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 diff --git a/korman/properties/prop_image.py b/korman/properties/prop_image.py index 14646d7..f530ea8 100644 --- a/korman/properties/prop_image.py +++ b/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(), + ) diff --git a/korman/properties/prop_lamp.py b/korman/properties/prop_lamp.py index c1c4d33..9eefe32 100644 --- a/korman/properties/prop_lamp.py +++ b/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) diff --git a/korman/properties/prop_object.py b/korman/properties/prop_object.py index 3498479..3a29ea6 100644 --- a/korman/properties/prop_object.py +++ b/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") diff --git a/korman/properties/prop_scene.py b/korman/properties/prop_scene.py index 81cb2c5..3116faa 100644 --- a/korman/properties/prop_scene.py +++ b/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"} + ) diff --git a/korman/properties/prop_sound.py b/korman/properties/prop_sound.py index a340b5d..7f410b1 100644 --- a/korman/properties/prop_sound.py +++ b/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(), + ) diff --git a/korman/properties/prop_text.py b/korman/properties/prop_text.py index 3746cf7..69a3cd8 100644 --- a/korman/properties/prop_text.py +++ b/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() + ) diff --git a/korman/properties/prop_texture.py b/korman/properties/prop_texture.py index 78d83ad..48182eb 100644 --- a/korman/properties/prop_texture.py +++ b/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) diff --git a/korman/properties/prop_world.py b/korman/properties/prop_world.py index 75191b2..9824750 100644 --- a/korman/properties/prop_world.py +++ b/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") diff --git a/korman/render.py b/korman/render.py index 2ce7de6..06a9cba 100644 --- a/korman/render.py +++ b/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") diff --git a/korman/ui/__init__.py b/korman/ui/__init__.py index d5a2ac6..f0102c7 100644 --- a/korman/ui/__init__.py +++ b/korman/ui/__init__.py @@ -32,5 +32,6 @@ from .ui_world import * def register(): ui_menus.register() + def unregister(): ui_menus.unregister() diff --git a/korman/ui/modifiers/anim.py b/korman/ui/modifiers/anim.py index f781d35..effbac5 100644 --- a/korman/ui/modifiers/anim.py +++ b/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] diff --git a/korman/ui/modifiers/avatar.py b/korman/ui/modifiers/avatar.py index 11db188..61409c0 100644 --- a/korman/ui/modifiers/avatar.py +++ b/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") diff --git a/korman/ui/modifiers/gui.py b/korman/ui/modifiers/gui.py index 3577cd5..94b3026 100644 --- a/korman/ui/modifiers/gui.py +++ b/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() diff --git a/korman/ui/modifiers/logic.py b/korman/ui/modifiers/logic.py index d657b5f..d9f69ef 100644 --- a/korman/ui/modifiers/logic.py +++ b/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") diff --git a/korman/ui/modifiers/physics.py b/korman/ui/modifiers/physics.py index c8d83a2..e27656f 100644 --- a/korman/ui/modifiers/physics.py +++ b/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 . + 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": diff --git a/korman/ui/modifiers/region.py b/korman/ui/modifiers/region.py index 52eb6e7..f3731c8 100644 --- a/korman/ui/modifiers/region.py +++ b/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 diff --git a/korman/ui/modifiers/render.py b/korman/ui/modifiers/render.py index b660872..ce21f2e 100644 --- a/korman/ui/modifiers/render.py +++ b/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") diff --git a/korman/ui/modifiers/sound.py b/korman/ui/modifiers/sound.py index 95a7826..4f9f7be 100644 --- a/korman/ui/modifiers/sound.py +++ b/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 diff --git a/korman/ui/modifiers/water.py b/korman/ui/modifiers/water.py index 08313b9..e30f5f0 100644 --- a/korman/ui/modifiers/water.py +++ b/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: diff --git a/korman/ui/ui_anim.py b/korman/ui/ui_anim.py index 3741854..c3a03f9 100644 --- a/korman/ui/ui_anim.py +++ b/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") diff --git a/korman/ui/ui_camera.py b/korman/ui/ui_camera.py index ca30deb..4c52331 100644 --- a/korman/ui/ui_camera.py +++ b/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] diff --git a/korman/ui/ui_image.py b/korman/ui/ui_image.py index aa17f8b..7f06318 100644 --- a/korman/ui/ui_image.py +++ b/korman/ui/ui_image.py @@ -15,6 +15,7 @@ import bpy + class PlasmaImageEditorHeader(bpy.types.Header): bl_space_type = "IMAGE_EDITOR" diff --git a/korman/ui/ui_lamp.py b/korman/ui/ui_lamp.py index 7c89b82..29b82eb 100644 --- a/korman/ui/ui_lamp.py +++ b/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 diff --git a/korman/ui/ui_list.py b/korman/ui/ui_list.py index 7f7e032..ef0414f 100644 --- a/korman/ui/ui_list.py +++ b/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 + ) diff --git a/korman/ui/ui_menus.py b/korman/ui/ui_menus.py index ad38020..44caeef 100644 --- a/korman/ui/ui_menus.py +++ b/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) diff --git a/korman/ui/ui_modifiers.py b/korman/ui/ui_modifiers.py index 6f9b815..f8d485f 100644 --- a/korman/ui/ui_modifiers.py +++ b/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 diff --git a/korman/ui/ui_object.py b/korman/ui/ui_object.py index 7b679c2..368a3ed 100644 --- a/korman/ui/ui_object.py +++ b/korman/ui/ui_object.py @@ -15,6 +15,7 @@ import bpy + class ObjectButtonsPanel: bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" diff --git a/korman/ui/ui_render_layer.py b/korman/ui/ui_render_layer.py index d87f04b..9549bf7 100644 --- a/korman/ui/ui_render_layer.py +++ b/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: diff --git a/korman/ui/ui_scene.py b/korman/ui/ui_scene.py index ec72d82..70fef84 100644 --- a/korman/ui/ui_scene.py +++ b/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") diff --git a/korman/ui/ui_text.py b/korman/ui/ui_text.py index 4d3fd12..ad2fe8f 100644 --- a/korman/ui/ui_text.py +++ b/korman/ui/ui_text.py @@ -15,6 +15,7 @@ import bpy + class PlasmaTextEditorHeader(bpy.types.Header): bl_space_type = "TEXT_EDITOR" diff --git a/korman/ui/ui_texture.py b/korman/ui/ui_texture.py index a346d47..62f1a48 100644 --- a/korman/ui/ui_texture.py +++ b/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, + ) diff --git a/korman/ui/ui_toolbox.py b/korman/ui/ui_toolbox.py index fac984a..fffaf6e 100644 --- a/korman/ui/ui_toolbox.py +++ b/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", + ) diff --git a/korman/ui/ui_world.py b/korman/ui/ui_world.py index 4fb0792..ffb4eba 100644 --- a/korman/ui/ui_world.py +++ b/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()