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. 8
      korman/exporter/convert.py
  3. 80
      korman/idprops.py
  4. 74
      korman/nodes/node_conditions.py
  5. 26
      korman/nodes/node_logic.py
  6. 79
      korman/nodes/node_messages.py
  7. 93
      korman/nodes/node_python.py
  8. 6
      korman/operators/op_ui.py
  9. 134
      korman/plasma_api.py
  10. 12
      korman/properties/modifiers/avatar.py
  11. 23
      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. 29
      korman/properties/modifiers/region.py
  17. 26
      korman/properties/modifiers/render.py
  18. 16
      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)

8
korman/exporter/convert.py

@ -510,8 +510,12 @@ class Exporter:
@handle_temporary.register(bpy.types.NodeTree)
def _(temporary, parent):
# NodeTrees are reuseable, so make sure we haven't already encountered it.
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":
parent_so = self.mgr.find_create_object(plSceneObject, bl=parent)
self.want_node_trees[temporary.name].add((parent, parent_so))
@ -594,6 +598,10 @@ class Exporter:
else:
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
def dat_only(self):
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):
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
def _upgrade_node_trees(dummy):
"""

74
korman/nodes/node_conditions.py

@ -21,11 +21,12 @@ import math
from PyHSPlasma import *
from typing import *
from .. import enum_props
from .node_core import *
from ..properties.modifiers.physics import bounds_types
from .node_deprecated import PlasmaVersionedNode
from .. import idprops
class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node):
class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaVersionedNode, bpy.types.Node):
bl_category = "CONDITIONS"
bl_idname = "PlasmaClickableNode"
bl_label = "Clickable"
@ -38,10 +39,13 @@ class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.N
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")
bounds = enum_props.bounds(
"clickable_object",
name="Bounds",
description="Clickable's bounds",
default="hull"
)
input_sockets: dict[str, Any] = {
"region": {
@ -151,8 +155,20 @@ class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.N
def _idprop_mapping(cls):
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_idname = "PlasmaClickableRegionNode"
bl_label = "Clickable Region Settings"
@ -162,10 +178,12 @@ class PlasmaClickableRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.t
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")
bounds = enum_props.bounds(
"region_object",
name="Bounds",
description="Physical object's bounds",
default="hull"
)
output_sockets = {
"satisfies": {
@ -215,6 +233,18 @@ class PlasmaClickableRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.t
def _idprop_mapping(cls):
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):
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="")
class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node):
class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaVersionedNode, bpy.types.Node):
bl_category = "CONDITIONS"
bl_idname = "PlasmaVolumeSensorNode"
bl_label = "Region Sensor"
@ -419,9 +449,11 @@ class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.type
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)
bounds = enum_props.bounds(
"region_object",
name="Bounds",
description="Physical object's bounds"
)
# Detector Properties
report_on = EnumProperty(name="Triggerers",
@ -586,6 +618,18 @@ class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.type
return (self.find_input_socket("exit").allow or
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):
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 PyHSPlasma import *
from .. import enum_props
from .node_core import *
from ..properties.modifiers.physics import bounds_types, bounds_type_index, bounds_type_str
from .. import idprops
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
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",
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")
bounds = enum_props.bounds(
"region_object",
name="Bounds",
description="Region bounds"
)
block_cameras = BoolProperty(
name="Block Cameras",
description="The region blocks cameras when it has been cleared"
)
input_sockets:dict[str, dict[str, Any]] = {
"safe_point": {

79
korman/nodes/node_messages.py

@ -21,6 +21,7 @@ from PyHSPlasma import *
from typing import *
from .. import enum_props
from .node_core import *
from ..properties.modifiers.physics import subworld_types
from ..properties.modifiers.region import footstep_surfaces, footstep_surface_ids
@ -113,39 +114,30 @@ class PlasmaAnimCmdMsgNode(idprops.IDPropMixin, PlasmaMessageWithCallbacksNode,
("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,
# any of the materials attached to the object?
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):
return True
return False
def _poll_target_object(self, value: bpy.types.Object) -> bool:
if self.anim_type == "TEXTURE":
return idprops.poll_drawable_objects(self, value)
elif self.anim_type == "MESH":
return idprops.poll_animated_objects(self, value)
else:
return True
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
raise RuntimeError()
target_object = PointerProperty(name="Object",
target_object = idprops.triprop_object(
"target_object", "target_material", "target_texture",
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)
poll=_poll_target_object
)
target_material = idprops.triprop_material(
"target_object", "target_material", "target_texture",
name="Material",
description="Target material"
)
target_texture = idprops.triprop_texture(
"target_object", "target_material", "target_texture",
name="Texture",
description="Target texture"
)
go_to = EnumProperty(name="Go To",
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")],
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]
return enum_props._get_object_animation_names(self, "target_object")
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]
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, "")]
return enum_props._get_texture_animation_names(self, "target_object", "target_material", "target_texture")
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, ""))
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",
description="Name of the animation to control",
items=_get_anim_names,

93
korman/nodes/node_python.py

@ -21,6 +21,7 @@ from contextlib import contextmanager
from pathlib import Path
from PyHSPlasma import *
from .. import enum_props
from .node_core import *
from .node_deprecated import PlasmaDeprecatedNode, PlasmaVersionedNode
from .. import idprops
@ -823,81 +824,43 @@ class PlasmaAttribTextureNode(idprops.IDPropMixin, PlasmaAttribNodeBase, bpy.typ
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)
return value in object_materials
return True
def _poll_texture(self, value: bpy.types.Texture) -> bool:
# is this the type of dealio that we're looking for?
attrib = self.to_socket
if attrib is not None:
attrib = attrib.attribute_type
if attrib == "ptAttribDynamicMap":
if not self._is_dyntext(value):
return False
elif attrib == "ptAttribMaterialAnimation":
if not self._is_animated(self.material, value):
return False
# must be a legal option... but is it a member of this material... 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):
if attrib == "ptAttribDynamicMap" and self._is_dyntext(value):
return True
elif attrib == "ptAttribMaterialAnimation" and not self._is_dyntext:
return True
return False
else:
# We're not hooked up to a PFM node yet, so let anything slide.
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",
target_object = idprops.triprop_object(
"target_object", "material", "texture",
name="Object",
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",
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, ""))
poll=_poll_texture
)
return items
anim_name = EnumProperty(name="Animation",
anim_name = enum_props.triprop_animation(
"target_object", "material", "texture",
name="Animation",
description="Name of the animation to control",
items=_get_anim_names,
options=set())
options=set()
)
def init(self, context):
super().init(context)
@ -967,10 +930,6 @@ class PlasmaAttribTextureNode(idprops.IDPropMixin, PlasmaAttribNodeBase, bpy.typ
return {"material_name": bpy.data.materials,
"texture_name": bpy.data.textures}
def _is_animated(self, material, texture):
return ((material.animation_data is not None and material.animation_data.action is not None)
or (texture.animation_data is not None and texture.animation_data.action is not None))
def _is_dyntext(self, texture):
return texture.type == "IMAGE" and texture.image is None

6
korman/operators/op_ui.py

@ -77,11 +77,17 @@ class CollectionRemoveOperator(UIOperator, bpy.types.Operator):
index_prop = StringProperty(name="Index Property",
description="Name of the active element index property",
options=set())
manual_index = IntProperty(name="Manual Index",
description="Manual integer index to remove",
options=set())
def execute(self, context):
props = getattr(context, self.context).path_resolve(self.group_path)
collection = getattr(props, self.collection_prop)
if self.index_prop:
index = getattr(props, self.index_prop)
else:
index = self.manual_index
if len(collection) > index:
collection.remove(index)
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
# 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"),
("kApproachLeft", "Left", "Approach from the left"),
("kApproachRight", "Right", "Approach from the right"),
@ -140,7 +130,7 @@ class PlasmaSittingBehavior(idprops.IDPropObjectMixin, PlasmaModifierProperties,
sittingmod.name = "SittingBeh"
# xSitCam.py PythonFileMod
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")
# Camera Object

23
korman/properties/modifiers/base.py

@ -26,6 +26,7 @@ if TYPE_CHECKING:
from ...nodes.node_python import *
from ... import helpers
from ... import plasma_api
class PlasmaModifierProperties(bpy.types.PropertyGroup):
@property
@ -180,19 +181,23 @@ class PlasmaModifierProperties(bpy.types.PropertyGroup):
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."""
"""Attempts to look up an already existing logic tree matching the name provided and returns
it, if found. If not, creates, converts, and returns an unmanaged NodeTree for this 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."""
name = kwargs.pop("name", self.key_name)
assert not "tree" in kwargs
tree = bpy.data.node_groups.new(name, "PlasmaNodeTree")
node_groups = bpy.data.node_groups
tree = node_groups.get(name)
if tree is None:
tree = node_groups.new(name, "PlasmaNodeTree")
kwargs["tree"] = tree
try:
self.logicwiz(bo, **kwargs)
except:
bpy.data.node_groups.remove(tree)
raise
else:
return tree
def _create_python_file_node(self, tree, filename: str, attributes: Dict[str, Any]) -> bpy.types.Node:
@ -207,6 +212,10 @@ class PlasmaModifierLogicWiz:
pfm_node.update()
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):
"""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
@ -263,7 +272,7 @@ def _restore_properties(dummy):
# again and BOOM--there are no deprecated properties available. Therefore,
# we reregister them here.
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,
# property keyword arguments). Interesting design decision :)
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
# from the modifiers to prevent sneaky zombie-data type export bugs
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)
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:
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(
name="Type",
description="Animation type to affect",
@ -233,23 +213,21 @@ class GameGuiAnimation(bpy.types.PropertyGroup):
default="OBJECT",
options=set()
)
target_object: bpy.types.Object = PointerProperty(
target_object: bpy.types.Object = idprops.triprop_object(
"target_object", "target_material", "target_texure",
name="Object",
description="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",
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",
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 .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.
_languages = [("Dutch", "Nederlands", "Dutch", 0),
("English", "English", "", 1),
@ -246,11 +219,11 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
def logicwiz(self, bo, tree, age_name, 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:
journalnode = self._create_standard_python_file_node(tree, "xSimpleJournal.py")
self._create_pots_nodes(bo, tree.nodes, journalnode, age_name)
else:
journalnode = self._create_standard_python_file_node(tree, "xJournalBookGUIPopup.py")
self._create_moul_nodes(bo, tree.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
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):
pl_id = "linkingbookmod"
@ -495,12 +431,11 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
yield self.seek_point.name
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:
linkingnode = self._create_standard_python_file_node(tree, "xSimpleLinkingBook.py")
self._create_pots_nodes(bo, tree.nodes, linkingnode, age_name)
else:
linkingnode = self._create_standard_python_file_node(tree, "xLinkingBookGUIPopup.py")
self._create_moul_nodes(bo, tree.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)
dialog_toggle = {
"filename": "xDialogToggle.py",
"attribs": (
{ 'id': 1, 'type': "ptAttribActivator", 'name': "Activate" },
{ 'id': 4, 'type': "ptAttribString", 'name': "Vignette" },
)
}
class PlasmaNotePopupModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz):
pl_id = "note_popup"
@ -755,11 +682,7 @@ class PlasmaNotePopupModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz):
nodes = tree.nodes
# xDialogToggle.py PythonFile Node
dialog_node = self._create_python_file_node(
tree,
dialog_toggle["filename"],
dialog_toggle["attribs"]
)
dialog_node = self._create_standard_python_file_node(tree, "xDialogToggle.py")
self._create_python_attribute(dialog_node, "Vignette", value=self.gui_page)
# Clickable

125
korman/properties/modifiers/logic.py

@ -15,10 +15,8 @@
from __future__ import annotations
import bmesh
import bpy
from bpy.props import *
import mathutils
from PyHSPlasma import *
from typing import *
@ -28,20 +26,16 @@ if TYPE_CHECKING:
from ...nodes.node_messages import *
from ...nodes.node_responder import *
from typing import *
if TYPE_CHECKING:
from ...exporter import Exporter
from ...addon_prefs import game_versions
from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz
from ... import enum_props
from ...exporter import ExportError, utils
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):
version = EnumProperty(name="Version",
@ -103,15 +97,6 @@ class PlasmaSpawnPoint(PlasmaModifierProperties, PlasmaModifierLogicWiz):
bl_description = "Point at which avatars link into the Age"
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(
name="Entry Camera",
description="Camera to use when the player spawns at this location",
@ -126,12 +111,10 @@ class PlasmaSpawnPoint(PlasmaModifierProperties, PlasmaModifierLogicWiz):
poll=idprops.poll_mesh_objects
)
bounds_type = EnumProperty(
bounds_type = enum_props.bounds(
"exit_region",
name="Bounds",
description="",
items=bounds_types,
get=_get_bounds,
set=_set_bounds,
default="hull"
)
@ -148,11 +131,7 @@ class PlasmaSpawnPoint(PlasmaModifierProperties, PlasmaModifierLogicWiz):
yield self.convert_logic(bo)
def logicwiz(self, bo, tree):
pfm_node = self._create_python_file_node(
tree,
entry_cam_pfm["filename"],
entry_cam_pfm["attribs"]
)
pfm_node = self._create_standard_python_file_node(tree, "xEntryCam.py")
volume_sensor: PlasmaVolumeSensorNode = tree.nodes.new("PlasmaVolumeSensorNode")
volume_sensor.find_input_socket("enter").allow = True
@ -204,15 +183,83 @@ class PlasmaMaintainersMarker(PlasmaModifierProperties):
return True
telescope_pfm = {
"filename": "xTelescope.py",
"attribs": (
{ 'id': 1, 'type': "ptAttribActivator", 'name': "Activate" },
{ 'id': 2, 'type': "ptAttribSceneobject", 'name': "Camera" },
{ 'id': 3, 'type': "ptAttribBehavior", 'name': "Behavior" },
{ 'id': 4, 'type': "ptAttribString", 'name': "Vignette" },
class PlasmaSDLIntState(bpy.types.PropertyGroup):
value: int = IntProperty(
name="State Value",
description="The object is shown when the SDL variable is set to this value",
min=0,
soft_max=255,
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):
@ -255,7 +302,7 @@ class PlasmaTelescope(PlasmaModifierProperties, PlasmaModifierLogicWiz):
nodes = tree.nodes
# 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
telescopeclick = nodes.new("PlasmaClickableNode")

18
korman/properties/modifiers/physics.py

@ -18,18 +18,10 @@ from bpy.props import *
from PyHSPlasma import *
from .base import PlasmaModifierProperties
from ...enum_props import _bounds_types
from ...exporter import ExportError
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
surface_types = (
# 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"),
)
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)
@ -78,7 +64,7 @@ class PlasmaCollider(PlasmaModifierProperties):
bounds = EnumProperty(name="Bounds Type",
description="",
items=bounds_types,
items=_bounds_types,
default="hull")
avatar_blocker = BoolProperty(name="Blocks Avatars",

29
korman/properties/modifiers/region.py

@ -22,9 +22,9 @@ from ...exporter import ExportError, ExportAssertionError
from ...helpers import bmesh_from_object
from ... import idprops
from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz
from .base import PlasmaModifierProperties, PlasmaModifierUpgradable, PlasmaModifierLogicWiz
from ... import enum_props
from ..prop_camera import PlasmaCameraProperties
from .physics import bounds_types
footstep_surface_ids = {
"dirt": 0,
@ -128,7 +128,7 @@ class PlasmaCameraRegion(PlasmaModifierProperties):
return self.camera_type == "auto_follow"
class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierLogicWiz):
class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierUpgradable, PlasmaModifierLogicWiz):
pl_id = "footstep"
bl_category = "Region"
@ -136,14 +136,17 @@ class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierLogicWiz):
bl_description = "Footstep Region"
bl_object_types = {"MESH"}
surface = EnumProperty(name="Surface",
surface = EnumProperty(
name="Surface",
description="What kind of surface are we walking on?",
items=footstep_surfaces,
default="stone")
bounds = EnumProperty(name="Region Bounds",
default="stone"
)
bounds = enum_props.bounds(
name="Region Bounds",
description="Physical object's bounds",
items=bounds_types,
default="hull")
default="hull"
)
def logicwiz(self, bo, tree):
nodes = tree.nodes
@ -172,6 +175,16 @@ class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierLogicWiz):
def key_name(self):
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):
pl_id = "paniclink"

26
korman/properties/modifiers/render.py

@ -624,30 +624,6 @@ class PlasmaLightingMod(PlasmaModifierProperties):
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):
pl_id = "dynatext"
pl_page_types = {"gui", "room"}
@ -738,7 +714,7 @@ class PlasmaLocalizedTextModifier(PlasmaModifierProperties, PlasmaModifierLogicW
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)
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)
self._create_python_attribute(pfm_node, "dynTextMap", "ptAttribDynamicMap",

16
korman/properties/modifiers/water.py

@ -182,8 +182,8 @@ class PlasmaWaterModifier(idprops.IDPropMixin, PlasmaModifierProperties, bpy.typ
bl_description = "Basic water properties"
bl_object_types = {"MESH"}
wind_object = PointerProperty(name="Wind Object",
description="Object whose Y axis represents the wind direction",
wind_object = PointerProperty(name="Reference Object",
description="Object whose Y axis represents the wind direction and whose Z axis represents the water height",
type=bpy.types.Object,
poll=idprops.poll_empty_objects)
wind_speed = FloatProperty(name="Wind Speed",
@ -244,15 +244,9 @@ 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 exporter.has_coordiface(self.wind_object):
if self.wind_object is not None:
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)
else:
# Stolen shamelessly from PyPRP
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:
mods.water_shore.convert_default(state)
def harvest_actors(self):
if self.wind_object is not None:
yield self.wind_object.name
@classmethod
def _idprop_mapping(cls):
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.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):
layout.prop(modifier, "clickable_region")
layout.prop(modifier, "seek_target_object", icon="EMPTY_DATA")

Loading…
Cancel
Save