scummvm/engines/icb/sound_logic.cpp
2022-07-30 15:14:30 +02:00

495 lines
16 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* Additional copyright for this file:
* Copyright (C) 1999-2000 Revolution Software Ltd.
* This code is based on source code created by Revolution Software,
* used with permission.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "engines/icb/sound_logic.h"
#include "engines/icb/global_objects.h"
#include "engines/icb/sound.h"
#include "engines/icb/floors.h"
#include "common/util.h"
namespace ICB {
void _sound_logic::Initialise() {
uint32 i;
m_nNumSubscribers = 0;
m_nSFXSubtitleTimer = 0;
m_pcSFXSubtitleText = nullptr;
m_nNumLinkedFloors = 0;
for (i = 0; i < SL_MAX_CONCURRENT_SOUNDS; ++i) {
m_pPositions[i].nSoundHash = NULL_HASH;
m_pPositions[i].nTimer = 0xffffffff;
}
}
void _sound_logic::DrawSubtitle() const {
// Safety net check to make sure the string has something in it.
if (strlen(m_pcSFXSubtitleText) > 0) {
// Yes we need to display speech text.
SetTextColour(SL_SUBTITLE_R, SL_SUBTITLE_G, SL_SUBTITLE_B);
MS->Create_remora_text(SL_SFX_SUBTITLE_X, SL_SFX_SUBTITLE_Y, m_pcSFXSubtitleText, 0, PIN_AT_CENTRE, 0, 0, SL_SUBTITLE_WIDTH);
MS->Render_speech(MS->text_bloc);
MS->Kill_remora_text();
}
}
void _sound_logic::SetSuspendedFlag(uint32 nID, bool8 bSuspend) {
uint32 nIndex;
// Look for the mega in the list of subscribers.
nIndex = FindMegaInList(nID);
// Running off the list is not an error, since this is called from the main event system, which has
// no knowledge of whether a mega has registered for a sound.
if (nIndex < m_nNumSubscribers)
m_pSubscribers[nIndex].SetSuspendedFlag(bSuspend);
}
void _sound_logic::ClearHeardFlag(uint32 nID) {
uint32 nIndex;
// Look for the mega in the list of subscribers.
nIndex = FindMegaInList(nID);
// Running off the list is not an error, since this is called from the main event system, which has
// no knowledge of whether a mega has registered for a sound.
if (nIndex < m_nNumSubscribers)
m_pSubscribers[nIndex].ClearHeardFlag();
}
void _sound_logic::Cycle() {
uint32 i;
// If a subtitle is being displayed, work the timer for it.
if (m_nSFXSubtitleTimer >= 1)
--m_nSFXSubtitleTimer;
// Update the age timers for the sounds we are keeping the positions of. These will wrap after about 10
// years, but Windows itself can't stand up that int32, so we won't worry about it.
for (i = 0; i < SL_MAX_CONCURRENT_SOUNDS; ++i)
m_pPositions[i].nTimer++;
}
void _sound_logic::AddSubscription(uint32 nID, const char *pcSoundID) {
uint32 nIndex;
bool8 bSuccess;
// Look for the mega already in the list.
nIndex = FindMegaInList(nID);
// If we ran off the list, we are extending the size of the list.
if (nIndex == m_nNumSubscribers) {
// Increase list size.
m_pSubscribers[nIndex].Initialise(nID);
++m_nNumSubscribers;
}
// Safe to add the subscription.
bSuccess = m_pSubscribers[nIndex].AddSoundRegistration(pcSoundID);
// Error if the list was already full.
if (!bSuccess)
Fatal_error("Unable to register mega %d to listen for [%s] - mega already registered for maximum %d sounds", nID, pcSoundID, SL_MAX_SOUND_REGISTRATIONS);
}
void _sound_logic::RemoveSubscription(uint32 nID, const char *pcSoundID) {
uint32 nIndex;
// Look for the mega in the list of subscribers.
nIndex = FindMegaInList(nID);
// If we ran off that list, the mega is not currently subscribed to be listening for anything, which
// makes it an error to be trying to unsubscribe them to listen for a sound.
if (nIndex == m_nNumSubscribers)
Fatal_error("Unable to unsubscribe object %d for sound [%s] because object is not listed in the sound logic engine", nID, pcSoundID);
// We found the object okay, so unsubscribe them for this sound.
m_pSubscribers[nIndex].RemoveSoundRegistration(pcSoundID);
}
void _sound_logic::RemoveAllSubscriptions(uint32 nID) {
uint32 nIndex;
// Look for the mega in the list of subscribers.
nIndex = FindMegaInList(nID);
// If we ran off that list, the mega is not currently subscribed to be listening for anything, which
// makes it an error to be trying to unsubscribe them to listen for a sound.
if (nIndex == m_nNumSubscribers)
Fatal_error("Unable to unsubscribe object %d for all sounds because it is not listed in the sound logic engine", nID);
// We found the object okay, so unsubscribe them for this sound.
m_pSubscribers[nIndex].Initialise(nID, FALSE8);
}
void _sound_logic::SetHearingSensitivity(uint32 nID, uint32 nSensitivity) {
uint32 nIndex;
if (nSensitivity > SL_MAX_HEARING_SENSITIVITY)
Fatal_error("Attempt to set mega hearing sensitivity at %d out of range %d-%d.", nSensitivity, SL_MIN_HEARING_SENSITIVITY, SL_MAX_HEARING_SENSITIVITY);
// Look for the mega in the list of subscribers.
nIndex = FindMegaInList(nID);
// If we ran off the list, we are extending the size of the list.
if (nIndex == m_nNumSubscribers) {
// Increase list size.
m_pSubscribers[nIndex].Initialise(nID);
++m_nNumSubscribers;
}
// Safe to set hearing sensitivity.
m_pSubscribers[nIndex].SetHearingSensitivity((uint8)nSensitivity);
}
bool8 _sound_logic::MegaHeardSomething(uint32 nID) {
uint32 nIndex;
// Look for the mega in the list of subscribers.
nIndex = FindMegaInList(nID);
// If the mega is in the list, return the flag for sound events, otherwise just return false.
if (nIndex < m_nNumSubscribers)
return (m_pSubscribers[nIndex].HeardSound());
else
return (FALSE8);
}
bool8 _sound_logic::MegaHeardThis(uint32 nID, const char *pcSoundID) {
uint32 nIndex;
// Look for the mega in the list of subscribers.
nIndex = FindMegaInList(nID);
if (nIndex < m_nNumSubscribers)
return (m_pSubscribers[nIndex].HeardThis(pcSoundID));
else
return (FALSE8);
}
void _sound_logic::NewSound(uint32 nObjectID, int32 nX, int32 nY, int32 nZ, const CSfx *pSFX, uint32 nSoundHash) {
uint32 i;
uint32 nMegaID;
uint32 nVolume;
uint32 nOldest, nPosition;
// First we jot down the location of the sound in case script asks for it. First, look for the
// sound already being in the list.
nPosition = 0;
while ((nPosition < SL_MAX_CONCURRENT_SOUNDS) && (m_pPositions[nPosition].nSoundHash != nSoundHash))
++nPosition;
// If it wasn't in the list, look for the oldest entry to age out.
if (nPosition == SL_MAX_CONCURRENT_SOUNDS) {
i = 0;
nOldest = 0;
nPosition = 0;
while ((i < SL_MAX_CONCURRENT_SOUNDS)) {
if (m_pPositions[i].nTimer > nOldest) {
nOldest = m_pPositions[i].nTimer;
nPosition = i;
}
++i;
}
}
// Don't put up a new SFX subtitle if one is being displayed already.
if (m_nSFXSubtitleTimer == 0) {
// Here, we put up a SFX subtitle if one is listed for the sound.
m_pcSFXSubtitleText = (const char *)LinkedDataObject::Try_fetch_item_by_hash(global_text, nSoundHash);
if (m_pcSFXSubtitleText)
m_nSFXSubtitleTimer = Get_reading_time(m_pcSFXSubtitleText);
else
m_nSFXSubtitleTimer = 0;
}
// We now have the position of where to write the entry.
m_pPositions[nPosition].nTimer = 0;
m_pPositions[nPosition].nX = nX;
m_pPositions[nPosition].nZ = nZ;
m_pPositions[nPosition].nSoundHash = nSoundHash; // Do this every time 'cos it's faster than checking to see if it needs doing.
// Loop for each mega we are running sound logic for.
for (i = 0; i < m_nNumSubscribers; ++i) {
// Get the mega's ID and logic structure.
nMegaID = m_pSubscribers[i].GetMegaID();
// Megas don't need to post sound events to themselves.
if (nMegaID != nObjectID) {
// Work out the volume at the mega character's position.
nVolume = CalculateEffectiveVolume(LOGIC_VOLUME, nMegaID, nX, nY, nZ, pSFX);
// Mega can't possibly hear sound if volume is zero.
if (nVolume > 0) {
// Supply the volume to the mega's entry so it can calculate whether or not the mega
// actually hears the sound (depends on hearing sensitivity).
m_pSubscribers[i].SoundReachedMega(nSoundHash, nVolume);
}
}
}
}
int32 _sound_logic::GetSoundX(uint32 nSoundHash) const {
uint32 nPosition;
// Look for the sound in the list.
nPosition = 0;
while ((nPosition < SL_MAX_CONCURRENT_SOUNDS) && (m_pPositions[nPosition].nSoundHash != nSoundHash))
++nPosition;
// Return a very big number if it wasn't found, otherwise return the real number.
if (nPosition == SL_MAX_CONCURRENT_SOUNDS)
return (SL_UNDEFINED_COORDINATE);
else
return (m_pPositions[nPosition].nX);
}
int32 _sound_logic::GetSoundZ(uint32 nSoundHash) const {
uint32 nPosition;
// Look for the sound in the list.
nPosition = 0;
while ((nPosition < SL_MAX_CONCURRENT_SOUNDS) && (m_pPositions[nPosition].nSoundHash != nSoundHash))
++nPosition;
// Return a very big number if it wasn't found, otherwise return the real number.
if (nPosition == SL_MAX_CONCURRENT_SOUNDS)
return (SL_UNDEFINED_COORDINATE);
else
return (m_pPositions[nPosition].nZ);
}
bool8 _sound_logic::SoundEventPendingForID(uint32 nID) {
uint32 i;
// Loop for each mega we are running sound logic for.
for (i = 0; i < m_nNumSubscribers; ++i) {
if (m_pSubscribers[i].GetMegaID() == nID)
return (m_pSubscribers[i].HeardSound());
}
// If we fell off the list, we are not currently running event processing for the object, so
/// just return false.
return (FALSE8);
}
void _sound_logic::LinkFloorsForSoundEvents(const char *pcFloor1, const char *pcFloor2) {
uint32 nFloor1Index, nFloor2Index;
// Check we have space to make a new link.
if (m_nNumLinkedFloors == SL_MAX_FLOOR_LINKS) {
return;
}
// Look for first floor.
nFloor1Index = MS->floor_def->Fetch_floor_number_by_name(pcFloor1);
if (nFloor1Index == PX_LINKED_DATA_FILE_ERROR) {
return;
}
// Look for second floor.
nFloor2Index = MS->floor_def->Fetch_floor_number_by_name(pcFloor2);
if (nFloor2Index == PX_LINKED_DATA_FILE_ERROR) {
return;
}
// Both floors found so link them.
m_pnLinkedFloors[m_nNumLinkedFloors][0] = nFloor1Index;
m_pnLinkedFloors[m_nNumLinkedFloors][1] = nFloor2Index;
// Keep track of how many we've linked.
++m_nNumLinkedFloors;
}
uint32 _sound_logic::FindMegaInList(uint32 nID) const {
uint32 i = 0;
while ((i < m_nNumSubscribers) && (m_pSubscribers[i].GetMegaID() != nID))
++i;
return (i);
}
uint8 _sound_logic::CalculateEffectiveVolume(SoundVolumeMode eMode, uint32 nMegaID, int32 nSoundX, int32 nSoundY, int32 nSoundZ, const CSfx *pSFX) const {
int32 nRawVolume;
int32 nMegaX, nMegaY, nMegaZ;
int32 nDeltaX, nDeltaY, nDeltaZ;
int32 nSqrMicDist, nSqrMinDist, nSqrMaxDist;
int32 nMaxDelta;
_logic *pMega;
// If the Remora is making the sound then it is automatically full volume.
if (nMegaID == SPECIAL_SOUND)
return (SL_MAX_VOLUME);
// If we are working out the actual volume to be used by the speakers, the sound gets cut dead
// unless it is on the same or a linked floor.
if ((eMode == ACTUAL_VOLUME) && !SoundAndEarOnSameOrLinkedFloors(nMegaID, (PXreal)nSoundX, (PXreal)nSoundY, (PXreal)nSoundZ)) {
return (0);
}
// We will work out a volume using the same function that the SFX themselves
// started out using. This is: if microphone nearer than MIN distance in SFX
// then volume is 127 if microphone further than MAX distance in SFX then volume
// is 0 else volume = ( ( MICROPHONE_DIST - MIN ) / ( MAX - MIN ) ) * 127
// First get the position of the mega hearing the sound.
pMega = MS->logic_structs[nMegaID];
// If the object is not a mega, something has broken.
if (pMega->image_type != VOXEL)
Fatal_error("Non-mega [%s] is subscribed to sound logic engine - only works for megas", pMega->GetName());
// Get the coordinates of the mega.
nMegaX = (int32)pMega->mega->actor_xyz.x;
nMegaZ = (int32)pMega->mega->actor_xyz.z;
nMegaY = (int32)pMega->mega->actor_xyz.y;
// Work out some terms.
nDeltaX = abs(nMegaX - nSoundX);
nDeltaY = abs(nMegaY - nSoundY);
nDeltaZ = abs(nMegaZ - nSoundZ);
// To approximate, we take just the largest term.
nMaxDelta = MAX(nDeltaX, MAX(nDeltaY, nDeltaZ));
// We deal with squared distances to avoid square root.
nSqrMicDist = (nMaxDelta * nMaxDelta);
// For debugging, the min and max values for the sound are simply bunged in at default values.
if (pSFX) {
nSqrMinDist = pSFX->m_min_distance * pSFX->m_min_distance;
nSqrMaxDist = pSFX->m_max_distance * pSFX->m_max_distance;
} else {
nSqrMinDist = SL_MIN_SOUND * SL_MIN_SOUND;
nSqrMaxDist = SL_MAX_SOUND * SL_MAX_SOUND;
}
// Now we have the terms to work out a first go at the volume.
if (nSqrMicDist <= nSqrMinDist) {
nRawVolume = SL_MAX_VOLUME;
} else if (nSqrMicDist >= nSqrMaxDist) {
nRawVolume = 0;
} else {
// Divide by 16 to keep within integer limits i.e. accurate to +/- ~16cm
nSqrMicDist = (nSqrMicDist >> 4);
nSqrMaxDist = (nSqrMaxDist >> 4);
nSqrMinDist = (nSqrMinDist >> 4);
nRawVolume = (int32)(((nSqrMaxDist - nSqrMicDist) * SL_MAX_VOLUME) / (nSqrMaxDist - nSqrMinDist));
}
// If we are working out the actual volume for the speakers then we already know that the sound
// and the player are on the same or a linked floor, so we simply return the volume we have calculated.
if (eMode == ACTUAL_VOLUME) {
return ((uint8)nRawVolume);
}
// If we are working out a volume for use by the event system and the sound and ear are not on the
// same or linked floor rectangles, we chop the sound by a factor.
if (!SoundAndEarOnSameOrLinkedFloors(nMegaID, (PXreal)nSoundX, (PXreal)nSoundY, (PXreal)nSoundZ)) {
// The sound and the ear are on different floors and the floors are not linked so we
// chop the sound by a factor.
nRawVolume /= SL_FLOOR_RECT_DROP_FACTOR;
}
// Return the volume.
return ((uint8)nRawVolume);
}
bool8 _sound_logic::SoundAndEarOnSameOrLinkedFloors(uint32 nEarID, PXreal fSoundX, PXreal fSoundY, PXreal fSoundZ) const {
uint32 i;
uint32 nEarRect, nEarCamera;
PXreal fGravitisedY;
uint32 nSoundRect;
uint32 nNumLinkedCameras;
// Get the floor and camera for the player.
nEarRect = MS->logic_structs[nEarID]->owner_floor_rect;
nEarCamera = MS->floor_to_camera_index[nEarRect];
// Now we need to place the origin of the sound on a floor rectangle.
fGravitisedY = MS->floor_def->Floor_safe_gravitise_y(fSoundY);
// First look for the sound being on the same floor as the player, adding a bounding box to the player's
// floor rectangle so that doors always make a sound no matter which floor the marker is on.
if (MS->floor_def->Point_on_rubber_floor(fSoundX, fSoundZ, fGravitisedY, SL_SAME_FLOOR_BOUNDARY, nEarRect))
return (TRUE8);
// Get the floor the sound is on.
nSoundRect = MS->floor_def->Return_floor_rect(fSoundX, fSoundZ, fGravitisedY, 0);
// If we failed to find a floor rectangle for the sound, then something has gone wrong.
if (nSoundRect == PXNULL)
return (FALSE8);
// See if the sound and the player share the same camera.
if (MS->floor_to_camera_index[nSoundRect] == nEarCamera)
return (TRUE8);
// See if the sound and ear are on floors linked for sound events.
if (FloorsLinkedForSounds(nSoundRect, nEarRect))
return (TRUE8);
// Check if they are on linked floors.
nNumLinkedCameras = MS->cam_floor_list[nEarCamera].num_extra_floors;
for (i = 0; i < nNumLinkedCameras; ++i) {
// yes, they are on different floors but the floors are linked.
if (MS->cam_floor_list[nEarCamera].extra_floors[i] == nSoundRect)
return (TRUE8);
}
// The sound and the ear do not share a camera and are not linked.
return (FALSE8);
}
bool8 _sound_logic::FloorsLinkedForSounds(uint32 nFloor1, uint32 nFloor2) const {
uint32 i;
// Loop through listed links.
for (i = 0; i < m_nNumLinkedFloors; ++i) {
// Need to check both ways round.
if (((m_pnLinkedFloors[i][0] == nFloor1) && (m_pnLinkedFloors[i][1] == nFloor2)) || ((m_pnLinkedFloors[i][1] == nFloor1) && (m_pnLinkedFloors[i][0] == nFloor2))) {
return (TRUE8);
}
}
// Didn't find a link.
return (FALSE8);
}
} // End of namespace ICB