Merge branch 'esm4sound' into 'master'
Some checks failed
Build and test / Ubuntu (push) Has been cancelled
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled

Initial support of ESM4 sounds

See merge request OpenMW/openmw!3784
This commit is contained in:
Petr Mikheev 2025-04-24 12:21:03 +00:00
commit 04d492be4f
11 changed files with 96 additions and 28 deletions

View file

@ -26,6 +26,15 @@ namespace
std::unique_ptr<VFS::Manager> 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

View file

@ -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(); });
}
}

View file

@ -5,7 +5,10 @@
#include <components/debug/debuglog.hpp>
#include <components/esm3/loadsoun.hpp>
#include <components/esm4/loadsndr.hpp>
#include <components/esm4/loadsoun.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/resource/resourcesystem.hpp>
#include <components/settings/values.hpp>
#include <components/vfs/pathutil.hpp>
@ -99,7 +102,12 @@ namespace MWSound
{
if (mBufferNameMap.empty())
{
for (const ESM::Sound& sound : MWBase::Environment::get().getESMStore()->get<ESM::Sound>())
const MWWorld::ESMStore* esmstore = MWBase::Environment::get().getESMStore();
for (const ESM::Sound& sound : esmstore->get<ESM::Sound>())
insertSound(sound.mId, sound);
for (const ESM4::Sound& sound : esmstore->get<ESM4::Sound>())
insertSound(sound.mId, sound);
for (const ESM4::SoundReference& sound : esmstore->get<ESM4::SoundReference>())
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)

View file

@ -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<Sound_Buffer*> 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();
};
}

View file

@ -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<ESM4::Land>, Store<ESM4::LandTexture>, Store<ESM4::LevelledCreature>, Store<ESM4::LevelledItem>,
Store<ESM4::LevelledNpc>, Store<ESM4::Light>, Store<ESM4::MiscItem>, Store<ESM4::MovableStatic>,
Store<ESM4::Npc>, Store<ESM4::Outfit>, Store<ESM4::Potion>, Store<ESM4::Race>, Store<ESM4::Reference>,
Store<ESM4::Static>, Store<ESM4::StaticCollection>, Store<ESM4::Terminal>, Store<ESM4::Tree>,
Store<ESM4::Weapon>, Store<ESM4::World>>;
Store<ESM4::Sound>, Store<ESM4::SoundReference>, Store<ESM4::Static>, Store<ESM4::StaticCollection>,
Store<ESM4::Terminal>, Store<ESM4::Tree>, Store<ESM4::Weapon>, Store<ESM4::World>>;
private:
template <typename T>

View file

@ -1349,6 +1349,8 @@ template class MWWorld::TypedDynamicStore<ESM4::Npc>;
template class MWWorld::TypedDynamicStore<ESM4::Outfit>;
template class MWWorld::TypedDynamicStore<ESM4::Potion>;
template class MWWorld::TypedDynamicStore<ESM4::Race>;
template class MWWorld::TypedDynamicStore<ESM4::Sound>;
template class MWWorld::TypedDynamicStore<ESM4::SoundReference>;
template class MWWorld::TypedDynamicStore<ESM4::Static>;
template class MWWorld::TypedDynamicStore<ESM4::StaticCollection>;
template class MWWorld::TypedDynamicStore<ESM4::Terminal>;

View file

@ -74,6 +74,8 @@
#include <components/esm4/loadrace.hpp>
#include <components/esm4/loadrefr.hpp>
#include <components/esm4/loadscol.hpp>
#include <components/esm4/loadsndr.hpp>
#include <components/esm4/loadsoun.hpp>
#include <components/esm4/loadstat.hpp>
#include <components/esm4/loadterm.hpp>
#include <components/esm4/loadtree.hpp>

View file

@ -33,14 +33,10 @@ bool Misc::ResourceHelpers::changeExtensionToDds(std::string& path)
return changeExtension(path, ".dds");
}
std::string Misc::ResourceHelpers::correctResourcePath(
std::span<const std::string_view> 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<const std::string_view> 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))
{

View file

@ -1,12 +1,12 @@
#ifndef MISC_RESOURCEHELPERS_H
#define MISC_RESOURCEHELPERS_H
#include <components/vfs/pathutil.hpp>
#include <span>
#include <string>
#include <string_view>
#include <components/vfs/pathutil.hpp>
namespace VFS
{
class Manager;
@ -25,8 +25,8 @@ namespace Misc
namespace ResourceHelpers
{
bool changeExtensionToDds(std::string& path);
std::string correctResourcePath(
std::span<const std::string_view> topLevelDirectories, std::string_view resPath, const VFS::Manager* vfs);
std::string correctResourcePath(std::span<const std::string_view> 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);

View file

@ -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

View file

@ -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