Browse Source

Merge pull request #251 from Hoikas/rndsound

Implement the Random Sound modifier.
pull/257/head
Adam Johnson 4 years ago committed by GitHub
parent
commit
d1972b7ec8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      korman/exporter/convert.py
  2. 8
      korman/exporter/physics.py
  3. 45
      korman/nodes/node_messages.py
  4. 25
      korman/properties/modifiers/physics.py
  5. 124
      korman/properties/modifiers/sound.py
  6. 1
      korman/ui/modifiers/physics.py
  7. 33
      korman/ui/modifiers/sound.py

1
korman/exporter/convert.py

@ -376,6 +376,7 @@ class Exporter:
for mod in bl_obj.plasma_modifiers.modifiers: for mod in bl_obj.plasma_modifiers.modifiers:
proc = getattr(mod, "post_export", None) proc = getattr(mod, "post_export", None)
if proc is not 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) proc(self, bl_obj, sceneobject)
inc_progress() inc_progress()

8
korman/exporter/physics.py

@ -184,6 +184,14 @@ class PhysicsConverter:
_set_phys_prop(plSimulationInterface.kCameraAvoidObject, simIface, physical) _set_phys_prop(plSimulationInterface.kCameraAvoidObject, simIface, physical)
if mod.terrain: if mod.terrain:
physical.LOSDBs |= plSimDefs.kLOSDBAvatarWalkable 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: else:
group_name = kwargs.get("member_group") group_name = kwargs.get("member_group")
if group_name: if group_name:

45
korman/nodes/node_messages.py

@ -733,6 +733,7 @@ class PlasmaSoundMsgNode(idprops.IDPropObjectMixin, PlasmaMessageWithCallbacksNo
subtype="PERCENTAGE") subtype="PERCENTAGE")
def convert_callback_message(self, exporter, so, msg, target, wait): 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 = plEventCallbackMsg()
cb.addReceiver(target) cb.addReceiver(target)
cb.event = kEnd cb.event = kEnd
@ -747,6 +748,33 @@ class PlasmaSoundMsgNode(idprops.IDPropObjectMixin, PlasmaMessageWithCallbacksNo
if not soundemit.enabled: if not soundemit.enabled:
self.raise_error("'{}' is not a valid Sound Emitter".format(self.emitter_object.name)) 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 # Always test the specified audible for validity
if self.sound_name and soundemit.sounds.get(self.sound_name, None) is None: 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)) self.raise_error("Invalid Sound '{}' requested from Sound Emitter '{}'".format(self.sound_name, self.emitter_object.name))
@ -787,6 +815,11 @@ class PlasmaSoundMsgNode(idprops.IDPropObjectMixin, PlasmaMessageWithCallbacksNo
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop(self, "emitter_object") layout.prop(self, "emitter_object")
# 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: if self.emitter_object is not None:
soundemit = self.emitter_object.plasma_modifiers.soundemit soundemit = self.emitter_object.plasma_modifiers.soundemit
if soundemit.enabled: if soundemit.enabled:
@ -797,16 +830,28 @@ class PlasmaSoundMsgNode(idprops.IDPropObjectMixin, PlasmaMessageWithCallbacksNo
layout.prop(self, "go_to") layout.prop(self, "go_to")
if self.go_to == "TIME": if self.go_to == "TIME":
layout.prop(self, "time") layout.prop(self, "time")
layout.prop(self, "action") layout.prop(self, "action")
if self.volume == "CUSTOM": if self.volume == "CUSTOM":
layout.prop(self, "volume_pct") layout.prop(self, "volume_pct")
if not random:
layout.prop(self, "looping") layout.prop(self, "looping")
layout.prop(self, "volume") layout.prop(self, "volume")
@property
def has_callbacks(self):
return not self.is_random_sound
@classmethod @classmethod
def _idprop_mapping(cls): def _idprop_mapping(cls):
return {"emitter_object": "object_name"} 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): class PlasmaTimerCallbackMsgNode(PlasmaMessageWithCallbacksNode, bpy.types.Node):
bl_category = "MSG" bl_category = "MSG"

25
korman/properties/modifiers/physics.py

@ -30,6 +30,25 @@ bounds_types = (
("trimesh", "Triangle Mesh", "Use the exact triangle mesh (SLOW!)") ("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): def bounds_type_index(key):
return list(zip(*bounds_types))[0].index(key) return list(zip(*bounds_types))[0].index(key)
@ -68,6 +87,12 @@ class PlasmaCollider(PlasmaModifierProperties):
type=bpy.types.Object, type=bpy.types.Object,
poll=idprops.poll_mesh_objects) 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): def export(self, exporter, bo, so):
# All modifier properties are examined by this little stinker... # All modifier properties are examined by this little stinker...
exporter.physics.generate_physical(bo, so) exporter.physics.generate_physical(bo, so)

124
korman/properties/modifiers/sound.py

@ -22,9 +22,133 @@ from PyHSPlasma import *
from ... import korlib from ... import korlib
from .base import PlasmaModifierProperties from .base import PlasmaModifierProperties
from .physics import surface_types
from ...exporter import ExportError from ...exporter import ExportError
from ... import idprops 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): class PlasmaSfxFade(bpy.types.PropertyGroup):
fade_type = EnumProperty(name="Type", fade_type = EnumProperty(name="Type",
description="Fade Type", description="Fade Type",

1
korman/ui/modifiers/physics.py

@ -15,6 +15,7 @@
def collision(modifier, layout, context): def collision(modifier, layout, context):
layout.prop(modifier, "bounds") layout.prop(modifier, "bounds")
layout.prop(modifier, "surface")
layout.separator() layout.separator()
split = layout.split() split = layout.split()

33
korman/ui/modifiers/sound.py

@ -17,6 +17,39 @@ import bpy
from .. import ui_list 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): def _draw_fade_ui(modifier, layout, label):
layout.label(label) layout.label(label)
layout.prop(modifier, "fade_type", text="") layout.prop(modifier, "fade_type", text="")

Loading…
Cancel
Save