diff --git a/apps/components_tests/misc/test_resourcehelpers.cpp b/apps/components_tests/misc/test_resourcehelpers.cpp index 05079ae875..b21ecb2e14 100644 --- a/apps/components_tests/misc/test_resourcehelpers.cpp +++ b/apps/components_tests/misc/test_resourcehelpers.cpp @@ -26,6 +26,15 @@ namespace std::unique_ptr mVFS = TestingOpenMW::createTestVFS({}); constexpr VFS::Path::NormalizedView path("sound/foo.wav"); EXPECT_EQ(correctSoundPath(path, *mVFS), "sound/foo.mp3"); + + auto correctESM4SoundPath = [](auto path, auto* vfs) { + return Misc::ResourceHelpers::correctResourcePath({ { "sound" } }, path, vfs, ".mp3"); + }; + + EXPECT_EQ(correctESM4SoundPath("foo.WAV", mVFS.get()), "sound\\foo.mp3"); + EXPECT_EQ(correctESM4SoundPath("SOUND/foo.WAV", mVFS.get()), "sound\\foo.mp3"); + EXPECT_EQ(correctESM4SoundPath("DATA\\SOUND\\foo.WAV", mVFS.get()), "sound\\foo.mp3"); + EXPECT_EQ(correctESM4SoundPath("\\Data/Sound\\foo.WAV", mVFS.get()), "sound\\foo.mp3"); } namespace diff --git a/apps/openmw/mwlua/types/door.cpp b/apps/openmw/mwlua/types/door.cpp index 58a53a7124..5db6f9b875 100644 --- a/apps/openmw/mwlua/types/door.cpp +++ b/apps/openmw/mwlua/types/door.cpp @@ -149,5 +149,9 @@ namespace MWLua addModelProperty(record); record["isAutomatic"] = sol::readonly_property( [](const ESM4::Door& rec) -> bool { return rec.mDoorFlags & ESM4::Door::Flag_AutomaticDoor; }); + record["openSound"] = sol::readonly_property( + [](const ESM4::Door& rec) -> std::string { return ESM::RefId(rec.mOpenSound).serializeText(); }); + record["closeSound"] = sol::readonly_property( + [](const ESM4::Door& rec) -> std::string { return ESM::RefId(rec.mCloseSound).serializeText(); }); } } diff --git a/apps/openmw/mwsound/sound_buffer.cpp b/apps/openmw/mwsound/sound_buffer.cpp index f28b268df2..4253c37d97 100644 --- a/apps/openmw/mwsound/sound_buffer.cpp +++ b/apps/openmw/mwsound/sound_buffer.cpp @@ -5,7 +5,10 @@ #include #include +#include +#include #include +#include #include #include @@ -99,7 +102,12 @@ namespace MWSound { if (mBufferNameMap.empty()) { - for (const ESM::Sound& sound : MWBase::Environment::get().getESMStore()->get()) + const MWWorld::ESMStore* esmstore = MWBase::Environment::get().getESMStore(); + for (const ESM::Sound& sound : esmstore->get()) + insertSound(sound.mId, sound); + for (const ESM4::Sound& sound : esmstore->get()) + insertSound(sound.mId, sound); + for (const ESM4::SoundReference& sound : esmstore->get()) insertSound(sound.mId, sound); } @@ -190,6 +198,28 @@ namespace MWSound return &sfx; } + Sound_Buffer* SoundBufferPool::insertSound(const ESM::RefId& soundId, const ESM4::Sound& sound) + { + std::string path = Misc::ResourceHelpers::correctResourcePath( + { { "sound" } }, sound.mSoundFile, MWBase::Environment::get().getResourceSystem()->getVFS(), ".mp3"); + float volume = 1, min = 1, max = 255; // TODO: needs research + Sound_Buffer& sfx = mSoundBuffers.emplace_back(VFS::Path::Normalized(std::move(path)), volume, min, max); + mBufferNameMap.emplace(soundId, &sfx); + return &sfx; + } + + Sound_Buffer* SoundBufferPool::insertSound(const ESM::RefId& soundId, const ESM4::SoundReference& sound) + { + std::string path = Misc::ResourceHelpers::correctResourcePath( + { { "sound" } }, sound.mSoundFile, MWBase::Environment::get().getResourceSystem()->getVFS(), ".mp3"); + float volume = 1, min = 1, max = 255; // TODO: needs research + // TODO: sound.mSoundId can link to another SoundReference, probably we will need to add additional lookups to + // ESMStore. + Sound_Buffer& sfx = mSoundBuffers.emplace_back(VFS::Path::Normalized(std::move(path)), volume, min, max); + mBufferNameMap.emplace(soundId, &sfx); + return &sfx; + } + void SoundBufferPool::unloadUnused() { while (!mUnusedBuffers.empty() && mBufferCacheSize > mBufferCacheMin) diff --git a/apps/openmw/mwsound/sound_buffer.hpp b/apps/openmw/mwsound/sound_buffer.hpp index 7de6dab9ae..93d28ff8ad 100644 --- a/apps/openmw/mwsound/sound_buffer.hpp +++ b/apps/openmw/mwsound/sound_buffer.hpp @@ -14,6 +14,12 @@ namespace ESM struct Sound; } +namespace ESM4 +{ + struct Sound; + struct SoundReference; +} + namespace VFS { class Manager; @@ -111,10 +117,12 @@ namespace MWSound // NOTE: unused buffers are stored in front-newest order. std::deque mUnusedBuffers; - inline Sound_Buffer* insertSound(const ESM::RefId& soundId, const ESM::Sound& sound); - inline Sound_Buffer* insertSound(std::string_view fileName); + Sound_Buffer* insertSound(const ESM::RefId& soundId, const ESM::Sound& sound); + Sound_Buffer* insertSound(const ESM::RefId& soundId, const ESM4::Sound& sound); + Sound_Buffer* insertSound(const ESM::RefId& soundId, const ESM4::SoundReference& sound); + Sound_Buffer* insertSound(std::string_view fileName); - inline void unloadUnused(); + void unloadUnused(); }; } diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp index 6c71ae0052..0c37f243e8 100644 --- a/apps/openmw/mwworld/esmstore.hpp +++ b/apps/openmw/mwworld/esmstore.hpp @@ -105,6 +105,8 @@ namespace ESM4 struct Potion; struct Race; struct Reference; + struct Sound; + struct SoundReference; struct Static; struct StaticCollection; struct Terminal; @@ -146,8 +148,8 @@ namespace MWWorld Store, Store, Store, Store, Store, Store, Store, Store, Store, Store, Store, Store, Store, - Store, Store, Store, Store, - Store, Store>; + Store, Store, Store, Store, + Store, Store, Store, Store>; private: template diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp index b0684b1ab4..80bcdb056a 100644 --- a/apps/openmw/mwworld/store.cpp +++ b/apps/openmw/mwworld/store.cpp @@ -1349,6 +1349,8 @@ template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; +template class MWWorld::TypedDynamicStore; +template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; diff --git a/components/esm/records.hpp b/components/esm/records.hpp index 0b60b44cf0..0b76fab0ff 100644 --- a/components/esm/records.hpp +++ b/components/esm/records.hpp @@ -74,6 +74,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/components/misc/resourcehelpers.cpp b/components/misc/resourcehelpers.cpp index 5279e2ad23..c3164b0dfe 100644 --- a/components/misc/resourcehelpers.cpp +++ b/components/misc/resourcehelpers.cpp @@ -33,14 +33,10 @@ bool Misc::ResourceHelpers::changeExtensionToDds(std::string& path) return changeExtension(path, ".dds"); } -std::string Misc::ResourceHelpers::correctResourcePath( - std::span topLevelDirectories, std::string_view resPath, const VFS::Manager* vfs) +// If `ext` is not empty we first search file with extension `ext`, then if not found fallback to original extension. +std::string Misc::ResourceHelpers::correctResourcePath(std::span topLevelDirectories, + std::string_view resPath, const VFS::Manager* vfs, std::string_view ext) { - /* Bethesda at some point converted all their BSA - * textures from tga to dds for increased load speed, but all - * texture file name references were kept as .tga. - */ - std::string correctedPath = Misc::StringUtils::lowerCase(resPath); // Flatten slashes @@ -80,14 +76,14 @@ std::string Misc::ResourceHelpers::correctResourcePath( std::string origExt = correctedPath; - // since we know all (GOTY edition or less) textures end - // in .dds, we change the extension - bool changedToDds = changeExtensionToDds(correctedPath); + // replace extension if `ext` is specified (used for .tga -> .dds, .wav -> .mp3) + bool isExtChanged = !ext.empty() && changeExtension(correctedPath, ext); + if (vfs->exists(correctedPath)) return correctedPath; - // if it turns out that the above wasn't true in all cases (not for vanilla, but maybe mods) - // verify, and revert if false (this call succeeds quickly, but fails slowly) - if (changedToDds && vfs->exists(origExt)) + + // fall back to original extension + if (isExtChanged && vfs->exists(origExt)) return origExt; // fall back to a resource in the top level directory if it exists @@ -98,7 +94,7 @@ std::string Misc::ResourceHelpers::correctResourcePath( if (vfs->exists(fallback)) return fallback; - if (changedToDds) + if (isExtChanged) { fallback = topLevelDirectories.front(); fallback += '\\'; @@ -110,19 +106,23 @@ std::string Misc::ResourceHelpers::correctResourcePath( return correctedPath; } +// Note: Bethesda at some point converted all their BSA textures from tga to dds for increased load speed, +// but all texture file name references were kept as .tga. So we pass ext=".dds" to all helpers +// looking for textures. + std::string Misc::ResourceHelpers::correctTexturePath(std::string_view resPath, const VFS::Manager* vfs) { - return correctResourcePath({ { "textures", "bookart" } }, resPath, vfs); + return correctResourcePath({ { "textures", "bookart" } }, resPath, vfs, ".dds"); } std::string Misc::ResourceHelpers::correctIconPath(std::string_view resPath, const VFS::Manager* vfs) { - return correctResourcePath({ { "icons" } }, resPath, vfs); + return correctResourcePath({ { "icons" } }, resPath, vfs, ".dds"); } std::string Misc::ResourceHelpers::correctBookartPath(std::string_view resPath, const VFS::Manager* vfs) { - return correctResourcePath({ { "bookart", "textures" } }, resPath, vfs); + return correctResourcePath({ { "bookart", "textures" } }, resPath, vfs, ".dds"); } std::string Misc::ResourceHelpers::correctBookartPath( @@ -199,6 +199,12 @@ std::string_view Misc::ResourceHelpers::meshPathForESM3(std::string_view resPath VFS::Path::Normalized Misc::ResourceHelpers::correctSoundPath( VFS::Path::NormalizedView resPath, const VFS::Manager& vfs) { + // Note: likely should be replaced with + // return correctResourcePath({ { "sound" } }, resPath, vfs, ".mp3"); + // but there is a slight difference in behaviour: + // - `correctResourcePath(..., ".mp3")` first checks `.mp3`, then tries the original extension + // - the implementation below first tries the original extension, then falls back to `.mp3`. + // Workaround: Bethesda at some point converted some of the files to mp3, but the references were kept as .wav. if (!vfs.exists(resPath)) { diff --git a/components/misc/resourcehelpers.hpp b/components/misc/resourcehelpers.hpp index 9aaa89a861..c9e65d0295 100644 --- a/components/misc/resourcehelpers.hpp +++ b/components/misc/resourcehelpers.hpp @@ -1,12 +1,12 @@ #ifndef MISC_RESOURCEHELPERS_H #define MISC_RESOURCEHELPERS_H -#include - #include #include #include +#include + namespace VFS { class Manager; @@ -25,8 +25,8 @@ namespace Misc namespace ResourceHelpers { bool changeExtensionToDds(std::string& path); - std::string correctResourcePath( - std::span topLevelDirectories, std::string_view resPath, const VFS::Manager* vfs); + std::string correctResourcePath(std::span topLevelDirectories, std::string_view resPath, + const VFS::Manager* vfs, std::string_view ext = ""); std::string correctTexturePath(std::string_view resPath, const VFS::Manager* vfs); std::string correctIconPath(std::string_view resPath, const VFS::Manager* vfs); std::string correctBookartPath(std::string_view resPath, const VFS::Manager* vfs); @@ -49,6 +49,7 @@ namespace Misc std::string_view meshPathForESM3(std::string_view resPath); VFS::Path::Normalized correctSoundPath(VFS::Path::NormalizedView resPath, const VFS::Manager& vfs); + std::string correctESM4SoundPath(std::string_view resPath, const VFS::Manager* vfs); /// marker objects that have a hardcoded function in the game logic, should be hidden from the player bool isHiddenMarker(const ESM::RefId& id); diff --git a/files/data/scripts/omw/activationhandlers.lua b/files/data/scripts/omw/activationhandlers.lua index a70696824f..aed68f9543 100644 --- a/files/data/scripts/omw/activationhandlers.lua +++ b/files/data/scripts/omw/activationhandlers.lua @@ -1,4 +1,5 @@ local async = require('openmw.async') +local core = require('openmw.core') local types = require('openmw.types') local world = require('openmw.world') @@ -6,8 +7,9 @@ local EnableObject = async:registerTimerCallback('EnableObject', function(obj) o local function ESM4DoorActivation(door, actor) -- TODO: Implement lockpicking minigame - -- TODO: Play door opening animation and sound + -- TODO: Play door opening animation local Door4 = types.ESM4Door + core.sound.playSound3d(Door4.record(door).openSound, actor) if Door4.isTeleport(door) then actor:teleport(Door4.destCell(door), Door4.destPosition(door), Door4.destRotation(door)) else diff --git a/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index 0012a51c41..68c041d583 100644 --- a/files/lua_api/openmw/types.lua +++ b/files/lua_api/openmw/types.lua @@ -2449,5 +2449,7 @@ -- @field #string id Record id -- @field #string name Human-readable name -- @field #string model VFS path to the model +-- @field #string openSound FormId of the door opening sound +-- @field #string closeSound FormId of the door closing sound return nil