diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index cf9c45f54e..5090039e46 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -998,7 +998,7 @@ void OMW::Engine::go() mWindowManager->pushGuiMode(MWGui::GM_MainMenu); if (mVFS->exists(MWSound::titleMusic)) - mSoundManager->streamMusic(MWSound::titleMusic, MWSound::MusicType::Special); + mSoundManager->streamMusic(MWSound::titleMusic, MWSound::MusicType::Normal); else Log(Debug::Warning) << "Title music not found"; diff --git a/apps/openmw/mwbase/soundmanager.hpp b/apps/openmw/mwbase/soundmanager.hpp index ab3f9c5605..9f1412dcbf 100644 --- a/apps/openmw/mwbase/soundmanager.hpp +++ b/apps/openmw/mwbase/soundmanager.hpp @@ -33,10 +33,8 @@ namespace MWSound enum class MusicType { - Special, - Explore, - Battle, - Scripted + Normal, + MWScript }; class Sound; @@ -126,11 +124,6 @@ namespace MWBase virtual bool isMusicPlaying() = 0; ///< Returns true if music is playing - virtual void playPlaylist(VFS::Path::NormalizedView playlist) = 0; - ///< Start playing music from the selected folder - /// \param name of the folder that contains the playlist - /// Title music playlist is predefined - virtual void say(const MWWorld::ConstPtr& reference, VFS::Path::NormalizedView filename) = 0; ///< Make an actor say some text. /// \param filename name of a sound file in the VFS diff --git a/apps/openmw/mwgui/levelupdialog.cpp b/apps/openmw/mwgui/levelupdialog.cpp index f1a40a3f16..87f2db55a5 100644 --- a/apps/openmw/mwgui/levelupdialog.cpp +++ b/apps/openmw/mwgui/levelupdialog.cpp @@ -218,7 +218,7 @@ namespace MWGui center(); // Play LevelUp Music - MWBase::Environment::get().getSoundManager()->streamMusic(MWSound::triumphMusic, MWSound::MusicType::Special); + MWBase::Environment::get().getSoundManager()->streamMusic(MWSound::triumphMusic, MWSound::MusicType::Normal); } void LevelupDialog::onOkButtonClicked(MyGUI::Widget* sender) diff --git a/apps/openmw/mwlua/soundbindings.cpp b/apps/openmw/mwlua/soundbindings.cpp index a5ec69b4fc..fbae24ae1e 100644 --- a/apps/openmw/mwlua/soundbindings.cpp +++ b/apps/openmw/mwlua/soundbindings.cpp @@ -141,7 +141,7 @@ namespace MWLua api["streamMusic"] = [](std::string_view fileName, const sol::optional& options) { auto args = getStreamMusicArgs(options); MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); - sndMgr->streamMusic(VFS::Path::Normalized(fileName), MWSound::MusicType::Scripted, args.mFade); + sndMgr->streamMusic(VFS::Path::Normalized(fileName), MWSound::MusicType::Normal, args.mFade); }; api["say"] diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 1bebd3feb8..db69f716dd 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -1290,40 +1290,6 @@ namespace MWMechanics } } - bool Actors::playerHasHostiles() const - { - const MWWorld::Ptr player = getPlayer(); - const osg::Vec3f playerPos = player.getRefData().getPosition().asVec3(); - bool hasHostiles = false; // need to know this to play Battle music - const bool aiActive = MWBase::Environment::get().getMechanicsManager()->isAIActive(); - - if (aiActive) - { - const int actorsProcessingRange = Settings::game().mActorsProcessingRange; - for (const Actor& actor : mActors) - { - if (actor.getPtr() == player) - continue; - - const bool inProcessingRange - = (playerPos - actor.getPtr().getRefData().getPosition().asVec3()).length2() - <= actorsProcessingRange * actorsProcessingRange; - if (inProcessingRange) - { - MWMechanics::CreatureStats& stats = actor.getPtr().getClass().getCreatureStats(actor.getPtr()); - bool isDead = stats.isDead() && stats.isDeathAnimationFinished(); - if (!isDead && stats.getAiSequence().isInCombat()) - { - hasHostiles = true; - break; - } - } - } - } - - return hasHostiles; - } - void Actors::predictAndAvoidCollisions(float duration) const { if (!MWBase::Environment::get().getMechanicsManager()->isAIActive()) @@ -1798,9 +1764,6 @@ 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( - MWSound::deathMusic, MWSound::MusicType::Special); } else { diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 2821df43e6..b575ec2827 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -162,8 +162,6 @@ 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; diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 048476c6ef..e9c34407e8 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -250,7 +250,7 @@ namespace MWMechanics , mClassSelected(false) , mRaceSelected(false) , mAI(true) - , mMusicType(MWSound::MusicType::Special) + , mMusicType(MWSound::MusicType::Normal) { // buildPlayer no longer here, needs to be done explicitly after all subsystems are up and running } @@ -334,8 +334,6 @@ namespace MWMechanics mActors.update(duration, paused); mObjects.update(duration, paused); - - updateMusicState(); } void MechanicsManager::processChangedSettings(const Settings::CategorySettingVector& changed) @@ -1665,31 +1663,6 @@ 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(MWSound::explorePlaylist); - mMusicType = MWSound::MusicType::Explore; - } - else if (mMusicType != MWSound::MusicType::Battle && hasHostiles) - { - MWBase::Environment::get().getSoundManager()->playPlaylist(MWSound::battlePlaylist); - mMusicType = MWSound::MusicType::Battle; - } - } - void MechanicsManager::startCombat( const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, const std::set* targetAllies) { diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index bf94589309..1ef4d9ab19 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -249,7 +249,6 @@ namespace MWMechanics 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 79bdf20160..c248c30520 100644 --- a/apps/openmw/mwscript/soundextensions.cpp +++ b/apps/openmw/mwscript/soundextensions.cpp @@ -67,7 +67,7 @@ namespace MWScript runtime.pop(); MWBase::Environment::get().getSoundManager()->streamMusic( - Misc::ResourceHelpers::correctMusicPath(music), MWSound::MusicType::Scripted); + Misc::ResourceHelpers::correctMusicPath(music), MWSound::MusicType::MWScript); } }; diff --git a/apps/openmw/mwsound/soundmanagerimp.cpp b/apps/openmw/mwsound/soundmanagerimp.cpp index bcaec8ddfd..38530d859f 100644 --- a/apps/openmw/mwsound/soundmanagerimp.cpp +++ b/apps/openmw/mwsound/soundmanagerimp.cpp @@ -262,7 +262,6 @@ namespace MWSound return; Log(Debug::Info) << "Playing \"" << filename << "\""; - mLastPlayedMusic = filename; DecoderPtr decoder = getDecoder(); try @@ -298,41 +297,6 @@ namespace MWSound mMusic->setFadeout(fadeOut); } - void SoundManager::startRandomTitle() - { - const auto playlist = mMusicFiles.find(mCurrentPlaylist); - - if (playlist == mMusicFiles.end() || playlist->second.empty()) - { - advanceMusic(VFS::Path::NormalizedView()); - return; - } - - const std::vector& filelist = playlist->second; - - auto& tracklist = mMusicToPlay[mCurrentPlaylist]; - - // Do a Fisher-Yates shuffle - - // Repopulate if playlist is empty - if (tracklist.empty()) - { - tracklist.resize(filelist.size()); - std::iota(tracklist.begin(), tracklist.end(), 0); - } - - int i = Misc::Rng::rollDice(tracklist.size()); - - // Reshuffle if last played music is the same after a repopulation - if (filelist[tracklist[i]] == mLastPlayedMusic) - i = (i + 1) % tracklist.size(); - - // Remove music from list after advancing music - advanceMusic(filelist[tracklist[i]]); - tracklist[i] = tracklist.back(); - tracklist.pop_back(); - } - bool SoundManager::isMusicPlaying() { return mMusic && mOutput->isStreamPlaying(mMusic.get()); @@ -343,45 +307,11 @@ namespace MWSound 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) + if (mechanicsManager->getMusicType() == MusicType::MWScript && type != MusicType::MWScript) return; mechanicsManager->setMusicType(type); advanceMusic(filename, fade); - if (type == MWSound::MusicType::Battle) - mCurrentPlaylist = battlePlaylist; - else if (type == MWSound::MusicType::Explore) - mCurrentPlaylist = explorePlaylist; - } - - void SoundManager::playPlaylist(VFS::Path::NormalizedView playlist) - { - if (mCurrentPlaylist == playlist) - return; - - auto it = mMusicFiles.find(playlist); - - if (it == mMusicFiles.end()) - { - std::vector filelist; - const VFS::Path::Normalized playlistPath - = Misc::ResourceHelpers::correctMusicPath(playlist) / VFS::Path::NormalizedView(); - for (const auto& name : mVFS->getRecursiveDirectoryIterator(VFS::Path::NormalizedView(playlistPath))) - filelist.push_back(name); - - it = mMusicFiles.emplace_hint(it, playlist, std::move(filelist)); - } - - // No Battle music? Use Explore playlist - if (playlist == battlePlaylist && it->second.empty()) - { - playPlaylist(explorePlaylist); - return; - } - - mCurrentPlaylist = playlist; - startRandomTitle(); } void SoundManager::say(const MWWorld::ConstPtr& ptr, VFS::Path::NormalizedView filename) @@ -1022,10 +952,6 @@ namespace MWSound duration = mTimePassed; mTimePassed = 0.0f; - // Make sure music is still playing - if (!isMusicPlaying() && !mCurrentPlaylist.value().empty()) - startRandomTitle(); - Environment env = Env_Normal; if (mListenerUnderwater) env = Env_Underwater; @@ -1165,7 +1091,7 @@ namespace MWSound if (isMainMenu && !isMusicPlaying()) { if (mVFS->exists(MWSound::titleMusic)) - streamMusic(MWSound::titleMusic, MWSound::MusicType::Special); + streamMusic(MWSound::titleMusic, MWSound::MusicType::Normal); } updateSounds(duration); diff --git a/apps/openmw/mwsound/soundmanagerimp.hpp b/apps/openmw/mwsound/soundmanagerimp.hpp index 1ba80f1d73..2e38215e14 100644 --- a/apps/openmw/mwsound/soundmanagerimp.hpp +++ b/apps/openmw/mwsound/soundmanagerimp.hpp @@ -52,12 +52,6 @@ namespace MWSound std::unique_ptr mOutput; - // Caches available music tracks by - std::unordered_map, VFS::Path::Hash, std::equal_to<>> - mMusicFiles; - std::unordered_map> mMusicToPlay; // A list with music files not yet played - VFS::Path::Normalized mLastPlayedMusic; // The music file that was last played - WaterSoundUpdater mWaterSoundUpdater; SoundBufferPool mSoundBuffers; @@ -127,7 +121,6 @@ namespace MWSound void streamMusicFull(VFS::Path::NormalizedView filename); void advanceMusic(VFS::Path::NormalizedView filename, float fadeOut = 1.f); - void startRandomTitle(); void cull3DSound(SoundBase* sound); @@ -185,11 +178,6 @@ namespace MWSound bool isMusicPlaying() override; ///< Returns true if music is playing - void playPlaylist(VFS::Path::NormalizedView playlist) override; - ///< Start playing music from the selected folder - /// \param name of the folder that contains the playlist - /// Title music playlist is predefined - void say(const MWWorld::ConstPtr& reference, VFS::Path::NormalizedView filename) override; ///< Make an actor say some text. /// \param filename name of a sound file in the VFS diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index a4464d70ad..5042acd198 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -396,7 +396,6 @@ namespace MWWorld { // 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(MWSound::explorePlaylist); MWBase::Environment::get().getWindowManager()->playVideo(video, true); } } diff --git a/docs/source/luadoc_data_paths.sh b/docs/source/luadoc_data_paths.sh index 6b41723f54..164d372bc2 100755 --- a/docs/source/luadoc_data_paths.sh +++ b/docs/source/luadoc_data_paths.sh @@ -6,10 +6,11 @@ paths=( scripts/omw/mechanics/animationcontroller.lua scripts/omw/input/gamepadcontrols.lua scripts/omw/camera/camera.lua + scripts/omw/music/music.lua scripts/omw/mwui/init.lua scripts/omw/settings/player.lua scripts/omw/ui.lua scripts/omw/usehandlers.lua scripts/omw/skillhandlers.lua ) -printf '%s\n' "${paths[@]}" \ No newline at end of file +printf '%s\n' "${paths[@]}" diff --git a/docs/source/reference/lua-scripting/api.rst b/docs/source/reference/lua-scripting/api.rst index ef6637cf7d..907c912fc5 100644 --- a/docs/source/reference/lua-scripting/api.rst +++ b/docs/source/reference/lua-scripting/api.rst @@ -41,6 +41,7 @@ Lua API reference interface_controls interface_gamepadcontrols interface_item_usage + interface_music interface_mwui interface_settings interface_skill_progression diff --git a/docs/source/reference/lua-scripting/interface_music.rst b/docs/source/reference/lua-scripting/interface_music.rst new file mode 100644 index 0000000000..2c0b37088c --- /dev/null +++ b/docs/source/reference/lua-scripting/interface_music.rst @@ -0,0 +1,8 @@ +Interface Music +=============== + +.. include:: version.rst + +.. raw:: html + :file: generated_html/scripts_omw_music_music.html + diff --git a/docs/source/reference/lua-scripting/tables/interfaces.rst b/docs/source/reference/lua-scripting/tables/interfaces.rst index 42a9cd70ba..936f692f2e 100644 --- a/docs/source/reference/lua-scripting/tables/interfaces.rst +++ b/docs/source/reference/lua-scripting/tables/interfaces.rst @@ -39,6 +39,9 @@ * - :ref:`MWUI ` - by player scripts - Morrowind-style UI templates. + * - :ref:`Music ` + - by player scripts + - Provides access to music playlists. * - :ref:`UI ` - by player scripts - | High-level UI modes interface. Allows to override parts diff --git a/files/data/CMakeLists.txt b/files/data/CMakeLists.txt index 662f00fd84..4e5db94703 100644 --- a/files/data/CMakeLists.txt +++ b/files/data/CMakeLists.txt @@ -48,6 +48,13 @@ set(BUILTIN_DATA_FILES l10n/OMWEngine/sv.yaml l10n/OMWEngine/fr.yaml + # L10n for music system + l10n/OMWMusic/de.yaml + l10n/OMWMusic/en.yaml + l10n/OMWMusic/ru.yaml + l10n/OMWMusic/sv.yaml + l10n/OMWMusic/fr.yaml + # L10n for post-processing HUD and built-in shaders l10n/OMWShaders/de.yaml l10n/OMWShaders/en.yaml @@ -79,6 +86,10 @@ set(BUILTIN_DATA_FILES scripts/omw/mechanics/animationcontroller.lua scripts/omw/mechanics/playercontroller.lua scripts/omw/settings/menu.lua + scripts/omw/music/actor.lua + scripts/omw/music/helpers.lua + scripts/omw/music/music.lua + scripts/omw/music/settings.lua scripts/omw/settings/player.lua scripts/omw/settings/global.lua scripts/omw/settings/common.lua diff --git a/files/data/builtin.omwscripts b/files/data/builtin.omwscripts index c38c3d243d..c44b335861 100644 --- a/files/data/builtin.omwscripts +++ b/files/data/builtin.omwscripts @@ -31,3 +31,8 @@ MENU: scripts/omw/console/menu.lua PLAYER: scripts/omw/console/player.lua GLOBAL: scripts/omw/console/global.lua CUSTOM: scripts/omw/console/local.lua + +# Music system +PLAYER: scripts/omw/music/music.lua +MENU: scripts/omw/music/settings.lua +NPC,CREATURE: scripts/omw/music/actor.lua diff --git a/files/data/l10n/OMWMusic/de.yaml b/files/data/l10n/OMWMusic/de.yaml new file mode 100755 index 0000000000..8b223164dd --- /dev/null +++ b/files/data/l10n/OMWMusic/de.yaml @@ -0,0 +1,7 @@ +Music: "OpenMW: Musik" +settingsPageDescription: "OpenMW-Musikeinstellungen" + +musicSettings: "Musik" + +CombatMusicEnabled: "Kampfmusik abspielen" +CombatMusicEnabledDescription: "Falls aktiviert, wechselt das Spiel zu Kampfmusik, sobald sich Akteure im Kampf befinden." diff --git a/files/data/l10n/OMWMusic/en.yaml b/files/data/l10n/OMWMusic/en.yaml new file mode 100755 index 0000000000..ae96774d58 --- /dev/null +++ b/files/data/l10n/OMWMusic/en.yaml @@ -0,0 +1,7 @@ +Music: "OpenMW Music" +settingsPageDescription: "OpenMW Music settings" + +musicSettings: "Music configuration" + +CombatMusicEnabled: "Play combat music" +CombatMusicEnabledDescription: "If enabled, the game switches to combat music if there are actors in combat." diff --git a/files/data/l10n/OMWMusic/fr.yaml b/files/data/l10n/OMWMusic/fr.yaml new file mode 100755 index 0000000000..0e15d0a411 --- /dev/null +++ b/files/data/l10n/OMWMusic/fr.yaml @@ -0,0 +1,7 @@ +#Music: "OpenMW Music" +#settingsPageDescription: "OpenMW Music settings" + +#musicSettings: "Music configuration" + +#CombatMusicEnabled: "Play combat music" +#CombatMusicEnabledDescription: "Whether to switch to combat music when there are actors in combat." diff --git a/files/data/l10n/OMWMusic/ru.yaml b/files/data/l10n/OMWMusic/ru.yaml new file mode 100755 index 0000000000..59982205c5 --- /dev/null +++ b/files/data/l10n/OMWMusic/ru.yaml @@ -0,0 +1,7 @@ +Music: "Музыка OpenMW" +settingsPageDescription: "Настройки музыки OpenMW" + +musicSettings: "Настройки музыки" + +CombatMusicEnabled: "Играть боевую музыку" +CombatMusicEnabledDescription: "Нужно ли переключаться на боевую музыку, когда есть сражающиеся персонажи." diff --git a/files/data/l10n/OMWMusic/sv.yaml b/files/data/l10n/OMWMusic/sv.yaml new file mode 100755 index 0000000000..4e6d70e32c --- /dev/null +++ b/files/data/l10n/OMWMusic/sv.yaml @@ -0,0 +1,7 @@ +Music: "OpenMW Musik" +settingsPageDescription: "OpenMW Musikinställningar" + +musicSettings: "Musikkonfiguration" + +CombatMusicEnabled: "Spela stridsmusik" +CombatMusicEnabledDescription: "Om aktiv kommer spelet byta till stridsmusik när det finns figurer som är i strid." diff --git a/files/data/scripts/omw/music/actor.lua b/files/data/scripts/omw/music/actor.lua new file mode 100755 index 0000000000..4890387e54 --- /dev/null +++ b/files/data/scripts/omw/music/actor.lua @@ -0,0 +1,63 @@ +local AI = require("openmw.interfaces").AI +local self = require("openmw.self") +local types = require("openmw.types") +local nearby = require("openmw.nearby") + +local targets = {} + +local function emitTargetsChanged() + for _, actor in ipairs(nearby.players) do + actor:sendEvent("OMWMusicCombatTargetsChanged", { actor = self, targets = targets }) + end +end + +local function onUpdate() + if types.Actor.isDeathFinished(self) or not types.Actor.isInActorsProcessingRange(self) then + if next(targets) ~= nil then + targets = {} + emitTargetsChanged() + end + + return + end + + -- Early-out for actors without targets and without combat state + -- TODO: use events or engine handlers to detect when targets change + local isStanceNothing = types.Actor.getStance(self) == types.Actor.STANCE.Nothing + if isStanceNothing and next(targets) == nil then + return + end + + local newTargets = AI.getTargets("Combat") + + local changed = false + if #newTargets ~= #targets then + changed = true + else + for i, target in ipairs(targets) do + if target ~= newTargets[i] then + changed = true + break + end + end + end + + targets = newTargets + if changed then + emitTargetsChanged() + end +end + +local function onInactive() + if next(targets) ~= nil then + targets = {} + emitTargetsChanged() + end +end + +return { + engineHandlers = { + onUpdate = onUpdate, + onInactive = onInactive, + }, +} diff --git a/files/data/scripts/omw/music/helpers.lua b/files/data/scripts/omw/music/helpers.lua new file mode 100755 index 0000000000..752c63a68e --- /dev/null +++ b/files/data/scripts/omw/music/helpers.lua @@ -0,0 +1,106 @@ +local debug = require('openmw.debug') +local storage = require('openmw.storage') +local vfs = require('openmw.vfs') + +local playlistsSection = storage.playerSection('OMWMusicPlaylistsTrackOrder') +playlistsSection:setLifeTime(storage.LIFE_TIME.GameSession) + +local function getTracksFromDirectory(path) + local result = {} + for fileName in vfs.pathsWithPrefix(path) do + table.insert(result, fileName) + end + + return result +end + +local function initMissingPlaylistFields(playlist) + if playlist.id == nil or playlist.priority == nil then + error("Can not register playlist: 'id' and 'priority' are mandatory fields") + end + + if playlist.tracks == nil then + playlist.tracks = getTracksFromDirectory(string.format("music/%s/", playlist.id)) + end + + if playlist.active == nil then + playlist.active = false + end + + if playlist.randomize == nil then + playlist.randomize = false + end + + if playlist.cycleTracks == nil then + playlist.cycleTracks = true + end + + if playlist.playOneTrack == nil then + playlist.playOneTrack = false + end +end + +local function shuffle(data) + for i = #data, 1, -1 do + local j = math.random(i) + data[i], data[j] = data[j], data[i] + end + return data +end + +local function initTracksOrder(tracks, randomize) + local tracksOrder = {} + for i, track in ipairs(tracks) do + tracksOrder[i] = i + end + + if randomize then + shuffle(tracksOrder) + end + + return tracksOrder +end + +local function isPlaylistActive(playlist) + return playlist.active and next(playlist.tracks) ~= nil +end + +local function getStoredTracksOrder() + -- We need a writeable playlists table here. + return playlistsSection:asTable() +end + +local function setStoredTracksOrder(playlistId, playlistTracksOrder) + playlistsSection:set(playlistId, playlistTracksOrder) +end + +local function getActivePlaylistByPriority(playlists) + local newPlaylist = nil + for _, playlist in pairs(playlists) do + if isPlaylistActive(playlist) then + if newPlaylist == nil or playlist.priority < newPlaylist.priority or + (playlist.priority == newPlaylist.priority and playlist.registrationOrder > newPlaylist.registrationOrder) then + newPlaylist = playlist + end + end + end + + return newPlaylist +end + +local function isInCombat(fightingActors) + return next(fightingActors) ~= nil and debug.isAIEnabled() +end + +local functions = { + getActivePlaylistByPriority = getActivePlaylistByPriority, + getStoredTracksOrder = getStoredTracksOrder, + getTracksFromDirectory = getTracksFromDirectory, + initMissingPlaylistFields = initMissingPlaylistFields, + initTracksOrder = initTracksOrder, + isInCombat = isInCombat, + isPlaylistActive = isPlaylistActive, + setStoredTracksOrder = setStoredTracksOrder +} + +return functions diff --git a/files/data/scripts/omw/music/music.lua b/files/data/scripts/omw/music/music.lua new file mode 100755 index 0000000000..f173e75ea2 --- /dev/null +++ b/files/data/scripts/omw/music/music.lua @@ -0,0 +1,189 @@ +local ambient = require('openmw.ambient') +local core = require('openmw.core') +local self = require('openmw.self') +local storage = require('openmw.storage') +local types = require('openmw.types') + +local musicSettings = storage.playerSection('SettingsOMWMusic') + +local helpers = require('scripts.omw.music.helpers') + +local registeredPlaylists = {} +local playlistsTracksOrder = helpers.getStoredTracksOrder() +local fightingActors = {} +local registrationOrder = 0 + +local currentPlaylist = nil +local skippedOneFrame = false + +local battlePriority = 10 +local explorePriority = 100 + +local function registerPlaylist(playlist) + helpers.initMissingPlaylistFields(playlist) + + local existingOrder = playlistsTracksOrder[playlist.id] + if not existingOrder or next(existingOrder) == nil or math.max(unpack(existingOrder)) > #playlist.tracks then + local newPlaylistOrder = helpers.initTracksOrder(playlist.tracks, playlist.randomize) + playlistsTracksOrder[playlist.id] = newPlaylistOrder + helpers.setStoredTracksOrder(playlist.id, newPlaylistOrder) + else + playlistsTracksOrder[playlist.id] = existingOrder + end + + if registeredPlaylists[playlist.id] == nil then + playlist.registrationOrder = registrationOrder + registrationOrder = registrationOrder + 1 + end + + registeredPlaylists[playlist.id] = playlist +end + +local function setPlaylistActive(id, state) + if id == nil then + error("Playlist ID is nil") + end + + local playlist = registeredPlaylists[id] + if playlist then + playlist.active = state + else + error(string.format("Playlist '%s' is not registered.", id)) + end +end + +local function onCombatTargetsChanged(eventData) + if eventData.actor == nil then return end + + if next(eventData.targets) ~= nil then + fightingActors[eventData.actor.id] = true + else + fightingActors[eventData.actor.id] = nil + end +end + +local function playerDied() + ambient.streamMusic("music/special/mw_death.mp3") +end + +local function switchPlaylist(newPlaylist) + local newPlaylistOrder = playlistsTracksOrder[newPlaylist.id] + local nextTrackIndex = table.remove(newPlaylistOrder) + + if nextTrackIndex == nil then + error("Can not fetch track: nextTrackIndex is nil") + end + + -- If there are no tracks left, fill playlist again. + if next(newPlaylistOrder) == nil then + newPlaylistOrder = helpers.initTracksOrder(newPlaylist.tracks, newPlaylist.randomize) + + if not newPlaylist.cycleTracks then + newPlaylist.deactivateAfterEnd = true + end + + -- If next track for randomized playist will be the same as one we want to play, swap it with random track. + if newPlaylist.randomize and #newPlaylistOrder > 1 and newPlaylistOrder[1] == nextTrackIndex then + local index = math.random(2, #newPlaylistOrder) + newPlaylistOrder[1], newPlaylistOrder[index] = newPlaylistOrder[index], newPlaylistOrder[1] + end + + playlistsTracksOrder[newPlaylist.id] = newPlaylistOrder + end + + helpers.setStoredTracksOrder(newPlaylist.id, newPlaylistOrder) + + local trackPath = newPlaylist.tracks[nextTrackIndex] + if trackPath == nil then + error(string.format("Can not fetch track with index %s from playlist '%s'.", nextTrackIndex, newPlaylist.id)) + else + ambient.streamMusic(trackPath, newPlaylist.fadeOut) + if newPlaylist.playOneTrack then + newPlaylist.deactivateAfterEnd = true + end + end + + currentPlaylist = newPlaylist +end + +local function onFrame(dt) + -- Skip a first frame to allow other scripts to update playlists state. + if not skippedOneFrame then + skippedOneFrame = true + return + end + + if not core.sound.isEnabled() then return end + + -- Do not allow to switch playlists when player is dead + local musicPlaying = ambient.isMusicPlaying() + if types.Actor.isDead(self) and musicPlaying then return end + + local combatMusicEnabled = musicSettings:get("CombatMusicEnabled") and helpers.isInCombat(fightingActors) + setPlaylistActive("battle", combatMusicEnabled) + + local newPlaylist = helpers.getActivePlaylistByPriority(registeredPlaylists) + + if not newPlaylist then + ambient.stopMusic() + + if currentPlaylist ~= nil then + currentPlaylist.deactivateAfterEnd = nil + end + + currentPlaylist = nil + return + end + + if newPlaylist == currentPlaylist and musicPlaying then return end + + if newPlaylist and newPlaylist.deactivateAfterEnd then + newPlaylist.deactivateAfterEnd = nil + newPlaylist.active = false + return + end + + switchPlaylist(newPlaylist) +end + +registerPlaylist({ id = "battle", priority = battlePriority, randomize = true }) +registerPlaylist({ id = "explore", priority = explorePriority, randomize = true, active = true }) + +return { + --- + -- @module Music + -- @usage require('openmw.interfaces').Music + interfaceName = 'Music', + interface = { + --- Interface version + -- @field [parent=#Music] #number version + version = 0, + --- + -- Set state for playlist with given ID + -- @function [parent=#Music] setPlaylistActive + -- @param #string id Playlist ID + -- @param #boolean state Playlist is active + setPlaylistActive = setPlaylistActive, + --- + -- Register given playlist + -- @function [parent=#Music] registerPlaylist + -- @param #table playlist Playlist data. Can contain: + -- + -- * `id` - #string, playlist ID + -- * `priority` - #number, playlist priority (lower value means higher priority) + -- * `fadeOut` - #number, Time in seconds to fade out current track before starting this one. If nil, allow the engine to choose the value. + -- * `tracks` - #list<#string>, Paths of track files for playlist (if nil, use all tracks from 'music/{id}/' folder) + -- * `active` - #boolean, tells if playlist is active (default is false) + -- * `randomize` - #boolean, tells if playlist should shuffle its tracks during playback (default is false). When all tracks are played, they are randomized again. + -- * `playOne` - #boolean, tells if playlist should be automatically deactivated after one track is played (default is false) + -- * `cycleTracks` - #boolean, if true, tells to start playlist from beginning once all tracks are played, otherwise playlist becomes deactivated (default is true). + registerPlaylist = registerPlaylist + }, + engineHandlers = { + onFrame = onFrame + }, + eventHandlers = { + Died = playerDied, + OMWMusicCombatTargetsChanged = onCombatTargetsChanged + } +} diff --git a/files/data/scripts/omw/music/settings.lua b/files/data/scripts/omw/music/settings.lua new file mode 100755 index 0000000000..5a03af05c8 --- /dev/null +++ b/files/data/scripts/omw/music/settings.lua @@ -0,0 +1,27 @@ +local I = require('openmw.interfaces') +local storage = require('openmw.storage') + +I.Settings.registerPage({ + key = 'OMWMusic', + l10n = 'OMWMusic', + name = 'Music', + description = 'settingsPageDescription', +}) + +I.Settings.registerGroup({ + key = "SettingsOMWMusic", + page = 'OMWMusic', + l10n = 'OMWMusic', + name = 'musicSettings', + permanentStorage = true, + order = 0, + settings = { + { + key = 'CombatMusicEnabled', + renderer = 'checkbox', + name = 'CombatMusicEnabled', + description = 'CombatMusicEnabledDescription', + default = true, + } + }, +}) diff --git a/files/lua_api/openmw/interfaces.lua b/files/lua_api/openmw/interfaces.lua index 5ed98bd8be..5cd72797ac 100644 --- a/files/lua_api/openmw/interfaces.lua +++ b/files/lua_api/openmw/interfaces.lua @@ -17,6 +17,9 @@ --- -- @field [parent=#interfaces] scripts.omw.mwui.init#scripts.omw.mwui.init MWUI +--- +-- @field [parent=#interfaces] scripts.omw.music.music#scripts.omw.music.music Music + --- -- @field [parent=#interfaces] scripts.omw.settings.player#scripts.omw.settings.player Settings