LASTEXPRESS: support for delay-activated sounds

Not very obvious, but noticeable e.g. when knocking on harem doors.
I suppose this is the problem that wiki describes
as "improper triggering of actions on sound end".
This commit is contained in:
Evgeny Grechnikov 2018-10-16 23:01:26 +03:00
parent cc5d858169
commit 29f6ce1d9a
5 changed files with 80 additions and 64 deletions

View File

@ -55,16 +55,15 @@ SoundEntry::SoundEntry(LastExpressEngine *engine) : _engine(engine) {
_field_34 = 0;
_field_38 = 0;
_field_3C = 0;
_variant = 0;
_volumeWithoutNIS = 0;
_entity = kEntityPlayer;
_field_48 = 0;
_initTimeMS = 0;
_activateDelayMS = 0;
_priority = 0;
_subtitle = NULL;
_soundStream = NULL;
_queued = false;
}
SoundEntry::~SoundEntry() {
@ -123,27 +122,17 @@ void SoundEntry::play() {
if (!_soundStream)
_soundStream = new StreamedSound();
// Compute current filter id
int32 filterId = _status & kSoundVolumeMask;
// TODO adjust status (based on stepIndex)
_stream->seek(0);
if (_queued) {
_soundStream->setFilterId(filterId);
} else {
_stream->seek(0);
// Load the stream and start playing
_soundStream->load(_stream, filterId);
_queued = true;
}
// Load the stream and start playing
_soundStream->load(_stream, _status & kSoundVolumeMask);
}
bool SoundEntry::isFinished() {
if (!_stream)
return true;
if (!_soundStream || !_queued)
if (!_soundStream)
return false;
// TODO check that all data has been queued
@ -254,8 +243,8 @@ void SoundEntry::update(uint val) {
if (val) {
if (getSoundQueue()->getFlag() & 32) {
_variant = val;
value2 = val * 2 + 1;
_volumeWithoutNIS = val;
value2 = val / 2 + 1;
}
_field_3C = value2;
@ -266,7 +255,7 @@ void SoundEntry::update(uint val) {
}
bool SoundEntry::updateSound() {
assert(_name2.size() <= 16);
assert(_name2.size() < 16);
bool result;
char sub[16];
@ -275,15 +264,16 @@ bool SoundEntry::updateSound() {
result = false;
} else {
if (_status & kSoundFlagDelayedActivate) {
if (_field_48 <= getSound()->getData2()) {
_status |= kSoundFlagPlayRequested;
// counter overflow is processed correctly
if (_engine->_system->getMillis() - _initTimeMS >= _activateDelayMS) {
_status &= ~kSoundFlagDelayedActivate;
strcpy(sub, _name2.c_str());
play();
// FIXME: Rewrite and document expected behavior
int l = strlen(sub) + 1;
if (l - 1 > 4)
sub[l - (1 + 4)] = 0;
// drop .SND extension
strcpy(sub, _name2.c_str());
int l = _name2.size();
if (l > 4)
sub[l - 4] = 0;
showSubtitle(sub);
}
} else {
@ -312,25 +302,33 @@ void SoundEntry::updateEntryFlag(SoundFlag flag) {
else
_status = flag + (_status & ~kSoundVolumeMask);
} else {
_variant = 0;
_volumeWithoutNIS = 0;
_status |= kSoundFlagMuteRequested;
_status &= ~(kSoundFlagVolumeChanging | kSoundVolumeMask);
}
if (_soundStream)
_soundStream->setFilterId(_status & kSoundVolumeMask);
}
void SoundEntry::updateState() {
void SoundEntry::adjustVolumeIfNISPlaying() {
if (getSoundQueue()->getFlag() & 32) {
if (_type != kSoundType9 && _type != kSoundType7 && _type != kSoundType5) {
uint32 variant = _status & kSoundVolumeMask;
uint32 baseVolume = _status & kSoundVolumeMask;
uint32 actualVolume = baseVolume / 2 + 1;
assert((actualVolume & kSoundVolumeMask) == actualVolume);
_volumeWithoutNIS = baseVolume;
_status &= ~kSoundVolumeMask;
_variant = variant;
_status |= variant * 2 + 1;
_status |= actualVolume;
}
}
}
_status |= kSoundFlagPlayRequested;
void SoundEntry::initDelayedActivate(unsigned activateDelay) {
_initTimeMS = _engine->_system->getMillis();
_activateDelayMS = activateDelay * 1000 / 15;
_status |= kSoundFlagDelayedActivate;
}
void SoundEntry::reset() {
@ -375,10 +373,18 @@ void SoundEntry::saveLoadWithSerializer(Common::Serializer &s) {
s.syncAsUint32LE(_field_38); // field_14;
s.syncAsUint32LE(_entity);
uint32 delta = (uint32)_field_48 - getSound()->getData2();
if (delta > 0x8000000u) // sanity check against overflow
delta = 0;
s.syncAsUint32LE(delta);
if (s.isLoading()) {
uint32 delta;
s.syncAsUint32LE(delta);
_initTimeMS = _engine->_system->getMillis();
_activateDelayMS = delta * 1000 / 15;
} else {
uint32 deltaMS = _initTimeMS + _activateDelayMS - _engine->_system->getMillis();
if (deltaMS > 0x8000000u) // sanity check against overflow
deltaMS = 0;
uint32 delta = deltaMS * 15 / 1000;
s.syncAsUint32LE(delta);
}
s.syncAsUint32LE(_priority);

View File

@ -54,7 +54,9 @@
uint32 {4} - ??
uint32 {4} - ??
uint32 {4} - ??
uint32 {4} - ??
uint32 {4} - base volume if NIS is playing
(the actual volume is reduced in half for non-NIS sounds;
this is used to restore the volume after NIS ends)
uint32 {4} - entity
uint32 {4} - ??
uint32 {4} - priority
@ -91,8 +93,10 @@ public:
bool isFinished();
void update(uint val);
bool updateSound();
void updateState();
void adjustVolumeIfNISPlaying();
void updateEntryFlag(SoundFlag flag);
// activateDelay is measured in main ticks, 15Hz timer
void initDelayedActivate(unsigned activateDelay);
// Subtitles
void showSubtitle(Common::String filename);
@ -101,10 +105,9 @@ public:
void saveLoadWithSerializer(Common::Serializer &ser);
// Accessors
void setStatus(uint32 status) { _status = status; }
void addStatusFlag(SoundFlag flag) { _status |= flag; }
void setType(SoundType type) { _type = type; }
void setEntity(EntityIndex entity) { _entity = entity; }
void setField48(int val) { _field_48 = val; }
uint32 getStatus() { return _status; }
SoundType getType() { return _type; }
@ -137,9 +140,13 @@ private:
int _field_34;
int _field_38;
int _field_3C;
int _variant;
int _volumeWithoutNIS;
EntityIndex _entity;
int _field_48;
// The original game uses one variable _activateTime = _initTime + _activateDelay
// and measures everything in sound ticks (30Hz timer).
// We use milliseconds and two variables to deal with possible overflow
// (probably paranoid, but nothing really complicated).
uint32 _initTimeMS, _activateDelayMS;
uint32 _priority;
Common::String _name1; //char[16];
Common::String _name2; //char[16];
@ -147,7 +154,6 @@ private:
SubtitleEntry *_subtitle;
// Sound buffer & stream
bool _queued;
StreamedSound *_soundStream; // the filtered sound stream
void setType(SoundFlag flag);

View File

@ -116,9 +116,6 @@ void SoundQueue::updateQueue() {
it = _soundList.reverse_erase(it);
continue;
}
// Queue the entry data, applying filtering
entry->play();
}
// Original update the current entry, loading another set of samples to be decoded
@ -177,7 +174,7 @@ void SoundQueue::clearQueue() {
//////////////////////////////////////////////////////////////////////////
void SoundQueue::clearStatus() {
for (Common::List<SoundEntry *>::iterator i = _soundList.begin(); i != _soundList.end(); ++i)
(*i)->setStatus((*i)->getStatus() | kSoundFlagCloseRequested);
(*i)->addStatusFlag(kSoundFlagCloseRequested);
}
//////////////////////////////////////////////////////////////////////////

View File

@ -135,7 +135,7 @@ SoundManager::~SoundManager() {
//////////////////////////////////////////////////////////////////////////
// Sound-related functions
//////////////////////////////////////////////////////////////////////////
void SoundManager::playSound(EntityIndex entity, Common::String filename, SoundFlag flag, byte a4) {
void SoundManager::playSound(EntityIndex entity, Common::String filename, SoundFlag flag, byte activateDelay) {
if (_queue->isBuffered(entity) && entity && entity < kEntityTrain)
_queue->removeFromQueue(entity);
@ -145,20 +145,26 @@ void SoundManager::playSound(EntityIndex entity, Common::String filename, SoundF
if (!filename.contains('.'))
filename += ".SND";
if (!playSoundWithSubtitles(filename, currentFlag, entity, a4))
if (!playSoundWithSubtitles(filename, currentFlag, entity, activateDelay))
if (entity)
getSavePoints()->push(kEntityPlayer, entity, kActionEndSound);
}
bool SoundManager::playSoundWithSubtitles(Common::String filename, uint32 flag, EntityIndex entity, byte a4) {
bool SoundManager::playSoundWithSubtitles(Common::String filename, uint32 flag, EntityIndex entity, unsigned activateDelay) {
SoundEntry *entry = new SoundEntry(_engine);
entry->open(filename, (SoundFlag)flag, 30);
entry->setEntity(entity);
if (a4) {
entry->setField48(_data2 + 2 * a4);
entry->setStatus(entry->getStatus() | kSoundFlagDelayedActivate);
// BUG: the original game skips adjustVolumeIfNISPlaying() for delayed-activate sounds.
// (the original code is structured in a slightly different way)
// Not sure whether it can be actually triggered,
// most delayed-activate sounds originate from user actions,
// all user actions are disabled while NIS is playing.
entry->adjustVolumeIfNISPlaying();
if (activateDelay) {
entry->initDelayedActivate(activateDelay);
} else {
// Get subtitles name
uint32 size = filename.size();
@ -166,7 +172,7 @@ bool SoundManager::playSoundWithSubtitles(Common::String filename, uint32 flag,
filename.deleteLastChar();
entry->showSubtitle(filename);
entry->updateState();
entry->play();
}
// Add entry to sound list
@ -175,7 +181,7 @@ bool SoundManager::playSoundWithSubtitles(Common::String filename, uint32 flag,
return (entry->getType() != kSoundTypeNone);
}
void SoundManager::playSoundEvent(EntityIndex entity, byte action, byte a3) {
void SoundManager::playSoundEvent(EntityIndex entity, byte action, byte activateDelay) {
int values[5];
if (getEntityData(entity)->car != getEntityData(kEntityPlayer)->car)
@ -193,14 +199,14 @@ void SoundManager::playSoundEvent(EntityIndex entity, byte action, byte a3) {
if (_param3 > 7) {
_data0 = (uint)_param3;
_data1 = _data2 + 2 * a3;
_data1 = _data2 + 2 * activateDelay;
}
break;
}
case 37:
_data0 = 7;
_data1 = _data2 + 2 * a3;
_data1 = _data2 + 2 * activateDelay;
break;
case 150:
@ -298,7 +304,7 @@ void SoundManager::playSoundEvent(EntityIndex entity, byte action, byte a3) {
}
if (_action && flag)
playSoundWithSubtitles(Common::String::format("LIB%03d.SND", _action), flag, kEntityPlayer, a3);
playSoundWithSubtitles(Common::String::format("LIB%03d.SND", _action), flag, kEntityPlayer, activateDelay);
}
void SoundManager::playSteam(CityIndex index) {

View File

@ -38,9 +38,10 @@ public:
~SoundManager();
// Sound playing
void playSound(EntityIndex entity, Common::String filename, SoundFlag flag = kSoundVolumeEntityDefault, byte a4 = 0);
bool playSoundWithSubtitles(Common::String filename, uint32 flag, EntityIndex entity, byte a4 = 0);
void playSoundEvent(EntityIndex entity, byte action, byte a3 = 0);
// the original game uses byte in playSound but unsigned in playSoundWithSubtitles for activateDelay, no idea why
void playSound(EntityIndex entity, Common::String filename, SoundFlag flag = kSoundVolumeEntityDefault, byte activateDelay = 0);
bool playSoundWithSubtitles(Common::String filename, uint32 flag, EntityIndex entity, unsigned activateDelay = 0);
void playSoundEvent(EntityIndex entity, byte action, byte activateDelay = 0);
void playDialog(EntityIndex entity, EntityIndex entityDialog, SoundFlag flag, byte a4);
void playSteam(CityIndex index);
void playFightSound(byte action, byte a4);