diff --git a/CHANGELOG.md b/CHANGELOG.md index fb0cb5d54e..83892500dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,7 @@ Feature #3537: Shader-based water ripples Feature #5492: Let rain and snow collide with statics Feature #6149: Dehardcode Lua API_REVISION + Feature #6152: Playing music via lua scripts Feature #6447: Add LOD support to Object Paging Feature #6491: Add support for Qt6 Feature #6556: Lua API for sounds @@ -99,6 +100,7 @@ Feature #7477: NegativeLight Magic Effect flag Feature #7499: OpenMW-CS: Generate record filters by drag & dropping cell content to the filters field Feature #7546: Start the game on Fredas + Feature #7568: Uninterruptable scripted music Task #5896: Do not use deprecated MyGUI properties Task #7113: Move from std::atoi to std::from_char Task #7117: Replace boost::scoped_array with std::vector diff --git a/CMakeLists.txt b/CMakeLists.txt index 380903f25c..d3a16c285f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,7 +71,7 @@ message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) set(OPENMW_VERSION_MINOR 49) set(OPENMW_VERSION_RELEASE 0) -set(OPENMW_LUA_API_REVISION 47) +set(OPENMW_LUA_API_REVISION 48) set(OPENMW_VERSION_COMMITHASH "") set(OPENMW_VERSION_TAGHASH "") diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 2966800014..df89cd3eb6 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -913,7 +913,13 @@ void OMW::Engine::go() { // start in main menu mWindowManager->pushGuiMode(MWGui::GM_MainMenu); - mSoundManager->playPlaylist("Title"); + + std::string titlefile = "music/special/morrowind title.mp3"; + if (mVFS->exists(titlefile)) + mSoundManager->streamMusic(titlefile, MWSound::MusicType::Special); + else + Log(Debug::Warning) << "Title music not found"; + std::string_view logo = Fallback::Map::getString("Movies_Morrowind_Logo"); if (!logo.empty()) mWindowManager->playVideo(logo, /*allowSkipping*/ true, /*overrideSounds*/ false); diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index 5319ab6dde..b8e0fd1bde 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -26,6 +26,11 @@ namespace ESM class ESMWriter; } +namespace MWSound +{ + enum class MusicType; +} + namespace MWWorld { class Ptr; @@ -282,6 +287,9 @@ namespace MWBase virtual float getAngleToPlayer(const MWWorld::Ptr& ptr) const = 0; virtual MWMechanics::GreetingState getGreetingState(const MWWorld::Ptr& ptr) const = 0; virtual bool isTurningToPlayer(const MWWorld::Ptr& ptr) const = 0; + + virtual MWSound::MusicType getMusicType() const = 0; + virtual void setMusicType(MWSound::MusicType type) = 0; }; } diff --git a/apps/openmw/mwbase/soundmanager.hpp b/apps/openmw/mwbase/soundmanager.hpp index 3dd9cd3b33..1f0337869b 100644 --- a/apps/openmw/mwbase/soundmanager.hpp +++ b/apps/openmw/mwbase/soundmanager.hpp @@ -29,6 +29,14 @@ namespace MWSound MaxCount }; + enum class MusicType + { + Special, + Explore, + Battle, + Scripted + }; + class Sound; class Stream; struct Sound_Decoder; @@ -101,12 +109,17 @@ namespace MWBase virtual void processChangedSettings(const std::set>& settings) = 0; + virtual bool isEnabled() const = 0; + ///< Returns true if sound system is enabled + virtual void stopMusic() = 0; ///< Stops music if it's playing - virtual void streamMusic(const std::string& filename) = 0; + virtual void streamMusic(const std::string& filename, MWSound::MusicType type, float fade = 1.f) = 0; ///< Play a soundifle - /// \param filename name of a sound file in "Music/" in the data directory. + /// \param filename name of a sound file in the data directory. + /// \param type music type. + /// \param fade time in seconds to fade out current track before start this one. virtual bool isMusicPlaying() = 0; ///< Returns true if music is playing diff --git a/apps/openmw/mwgui/levelupdialog.cpp b/apps/openmw/mwgui/levelupdialog.cpp index b13fdbeeb9..41b2dadeb9 100644 --- a/apps/openmw/mwgui/levelupdialog.cpp +++ b/apps/openmw/mwgui/levelupdialog.cpp @@ -214,7 +214,8 @@ namespace MWGui center(); // Play LevelUp Music - MWBase::Environment::get().getSoundManager()->streamMusic("Special/MW_Triumph.mp3"); + MWBase::Environment::get().getSoundManager()->streamMusic( + "Music/Special/MW_Triumph.mp3", MWSound::MusicType::Special); } void LevelupDialog::onOkButtonClicked(MyGUI::Widget* sender) diff --git a/apps/openmw/mwlua/soundbindings.cpp b/apps/openmw/mwlua/soundbindings.cpp index b5dae8a7a8..dc45a672b4 100644 --- a/apps/openmw/mwlua/soundbindings.cpp +++ b/apps/openmw/mwlua/soundbindings.cpp @@ -95,6 +95,15 @@ namespace MWLua return MWBase::Environment::get().getSoundManager()->getSoundPlaying(MWWorld::Ptr(), fileName); }; + api["streamMusic"] = [](std::string_view fileName) { + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + sndMgr->streamMusic(std::string(fileName), MWSound::MusicType::Scripted); + }; + + api["isMusicPlaying"] = []() { return MWBase::Environment::get().getSoundManager()->isMusicPlaying(); }; + + api["stopMusic"] = []() { MWBase::Environment::get().getSoundManager()->stopMusic(); }; + return LuaUtil::makeReadOnly(api); } @@ -103,6 +112,8 @@ namespace MWLua sol::state_view& lua = context.mLua->sol(); sol::table api(lua, sol::create); + api["isEnabled"] = []() { return MWBase::Environment::get().getSoundManager()->isEnabled(); }; + api["playSound3d"] = [](std::string_view soundId, const Object& object, const sol::optional& options) { auto args = getPlaySoundArgs(options); diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index ec53bdec71..743c5d5ab5 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -1285,7 +1285,7 @@ namespace MWMechanics } } - void Actors::updateCombatMusic() + bool Actors::playerHasHostiles() const { const MWWorld::Ptr player = getPlayer(); const osg::Vec3f playerPos = player.getRefData().getPosition().asVec3(); @@ -1315,19 +1315,7 @@ namespace MWMechanics } } - // check if we still have any player enemies to switch music - if (mCurrentMusic != MusicType::Explore && !hasHostiles - && !(player.getClass().getCreatureStats(player).isDead() - && MWBase::Environment::get().getSoundManager()->isMusicPlaying())) - { - MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Explore")); - mCurrentMusic = MusicType::Explore; - } - else if (mCurrentMusic != MusicType::Battle && hasHostiles) - { - MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Battle")); - mCurrentMusic = MusicType::Battle; - } + return hasHostiles; } void Actors::predictAndAvoidCollisions(float duration) const @@ -1735,8 +1723,6 @@ namespace MWMechanics killDeadActors(); updateSneaking(playerCharacter, duration); } - - updateCombatMusic(); } void Actors::notifyDied(const MWWorld::Ptr& actor) @@ -1806,7 +1792,8 @@ namespace MWMechanics // player's death animation is over MWBase::Environment::get().getStateManager()->askLoadRecent(); // Play Death Music if it was the player dying - MWBase::Environment::get().getSoundManager()->streamMusic("Special/MW_Death.mp3"); + MWBase::Environment::get().getSoundManager()->streamMusic( + "Music/Special/MW_Death.mp3", MWSound::MusicType::Special); } else { diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 1c5799159e..98c64397ab 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -74,9 +74,6 @@ namespace MWMechanics void dropActors(const MWWorld::CellStore* cellStore, const MWWorld::Ptr& ignore); ///< Deregister all actors (except for \a ignore) in the given cell. - void updateCombatMusic(); - ///< Update combat music state - void update(float duration, bool paused); ///< Update actor stats and store desired velocity vectors in \a movement @@ -159,19 +156,14 @@ namespace MWMechanics bool isReadyToBlock(const MWWorld::Ptr& ptr) const; bool isAttackingOrSpell(const MWWorld::Ptr& ptr) const; + bool playerHasHostiles() const; + int getGreetingTimer(const MWWorld::Ptr& ptr) const; float getAngleToPlayer(const MWWorld::Ptr& ptr) const; GreetingState getGreetingState(const MWWorld::Ptr& ptr) const; bool isTurningToPlayer(const MWWorld::Ptr& ptr) const; private: - enum class MusicType - { - Title, - Explore, - Battle - }; - std::map mDeathCount; std::list mActors; std::map::iterator> mIndex; @@ -182,7 +174,6 @@ namespace MWMechanics float mTimerUpdateHello = 0; float mSneakTimer = 0; // Times update of sneak icon float mSneakSkillTimer = 0; // Times sneak skill progress from "avoid notice" - MusicType mCurrentMusic = MusicType::Title; void updateVisibility(const MWWorld::Ptr& ptr, CharacterController& ctrl) const; diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index b960e0d38f..071ac164f3 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -24,6 +24,7 @@ #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/soundmanager.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" @@ -257,6 +258,7 @@ namespace MWMechanics , mClassSelected(false) , mRaceSelected(false) , mAI(true) + , mMusicType(MWSound::MusicType::Special) { // buildPlayer no longer here, needs to be done explicitly after all subsystems are up and running } @@ -340,6 +342,8 @@ namespace MWMechanics mActors.update(duration, paused); mObjects.update(duration, paused); + + updateMusicState(); } void MechanicsManager::processChangedSettings(const Settings::CategorySettingVector& changed) @@ -1572,6 +1576,31 @@ namespace MWMechanics return (Misc::Rng::roll0to99(prng) >= target); } + void MechanicsManager::updateMusicState() + { + bool musicPlaying = MWBase::Environment::get().getSoundManager()->isMusicPlaying(); + + // Can not interrupt scripted music by built-in playlists + if (mMusicType == MWSound::MusicType::Scripted && musicPlaying) + return; + + const MWWorld::Ptr& player = MWMechanics::getPlayer(); + bool hasHostiles = mActors.playerHasHostiles(); + + // check if we still have any player enemies to switch music + if (mMusicType != MWSound::MusicType::Explore && !hasHostiles + && !(player.getClass().getCreatureStats(player).isDead() && musicPlaying)) + { + MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Explore")); + mMusicType = MWSound::MusicType::Explore; + } + else if (mMusicType != MWSound::MusicType::Battle && hasHostiles) + { + MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Battle")); + mMusicType = MWSound::MusicType::Battle; + } + } + void MechanicsManager::startCombat(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) { CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index 748965a682..36bb18e022 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -11,6 +11,11 @@ #include "npcstats.hpp" #include "objects.hpp" +namespace MWSound +{ + enum class MusicType; +} + namespace MWWorld { class CellStore; @@ -33,6 +38,8 @@ namespace MWMechanics typedef std::map StolenItemsMap; StolenItemsMap mStolenItems; + MWSound::MusicType mMusicType; + public: void buildPlayer(); ///< build player according to stored class/race/birthsign information. Will @@ -232,7 +239,11 @@ namespace MWMechanics GreetingState getGreetingState(const MWWorld::Ptr& ptr) const override; bool isTurningToPlayer(const MWWorld::Ptr& ptr) const override; + MWSound::MusicType getMusicType() const override { return mMusicType; } + void setMusicType(MWSound::MusicType type) override { mMusicType = type; } + private: + void updateMusicState(); bool canCommitCrimeAgainst(const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker); bool canReportCrime( const MWWorld::Ptr& actor, const MWWorld::Ptr& victim, std::set& playerFollowers); diff --git a/apps/openmw/mwscript/soundextensions.cpp b/apps/openmw/mwscript/soundextensions.cpp index f1ac2a7a08..44cdc25064 100644 --- a/apps/openmw/mwscript/soundextensions.cpp +++ b/apps/openmw/mwscript/soundextensions.cpp @@ -63,10 +63,11 @@ namespace MWScript public: void execute(Interpreter::Runtime& runtime) override { - std::string sound{ runtime.getStringLiteral(runtime[0].mInteger) }; + std::string music{ runtime.getStringLiteral(runtime[0].mInteger) }; runtime.pop(); - MWBase::Environment::get().getSoundManager()->streamMusic(sound); + MWBase::Environment::get().getSoundManager()->streamMusic( + Misc::ResourceHelpers::correctMusicPath(music), MWSound::MusicType::Scripted); } }; diff --git a/apps/openmw/mwsound/soundmanagerimp.cpp b/apps/openmw/mwsound/soundmanagerimp.cpp index d5f1758f03..be8bae20a6 100644 --- a/apps/openmw/mwsound/soundmanagerimp.cpp +++ b/apps/openmw/mwsound/soundmanagerimp.cpp @@ -14,6 +14,7 @@ #include #include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/world.hpp" @@ -131,15 +132,6 @@ namespace MWSound Log(Debug::Info) << stream.str(); } - - // TODO: dehardcode this - std::vector titleMusic; - std::string_view titlefile = "music/special/morrowind title.mp3"; - if (mVFS->exists(titlefile)) - titleMusic.emplace_back(titlefile); - else - Log(Debug::Warning) << "Title music not found"; - mMusicFiles["Title"] = titleMusic; } SoundManager::~SoundManager() @@ -250,7 +242,7 @@ namespace MWSound if (filename.empty()) return; - Log(Debug::Info) << "Playing " << filename; + Log(Debug::Info) << "Playing \"" << filename << "\""; mLastPlayedMusic = filename; DecoderPtr decoder = getDecoder(); @@ -260,7 +252,7 @@ namespace MWSound } catch (std::exception& e) { - Log(Debug::Error) << "Failed to load audio from " << filename << ": " << e.what(); + Log(Debug::Error) << "Failed to load audio from \"" << filename << "\": " << e.what(); return; } @@ -274,7 +266,7 @@ namespace MWSound mOutput->streamSound(decoder, mMusic.get()); } - void SoundManager::advanceMusic(const std::string& filename) + void SoundManager::advanceMusic(const std::string& filename, float fadeOut) { if (!isMusicPlaying()) { @@ -284,7 +276,7 @@ namespace MWSound mNextMusic = filename; - mMusic->setFadeout(1.f); + mMusic->setFadeout(fadeOut); } void SoundManager::startRandomTitle() @@ -319,14 +311,28 @@ namespace MWSound tracklist.pop_back(); } - void SoundManager::streamMusic(const std::string& filename) + bool SoundManager::isMusicPlaying() { - advanceMusic("Music/" + filename); + return mMusic && mOutput->isStreamPlaying(mMusic.get()); } - bool SoundManager::isMusicPlaying() + void SoundManager::streamMusic(const std::string& filename, MusicType type, float fade) { - return mMusic && mOutput->isStreamPlaying(mMusic.get()); + const auto mechanicsManager = MWBase::Environment::get().getMechanicsManager(); + + // Can not interrupt scripted music by built-in playlists + if (mechanicsManager->getMusicType() == MusicType::Scripted && type != MusicType::Scripted + && type != MusicType::Special) + return; + + std::string normalizedName = VFS::Path::normalizeFilename(filename); + + mechanicsManager->setMusicType(type); + advanceMusic(normalizedName, fade); + if (type == MWSound::MusicType::Battle) + mCurrentPlaylist = "Battle"; + else if (type == MWSound::MusicType::Explore) + mCurrentPlaylist = "Explore"; } void SoundManager::playPlaylist(const std::string& playlist) @@ -337,7 +343,8 @@ namespace MWSound if (mMusicFiles.find(playlist) == mMusicFiles.end()) { std::vector filelist; - for (const auto& name : mVFS->getRecursiveDirectoryIterator("Music/" + playlist + '/')) + auto playlistPath = Misc::ResourceHelpers::correctMusicPath(playlist) + '/'; + for (const auto& name : mVFS->getRecursiveDirectoryIterator(playlistPath)) filelist.push_back(name); mMusicFiles[playlist] = filelist; @@ -1127,6 +1134,14 @@ namespace MWSound if (!mOutput->isInitialized() || mPlaybackPaused) return; + MWBase::StateManager::State state = MWBase::Environment::get().getStateManager()->getState(); + if (state == MWBase::StateManager::State_NoGame && !isMusicPlaying()) + { + std::string titlefile = "music/special/morrowind title.mp3"; + if (mVFS->exists(titlefile)) + streamMusic(titlefile, MWSound::MusicType::Special); + } + updateSounds(duration); if (MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_NoGame) { diff --git a/apps/openmw/mwsound/soundmanagerimp.hpp b/apps/openmw/mwsound/soundmanagerimp.hpp index 7453ce86f4..94d407c11b 100644 --- a/apps/openmw/mwsound/soundmanagerimp.hpp +++ b/apps/openmw/mwsound/soundmanagerimp.hpp @@ -127,7 +127,7 @@ namespace MWSound StreamPtr playVoice(DecoderPtr decoder, const osg::Vec3f& pos, bool playlocal); void streamMusicFull(const std::string& filename); - void advanceMusic(const std::string& filename); + void advanceMusic(const std::string& filename, float fadeOut = 1.f); void startRandomTitle(); void cull3DSound(SoundBase* sound); @@ -173,12 +173,17 @@ namespace MWSound void processChangedSettings(const Settings::CategorySettingVector& settings) override; + bool isEnabled() const override { return mOutput->isInitialized(); } + ///< Returns true if sound system is enabled + void stopMusic() override; ///< Stops music if it's playing - void streamMusic(const std::string& filename) override; + void streamMusic(const std::string& filename, MWSound::MusicType type, float fade = 1.f) override; ///< Play a soundifle - /// \param filename name of a sound file in "Music/" in the data directory. + /// \param filename name of a sound file in the data directory. + /// \param type music type. + /// \param fade time in seconds to fade out current track before start this one. bool isMusicPlaying() override; ///< Returns true if music is playing diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 241d252615..748187d868 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -391,7 +391,12 @@ namespace MWWorld { std::string_view video = Fallback::Map::getString("Movies_New_Game"); if (!video.empty()) + { + // Make sure that we do not continue to play a Title music after a new game video. + MWBase::Environment::get().getSoundManager()->stopMusic(); + MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Explore")); MWBase::Environment::get().getWindowManager()->playVideo(video, true); + } } // enable collision diff --git a/components/misc/resourcehelpers.cpp b/components/misc/resourcehelpers.cpp index 55fef5cdbd..0ae45c6c26 100644 --- a/components/misc/resourcehelpers.cpp +++ b/components/misc/resourcehelpers.cpp @@ -156,6 +156,11 @@ std::string Misc::ResourceHelpers::correctSoundPath(const std::string& resPath) return "sound\\" + resPath; } +std::string Misc::ResourceHelpers::correctMusicPath(const std::string& resPath) +{ + return "music\\" + resPath; +} + std::string_view Misc::ResourceHelpers::meshPathForESM3(std::string_view resPath) { constexpr std::string_view prefix = "meshes"; diff --git a/components/misc/resourcehelpers.hpp b/components/misc/resourcehelpers.hpp index 0597c7bc16..37932ea155 100644 --- a/components/misc/resourcehelpers.hpp +++ b/components/misc/resourcehelpers.hpp @@ -38,6 +38,9 @@ namespace Misc // Adds "sound\\". std::string correctSoundPath(const std::string& resPath); + // Adds "music\\". + std::string correctMusicPath(const std::string& resPath); + // Removes "meshes\\". std::string_view meshPathForESM3(std::string_view resPath); diff --git a/files/lua_api/openmw/ambient.lua b/files/lua_api/openmw/ambient.lua index 3722cb8c0f..917ec86c85 100644 --- a/files/lua_api/openmw/ambient.lua +++ b/files/lua_api/openmw/ambient.lua @@ -72,4 +72,21 @@ -- @return #boolean -- @usage local isPlaying = ambient.isSoundFilePlaying("Sound\\test.mp3"); +--- +-- Play a sound file as a music track +-- @function [parent=#ambient] streamMusic +-- @param #string fileName Path to file in VFS +-- @usage ambient.streamMusic("Music\\Test\\Test.mp3"); + +--- +-- Stop to play current music +-- @function [parent=#ambient] stopMusic +-- @usage ambient.stopMusic(); + +--- +-- Check if music is playing +-- @function [parent=#ambient] isMusicPlaying +-- @return #boolean +-- @usage local isPlaying = ambient.isMusicPlaying(); + return nil diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index af09274981..a6e9cb361c 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -749,6 +749,13 @@ --- @{#Sound}: Sounds and Speech -- @field [parent=#core] #Sound sound +--- +-- Checks if sound system is enabled (any functions to play sounds are no-ops when it is disabled). +-- It can not be enabled or disabled during runtime. +-- @function [parent=#Sound] isEnabled +-- @return #boolean +-- @usage local enabled = core.sound.isEnabled(); + --- -- Play a 3D sound, attached to object -- @function [parent=#Sound] playSound3d