Compare commits

...

9 Commits

Author SHA1 Message Date
Adam Johnson 9244d3abc7
Fix #409. 5 months ago
Adam Johnson 73639302cc
Merge pull request #414 from Hoikas/phony_pfm_reorg 5 months ago
Adam Johnson 9dc7f6819d
Merge pull request #415 from Hoikas/bounds_storage_overhaul 5 months ago
Adam Johnson 0f0d6e1086
Add the SDL Show/Hide Modifier. 5 months ago
Adam Johnson adb8b23284
Unify animation naming and polling. 5 months ago
Adam Johnson 0d84b78762
Migrate duplicated bounds properties. 5 months ago
Adam Johnson 5b2c16cfb2
Make bounds enum properties less verbose. 5 months ago
Adam Johnson 1cf33ba286
Allow `NodeTree`s to be reused in the `pre_export` phase. 5 months ago
Adam Johnson 778caadf11
Centralize the definitions for standard Plasma APIs. 5 months ago
  1. 167
      korman/enum_props.py
  2. 12
      korman/exporter/convert.py
  3. 80
      korman/idprops.py
  4. 74
      korman/nodes/node_conditions.py
  5. 26
      korman/nodes/node_logic.py
  6. 81
      korman/nodes/node_messages.py
  7. 101
      korman/nodes/node_python.py
  8. 8
      korman/operators/op_ui.py
  9. 134
      korman/plasma_api.py
  10. 12
      korman/properties/modifiers/avatar.py
  11. 37
      korman/properties/modifiers/base.py
  12. 34
      korman/properties/modifiers/game_gui.py
  13. 87
      korman/properties/modifiers/gui.py
  14. 125
      korman/properties/modifiers/logic.py
  15. 18
      korman/properties/modifiers/physics.py
  16. 35
      korman/properties/modifiers/region.py
  17. 26
      korman/properties/modifiers/render.py
  18. 20
      korman/properties/modifiers/water.py
  19. 37
      korman/ui/modifiers/logic.py

167
korman/enum_props.py

@ -0,0 +1,167 @@
# This file is part of Korman.
#
# Korman is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Korman is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from bpy.props import *
from typing import *
import warnings
# Workaround for Blender memory management limitation,
# don't change this to a literal in the code!
_ENTIRE_ANIMATION = "(Entire Animation)"
def _get_object_animation_names(self, object_attr: str) -> Sequence[Tuple[str, str, str]]:
target_object = getattr(self, object_attr)
if target_object is not None:
items = [(anim.animation_name, anim.animation_name, "")
for anim in target_object.plasma_modifiers.animation.subanimations]
else:
items = [(_ENTIRE_ANIMATION, _ENTIRE_ANIMATION, "")]
# We always want "(Entire Animation)", if it exists, to be the first item.
entire = items.index((_ENTIRE_ANIMATION, _ENTIRE_ANIMATION, ""))
if entire not in (-1, 0):
items.pop(entire)
items.insert(0, (_ENTIRE_ANIMATION, _ENTIRE_ANIMATION, ""))
return items
def animation(object_attr: str, **kwargs) -> str:
def get_items(self, context):
return _get_object_animation_names(self, object_attr)
return EnumProperty(items=get_items, **kwargs)
# These are the kinds of physical bounds Plasma can work with.
# This sequence is acceptable in any EnumProperty
_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!)")
)
def _bounds_type_index(key: str) -> int:
return list(zip(*_bounds_types))[0].index(key)
def _bounds_type_str(idx: int) -> str:
return _bounds_types[idx][0]
def _get_bounds(physics_attr: Optional[str]) -> Callable[[Any], int]:
def getter(self) -> int:
physics_object = getattr(self, physics_attr) if physics_attr is not None else self.id_data
if physics_object is not None:
return _bounds_type_index(physics_object.plasma_modifiers.collision.bounds)
return _bounds_type_index("hull")
return getter
def _set_bounds(physics_attr: Optional[str]) -> Callable[[Any, int], None]:
def setter(self, value: int):
physics_object = getattr(self, physics_attr) if physics_attr is not None else self.id_data
if physics_object is not None:
physics_object.plasma_modifiers.collision.bounds = _bounds_type_str(value)
return setter
def bounds(physics_attr: Optional[str] = None, store_on_collider: bool = True, **kwargs) -> str:
assert not {"items", "get", "set"} & kwargs.keys(), "You cannot use the `items`, `get`, or `set` keyword arguments"
if store_on_collider:
kwargs["get"] = _get_bounds(physics_attr)
kwargs["set"] = _set_bounds(physics_attr)
else:
warnings.warn("Storing bounds properties outside of the collision modifier is deprecated.", category=DeprecationWarning)
if "default" not in kwargs:
kwargs["default"] = "hull"
return EnumProperty(
items=_bounds_types,
**kwargs
)
def upgrade_bounds(bl, bounds_attr: str) -> None:
# Only perform this process if the property has a value. Otherwise, we'll
# wind up blowing away the collision modifier's settings with nonsense.
if not bl.is_property_set(bounds_attr):
return
# Before we unregister anything, grab a copy of what the collision modifier currently thinks.
bounds_value_curr = getattr(bl, bounds_attr)
# So, here's the deal. If someone has been playing with nodes and changed the bounds type,
# Blender will think the property has been set, even if they wound up with the property
# at the default value. I don't know that we can really trust the default in the property
# definition to be the same as the old default (they shouldn't be different, but let's be safe).
# So, let's apply rough justice. If the destination property thinks it's a triangle mesh, we
# don't need to blow that away - it's a very specific non default setting.
if bounds_value_curr == "trimesh":
return
# Unregister the new/correct proxy bounds property (with getter/setter) and re-register
# the property without the proxy functions to get the old value. Reregister the new property
# again and set it.
cls = bl.__class__
prop_func, prop_def = getattr(cls, bounds_attr)
RemoveProperty(cls, attr=bounds_attr)
del prop_def["attr"]
# Remove the things we don't want in a copy to prevent hosing the new property.
old_prop_def = dict(prop_def)
del old_prop_def["get"]
del old_prop_def["set"]
setattr(cls, bounds_attr, prop_func(**old_prop_def))
bounds_value_new = getattr(bl, bounds_attr)
# Re-register new property.
RemoveProperty(cls, attr=bounds_attr)
setattr(cls, bounds_attr, prop_func(**prop_def))
# Only set the property if the value different to avoid thrashing and log spam.
if bounds_value_curr != bounds_value_new:
print(f"Stashing bounds property: [{bl.name}] ({cls.__name__}) {bounds_value_curr} -> {bounds_value_new}") # TEMP
setattr(bl, bounds_attr, bounds_value_new)
def _get_texture_animation_names(self, object_attr: str, material_attr: str, texture_attr: str) -> Sequence[Tuple[str, str, str]]:
target_object = getattr(self, object_attr)
material = getattr(self, material_attr)
texture = getattr(self, texture_attr)
if texture is not None:
items = [(anim.animation_name, anim.animation_name, "")
for anim in texture.plasma_layer.subanimations]
elif material is not None or target_object is not None:
if material is None:
materials = (i.material for i in target_object.material_slots if i and i.material)
else:
materials = (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))
items = [(i, i, "") for i in all_anims]
else:
items = [(_ENTIRE_ANIMATION, _ENTIRE_ANIMATION, "")]
# We always want "(Entire Animation)", if it exists, to be the first item.
entire = items.index((_ENTIRE_ANIMATION, _ENTIRE_ANIMATION, ""))
if entire not in (-1, 0):
items.pop(entire)
items.insert(0, (_ENTIRE_ANIMATION, _ENTIRE_ANIMATION, ""))
return items
def triprop_animation(object_attr: str, material_attr: str, texture_attr: str, **kwargs) -> str:
def get_items(self, context):
return _get_texture_animation_names(self, object_attr, material_attr, texture_attr)
assert not {"items", "get", "set"} & kwargs.keys(), "You cannot use the `items`, `get`, or `set` keyword arguments"
return EnumProperty(items=get_items, **kwargs)

12
korman/exporter/convert.py

@ -510,8 +510,12 @@ class Exporter:
@handle_temporary.register(bpy.types.NodeTree) @handle_temporary.register(bpy.types.NodeTree)
def _(temporary, parent): def _(temporary, parent):
self.exit_stack.enter_context(TemporaryObject(temporary, bpy.data.node_groups.remove)) # NodeTrees are reuseable, so make sure we haven't already encountered it.
log_msg(f"'{parent.name}' generated NodeTree '{temporary.name}'") if not temporary.name in self.want_node_trees:
self.exit_stack.enter_context(TemporaryObject(temporary, bpy.data.node_groups.remove))
log_msg(f"'{parent.name}' generated NodeTree '{temporary.name}'")
else:
log_msg(f"'{parent.name}' reused NodeTree '{temporary.name}'")
if temporary.bl_idname == "PlasmaNodeTree": if temporary.bl_idname == "PlasmaNodeTree":
parent_so = self.mgr.find_create_object(plSceneObject, bl=parent) parent_so = self.mgr.find_create_object(plSceneObject, bl=parent)
self.want_node_trees[temporary.name].add((parent, parent_so)) self.want_node_trees[temporary.name].add((parent, parent_so))
@ -594,6 +598,10 @@ class Exporter:
else: else:
return bpy.context.scene.world.plasma_age.age_name return bpy.context.scene.world.plasma_age.age_name
@property
def age_sdl(self) -> bool:
return bpy.context.scene.world.plasma_age.age_sdl
@property @property
def dat_only(self): def dat_only(self):
return self._op.dat_only return self._op.dat_only

80
korman/idprops.py

@ -145,6 +145,86 @@ def poll_visregion_objects(self, value):
def poll_envmap_textures(self, value): def poll_envmap_textures(self, value):
return isinstance(value, bpy.types.EnvironmentMapTexture) return isinstance(value, bpy.types.EnvironmentMapTexture)
def triprop_material(object_attr: str, material_attr: str, texture_attr: str, **kwargs) -> bpy.types.Material:
user_poll = kwargs.pop("poll", None)
def poll_proc(self, value: bpy.types.Material) -> bool:
target_object = getattr(self, object_attr)
if target_object is None:
target_object = getattr(self, "id_data", None)
# 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 target_object is not None:
object_materials = (slot.material for slot in target_object.material_slots if slot and slot.material)
result = value in object_materials
else:
result = True
# Downstream processing, if any.
if result and user_poll is not None:
result = user_poll(self, value)
return result
assert not "type" in kwargs
return PointerProperty(
type=bpy.types.Material,
poll=poll_proc,
**kwargs
)
def triprop_object(object_attr: str, material_attr: str, texture_attr: str, **kwargs) -> bpy.types.Texture:
assert not "type" in kwargs
if not "poll" in kwargs:
kwargs["poll"] = poll_drawable_objects
return PointerProperty(
type=bpy.types.Object,
**kwargs
)
def triprop_texture(object_attr: str, material_attr: str, texture_attr: str, **kwargs) -> bpy.types.Object:
user_poll = kwargs.pop("poll", None)
def poll_proc(self, value: bpy.types.Texture) -> bool:
target_material = getattr(self, material_attr)
target_object = getattr(self, object_attr)
if target_object is None:
target_object = getattr(self, "id_data", None)
# must be a legal option... but is it a member of this material... or, if no material,
# any of the materials attached to the object?
if target_material is not None:
result = value.name in target_material.texture_slots
elif target_object is not None:
for i in (slot.material for slot in 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):
result = True
break
else:
result = False
else:
result = False
# Is it animated?
if result and target_material is not None:
result = (
(target_material.animation_data is not None and target_material.animation_data.action is not None)
or (value.animation_data is not None and value.animation_data.action is not None)
)
# Downstream processing, if any.
if result and user_poll:
result = user_poll(self, value)
return result
assert not "type" in kwargs
return PointerProperty(
type=bpy.types.Texture,
poll=poll_proc,
**kwargs
)
@bpy.app.handlers.persistent @bpy.app.handlers.persistent
def _upgrade_node_trees(dummy): def _upgrade_node_trees(dummy):
""" """

74
korman/nodes/node_conditions.py

@ -21,11 +21,12 @@ import math
from PyHSPlasma import * from PyHSPlasma import *
from typing import * from typing import *
from .. import enum_props
from .node_core import * from .node_core import *
from ..properties.modifiers.physics import bounds_types from .node_deprecated import PlasmaVersionedNode
from .. import idprops from .. import idprops
class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node): class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaVersionedNode, bpy.types.Node):
bl_category = "CONDITIONS" bl_category = "CONDITIONS"
bl_idname = "PlasmaClickableNode" bl_idname = "PlasmaClickableNode"
bl_label = "Clickable" bl_label = "Clickable"
@ -38,10 +39,13 @@ class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.N
description="Mesh object that is clickable", description="Mesh object that is clickable",
type=bpy.types.Object, type=bpy.types.Object,
poll=idprops.poll_mesh_objects) poll=idprops.poll_mesh_objects)
bounds = EnumProperty(name="Bounds",
description="Clickable's bounds (NOTE: only used if your clickable is not a collider)", bounds = enum_props.bounds(
items=bounds_types, "clickable_object",
default="hull") name="Bounds",
description="Clickable's bounds",
default="hull"
)
input_sockets: dict[str, Any] = { input_sockets: dict[str, Any] = {
"region": { "region": {
@ -151,8 +155,20 @@ class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.N
def _idprop_mapping(cls): def _idprop_mapping(cls):
return {"clickable_object": "clickable"} return {"clickable_object": "clickable"}
@property
def latest_version(self):
return 2
def upgrade(self):
# In version 1 of this node, the bounds type was stored on this node. This could
# be overridden by whatever was in the collision modifier. Version 2 changes the
# bounds property to proxy to the collision modifier's bounds settings.
if self.version == 2:
enum_props.upgrade_bounds(self, "bounds")
self.version = 2
class PlasmaClickableRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node):
class PlasmaClickableRegionNode(idprops.IDPropObjectMixin, PlasmaVersionedNode, bpy.types.Node):
bl_category = "CONDITIONS" bl_category = "CONDITIONS"
bl_idname = "PlasmaClickableRegionNode" bl_idname = "PlasmaClickableRegionNode"
bl_label = "Clickable Region Settings" bl_label = "Clickable Region Settings"
@ -162,10 +178,12 @@ class PlasmaClickableRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.t
description="Object that defines the region mesh", description="Object that defines the region mesh",
type=bpy.types.Object, type=bpy.types.Object,
poll=idprops.poll_mesh_objects) poll=idprops.poll_mesh_objects)
bounds = EnumProperty(name="Bounds", bounds = enum_props.bounds(
description="Physical object's bounds (NOTE: only used if your clickable is not a collider)", "region_object",
items=bounds_types, name="Bounds",
default="hull") description="Physical object's bounds",
default="hull"
)
output_sockets = { output_sockets = {
"satisfies": { "satisfies": {
@ -215,6 +233,18 @@ class PlasmaClickableRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.t
def _idprop_mapping(cls): def _idprop_mapping(cls):
return {"region_object": "region"} return {"region_object": "region"}
@property
def latest_version(self):
return 2
def upgrade(self):
# In version 1 of this node, the bounds type was stored on this node. This could
# be overridden by whatever was in the collision modifier. Version 2 changes the
# bounds property to proxy to the collision modifier's bounds settings.
if self.version == 1:
enum_props.upgrade_bounds(self, "bounds")
self.version = 2
class PlasmaClickableRegionSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): class PlasmaClickableRegionSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
bl_color = (0.412, 0.0, 0.055, 1.0) bl_color = (0.412, 0.0, 0.055, 1.0)
@ -395,7 +425,7 @@ class PlasmaVolumeReportNode(PlasmaNodeBase, bpy.types.Node):
row.prop(self, "threshold", text="") row.prop(self, "threshold", text="")
class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node): class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaVersionedNode, bpy.types.Node):
bl_category = "CONDITIONS" bl_category = "CONDITIONS"
bl_idname = "PlasmaVolumeSensorNode" bl_idname = "PlasmaVolumeSensorNode"
bl_label = "Region Sensor" bl_label = "Region Sensor"
@ -419,9 +449,11 @@ class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.type
description="Object that defines the region mesh", description="Object that defines the region mesh",
type=bpy.types.Object, type=bpy.types.Object,
poll=idprops.poll_mesh_objects) poll=idprops.poll_mesh_objects)
bounds = EnumProperty(name="Bounds", bounds = enum_props.bounds(
description="Physical object's bounds", "region_object",
items=bounds_types) name="Bounds",
description="Physical object's bounds"
)
# Detector Properties # Detector Properties
report_on = EnumProperty(name="Triggerers", report_on = EnumProperty(name="Triggerers",
@ -586,6 +618,18 @@ class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.type
return (self.find_input_socket("exit").allow or return (self.find_input_socket("exit").allow or
self.find_input("exit", "PlasmaVolumeReportNode") is not None) self.find_input("exit", "PlasmaVolumeReportNode") is not None)
@property
def latest_version(self):
return 2
def upgrade(self):
# In version 1 of this node, the bounds type was stored on this node. This could
# be overridden by whatever was in the collision modifier. Version 2 changes the
# bounds property to proxy to the collision modifier's bounds settings.
if self.version == 1:
enum_props.upgrade_bounds(self, "bounds")
self.version = 2
class PlasmaVolumeSettingsSocket(PlasmaNodeSocketBase): class PlasmaVolumeSettingsSocket(PlasmaNodeSocketBase):
bl_color = (43.1, 24.7, 0.0, 1.0) bl_color = (43.1, 24.7, 0.0, 1.0)

26
korman/nodes/node_logic.py

@ -20,8 +20,8 @@ from bpy.props import *
from typing import * from typing import *
from PyHSPlasma import * from PyHSPlasma import *
from .. import enum_props
from .node_core import * from .node_core import *
from ..properties.modifiers.physics import bounds_types, bounds_type_index, bounds_type_str
from .. import idprops from .. import idprops
class PlasmaExcludeRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node): class PlasmaExcludeRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node):
@ -33,25 +33,19 @@ class PlasmaExcludeRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.typ
# ohey, this can be a Python attribute # ohey, this can be a Python attribute
pl_attrib = {"ptAttribExcludeRegion"} pl_attrib = {"ptAttribExcludeRegion"}
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("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", region_object = PointerProperty(name="Region",
description="Region object's name", description="Region object's name",
type=bpy.types.Object, type=bpy.types.Object,
poll=idprops.poll_mesh_objects) poll=idprops.poll_mesh_objects)
bounds = EnumProperty(name="Bounds", bounds = enum_props.bounds(
description="Region bounds", "region_object",
items=bounds_types, name="Bounds",
get=_get_bounds, description="Region bounds"
set=_set_bounds) )
block_cameras = BoolProperty(name="Block Cameras", block_cameras = BoolProperty(
description="The region blocks cameras when it has been cleared") name="Block Cameras",
description="The region blocks cameras when it has been cleared"
)
input_sockets:dict[str, dict[str, Any]] = { input_sockets:dict[str, dict[str, Any]] = {
"safe_point": { "safe_point": {

81
korman/nodes/node_messages.py

@ -21,6 +21,7 @@ from PyHSPlasma import *
from typing import * from typing import *
from .. import enum_props
from .node_core import * from .node_core import *
from ..properties.modifiers.physics import subworld_types from ..properties.modifiers.physics import subworld_types
from ..properties.modifiers.region import footstep_surfaces, footstep_surface_ids from ..properties.modifiers.region import footstep_surfaces, footstep_surface_ids
@ -113,39 +114,30 @@ class PlasmaAnimCmdMsgNode(idprops.IDPropMixin, PlasmaMessageWithCallbacksNode,
("TEXTURE", "Texture", "Texture Action")], ("TEXTURE", "Texture", "Texture Action")],
default="OBJECT") default="OBJECT")
def _poll_texture(self, value): def _poll_target_object(self, value: bpy.types.Object) -> bool:
# must be a legal option... but is it a member of this material... or, if no material, if self.anim_type == "TEXTURE":
# any of the materials attached to the object? return idprops.poll_drawable_objects(self, value)
if self.target_material is not None: elif self.anim_type == "MESH":
return value.name in self.target_material.texture_slots return idprops.poll_animated_objects(self, value)
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):
return True
return False
else: else:
return True raise RuntimeError()
def _poll_material(self, value):
# 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)
return value in object_materials
return True
target_object = PointerProperty(name="Object", target_object = idprops.triprop_object(
description="Target object", "target_object", "target_material", "target_texture",
type=bpy.types.Object) name="Object",
target_material = PointerProperty(name="Material", description="Target object",
description="Target material", poll=_poll_target_object
type=bpy.types.Material, )
poll=_poll_material) target_material = idprops.triprop_material(
target_texture = PointerProperty(name="Texture", "target_object", "target_material", "target_texture",
description="Target texture", name="Material",
type=bpy.types.Texture, description="Target material"
poll=_poll_texture) )
target_texture = idprops.triprop_texture(
"target_object", "target_material", "target_texture",
name="Texture",
description="Target texture"
)
go_to = EnumProperty(name="Go To", go_to = EnumProperty(name="Go To",
description="Where should the animation start?", description="Where should the animation start?",
@ -205,37 +197,14 @@ class PlasmaAnimCmdMsgNode(idprops.IDPropMixin, PlasmaMessageWithCallbacksNode,
("kStop", "Stop", "When the action is stopped by a message")], ("kStop", "Stop", "When the action is stopped by a message")],
default="kEnd") default="kEnd")
# Blender memory workaround
_ENTIRE_ANIMATION = "(Entire Animation)"
def _get_anim_names(self, context): def _get_anim_names(self, context):
if self.anim_type == "OBJECT": if self.anim_type == "OBJECT":
items = [(anim.animation_name, anim.animation_name, "") return enum_props._get_object_animation_names(self, "target_object")
for anim in self.target_object.plasma_modifiers.animation.subanimations]
elif self.anim_type == "TEXTURE": elif self.anim_type == "TEXTURE":
if self.target_texture is not None: return enum_props._get_texture_animation_names(self, "target_object", "target_material", "target_texture")
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)
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))
items = [(i, i, "") for i in all_anims]
else:
items = [(PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, "")]
else: else:
raise RuntimeError() raise RuntimeError()
# We always want "(Entire Animation)", if it exists, to be the first item.
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, ""))
return items
anim_name = EnumProperty(name="Animation", anim_name = EnumProperty(name="Animation",
description="Name of the animation to control", description="Name of the animation to control",
items=_get_anim_names, items=_get_anim_names,

101
korman/nodes/node_python.py

@ -21,6 +21,7 @@ from contextlib import contextmanager
from pathlib import Path from pathlib import Path
from PyHSPlasma import * from PyHSPlasma import *
from .. import enum_props
from .node_core import * from .node_core import *
from .node_deprecated import PlasmaDeprecatedNode, PlasmaVersionedNode from .node_deprecated import PlasmaDeprecatedNode, PlasmaVersionedNode
from .. import idprops from .. import idprops
@ -823,81 +824,43 @@ class PlasmaAttribTextureNode(idprops.IDPropMixin, PlasmaAttribNodeBase, bpy.typ
pl_attrib = ("ptAttribMaterial", "ptAttribMaterialList", pl_attrib = ("ptAttribMaterial", "ptAttribMaterialList",
"ptAttribDynamicMap", "ptAttribMaterialAnimation") "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)
return value in object_materials
return True
def _poll_texture(self, value: bpy.types.Texture) -> bool: def _poll_texture(self, value: bpy.types.Texture) -> bool:
# is this the type of dealio that we're looking for? # is this the type of dealio that we're looking for?
attrib = self.to_socket attrib = self.to_socket
if attrib is not None: if attrib is not None:
attrib = attrib.attribute_type attrib = attrib.attribute_type
if attrib == "ptAttribDynamicMap": if attrib == "ptAttribDynamicMap" and self._is_dyntext(value):
if not self._is_dyntext(value): return True
return False elif attrib == "ptAttribMaterialAnimation" and not self._is_dyntext:
elif attrib == "ptAttribMaterialAnimation": return True
if not self._is_animated(self.material, value):
return False
# must be a legal option... but is it a member of this material... or, if no material,
# any of the materials attached to the object?
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):
return True
return False return False
else:
return True
target_object = PointerProperty(name="Object", # We're not hooked up to a PFM node yet, so let anything slide.
description="", return True
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)
# Blender memory workaround
_ENTIRE_ANIMATION = "(Entire Animation)"
def _get_anim_names(self, context):
if self.texture is not None:
items = [(anim.animation_name, anim.animation_name, "")
for anim in self.texture.plasma_layer.subanimations]
elif self.material is not None or self.target_object is not None:
if self.material is None:
materials = (i.material for i in self.target_object.material_slots if i and i.material)
else:
materials = (self.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))
items = [(i, i, "") for i in all_anims]
else:
items = [(PlasmaAttribTextureNode._ENTIRE_ANIMATION, PlasmaAttribTextureNode._ENTIRE_ANIMATION, "")]
# We always want "(Entire Animation)", if it exists, to be the first item.
entire = items.index((PlasmaAttribTextureNode._ENTIRE_ANIMATION, PlasmaAttribTextureNode._ENTIRE_ANIMATION, ""))
if entire not in (-1, 0):
items.pop(entire)
items.insert(0, (PlasmaAttribTextureNode._ENTIRE_ANIMATION, PlasmaAttribTextureNode._ENTIRE_ANIMATION, ""))
return items
anim_name = EnumProperty(name="Animation", target_object = idprops.triprop_object(
description="Name of the animation to control", "target_object", "material", "texture",
items=_get_anim_names, name="Object",
options=set()) description="Target object"
)
material = idprops.triprop_material(
"target_object", "material", "texture",
name="Material",
description="Material the texture is attached to"
)
texture = idprops.triprop_texture(
"target_object", "material", "texture",
name="Texture",
description="Texture to expose to Python",
poll=_poll_texture
)
anim_name = enum_props.triprop_animation(
"target_object", "material", "texture",
name="Animation",
description="Name of the animation to control",
options=set()
)
def init(self, context): def init(self, context):
super().init(context) super().init(context)
@ -967,10 +930,6 @@ class PlasmaAttribTextureNode(idprops.IDPropMixin, PlasmaAttribNodeBase, bpy.typ
return {"material_name": bpy.data.materials, return {"material_name": bpy.data.materials,
"texture_name": bpy.data.textures} "texture_name": bpy.data.textures}
def _is_animated(self, material, texture):
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): def _is_dyntext(self, texture):
return texture.type == "IMAGE" and texture.image is None return texture.type == "IMAGE" and texture.image is None

8
korman/operators/op_ui.py

@ -77,11 +77,17 @@ class CollectionRemoveOperator(UIOperator, bpy.types.Operator):
index_prop = StringProperty(name="Index Property", index_prop = StringProperty(name="Index Property",
description="Name of the active element index property", description="Name of the active element index property",
options=set()) options=set())
manual_index = IntProperty(name="Manual Index",
description="Manual integer index to remove",
options=set())
def execute(self, context): def execute(self, context):
props = getattr(context, self.context).path_resolve(self.group_path) props = getattr(context, self.context).path_resolve(self.group_path)
collection = getattr(props, self.collection_prop) collection = getattr(props, self.collection_prop)
index = getattr(props, self.index_prop) if self.index_prop:
index = getattr(props, self.index_prop)
else:
index = self.manual_index
if len(collection) > index: if len(collection) > index:
collection.remove(index) collection.remove(index)
setattr(props, self.index_prop, index - 1) setattr(props, self.index_prop, index - 1)

134
korman/plasma_api.py

@ -0,0 +1,134 @@
# This file is part of Korman.
#
# Korman is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Korman is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
python_files = {
"xAgeSDLBoolShowHide.py": (
{ "id": 1, "type": "ptAttribString", "name": "sdlName" },
{ "id": 2, "type": "ptAttribBoolean", "name": "showOnTrue" },
# --- CWE Only Below ---
{ "id": 3, "type": "ptAttribBoolean", "name": "defaultValue" },
{ "id": 4, "type": "ptAttribBoolean", "name": "evalOnFirstUpdate "},
),
"xAgeSDLIntShowHide.py": (
{ "id": 1, "type": "ptAttribString", "name": "stringVarName" },
{ "id": 2, "type": "ptAttribString", "name": "stringShowStates" },
# --- CWE Only Below ---
{ "id": 3, "type": "ptAttribInt", "name": "intDefault" },
{ "id": 4, "type": "ptAttribBoolean", "name": "boolFirstUpdate "},
),
# Provided by all variants of Uru and Myst V
"xDialogToggle.py": (
{ "id": 1, "type": "ptAttribActivator", "name": "Activate" },
{ "id": 4, "type": "ptAttribString", "name": "Vignette" },
),
# Provided by CWE or OfflineKI
"xDynTextLoc.py": (
{ "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": 19, "type": "ptAttribBoolean", "name": "blockRGB" },
),
# Provided by CWE and OfflineKI
"xEntryCam.py": (
{ "id": 1, "type": "ptAttribActivator", "name": "actRegionSensor" },
{ "id": 2, "type": "ptAttribSceneobject", "name": "camera" },
{ "id": 3, "type": "ptAttribBoolean", "name": "undoFirstPerson" },
),
# Provided by CWE
"xJournalBookGUIPopup.py": (
{ "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" },
),
# Provided by all variants of Uru and Myst V
"xLinkingBookGUIPopup.py": (
{ "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" },
),
# Supplied by the OfflineKI script:
# https://gitlab.com/diafero/offline-ki/blob/master/offlineki/xSimpleJournal.py
"xSimpleJournal.py": (
{ "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" },
),
# Supplied by the OfflineKI script:
# https://gitlab.com/diafero/offline-ki/blob/master/offlineki/xSimpleLinkingBook.py
"xSimpleLinkingBook.py": (
{ "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" },
),
# Provided by CWE or OfflineKI
"xSitCam.py": (
{ "id": 1, "type": "ptAttribActivator", "name": "sitAct" },
{ "id": 2, "type": "ptAttribSceneobject", "name": "sitCam" },
),
# Provided by all variants of Uru and Myst V
"xTelescope.py": (
{ "id": 1, "type": "ptAttribActivator", "name": "Activate" },
{ "id": 2, "type": "ptAttribSceneobject", "name": "Camera" },
{ "id": 3, "type": "ptAttribBehavior", "name": "Behavior" },
{ "id": 4, "type": "ptAttribString", "name": "Vignette" },
)
}

12
korman/properties/modifiers/avatar.py

@ -78,16 +78,6 @@ class PlasmaLadderModifier(PlasmaModifierProperties):
return True return True
# Use xSitCam.py for when we want a camera to pop up
sitting_pfm = {
"filename": "xSitCam.py",
"attribs": (
{ 'id': 1, 'type': "ptAttribActivator", 'name': "sitAct" },
{ 'id': 2, 'type': "ptAttribSceneobject", 'name': "sitCam" },
)
}
sitting_approach_flags = [("kApproachFront", "Front", "Approach from the font"), sitting_approach_flags = [("kApproachFront", "Front", "Approach from the font"),
("kApproachLeft", "Left", "Approach from the left"), ("kApproachLeft", "Left", "Approach from the left"),
("kApproachRight", "Right", "Approach from the right"), ("kApproachRight", "Right", "Approach from the right"),
@ -140,7 +130,7 @@ class PlasmaSittingBehavior(idprops.IDPropObjectMixin, PlasmaModifierProperties,
sittingmod.name = "SittingBeh" sittingmod.name = "SittingBeh"
# xSitCam.py PythonFileMod # xSitCam.py PythonFileMod
if self.sitting_camera is not None: if self.sitting_camera is not None:
sittingpynode = self._create_python_file_node(tree, sitting_pfm["filename"], sitting_pfm["attribs"]) sittingpynode = self._create_python_standard_file_node(tree, "xSitCam.py")
sittingmod.link_output(sittingpynode, "satisfies", "sitAct") sittingmod.link_output(sittingpynode, "satisfies", "sitAct")
# Camera Object # Camera Object

37
korman/properties/modifiers/base.py

@ -26,6 +26,7 @@ if TYPE_CHECKING:
from ...nodes.node_python import * from ...nodes.node_python import *
from ... import helpers from ... import helpers
from ... import plasma_api
class PlasmaModifierProperties(bpy.types.PropertyGroup): class PlasmaModifierProperties(bpy.types.PropertyGroup):
@property @property
@ -180,20 +181,24 @@ class PlasmaModifierProperties(bpy.types.PropertyGroup):
class PlasmaModifierLogicWiz: class PlasmaModifierLogicWiz:
def convert_logic(self, bo, **kwargs): def convert_logic(self, bo, **kwargs):
"""Creates, converts, and returns an unmanaged NodeTree for this logic wizard. If the wizard """Attempts to look up an already existing logic tree matching the name provided and returns
fails during conversion, the temporary tree is deleted for you. However, on success, you it, if found. If not, creates, converts, and returns an unmanaged NodeTree for this wizard.
are responsible for removing the tree from Blender, if applicable.""" 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."""
name = kwargs.pop("name", self.key_name) name = kwargs.pop("name", self.key_name)
assert not "tree" in kwargs assert not "tree" in kwargs
tree = bpy.data.node_groups.new(name, "PlasmaNodeTree")
kwargs["tree"] = tree node_groups = bpy.data.node_groups
try: tree = node_groups.get(name)
self.logicwiz(bo, **kwargs) if tree is None:
except: tree = node_groups.new(name, "PlasmaNodeTree")
bpy.data.node_groups.remove(tree) kwargs["tree"] = tree
raise try:
else: self.logicwiz(bo, **kwargs)
return tree except:
bpy.data.node_groups.remove(tree)
raise
return tree
def _create_python_file_node(self, tree, filename: str, attributes: Dict[str, Any]) -> bpy.types.Node: def _create_python_file_node(self, tree, filename: str, attributes: Dict[str, Any]) -> bpy.types.Node:
pfm_node = tree.nodes.new("PlasmaPythonFileNode") pfm_node = tree.nodes.new("PlasmaPythonFileNode")
@ -207,6 +212,10 @@ class PlasmaModifierLogicWiz:
pfm_node.update() pfm_node.update()
return pfm_node return pfm_node
def _create_standard_python_file_node(self, tree, filename: str) -> bpy.types.Node:
"""Create a Python File Node for a standard Plasma Python API file (e.g. xAgeSDLBoolShowHide.py)"""
return self._create_python_file_node(tree, filename, plasma_api.python_files[filename])
def _create_python_attribute(self, pfm_node: PlasmaPythonFileNode, attribute_name: str, **kwargs): def _create_python_attribute(self, pfm_node: PlasmaPythonFileNode, attribute_name: str, **kwargs):
"""Creates and links a Python Attribute Node to the Python File Node given by `pfm_node`. """Creates and links a Python Attribute Node to the Python File Node given by `pfm_node`.
For attribute nodes that require multiple values, the `value` may be set to None and For attribute nodes that require multiple values, the `value` may be set to None and
@ -263,7 +272,7 @@ def _restore_properties(dummy):
# again and BOOM--there are no deprecated properties available. Therefore, # again and BOOM--there are no deprecated properties available. Therefore,
# we reregister them here. # we reregister them here.
for mod_cls in PlasmaModifierUpgradable.__subclasses__(): for mod_cls in PlasmaModifierUpgradable.__subclasses__():
for prop_name in mod_cls.deprecated_properties: for prop_name in getattr(mod_cls, "deprecated_properties", []):
# Unregistered propertes are a sequence of (property function, # Unregistered propertes are a sequence of (property function,
# property keyword arguments). Interesting design decision :) # property keyword arguments). Interesting design decision :)
prop_cb, prop_kwargs = getattr(mod_cls, prop_name) prop_cb, prop_kwargs = getattr(mod_cls, prop_name)
@ -283,6 +292,6 @@ def _upgrade_modifiers(dummy):
# Now that everything is upgraded, forcibly remove all properties # Now that everything is upgraded, forcibly remove all properties
# from the modifiers to prevent sneaky zombie-data type export bugs # from the modifiers to prevent sneaky zombie-data type export bugs
for mod_cls in PlasmaModifierUpgradable.__subclasses__(): for mod_cls in PlasmaModifierUpgradable.__subclasses__():
for prop in mod_cls.deprecated_properties: for prop in getattr(mod_cls, "deprecated_properties", []):
RemoveProperty(mod_cls, attr=prop) RemoveProperty(mod_cls, attr=prop)
bpy.app.handlers.load_post.append(_upgrade_modifiers) bpy.app.handlers.load_post.append(_upgrade_modifiers)

34
korman/properties/modifiers/game_gui.py

@ -203,26 +203,6 @@ class GameGuiAnimation(bpy.types.PropertyGroup):
else: else:
return idprops.poll_drawable_objects(self, value) return idprops.poll_drawable_objects(self, value)
def _poll_texture(self, value):
# must be a legal option... but is it a member of this material... or, if no material,
# any of the materials attached to the object?
if self.target_material is not None:
return value.name in self.target_material.texture_slots
else:
target_object = self.target_object if self.target_object is not None else self.id_data
for i in (slot.material for slot in 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
def _poll_material(self, value):
# 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.
target_object = self.target_object if self.target_object is not None else self.id_data
object_materials = (slot.material for slot in target_object.material_slots if slot and slot.material)
return value in object_materials
anim_type: str = EnumProperty( anim_type: str = EnumProperty(
name="Type", name="Type",
description="Animation type to affect", description="Animation type to affect",
@ -233,23 +213,21 @@ class GameGuiAnimation(bpy.types.PropertyGroup):
default="OBJECT", default="OBJECT",
options=set() options=set()
) )
target_object: bpy.types.Object = PointerProperty( target_object: bpy.types.Object = idprops.triprop_object(
"target_object", "target_material", "target_texure",
name="Object", name="Object",
description="Target object", description="Target object",
poll=_poll_target_object, poll=_poll_target_object,
type=bpy.types.Object
) )
target_material: bpy.types.Material = PointerProperty( target_material: bpy.types.Material = idprops.triprop_material(
"target_object", "target_material", "target_texure",
name="Material", name="Material",
description="Target material", description="Target material",
type=bpy.types.Material,
poll=_poll_material
) )
target_texture: bpy.types.Texture = PointerProperty( target_texture: bpy.types.Texture = idprops.triprop_texture(
"target_object", "target_material", "target_texure",
name="Texture", name="Texture",
description="Target texture", description="Target texture",
type=bpy.types.Texture,
poll=_poll_texture
) )

87
korman/properties/modifiers/gui.py

@ -35,33 +35,6 @@ if TYPE_CHECKING:
from ...exporter import Exporter from ...exporter import Exporter
from .game_gui import PlasmaGameGuiDialogModifier from .game_gui import PlasmaGameGuiDialogModifier
journal_pfms = {
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" },
)
},
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" },
)
},
}
# Do not change the numeric IDs. They allow the list to be rearranged. # Do not change the numeric IDs. They allow the list to be rearranged.
_languages = [("Dutch", "Nederlands", "Dutch", 0), _languages = [("Dutch", "Nederlands", "Dutch", 0),
("English", "English", "", 1), ("English", "English", "", 1),
@ -246,11 +219,11 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
def logicwiz(self, bo, tree, age_name, version): def logicwiz(self, bo, tree, age_name, version):
# Assign journal script based on target 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"])
if version <= pvPots: if version <= pvPots:
journalnode = self._create_standard_python_file_node(tree, "xSimpleJournal.py")
self._create_pots_nodes(bo, tree.nodes, journalnode, age_name) self._create_pots_nodes(bo, tree.nodes, journalnode, age_name)
else: else:
journalnode = self._create_standard_python_file_node(tree, "xJournalBookGUIPopup.py")
self._create_moul_nodes(bo, tree.nodes, journalnode, age_name) self._create_moul_nodes(bo, tree.nodes, journalnode, age_name)
def _create_pots_nodes(self, clickable_object, nodes, journalnode, age_name): def _create_pots_nodes(self, clickable_object, nodes, journalnode, age_name):
@ -324,43 +297,6 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
return self.journal_translations return self.journal_translations
linking_pfms = {
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" },
)
},
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" },
)
},
}
class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz): class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz):
pl_id = "linkingbookmod" pl_id = "linkingbookmod"
@ -495,12 +431,11 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
yield self.seek_point.name yield self.seek_point.name
def logicwiz(self, bo, tree, age_name, version): def logicwiz(self, bo, tree, age_name, version):
# 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"])
if version <= pvPots: if version <= pvPots:
linkingnode = self._create_standard_python_file_node(tree, "xSimpleLinkingBook.py")
self._create_pots_nodes(bo, tree.nodes, linkingnode, age_name) self._create_pots_nodes(bo, tree.nodes, linkingnode, age_name)
else: else:
linkingnode = self._create_standard_python_file_node(tree, "xLinkingBookGUIPopup.py")
self._create_moul_nodes(bo, tree.nodes, linkingnode, age_name) self._create_moul_nodes(bo, tree.nodes, linkingnode, age_name)
def _create_pots_nodes(self, clickable_object, nodes, linkingnode, age_name): def _create_pots_nodes(self, clickable_object, nodes, linkingnode, age_name):
@ -667,14 +602,6 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
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)
dialog_toggle = {
"filename": "xDialogToggle.py",
"attribs": (
{ 'id': 1, 'type': "ptAttribActivator", 'name': "Activate" },
{ 'id': 4, 'type': "ptAttribString", 'name': "Vignette" },
)
}
class PlasmaNotePopupModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz): class PlasmaNotePopupModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz):
pl_id = "note_popup" pl_id = "note_popup"
@ -755,11 +682,7 @@ class PlasmaNotePopupModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz):
nodes = tree.nodes nodes = tree.nodes
# xDialogToggle.py PythonFile Node # xDialogToggle.py PythonFile Node
dialog_node = self._create_python_file_node( dialog_node = self._create_standard_python_file_node(tree, "xDialogToggle.py")
tree,
dialog_toggle["filename"],
dialog_toggle["attribs"]
)
self._create_python_attribute(dialog_node, "Vignette", value=self.gui_page) self._create_python_attribute(dialog_node, "Vignette", value=self.gui_page)
# Clickable # Clickable

125
korman/properties/modifiers/logic.py

@ -15,10 +15,8 @@
from __future__ import annotations from __future__ import annotations
import bmesh
import bpy import bpy
from bpy.props import * from bpy.props import *
import mathutils
from PyHSPlasma import * from PyHSPlasma import *
from typing import * from typing import *
@ -28,20 +26,16 @@ if TYPE_CHECKING:
from ...nodes.node_messages import * from ...nodes.node_messages import *
from ...nodes.node_responder import * from ...nodes.node_responder import *
from typing import *
if TYPE_CHECKING:
from ...exporter import Exporter
from ...addon_prefs import game_versions from ...addon_prefs import game_versions
from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz
from ... import enum_props
from ...exporter import ExportError, utils from ...exporter import ExportError, utils
from ... import idprops from ... import idprops
from .physics import bounds_type_index, bounds_type_str, bounds_types
entry_cam_pfm = {
"filename": "xEntryCam.py",
"attribs": (
{ 'id': 1, 'type': "ptAttribActivator", 'name': "actRegionSensor" },
{ 'id': 2, 'type': "ptAttribSceneobject", 'name': "camera" },
{ 'id': 3, 'type': "ptAttribBoolean", 'name': "undoFirstPerson" },
)
}
class PlasmaVersionedNodeTree(idprops.IDPropMixin, bpy.types.PropertyGroup): class PlasmaVersionedNodeTree(idprops.IDPropMixin, bpy.types.PropertyGroup):
version = EnumProperty(name="Version", version = EnumProperty(name="Version",
@ -103,15 +97,6 @@ class PlasmaSpawnPoint(PlasmaModifierProperties, PlasmaModifierLogicWiz):
bl_description = "Point at which avatars link into the Age" bl_description = "Point at which avatars link into the Age"
bl_object_types = {"EMPTY"} bl_object_types = {"EMPTY"}
def _get_bounds(self) -> int:
if self.exit_region is not None:
return bounds_type_index(self.exit_region.plasma_modifiers.collision.bounds_type)
return bounds_type_index("hull")
def _set_bounds(self, value: int) -> None:
if self.exit_region is not None:
self.exit_region.plasma_modifiers.collision.bounds_type = bounds_type_str(value)
entry_camera = PointerProperty( entry_camera = PointerProperty(
name="Entry Camera", name="Entry Camera",
description="Camera to use when the player spawns at this location", description="Camera to use when the player spawns at this location",
@ -126,12 +111,10 @@ class PlasmaSpawnPoint(PlasmaModifierProperties, PlasmaModifierLogicWiz):
poll=idprops.poll_mesh_objects poll=idprops.poll_mesh_objects
) )
bounds_type = EnumProperty( bounds_type = enum_props.bounds(
"exit_region",
name="Bounds", name="Bounds",
description="", description="",
items=bounds_types,
get=_get_bounds,
set=_set_bounds,
default="hull" default="hull"
) )
@ -148,11 +131,7 @@ class PlasmaSpawnPoint(PlasmaModifierProperties, PlasmaModifierLogicWiz):
yield self.convert_logic(bo) yield self.convert_logic(bo)
def logicwiz(self, bo, tree): def logicwiz(self, bo, tree):
pfm_node = self._create_python_file_node( pfm_node = self._create_standard_python_file_node(tree, "xEntryCam.py")
tree,
entry_cam_pfm["filename"],
entry_cam_pfm["attribs"]
)
volume_sensor: PlasmaVolumeSensorNode = tree.nodes.new("PlasmaVolumeSensorNode") volume_sensor: PlasmaVolumeSensorNode = tree.nodes.new("PlasmaVolumeSensorNode")
volume_sensor.find_input_socket("enter").allow = True volume_sensor.find_input_socket("enter").allow = True
@ -204,15 +183,83 @@ class PlasmaMaintainersMarker(PlasmaModifierProperties):
return True return True
telescope_pfm = { class PlasmaSDLIntState(bpy.types.PropertyGroup):
"filename": "xTelescope.py", value: int = IntProperty(
"attribs": ( name="State Value",
{ 'id': 1, 'type': "ptAttribActivator", 'name': "Activate" }, description="The object is shown when the SDL variable is set to this value",
{ 'id': 2, 'type': "ptAttribSceneobject", 'name': "Camera" }, min=0,
{ 'id': 3, 'type': "ptAttribBehavior", 'name': "Behavior" }, soft_max=255,
{ 'id': 4, 'type': "ptAttribString", 'name': "Vignette" }, options=set()
) )
}
class PlasmaSDLShowHide(PlasmaModifierProperties, PlasmaModifierLogicWiz):
pl_id = "sdl_showhide"
bl_category = "Logic"
bl_label = "SDL Show/Hide"
bl_description = "Show/Hide an object based on an SDL Variable"
bl_object_types = {"MESH", "FONT"}
bl_icon = "VISIBLE_IPO_OFF"
sdl_variable: str = StringProperty(
name="SDL Variable",
description="Name of the SDL variable that controls visibility",
options=set()
)
variable_type: str = EnumProperty(
name="Type",
description="Data type of the SDL variable",
items=[
("bool", "Boolean", "A boolean, used to represent simple on/off for a single state"),
("int", "Integer", "An integer, used to represent multiple state combinations"),
],
options=set()
)
int_states = CollectionProperty(type=PlasmaSDLIntState)
bool_state: bool = BoolProperty(
name="Show When True",
description="If checked, show this object when the SDL Variable is TRUE. If not, hide it when TRUE.",
default=True,
options=set()
)
def created(self):
# Ensure at least one SDL int state is precreated for ease of use.
# REMEMBER: Blender's "sequences" don't do truthiness correctly...
if len(self.int_states) == 0:
self.int_states.add()
def sanity_check(self, exporter: Exporter):
if not exporter.age_sdl:
raise ExportError(f"'{self.id_data.name}': Age Global SDL is required for the SDL Show/Hide modifier!")
if not self.sdl_variable.strip():
raise ExportError(f"'{self.id_data.name}': A valid SDL variable is required for the SDL Show/Hide modifier!")
def logicwiz(self, bo, tree):
if self.variable_type == "bool":
pfm_node = self._create_standard_python_file_node(tree, "xAgeSDLBoolShowHide.py")
self._create_python_attribute(pfm_node, "sdlName", value=self.sdl_variable)
self._create_python_attribute(pfm_node, "showOnTrue", value=self.bool_state)
elif self.variable_type == "int":
pfm_node = self._create_standard_python_file_node(tree, "xAgeSDLIntShowHide.py")
self._create_python_attribute(pfm_node, "stringVarName", value=self.sdl_variable)
self._create_python_attribute(pfm_node, "stringShowStates", value=",".join(self._states))
else:
raise RuntimeError()
@property
def key_name(self):
if self.variable_type == "bool":
return f"cPythBoolShowHide_{self.sdl_variable}_{self.bool_state:d}"
elif self.variable_type == "int":
return f"cPythIntShowHide_{self.sdl_variable}_{'-'.join(self._states)}"
@property
def _states(self) -> Iterable[str]:
"""Returns a sorted, deduplicated iterable of the integer (converted to strings) states we should be visible in."""
return (str(i) for i in sorted(frozenset((i.value for i in self.int_states))))
class PlasmaTelescope(PlasmaModifierProperties, PlasmaModifierLogicWiz): class PlasmaTelescope(PlasmaModifierProperties, PlasmaModifierLogicWiz):
@ -255,7 +302,7 @@ class PlasmaTelescope(PlasmaModifierProperties, PlasmaModifierLogicWiz):
nodes = tree.nodes nodes = tree.nodes
# Create Python Node # Create Python Node
telescopepynode = self._create_python_file_node(tree, telescope_pfm["filename"], telescope_pfm["attribs"]) telescopepynode = self._create_standard_python_file_node(tree, "xTelescope.py")
# Clickable # Clickable
telescopeclick = nodes.new("PlasmaClickableNode") telescopeclick = nodes.new("PlasmaClickableNode")

18
korman/properties/modifiers/physics.py

@ -18,18 +18,10 @@ from bpy.props import *
from PyHSPlasma import * from PyHSPlasma import *
from .base import PlasmaModifierProperties from .base import PlasmaModifierProperties
from ...enum_props import _bounds_types
from ...exporter import ExportError from ...exporter import ExportError
from ... import idprops from ... import idprops
# These are the kinds of physical bounds Plasma can work with.
# This sequence is acceptable in any EnumProperty
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!)")
)
# These are the collision sound surface types # These are the collision sound surface types
surface_types = ( surface_types = (
# Danger: do not reorder this one. # Danger: do not reorder this one.
@ -55,12 +47,6 @@ subworld_types = (
("subworld", "Separate World", "Causes all objects to be placed in a separate physics simulation"), ("subworld", "Separate World", "Causes all objects to be placed in a separate physics simulation"),
) )
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): def _set_phys_prop(prop, sim, phys, value=True):
"""Sets properties on plGenericPhysical and plSimulationInterface (seeing as how they are duped)""" """Sets properties on plGenericPhysical and plSimulationInterface (seeing as how they are duped)"""
sim.setProperty(prop, value) sim.setProperty(prop, value)
@ -78,7 +64,7 @@ class PlasmaCollider(PlasmaModifierProperties):
bounds = EnumProperty(name="Bounds Type", bounds = EnumProperty(name="Bounds Type",
description="", description="",
items=bounds_types, items=_bounds_types,
default="hull") default="hull")
avatar_blocker = BoolProperty(name="Blocks Avatars", avatar_blocker = BoolProperty(name="Blocks Avatars",

35
korman/properties/modifiers/region.py

@ -22,9 +22,9 @@ from ...exporter import ExportError, ExportAssertionError
from ...helpers import bmesh_from_object from ...helpers import bmesh_from_object
from ... import idprops from ... import idprops
from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz from .base import PlasmaModifierProperties, PlasmaModifierUpgradable, PlasmaModifierLogicWiz
from ... import enum_props
from ..prop_camera import PlasmaCameraProperties from ..prop_camera import PlasmaCameraProperties
from .physics import bounds_types
footstep_surface_ids = { footstep_surface_ids = {
"dirt": 0, "dirt": 0,
@ -128,7 +128,7 @@ class PlasmaCameraRegion(PlasmaModifierProperties):
return self.camera_type == "auto_follow" return self.camera_type == "auto_follow"
class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierLogicWiz): class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierUpgradable, PlasmaModifierLogicWiz):
pl_id = "footstep" pl_id = "footstep"
bl_category = "Region" bl_category = "Region"
@ -136,14 +136,17 @@ class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierLogicWiz):
bl_description = "Footstep Region" bl_description = "Footstep Region"
bl_object_types = {"MESH"} bl_object_types = {"MESH"}
surface = EnumProperty(name="Surface", surface = EnumProperty(
description="What kind of surface are we walking on?", name="Surface",
items=footstep_surfaces, description="What kind of surface are we walking on?",
default="stone") items=footstep_surfaces,
bounds = EnumProperty(name="Region Bounds", default="stone"
description="Physical object's bounds", )
items=bounds_types, bounds = enum_props.bounds(
default="hull") name="Region Bounds",
description="Physical object's bounds",
default="hull"
)
def logicwiz(self, bo, tree): def logicwiz(self, bo, tree):
nodes = tree.nodes nodes = tree.nodes
@ -172,6 +175,16 @@ class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierLogicWiz):
def key_name(self): def key_name(self):
return "{}_FootRgn".format(self.id_data.name) return "{}_FootRgn".format(self.id_data.name)
@property
def latest_version(self):
return 2
def upgrade(self):
# Version 2 converts the bounds type to a proxy to the collision modifier.
if self.current_version < 2:
enum_props.upgrade_bounds(self, "bounds")
self.current_version = 2
class PlasmaPanicLinkRegion(PlasmaModifierProperties): class PlasmaPanicLinkRegion(PlasmaModifierProperties):
pl_id = "paniclink" pl_id = "paniclink"

26
korman/properties/modifiers/render.py

@ -624,30 +624,6 @@ class PlasmaLightingMod(PlasmaModifierProperties):
return False return False
_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" },
# 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': 19, 'type': "ptAttribBoolean", 'name': "blockRGB" },
)
class PlasmaLocalizedTextModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz, TranslationMixin): class PlasmaLocalizedTextModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz, TranslationMixin):
pl_id = "dynatext" pl_id = "dynatext"
pl_page_types = {"gui", "room"} pl_page_types = {"gui", "room"}
@ -738,7 +714,7 @@ class PlasmaLocalizedTextModifier(PlasmaModifierProperties, PlasmaModifierLogicW
self._create_nodes(bo, tree, age_name=age_name, version=version) 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): 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) pfm_node = self._create_standard_python_file_node(tree, "xDynTextLoc.py")
loc_path = self.key_name if version <= pvPots else "{}.{}.{}".format(age_name, self.localization_set, self.key_name) 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", self._create_python_attribute(pfm_node, "dynTextMap", "ptAttribDynamicMap",

20
korman/properties/modifiers/water.py

@ -182,8 +182,8 @@ class PlasmaWaterModifier(idprops.IDPropMixin, PlasmaModifierProperties, bpy.typ
bl_description = "Basic water properties" bl_description = "Basic water properties"
bl_object_types = {"MESH"} bl_object_types = {"MESH"}
wind_object = PointerProperty(name="Wind Object", wind_object = PointerProperty(name="Reference Object",
description="Object whose Y axis represents the wind direction", description="Object whose Y axis represents the wind direction and whose Z axis represents the water height",
type=bpy.types.Object, type=bpy.types.Object,
poll=idprops.poll_empty_objects) poll=idprops.poll_empty_objects)
wind_speed = FloatProperty(name="Wind Speed", wind_speed = FloatProperty(name="Wind Speed",
@ -244,15 +244,9 @@ class PlasmaWaterModifier(idprops.IDPropMixin, PlasmaModifierProperties, bpy.typ
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
waveset = exporter.mgr.find_create_object(plWaveSet7, name=bo.name, so=so) waveset = exporter.mgr.find_create_object(plWaveSet7, name=bo.name, so=so)
if self.wind_object: if self.wind_object is not None:
if exporter.has_coordiface(self.wind_object): waveset.refObj = exporter.mgr.find_create_key(plSceneObject, bl=self.wind_object)
waveset.refObj = exporter.mgr.find_create_key(plSceneObject, bl=self.wind_object) waveset.setFlag(plWaveSet7.kHasRefObject, True)
waveset.setFlag(plWaveSet7.kHasRefObject, True)
# This is much like what happened in PyPRP
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)
else: else:
# Stolen shamelessly from PyPRP # Stolen shamelessly from PyPRP
wind_dir = hsVector3(0.0871562, 0.996195, 0.0) wind_dir = hsVector3(0.0871562, 0.996195, 0.0)
@ -296,6 +290,10 @@ class PlasmaWaterModifier(idprops.IDPropMixin, PlasmaModifierProperties, bpy.typ
if not mods.water_shore.enabled: if not mods.water_shore.enabled:
mods.water_shore.convert_default(state) mods.water_shore.convert_default(state)
def harvest_actors(self):
if self.wind_object is not None:
yield self.wind_object.name
@classmethod @classmethod
def _idprop_mapping(cls): def _idprop_mapping(cls):
return {"wind_object": "wind_object_name", return {"wind_object": "wind_object_name",

37
korman/ui/modifiers/logic.py

@ -60,6 +60,43 @@ def maintainersmarker(modifier, layout, context):
layout.label(text="Positive Y is North, positive Z is up.") layout.label(text="Positive Y is North, positive Z is up.")
layout.prop(modifier, "calibration") layout.prop(modifier, "calibration")
def sdl_showhide(modifier: PlasmaSDLShowHide, layout, context):
if not context.scene.world.plasma_age.age_sdl:
layout.label("This modifier requires Age Global SDL!", icon="ERROR")
return
valid_variable = modifier.sdl_variable.strip()
layout.alert = not valid_variable
layout.prop(modifier, "sdl_variable")
if not valid_variable:
layout.label("A valid SDL variable is required!", icon="ERROR")
layout.alert = False
layout.prop(modifier, "variable_type")
layout.separator()
def setup_collection_operator(op):
op.context = "object"
op.group_path = modifier.path_from_id()
op.collection_prop = "int_states"
op.index_prop = ""
if modifier.variable_type == "bool":
layout.prop(modifier, "bool_state")
elif modifier.variable_type == "int":
layout.label("Show when SDL variable is:")
sub = layout.column_flow()
for i, state in enumerate(modifier.int_states):
row = sub.row(align=True)
row.prop(state, "value", text="Value")
op = row.operator("ui.plasma_collection_remove", icon="ZOOMOUT", text="")
setup_collection_operator(op)
op.manual_index = i
op = layout.operator("ui.plasma_collection_add", icon="ZOOMIN", text="Add State Value")
setup_collection_operator(op)
else:
raise RuntimeError()
def telescope(modifier, layout, context): def telescope(modifier, layout, context):
layout.prop(modifier, "clickable_region") layout.prop(modifier, "clickable_region")
layout.prop(modifier, "seek_target_object", icon="EMPTY_DATA") layout.prop(modifier, "seek_target_object", icon="EMPTY_DATA")

Loading…
Cancel
Save