From ccf78561f19004a0d907191de568f98bd5f110d5 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 26 Dec 2017 18:43:34 -0500 Subject: [PATCH] Implement subworlds This commit includes some bonus improvements for animated parenting situations that I discovered through sciencing the various MOUL and PotS PRPs. Hopefully it all works as advertised. --- korman/exporter/convert.py | 1 + korman/exporter/physics.py | 63 +++++++++++++++++++- korman/idprops.py | 3 + korman/properties/modifiers/physics.py | 80 +++++++++++++++++--------- korman/properties/modifiers/region.py | 71 ++++++++++++++++++++++- korman/properties/prop_object.py | 10 ++++ korman/ui/modifiers/physics.py | 5 ++ korman/ui/modifiers/region.py | 6 ++ 8 files changed, 211 insertions(+), 28 deletions(-) diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index 801a6ba..8bf5da0 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -307,6 +307,7 @@ class Exporter: self.report.progress_advance() self.report.progress_range = len(self._objects) inc_progress = self.report.progress_increment + self.report.msg("\nPost-Processing SceneObjects...") mat_mgr = self.mesh.material for bl_obj in self._objects: diff --git a/korman/exporter/physics.py b/korman/exporter/physics.py index 90fd367..1c67529 100644 --- a/korman/exporter/physics.py +++ b/korman/exporter/physics.py @@ -18,10 +18,15 @@ import mathutils from PyHSPlasma import * import weakref -from .explosions import ExportAssertionError +from .explosions import ExportError, ExportAssertionError from ..helpers import TemporaryObject from . import utils +def _set_phys_prop(prop, sim, phys, value=True): + """Sets properties on plGenericPhysical and plSimulationInterface (seeing as how they are duped)""" + sim.setProperty(prop, value) + phys.setProperty(prop, value) + class PhysicsConverter: def __init__(self, exporter): self._exporter = weakref.ref(exporter) @@ -123,6 +128,50 @@ class PhysicsConverter: physical.object = so.key physical.sceneNode = self._mgr.get_scene_node(bl=bo) + # Got subworlds? + subworld = bo.plasma_object.subworld + if self.is_dedicated_subworld(subworld, sanity_check=False): + physical.subWorld = self._mgr.find_create_key(plHKSubWorld, bl=subworld) + + # Ensure this thing is set up properly for animations. + # This was previously the collision modifier's postexport method, but that + # would miss cases where we have animated detectors (subworlds!!!) + def _iter_object_tree(bo, stop_at_subworld): + while bo is not None: + if stop_at_subworld and self.is_dedicated_subworld(bo, sanity_check=False): + return + yield bo + bo = bo.parent + + ver = self._mgr.getVer() + for i in _iter_object_tree(bo, ver == pvMoul): + if i.plasma_object.has_transform_animation: + tree_xformed = True + break + else: + tree_xformed = False + + if tree_xformed: + bo_xformed = bo.plasma_object.has_transform_animation + + # MOUL: only objects that have animation data are kPhysAnim + if ver != pvMoul or bo_xformed: + _set_phys_prop(plSimulationInterface.kPhysAnim, simIface, physical) + # PotS: objects inheriting parent animation only are not pinned + # MOUL: animated objects in subworlds are not pinned + if bo_xformed and (ver != pvMoul or subworld is None): + _set_phys_prop(plSimulationInterface.kPinned, simIface, physical) + # MOUL: child objects are kPassive + if ver == pvMoul and bo.parent is not None: + _set_phys_prop(plSimulationInterface.kPassive, simIface, physical) + # FilterCoordinateInterfaces are kPassive + if bo.plasma_object.ci_type == plFilterCoordInterface: + _set_phys_prop(plSimulationInterface.kPassive, simIface, physical) + + # If the mass is zero, then we will fail to animate. Fix that. + if physical.mass == 0.0: + physical.mass = 1.0 + getattr(self, "_export_{}".format(bounds))(bo, physical) else: simIface = so.sim.object @@ -176,6 +225,18 @@ class PhysicsConverter: physical.verts = vertices physical.indices = indices + def is_dedicated_subworld(self, bo, sanity_check=True): + """Determines if a subworld object defines an alternate physics world""" + if bo is None: + return False + subworld_mod = bo.plasma_modifiers.subworld_def + if not subworld_mod.enabled: + if sanity_check: + raise ExportError("'{}' is not a subworld".format(bo.name)) + else: + return False + return subworld_mod.is_dedicated_subworld(self._exporter()) + @property def _mgr(self): return self._exporter().mgr diff --git a/korman/idprops.py b/korman/idprops.py index 45a6fab..1423943 100644 --- a/korman/idprops.py +++ b/korman/idprops.py @@ -133,6 +133,9 @@ def poll_mesh_objects(self, value): def poll_softvolume_objects(self, value): return value.plasma_modifiers.softvolume.enabled +def poll_subworld_objects(self, value): + return value.plasma_modifiers.subworld_def.enabled + def poll_visregion_objects(self, value): return value.plasma_modifiers.visregion.enabled diff --git a/korman/properties/modifiers/physics.py b/korman/properties/modifiers/physics.py index aa983a3..e84c699 100644 --- a/korman/properties/modifiers/physics.py +++ b/korman/properties/modifiers/physics.py @@ -18,6 +18,7 @@ from bpy.props import * from PyHSPlasma import * from .base import PlasmaModifierProperties +from ...exporter import ExportError # These are the kinds of physical bounds Plasma can work with. # This sequence is acceptable in any EnumProperty @@ -84,32 +85,6 @@ class PlasmaCollider(PlasmaModifierProperties): if self.terrain: physical.LOSDBs |= plSimDefs.kLOSDBAvatarWalkable - def _make_physical_movable(self, so): - sim = so.sim - if sim is not None: - sim = sim.object - phys = sim.physical.object - _set_phys_prop(plSimulationInterface.kPhysAnim, sim, phys) - - # If the mass is zero, then we will fail to animate. Fix that. - if phys.mass == 0.0: - phys.mass = 1.0 - - # set kPinned so it doesn't fall through - _set_phys_prop(plSimulationInterface.kPinned, sim, phys) - - # Do the same for child objects - for child in so.coord.object.children: - self._make_physical_movable(child.object) - - def post_export(self, exporter, bo, so): - test_bo = bo - while test_bo is not None: - if test_bo.plasma_object.has_transform_animation: - self._make_physical_movable(so) - break - test_bo = test_bo.parent - @property def key_name(self): return "{}_Collision".format(self.id_data.name) @@ -117,3 +92,56 @@ class PlasmaCollider(PlasmaModifierProperties): @property def requires_actor(self): return self.dynamic + + +class PlasmaSubworld(PlasmaModifierProperties): + pl_id = "subworld_def" + + bl_category = "Physics" + bl_label = "Subworld" + bl_description = "Subworld definition" + bl_icon = "WORLD" + + sub_type = EnumProperty(name="Subworld Type", + description="Specifies the physics strategy to use for this subworld", + items=[("auto", "Auto", "Korman will decide which physics strategy to use"), + ("dynamicav", "Dynamic Avatar", "Allows the avatar to affected by dynamic physicals"), + ("subworld", "Separate World", "Causes all objects to be placed in a separate physics simulation")], + default="auto", + options=set()) + gravity = FloatVectorProperty(name="Gravity", + description="Subworld's gravity defined in feet per second squared", + size=3, default=(0.0, 0.0, -32.174), precision=3, + subtype="ACCELERATION", unit="ACCELERATION") + + def export(self, exporter, bo, so): + if self.is_dedicated_subworld(exporter): + # NOTE to posterity... Cyan's PotS/Havok subworlds appear to have a + # plHKPhysical object that is set as LOSOnly convex hull. They appear to + # be a bounding box. PyPRP generated PRPs do not do this and work just fine, + # however, so this is probably just a quirk of the Havok-era PlasmaMAX + subworld = exporter.mgr.find_create_object(plHKSubWorld, so=so) + subworld.gravity = hsVector3(*self.gravity) + + def is_dedicated_subworld(self, exporter): + if exporter.mgr.getVer() != pvMoul: + return True + if self.sub_type == "subworld": + return True + elif self.sub_type == "dynamicav": + return False + else: + return not self.property_unset("gravity") + + def post_export(self, exporter, bo, so): + # It appears PotS does something really fancy with subworlds under the hood such that + # if you make a subworld that has collision, it will get into an infinite loop in + # plCoordinateInterface::IGetRoot. Not really sure why this happens (nor do I care), + # but we definitely don't want it to happen. + if bo.type != "EMPTY": + exporter.report.warn("Subworld '{}' is attached to a '{}'--this should be an empty.", bo.name, bo.type, indent=1) + if so.sim: + if exporter.mgr.getVer() > pvPots: + exporter.report.port("Subworld '{}' has physics data--this will cause PotS to crash.", bo.name, indent=1) + else: + raise ExportError("Subworld '{}' cannot have physics data (should be an empty).".format(bo.name)) diff --git a/korman/properties/modifiers/region.py b/korman/properties/modifiers/region.py index f63ab44..622f1c0 100644 --- a/korman/properties/modifiers/region.py +++ b/korman/properties/modifiers/region.py @@ -17,7 +17,7 @@ import bpy from bpy.props import * from PyHSPlasma import * -from ...exporter import ExportError +from ...exporter import ExportError, ExportAssertionError from ...helpers import TemporaryObject from ... import idprops @@ -240,3 +240,72 @@ class PlasmaSoftVolume(idprops.IDPropMixin, PlasmaModifierProperties): def _idprop_sources(self): return {"node_tree_name": bpy.data.node_groups} + + +class PlasmaSubworldRegion(PlasmaModifierProperties): + pl_id = "subworld_rgn" + + bl_category = "Region" + bl_label = "Subworld Region" + bl_description = "Subworld transition region" + + subworld = PointerProperty(name="Subworld", + description="Subworld to transition into", + type=bpy.types.Object, + poll=idprops.poll_subworld_objects) + transition = EnumProperty(name="Transition", + description="When to transition to the new subworld", + items=[("enter", "On Enter", "Transition when the avatar enters the region"), + ("exit", "On Exit", "Transition when the avatar exits the region")], + default="enter", + options=set()) + + def export(self, exporter, bo, so): + # Due to the fact that our subworld modifier can produce both RidingAnimatedPhysical + # and [HK|PX]Subworlds depending on the situation, this could get hairy, fast. + # Start by surveying the lay of the land. + from_sub, to_sub = bo.plasma_object.subworld, self.subworld + from_isded = exporter.physics.is_dedicated_subworld(from_sub) + to_isded = exporter.physics.is_dedicated_subworld(to_sub) + if 1: + def get_log_text(bo, isded): + main = "[Main World]" if bo is None else bo.name + sub = "Subworld" if isded or bo is None else "RidingAnimatedPhysical" + return main, sub + from_name, from_type = get_log_text(from_sub, from_isded) + to_name, to_type = get_log_text(to_sub, to_isded) + exporter.report.msg("Transition from '{}' ({}) to '{}' ({})", + from_name, from_type, to_name, to_type, + indent=2) + + # I think the best solution here is to not worry about the excitement mentioned above. + # If we encounter anything truly interesting, we can fix it in CWE more easily IMO because + # the game actually knows more about the avatar's state than we do here in the exporter. + if to_isded or (from_isded and to_sub is None): + region = exporter.mgr.find_create_object(plSubworldRegionDetector, so=so) + if to_sub is not None: + region.subworld = exporter.mgr.find_create_key(plSceneObject, bl=to_sub) + region.onExit = self.transition == "exit" + else: + msg = plRideAnimatedPhysMsg() + msg.BCastFlags |= plMessage.kLocalPropagate | plMessage.kPropagateToModifiers + msg.sender = so.key + msg.entering = to_sub is not None + + # In Cyan's PlasmaMAX RAP detector, it acts as more of a traditional region + # that changes us over to a dynamic character controller on region enter and + # reverts on region exit. We're going for an approach that is backwards compatible + # with subworlds, so our enter/exit regions are separate. Here, enter/exit message + # corresponds with when we should trigger the transition. + region = exporter.mgr.find_create_object(plRidingAnimatedPhysicalDetector, so=so) + if self.transition == "enter": + region.enterMsg = msg + elif self.transition == "exit": + region.exitMsg = msg + else: + raise ExportAssertionError() + + # Fancy pants region collider type shit + simIface, physical = exporter.physics.generate_physical(bo, so, self.id_data.plasma_modifiers.collision.bounds, self.key_name) + physical.memberGroup = plSimDefs.kGroupDetector + physical.reportGroup |= 1 << plSimDefs.kGroupAvatar diff --git a/korman/properties/prop_object.py b/korman/properties/prop_object.py index 4cd02d0..e9de5e9 100644 --- a/korman/properties/prop_object.py +++ b/korman/properties/prop_object.py @@ -81,6 +81,16 @@ class PlasmaObject(bpy.types.PropertyGroup): return {"location", "rotation_euler", "scale"} & data_paths return False + @property + def subworld(self): + bo = self.id_data + while bo is not None: + if bo.plasma_modifiers.subworld_def.enabled: + return bo + else: + bo = bo.parent + return None + class PlasmaNet(bpy.types.PropertyGroup): manual_sdl = BoolProperty(name="Override SDL", diff --git a/korman/ui/modifiers/physics.py b/korman/ui/modifiers/physics.py index fc72225..615e476 100644 --- a/korman/ui/modifiers/physics.py +++ b/korman/ui/modifiers/physics.py @@ -38,3 +38,8 @@ def collision(modifier, layout, context): col = split.column() col.active = modifier.dynamic col.prop(modifier, "mass") + +def subworld_def(modifier, layout, context): + layout.prop(modifier, "sub_type") + if modifier.sub_type != "dynamicav": + layout.prop(modifier, "gravity") diff --git a/korman/ui/modifiers/region.py b/korman/ui/modifiers/region.py index ea82bd6..58b6b23 100644 --- a/korman/ui/modifiers/region.py +++ b/korman/ui/modifiers/region.py @@ -39,3 +39,9 @@ def softvolume(modifier, layout, context): col = split.column() col.prop(modifier, "invert") col.prop(modifier, "soft_distance") + +def subworld_rgn(modifier, layout, context): + layout.prop(modifier, "subworld") + collision_mod = modifier.id_data.plasma_modifiers.collision + layout.prop(collision_mod, "bounds") + layout.prop(modifier, "transition")