diff --git a/korman/exporter/physics.py b/korman/exporter/physics.py index 840b60d..b02a249 100644 --- a/korman/exporter/physics.py +++ b/korman/exporter/physics.py @@ -30,6 +30,22 @@ def _set_phys_prop(prop, sim, phys, value=True): class PhysicsConverter: def __init__(self, exporter): self._exporter = weakref.ref(exporter) + self._bounds_converters = { + "box": self._export_box, + "sphere": self._export_sphere, + "hull": self._export_hull, + "trimesh": self._export_trimesh, + } + + def _apply_props(self, simIface, physical, props): + for i in props.get("properties", []): + _set_phys_prop(getattr(plSimulationInterface, i), simIface, physical) + for i in props.get("losdbs", []): + physical.LOSDBs |= getattr(plSimDefs, i) + for i in props.get("report_groups", []): + physical.reportGroup |= 1 << getattr(plSimDefs, i) + for i in props.get("collide_groups", []): + physical.collideGroup |= 1 << getattr(plSimDefs, i) def _convert_indices(self, mesh): indices = [] @@ -42,7 +58,7 @@ class PhysicsConverter: indices += (v[0], v[2], v[3],) return indices - def _convert_mesh_data(self, bo, physical, indices=True, mesh_func=None): + def _convert_mesh_data(self, bo, physical, local_space, indices=True, mesh_func=None): try: mesh = bo.to_mesh(bpy.context.scene, True, "RENDER", calc_tessface=False) except: @@ -54,8 +70,7 @@ class PhysicsConverter: if mesh_func is not None: mesh_func(mesh) - # We can only use the plPhysical xforms if there is a CI... - if self._exporter().has_coordiface(bo): + if local_space: mesh.update(calc_tessface=indices) physical.pos = hsVector3(*mat.to_translation()) physical.rot = utils.quaternion(mat.to_quaternion()) @@ -79,12 +94,11 @@ class PhysicsConverter: else: return vertices - def generate_flat_proxy(self, bo, so, z_coord=None, name=None): + def generate_flat_proxy(self, bo, so, **kwargs): """Generates a flat physical object""" - if so.sim is None: - if name is None: - name = bo.name + z_coord = kwargs.pop("z_coord", None) + if so.sim is None: simIface = self._mgr.add_object(pl=plSimulationInterface, bl=bo) physical = self._mgr.add_object(pl=plGenericPhysical, bl=bo, name=name) @@ -94,7 +108,7 @@ class PhysicsConverter: mesh = bo.to_mesh(bpy.context.scene, True, "RENDER", calc_tessface=False) with TemporaryObject(mesh, bpy.data.meshes.remove): - # We will apply all xform, seeing as how this is a special case... + # No mass and no emedded xform, so we force worldspace collision. mesh.transform(bo.matrix_world) mesh.update(calc_tessface=True) @@ -111,22 +125,36 @@ class PhysicsConverter: physical.verts = vertices physical.indices = self._convert_indices(mesh) physical.boundsType = plSimDefs.kProxyBounds + + group_name = kwargs.get("member_group") + if group_name: + physical.memberGroup = getattr(plSimDefs, group_name) else: simIface = so.sim.object physical = simIface.physical.object - if name is not None: - physical.key.name = name - - return (simIface, physical) - def generate_physical(self, bo, so, bounds, name=None): - """Generates a physical object for the given object pair""" + member_group = getattr(plSimDefs, kwargs.get("member_group", "kGroupLOSOnly")) + if physical.memberGroup != member_group and member_group != plSimDefs.kGroupLOSOnly: + self._report.warn("{}: Physical memberGroup overwritten!", bo.name) + physical.memberGroup = member_group + self._apply_props(simIface, physical, kwargs) + + def generate_physical(self, bo, so, **kwargs): + """Generates a physical object for the given object pair. + The following optional arguments are allowed: + - bounds: (defaults to collision modifier setting) + - member_group: str attribute of plSimDefs, defaults to kGroupStatic + NOTE that kGroupLOSOnly generation will only succeed if no one else + has generated this physical in another group + - properties: sequence of str bit names from plSimulationInterface + - losdbs: sequence of str bit names from plSimDefs + - report_groups: sequence of str bit names from plSimDefs + - collide_groups: sequence of str bit names from plSimDefs + """ if so.sim is None: - if name is None: - name = bo.name - simIface = self._mgr.add_object(pl=plSimulationInterface, bl=bo) - physical = self._mgr.add_object(pl=plGenericPhysical, bl=bo, name=name) + physical = self._mgr.add_object(pl=plGenericPhysical, bl=bo) + ver = self._mgr.getVer() simIface.physical = physical.key physical.object = so.key @@ -137,6 +165,37 @@ class PhysicsConverter: if self.is_dedicated_subworld(subworld, sanity_check=False): physical.subWorld = self._mgr.find_create_key(plHKSubWorld, bl=subworld) + # Export the collision modifier here since we like stealing from it anyway. + mod = bo.plasma_modifiers.collision + bounds = kwargs.get("bounds", mod.bounds) + if mod.enabled: + physical.friction = mod.friction + physical.restitution = mod.restitution + + if mod.dynamic: + if ver <= pvPots: + physical.collideGroup = (1 << plSimDefs.kGroupDynamic) | \ + (1 << plSimDefs.kGroupStatic) + physical.memberGroup = plSimDefs.kGroupDynamic + physical.mass = mod.mass + _set_phys_prop(plSimulationInterface.kStartInactive, simIface, physical, + value=mod.start_asleep) + elif not mod.avatar_blocker: + physical.memberGroup = plSimDefs.kGroupLOSOnly + else: + physical.memberGroup = plSimDefs.kGroupStatic + + # Line of Sight DB + if mod.camera_blocker: + physical.LOSDBs |= plSimDefs.kLOSDBCameraBlockers + _set_phys_prop(plSimulationInterface.kCameraAvoidObject, simIface, physical) + if mod.terrain: + physical.LOSDBs |= plSimDefs.kLOSDBAvatarWalkable + else: + group_name = kwargs.get("member_group") + if group_name: + physical.memberGroup = getattr(plSimDefs, group_name) + # 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!!!) @@ -147,7 +206,6 @@ class PhysicsConverter: 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 @@ -176,23 +234,30 @@ class PhysicsConverter: if physical.mass == 0.0: physical.mass = 1.0 - getattr(self, "_export_{}".format(bounds))(bo, physical) + if ver <= pvPots: + local_space = physical.mass > 0.0 + else: + local_space = self._exporter().has_coordiface(bo) + self._bounds_converters[bounds](bo, physical, local_space) else: simIface = so.sim.object physical = simIface.physical.object - if name is not None: - physical.key.name = name - return (simIface, physical) + member_group = getattr(plSimDefs, kwargs.get("member_group", "kGroupLOSOnly")) + if physical.memberGroup != member_group and member_group != plSimDefs.kGroupLOSOnly: + self._report.warn("{}: Physical memberGroup overwritten!", bo.name, indent=2) + physical.memberGroup = member_group - def _export_box(self, bo, physical): + self._apply_props(simIface, physical, kwargs) + + def _export_box(self, bo, physical, local_space): """Exports box bounds based on the object""" physical.boundsType = plSimDefs.kBoxBounds - vertices = self._convert_mesh_data(bo, physical, indices=False) + vertices = self._convert_mesh_data(bo, physical, local_space, indices=False) physical.calcBoxBounds(vertices) - def _export_hull(self, bo, physical): + def _export_hull(self, bo, physical, local_space): """Exports convex hull bounds based on the object""" physical.boundsType = plSimDefs.kHullBounds @@ -212,20 +277,21 @@ class PhysicsConverter: bpy.ops.mesh.convex_hull(use_existing_faces=False) bpy.ops.object.mode_set(mode="OBJECT") - physical.verts = self._convert_mesh_data(bo, physical, indices=False, mesh_func=_bake_hull) + physical.verts = self._convert_mesh_data(bo, physical, local_space, indices=False, + mesh_func=_bake_hull) - def _export_sphere(self, bo, physical): + def _export_sphere(self, bo, physical, local_space): """Exports sphere bounds based on the object""" physical.boundsType = plSimDefs.kSphereBounds - vertices = self._convert_mesh_data(bo, physical, indices=False) + vertices = self._convert_mesh_data(bo, physical, local_space, indices=False) physical.calcSphereBounds(vertices) - def _export_trimesh(self, bo, physical): + def _export_trimesh(self, bo, physical, local_space): """Exports an object's mesh as exact physical bounds""" physical.boundsType = plSimDefs.kExplicitBounds - vertices, indices = self._convert_mesh_data(bo, physical) + vertices, indices = self._convert_mesh_data(bo, physical, local_space) physical.verts = vertices physical.indices = indices @@ -244,3 +310,7 @@ class PhysicsConverter: @property def _mgr(self): return self._exporter().mgr + + @property + def _report(self): + return self._exporter().report diff --git a/korman/nodes/node_conditions.py b/korman/nodes/node_conditions.py index 9a9de9d..988346b 100644 --- a/korman/nodes/node_conditions.py +++ b/korman/nodes/node_conditions.py @@ -91,17 +91,10 @@ class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.N bounds = phys_mod.bounds if phys_mod.enabled else self.bounds # The actual physical object that does the cursor LOS - made_the_phys = (clickable_so.sim is None) - phys_name = "{}_ClickableLOS".format(clickable_bo.name) - simIface, physical = exporter.physics.generate_physical(clickable_bo, clickable_so, bounds, phys_name) - simIface.setProperty(plSimulationInterface.kPinned, True) - physical.setProperty(plSimulationInterface.kPinned, True) - if made_the_phys: - # we assume that the collision modifier will do this if they want it to be intangible - physical.memberGroup = plSimDefs.kGroupLOSOnly - if physical.mass == 0.0: - physical.mass = 1.0 - physical.LOSDBs |= plSimDefs.kLOSDBUIItems + exporter.physics.generate_physical(clickable_bo, clickable_so, bounds=bounds, + member_group="kGroupLOSOnly", + properties=["kPinned"], + losdbs=["kLOSDBUIItems"]) # Picking Detector -- detect when the physical is clicked detector = self._find_create_object(plPickingDetector, exporter, bl=clickable_bo, so=clickable_so) @@ -195,10 +188,9 @@ class PlasmaClickableRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.t bounds = phys_mod.bounds if phys_mod.enabled else self.bounds # Our physical is a detector and it only detects avatars... - phys_name = "{}_ClickableAvRegion".format(region_bo.name) - simIface, physical = exporter.physics.generate_physical(region_bo, region_so, bounds, phys_name) - physical.memberGroup = plSimDefs.kGroupDetector - physical.reportGroup |= 1 << plSimDefs.kGroupAvatar + exporter.physics.generate_physical(region_bo, region_so, bounds=bounds, + member_group="kGroupDetector", + report_groups=["kGroupAvatar"]) # I'm glad this crazy mess made sense to someone at Cyan... # ObjectInVolumeDetector can notify multiple logic mods. This implies we could share this @@ -352,9 +344,9 @@ class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.type report_on = EnumProperty(name="Triggerers", description="What triggers this region?", options={"ANIMATABLE", "ENUM_FLAG"}, - items=[("avatar", "Avatars", "Avatars trigger this region"), - ("dynamics", "Dynamics", "Any non-avatar dynamic physical object (eg kickables)")], - default={"avatar"}) + items=[("kGroupAvatar", "Avatars", "Avatars trigger this region"), + ("kGroupDynamic", "Dynamics", "Any non-avatar dynamic physical object (eg kickables)")], + default={"kGroupAvatar"}) input_sockets = OrderedDict([ ("enter", { @@ -436,14 +428,9 @@ class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.type interface.addIntfKey(key) # Don't forget to export the physical object itself! - # [trollface.jpg] - simIface, physical = exporter.physics.generate_physical(region_bo, region_so, self.bounds, "{}_VolumeSensor".format(region_bo.name)) - - physical.memberGroup = plSimDefs.kGroupDetector - if "avatar" in self.report_on: - physical.reportGroup |= 1 << plSimDefs.kGroupAvatar - if "dynamics" in self.report_on: - physical.reportGroup |= 1 << plSimDefs.kGroupDynamic + exporter.physics.generate_physical(region_bo, region_so, bounds=self.bounds, + member_group="kGroupDetector", + report_groups=self.report_on) def _export_volume_event(self, exporter, bo, so, event, settings): if event == plVolumeSensorConditionalObject.kTypeEnter: diff --git a/korman/nodes/node_logic.py b/korman/nodes/node_logic.py index 1f5ad70..a9a38e3 100644 --- a/korman/nodes/node_logic.py +++ b/korman/nodes/node_logic.py @@ -19,7 +19,7 @@ from collections import OrderedDict from PyHSPlasma import * from .node_core import * -from ..properties.modifiers.physics import bounds_types, bounds_type_index +from ..properties.modifiers.physics import bounds_types, bounds_type_index, bounds_type_str from .. import idprops class PlasmaExcludeRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node): @@ -37,7 +37,7 @@ class PlasmaExcludeRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.typ return bounds_type_index("hull") def _set_bounds(self, value): if self.region_object is not None: - self.region_object.plasma_modifiers.collision.bounds = value + self.region_object.plasma_modifiers.collision.bounds = bounds_type_str(value) region_object = PointerProperty(name="Region", description="Region object's name", @@ -99,14 +99,17 @@ class PlasmaExcludeRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.typ excludergn.addSafePoint(exporter.mgr.find_create_key(plSceneObject, bl=safept)) # Ensure the region is exported - phys_name = "{}_XRgn".format(self.region_object.name) - simIface, physical = exporter.physics.generate_physical(self.region_object, region_so, self.bounds, phys_name) - simIface.setProperty(plSimulationInterface.kPinned, True) - physical.setProperty(plSimulationInterface.kPinned, True) - physical.LOSDBs |= plSimDefs.kLOSDBUIBlockers - if exporter.mgr.getVer() < pvMoul: - physical.memberGroup = plSimDefs.kGroupDetector - physical.collideGroup |= 1 << plSimDefs.kGroupDynamic + if exporter.mgr.getVer() <= pvPots: + member_group = "kGroupDetector" + collide_groups = ["kGroupDynamic"] + else: + member_group = "kGroupStatic" + collide_groups = [] + exporter.physics.generate_physical(self.region_object, region_so, bounds=self.bounds, + properties=["kPinned"], + losdbs=["kLOSDBUIBlockers"], + member_group=member_group, + collide_groups=collide_groups) @property def export_once(self): diff --git a/korman/properties/modifiers/avatar.py b/korman/properties/modifiers/avatar.py index bb0110f..c5f1ed1 100644 --- a/korman/properties/modifiers/avatar.py +++ b/korman/properties/modifiers/avatar.py @@ -72,15 +72,9 @@ class PlasmaLadderModifier(PlasmaModifierProperties): mod.ladderView.normalize() # Generate the detector's physical bounds - det_name = "{}_LadderDetector".format(self.id_data.name) bounds = "hull" if not bo.plasma_modifiers.collision.enabled else bo.plasma_modifiers.collision.bounds - simIface, physical = exporter.physics.generate_physical(bo, so, bounds, det_name) - physical.memberGroup = plSimDefs.kGroupDetector - physical.reportGroup |= 1 << plSimDefs.kGroupAvatar - physical.setProperty(plSimulationInterface.kPinned, True) - simIface.setProperty(plSimulationInterface.kPinned, True) - if physical.mass == 0.0: - physical.mass = 1.0 + exporter.physics.generate_physical(bo, so, bounds=bounds, member_group="kGroupDetector", + report_groups=["kGroupAvatar"], properties=["kPinned"]) @property def requires_actor(self): diff --git a/korman/properties/modifiers/physics.py b/korman/properties/modifiers/physics.py index 227e16f..f561144 100644 --- a/korman/properties/modifiers/physics.py +++ b/korman/properties/modifiers/physics.py @@ -32,6 +32,9 @@ bounds_types = ( 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) @@ -60,36 +63,8 @@ class PlasmaCollider(PlasmaModifierProperties): start_asleep = BoolProperty(name="Start Asleep", description="Object is not active until influenced by another object", default=False) def export(self, exporter, bo, so): - simIface, physical = exporter.physics.generate_physical(bo, so, self.bounds, self.key_name) - - # Common props - physical.friction = self.friction - physical.restitution = self.restitution - - # Collision groups and such - if self.dynamic: - if exporter.mgr.getVer() < pvMoul: - physical.collideGroup = (1 << plSimDefs.kGroupDynamic) | (1 << plSimDefs.kGroupStatic) - physical.memberGroup = plSimDefs.kGroupDynamic - physical.mass = self.mass - _set_phys_prop(plSimulationInterface.kStartInactive, simIface, physical, value=self.start_asleep) - elif not self.avatar_blocker: - # the UI is kind of misleading on this count. oh well. - physical.memberGroup = plSimDefs.kGroupLOSOnly - else: - physical.memberGroup = plSimDefs.kGroupStatic - - # Line of Sight DB - if self.camera_blocker: - physical.LOSDBs |= plSimDefs.kLOSDBCameraBlockers - # This appears to be dead in CWE, but we'll set it anyway - _set_phys_prop(plSimulationInterface.kCameraAvoidObject, simIface, physical) - if self.terrain: - physical.LOSDBs |= plSimDefs.kLOSDBAvatarWalkable - - @property - def key_name(self): - return "{}_Collision".format(self.id_data.name) + # All modifier properties are examined by this little stinker... + exporter.physics.generate_physical(bo, so) @property def requires_actor(self): diff --git a/korman/properties/modifiers/region.py b/korman/properties/modifiers/region.py index 1ce022a..eefecf0 100644 --- a/korman/properties/modifiers/region.py +++ b/korman/properties/modifiers/region.py @@ -97,11 +97,9 @@ class PlasmaCameraRegion(PlasmaModifierProperties): # Setup physical stuff phys_mod = bo.plasma_modifiers.collision - simIface, physical = exporter.physics.generate_physical(bo, so, phys_mod.bounds, self.key_name) - physical.memberGroup = plSimDefs.kGroupDetector - physical.reportGroup = 1 << plSimDefs.kGroupAvatar - simIface.setProperty(plSimulationInterface.kPinned, True) - physical.setProperty(plSimulationInterface.kPinned, True) + exporter.physics.generate_physical(bo, so, member_group="kGroupDetector", + report_groups=["kGroupAvatar"], + properties=["kPinned"]) # I don't feel evil enough to make this generate a logic tree... msg = plCameraMsg() @@ -188,12 +186,8 @@ class PlasmaPanicLinkRegion(PlasmaModifierProperties): default=True) def export(self, exporter, bo, so): - phys_mod = bo.plasma_modifiers.collision - simIface, physical = exporter.physics.generate_physical(bo, so, phys_mod.bounds, self.key_name) - - # Now setup the region detector properties - physical.memberGroup = plSimDefs.kGroupDetector - physical.reportGroup = 1 << plSimDefs.kGroupAvatar + exporter.physics.generate_physical(bo, so, member_group="kGroupDetector", + report_groups=["kGroupAvatar"]) # Finally, the panic link region proper reg = exporter.mgr.add_object(plPanicLinkRegion, name=self.key_name, so=so) @@ -375,6 +369,5 @@ class PlasmaSubworldRegion(PlasmaModifierProperties): 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 + exporter.physics.generate_physical(bo, so, member_group="kGroupDetector", + report_groups=["kGroupAvatar"]) diff --git a/korman/properties/modifiers/water.py b/korman/properties/modifiers/water.py index e97d554..eafa22f 100644 --- a/korman/properties/modifiers/water.py +++ b/korman/properties/modifiers/water.py @@ -111,28 +111,26 @@ class PlasmaSwimRegion(idprops.IDPropObjectMixin, PlasmaModifierProperties, bpy. # the surface of the water BUT wave sets are supposed to conform to the bottom of the # pool. Therefore, we need to flatten out a temporary mesh in that case. # Ohey! CWE doesn't let you swim at all if the surface isn't flat... - swim_phys_name = "{}_SwimSurfaceLOS".format(bo.name) + losdbs = ["kLOSDBSwimRegion"] + member_group = "kGroupLOSOnly" if exporter.mgr.getVer() != pvMoul else "kGroupStatic" if bo.plasma_modifiers.water_basic.enabled: - simIface, physical = exporter.physics.generate_flat_proxy(bo, so, bo.location[2], swim_phys_name) + exporter.physics.generate_flat_proxy(bo, so, z_coord=bo.location[2], + member_group=member_group, + losdbs=losdbs) else: - simIface, physical = exporter.physics.generate_physical(bo, so, "trimesh", swim_phys_name) - physical.LOSDBs |= plSimDefs.kLOSDBSwimRegion - if exporter.mgr.getVer() != pvMoul: - physical.memberGroup = plSimDefs.kGroupLOSOnly + exporter.physics.generate_physical(bo, so, bounds="trimesh", + member_group=member_group, + losdbs=losdbs) # Detector region bounds if self.region is not None: region_so = exporter.mgr.find_create_object(plSceneObject, bl=self.region) # Good news: if this phys has already been exported, this is basically a noop - det_name = "{}_SwimDetector".format(self.region.name) - bounds = self.region.plasma_modifiers.collision.bounds - simIface, physical = exporter.physics.generate_physical(self.region, region_so, bounds, det_name) - if exporter.mgr.getVer() == pvMoul: - physical.memberGroup = plSimDefs.kGroupDetector - else: - physical.memberGroup = plSimDefs.kGroupLOSOnly - physical.reportGroup |= 1 << plSimDefs.kGroupAvatar + member_group = "kGroupDetector" if exporter.mgr.getVer() == "pvMoul" else "kGroupLOSOnly" + exporter.physics.generate_physical(self.region, region_so, + member_group=member_group, + report_groups=["kGroupAvatar"]) # I am a little concerned if we already have a plSwimDetector... I am not certain how # well Plasma would tolerate having a plSwimMsg with multiple regions referenced. @@ -140,7 +138,7 @@ class PlasmaSwimRegion(idprops.IDPropObjectMixin, PlasmaModifierProperties, bpy. # What? Me test it? # I are chicken. # Mmmmm chicken ***drool*** - if exporter.mgr.find_key(plSwimDetector, name=det_name, so=region_so) is None: + if exporter.mgr.find_key(plSwimDetector, so=region_so) is None: enter_msg, exit_msg = plSwimMsg(), plSwimMsg() for i in (enter_msg, exit_msg): i.BCastFlags = plMessage.kLocalPropagate | plMessage.kPropagateToModifiers @@ -149,7 +147,7 @@ class PlasmaSwimRegion(idprops.IDPropObjectMixin, PlasmaModifierProperties, bpy. enter_msg.isEntering = True exit_msg.isEntering = False - detector = exporter.mgr.add_object(plSwimDetector, name=det_name, so=region_so) + detector = exporter.mgr.add_object(plSwimDetector, so=region_so) detector.enterMsg = enter_msg detector.exitMsg = exit_msg else: