From 0ac6be879e7c1f16ee2ba65da0822fc833e033ff Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 2 May 2021 23:24:19 -0400 Subject: [PATCH 1/3] Implement the Random Sound modifier. --- korman/exporter/convert.py | 1 + korman/exporter/physics.py | 8 ++ korman/properties/modifiers/physics.py | 22 +++++ korman/properties/modifiers/sound.py | 124 +++++++++++++++++++++++++ korman/ui/modifiers/physics.py | 1 + korman/ui/modifiers/sound.py | 33 +++++++ 6 files changed, 189 insertions(+) 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/properties/modifiers/physics.py b/korman/properties/modifiers/physics.py index 907875d..38a34f2 100644 --- a/korman/properties/modifiers/physics.py +++ b/korman/properties/modifiers/physics.py @@ -30,6 +30,22 @@ 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", ""), +) + def bounds_type_index(key): return list(zip(*bounds_types))[0].index(key) @@ -68,6 +84,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="") From 1ccdd2e1162f3cfe4c6eeb96b852e7b49a2cdb59 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 5 May 2021 07:12:37 -0400 Subject: [PATCH 2/3] Adapt sound message node for random sound emitters. When an emitter is a random sound emitter, then it is no longer possible to address the individual sounds on that emitter. Instead, you can now only stop or resume the randomization OR set the volume of the active sound. --- korman/nodes/node_messages.py | 65 +++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 10 deletions(-) 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" From 484813955b6b932a2cc5813f2f7f6fc8541eb478 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 30 May 2021 19:21:48 -0400 Subject: [PATCH 3/3] Add the "User" surface types. Because Cyan used these in Relto, and we need them to be available for ZLZ doggone it. --- korman/properties/modifiers/physics.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/korman/properties/modifiers/physics.py b/korman/properties/modifiers/physics.py index 38a34f2..bf4c7ff 100644 --- a/korman/properties/modifiers/physics.py +++ b/korman/properties/modifiers/physics.py @@ -44,6 +44,9 @@ surface_types = ( ("kStone", "Stone", ""), ("kWater", "Water", ""), ("kWood", "Wood", ""), + ("kUser1", "User 1", ""), + ("kUser2", "User 2", ""), + ("kUser3", "User 3", ""), ) def bounds_type_index(key):