diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index 5f2555d..d338a42 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -376,6 +376,7 @@ class Exporter: for mod in bl_obj.plasma_modifiers.modifiers: proc = getattr(mod, "post_export", None) if proc is not None: + self.report.msg("Post processing '{}' modifier '{}'", bl_obj.name, mod.bl_label, indent=1) proc(self, bl_obj, sceneobject) inc_progress() diff --git a/korman/exporter/physics.py b/korman/exporter/physics.py index 4692354..e93bc06 100644 --- a/korman/exporter/physics.py +++ b/korman/exporter/physics.py @@ -184,6 +184,14 @@ class PhysicsConverter: _set_phys_prop(plSimulationInterface.kCameraAvoidObject, simIface, physical) if mod.terrain: physical.LOSDBs |= plSimDefs.kLOSDBAvatarWalkable + + # Hacky? We'd like to share the simple surface descriptors(TM) as much as possible... + # This could result in a few orphaned PhysicalSndGroups, but I think that's preferable + # to having a bunch of empty objects...? + if mod.surface != "kNone": + sndgroup = self._mgr.find_create_object(plPhysicalSndGroup, so=so, name="SURFACEGEN_{}".format(mod.surface)) + sndgroup.group = getattr(plPhysicalSndGroup, mod.surface) + physical.soundGroup = sndgroup.key else: group_name = kwargs.get("member_group") if group_name: diff --git a/korman/nodes/node_messages.py b/korman/nodes/node_messages.py index 64c9e03..594266b 100644 --- a/korman/nodes/node_messages.py +++ b/korman/nodes/node_messages.py @@ -733,6 +733,7 @@ class PlasmaSoundMsgNode(idprops.IDPropObjectMixin, PlasmaMessageWithCallbacksNo subtype="PERCENTAGE") def convert_callback_message(self, exporter, so, msg, target, wait): + assert not self.is_random_sound, "Callbacks are not available for random sounds" cb = plEventCallbackMsg() cb.addReceiver(target) cb.event = kEnd @@ -747,6 +748,33 @@ class PlasmaSoundMsgNode(idprops.IDPropObjectMixin, PlasmaMessageWithCallbacksNo if not soundemit.enabled: self.raise_error("'{}' is not a valid Sound Emitter".format(self.emitter_object.name)) + if self.is_random_sound: + yield from self._convert_random_sound_msg(exporter, so) + else: + yield from self._convert_sound_emitter_msg(exporter, so) + + def _convert_random_sound_msg(self, exporter, so): + # Yas, plAnimCmdMsg + msg = plAnimCmdMsg() + msg.addReceiver(exporter.mgr.find_key(plRandomSoundMod, bl=self.emitter_object)) + + if self.action == "kPlay": + msg.setCmd(plAnimCmdMsg.kContinue, True) + elif self.action == "kStop": + msg.setCmd(plAnimCmdMsg.kStop, True) + elif self.action == "kToggleState": + msg.setCmd(plAnimCmdMsg.kToggleState, True) + + if self.volume != "CURRENT": + # No, you are not imagining things... + msg.setCmd(plAnimCmdMsg.kSetSpeed, True) + msg.speed = self.volume_pct / 100.0 if self.volume == "CUSTOM" else 0.0 + + yield msg + + def _convert_sound_emitter_msg(self, exporter, so): + soundemit = self.emitter_object.plasma_modifiers.soundemit + # Always test the specified audible for validity if self.sound_name and soundemit.sounds.get(self.sound_name, None) is None: self.raise_error("Invalid Sound '{}' requested from Sound Emitter '{}'".format(self.sound_name, self.emitter_object.name)) @@ -787,26 +815,43 @@ class PlasmaSoundMsgNode(idprops.IDPropObjectMixin, PlasmaMessageWithCallbacksNo def draw_buttons(self, context, layout): layout.prop(self, "emitter_object") - if self.emitter_object is not None: - soundemit = self.emitter_object.plasma_modifiers.soundemit - if soundemit.enabled: - layout.prop_search(self, "sound_name", soundemit, "sounds", icon="SOUND") - else: - layout.label("Not a Sound Emitter", icon="ERROR") - layout.prop(self, "go_to") - if self.go_to == "TIME": - layout.prop(self, "time") + # Random Sound emitters can only control the entire emitter object, not the + # individual sounds. + random = self.is_random_sound + if not random: + if self.emitter_object is not None: + soundemit = self.emitter_object.plasma_modifiers.soundemit + if soundemit.enabled: + layout.prop_search(self, "sound_name", soundemit, "sounds", icon="SOUND") + else: + layout.label("Not a Sound Emitter", icon="ERROR") + + layout.prop(self, "go_to") + if self.go_to == "TIME": + layout.prop(self, "time") + layout.prop(self, "action") if self.volume == "CUSTOM": layout.prop(self, "volume_pct") - layout.prop(self, "looping") + if not random: + layout.prop(self, "looping") layout.prop(self, "volume") + @property + def has_callbacks(self): + return not self.is_random_sound + @classmethod def _idprop_mapping(cls): return {"emitter_object": "object_name"} + @property + def is_random_sound(self): + if self.emitter_object is not None: + return self.emitter_object.plasma_modifiers.random_sound.enabled + return False + class PlasmaTimerCallbackMsgNode(PlasmaMessageWithCallbacksNode, bpy.types.Node): bl_category = "MSG" diff --git a/korman/properties/modifiers/physics.py b/korman/properties/modifiers/physics.py index 907875d..bf4c7ff 100644 --- a/korman/properties/modifiers/physics.py +++ b/korman/properties/modifiers/physics.py @@ -30,6 +30,25 @@ bounds_types = ( ("trimesh", "Triangle Mesh", "Use the exact triangle mesh (SLOW!)") ) +# These are the collision sound surface types +surface_types = ( + # Danger: do not reorder this one. + ("kNone", "[None]", ""), + # Reorder away down here... + ("kBone", "Bone", ""), + ("kDirt", "Dirt", ""), + ("kGrass", "Grass", ""), + ("kMetal", "Metal", ""), + ("kCone", "Plastic", ""), + ("kRug", "Rug", ""), + ("kStone", "Stone", ""), + ("kWater", "Water", ""), + ("kWood", "Wood", ""), + ("kUser1", "User 1", ""), + ("kUser2", "User 2", ""), + ("kUser3", "User 3", ""), +) + def bounds_type_index(key): return list(zip(*bounds_types))[0].index(key) @@ -68,6 +87,12 @@ class PlasmaCollider(PlasmaModifierProperties): type=bpy.types.Object, poll=idprops.poll_mesh_objects) + surface = EnumProperty(name="Surface Type", + description="Type of surface sound effect to play on collision", + items=surface_types, + default="kNone", + options=set()) + def export(self, exporter, bo, so): # All modifier properties are examined by this little stinker... exporter.physics.generate_physical(bo, so) diff --git a/korman/properties/modifiers/sound.py b/korman/properties/modifiers/sound.py index 71b37fd..de148f4 100644 --- a/korman/properties/modifiers/sound.py +++ b/korman/properties/modifiers/sound.py @@ -22,9 +22,133 @@ from PyHSPlasma import * from ... import korlib from .base import PlasmaModifierProperties +from .physics import surface_types from ...exporter import ExportError from ... import idprops +_randomsound_modes = { + "normal": plRandomSoundMod.kNormal, + "norepeat": plRandomSoundMod.kNoRepeats, + "coverall": plRandomSoundMod.kCoverall | plRandomSoundMod.kNoRepeats, + "sequential": plRandomSoundMod.kSequential +} + +class PlasmaRandomSound(PlasmaModifierProperties): + pl_id = "random_sound" + pl_depends = {"soundemit"} + + bl_category = "Logic" + bl_label = "Random Sound" + bl_description = "" + + mode = EnumProperty(name="Mode", + description="Playback Type", + items=[("random", "Random Time", "Plays a random sound from the emitter at a random time"), + ("collision", "Collision Surface", "Plays a random sound when the object's parent collides")], + default="random", + options=set()) + + # Physical (read: collision) sounds + play_on = EnumProperty(name="Play On", + description="Play sounds on this collision event", + items=[("slide", "Slide", "Plays a random sound on object slide"), + ("impact", "Impact", "Plays a random sound on object slide")], + options=set()) + surfaces = EnumProperty(name="Play Against", + description="Sounds are played on collision against these surfaces", + items=surface_types[1:], + options={"ENUM_FLAG"}) + + # Timed random sounds + auto_start = BoolProperty(name="Auto Start", + description="Start playing when the Age loads", + default=True, + options=set()) + play_mode = EnumProperty(name="Play Mode", + description="", + items=[("normal", "Any", "Plays any attached sound"), + ("norepeat", "No Repeats", "Do not replay a sound immediately after itself"), + ("coverall", "Full Set", "Once a sound is played, do not replay it until after all sounds are played"), + ("sequential", "Sequential", "Play sounds in the order they appear in the emitter")], + default="norepeat", + options=set()) + stop_after_set = BoolProperty(name="Stop After Set", + description="Stop playing after all sounds are played", + default=False, + options=set()) + stop_after_play = BoolProperty(name="Stop After Play", + description="Stop playing after one sound is played", + default=False, + options=set()) + min_delay = FloatProperty(name="Min Delay", + description="Minimum delay length", + min=0.0, + subtype="TIME", unit="TIME", + options=set()) + max_delay = FloatProperty(name="Max Delay", + description="Maximum delay length", + min=0.0, + subtype="TIME", unit="TIME", + options=set()) + + def export(self, exporter, bo, so): + rndmod = exporter.mgr.find_create_object(plRandomSoundMod, bl=bo, so=so) + if self.mode == "random": + if not self.auto_start: + rndmod.state = plRandomSoundMod.kStopped + if self.stop_after_play: + rndmod.mode |= plRandomSoundMod.kOneCmd + else: + rndmod.minDelay = min(self.min_delay, self.max_delay) + rndmod.maxDelay = max(self.min_delay, self.max_delay) + # Delaying from the start makes ZERO sense. Screw that. + rndmod.mode |= plRandomSoundMod.kDelayFromEnd + rndmod.mode |= _randomsound_modes[self.play_mode] + if self.stop_after_set: + rndmod.mode |= plRandomSoundMod.kOneCycle + elif self.mode == "collision": + rndmod.mode = plRandomSoundMod.kNoRepeats | plRandomSoundMod.kOneCmd + rndmod.state = plRandomSoundMod.kStopped + else: + raise RuntimeError() + + def post_export(self, exporter, bo, so): + if self.mode == "collision": + parent_bo = bo.parent + if parent_bo is None: + raise ExportError("[{}]: Collision sound objects MUST be parented directly to the collider object.", bo.name) + phys = exporter.mgr.find_object(plGenericPhysical, bl=parent_bo) + if phys is None: + raise ExportError("[{}]: Collision sound objects MUST be parented directly to the collider object.", bo.name) + + # The soundGroup on the physical may or may not be the generic "this is my surface type" + # soundGroup with no actual sounds attached. So, we need to lookup the actual one. + sndgroup = exporter.mgr.find_create_object(plPhysicalSndGroup, bl=parent_bo) + sndgroup.group = getattr(plPhysicalSndGroup, parent_bo.plasma_modifiers.collision.surface) + phys.soundGroup = sndgroup.key + + rndmod = exporter.mgr.find_key(plRandomSoundMod, bl=bo, so=so) + if self.play_on == "slide": + groupattr = "slideSounds" + elif self.play_on == "impact": + groupattr = "impactSounds" + else: + raise RuntimeError() + + sounds = { i: sound for i, sound in enumerate(getattr(sndgroup, groupattr)) } + for surface_name in self.surfaces: + surface_id = getattr(plPhysicalSndGroup, surface_name) + if surface_id in sounds: + exporter.report.warn("Overwriting physical {} surface '{}' ID:{}", + groupattr, surface_name, surface_id, indent=2) + else: + exporter.report.msg("Got physical {} surface '{}' ID:{}", + groupattr, surface_name, surface_id, indent=2) + sounds[surface_id] = rndmod + # Keeps the LUT (or should that be lookup vector?) as small as possible + setattr(sndgroup, groupattr, [sounds.get(i) for i in range(max(sounds.keys()) + 1)]) + + class PlasmaSfxFade(bpy.types.PropertyGroup): fade_type = EnumProperty(name="Type", description="Fade Type", diff --git a/korman/ui/modifiers/physics.py b/korman/ui/modifiers/physics.py index fa9d8e9..c8d83a2 100644 --- a/korman/ui/modifiers/physics.py +++ b/korman/ui/modifiers/physics.py @@ -15,6 +15,7 @@ def collision(modifier, layout, context): layout.prop(modifier, "bounds") + layout.prop(modifier, "surface") layout.separator() split = layout.split() diff --git a/korman/ui/modifiers/sound.py b/korman/ui/modifiers/sound.py index f80fae6..95a7826 100644 --- a/korman/ui/modifiers/sound.py +++ b/korman/ui/modifiers/sound.py @@ -17,6 +17,39 @@ import bpy from .. import ui_list +def random_sound(modifier, layout, context): + parent_bo = modifier.id_data.parent + collision_bad = (modifier.mode == "collision" and (parent_bo is None or + not parent_bo.plasma_modifiers.collision.enabled)) + layout.alert = collision_bad + layout.prop(modifier, "mode") + if collision_bad: + layout.label(icon="ERROR", text="Sound emitter must be parented to a collider.") + layout.alert = False + + layout.separator() + if modifier.mode == "random": + split = layout.split() + col = split.column() + col.prop(modifier, "play_mode", text="") + col.prop(modifier, "auto_start") + col = col.column() + col.active = not modifier.stop_after_play + col.prop(modifier, "stop_after_set") + + col = split.column() + col.prop(modifier, "stop_after_play") + col = col.column(align=True) + col.active = not modifier.stop_after_play + col.alert = modifier.min_delay > modifier.max_delay + col.prop(modifier, "min_delay") + col.prop(modifier, "max_delay") + elif modifier.mode == "collision": + layout.prop(modifier, "play_on") + # Ugh, Blender... + layout.alert = len(modifier.surfaces) == 0 + layout.prop_menu_enum(modifier, "surfaces") + def _draw_fade_ui(modifier, layout, label): layout.label(label) layout.prop(modifier, "fade_type", text="")