Implement Lua-based music

esm4-texture
Andrei Kortunov 1 year ago
parent c3d02c0b41
commit 5a1ec8ce87

@ -998,7 +998,7 @@ void OMW::Engine::go()
mWindowManager->pushGuiMode(MWGui::GM_MainMenu); mWindowManager->pushGuiMode(MWGui::GM_MainMenu);
if (mVFS->exists(MWSound::titleMusic)) if (mVFS->exists(MWSound::titleMusic))
mSoundManager->streamMusic(MWSound::titleMusic, MWSound::MusicType::Special); mSoundManager->streamMusic(MWSound::titleMusic, MWSound::MusicType::Normal);
else else
Log(Debug::Warning) << "Title music not found"; Log(Debug::Warning) << "Title music not found";

@ -33,10 +33,8 @@ namespace MWSound
enum class MusicType enum class MusicType
{ {
Special, Normal,
Explore, MWScript
Battle,
Scripted
}; };
class Sound; class Sound;
@ -126,11 +124,6 @@ namespace MWBase
virtual bool isMusicPlaying() = 0; virtual bool isMusicPlaying() = 0;
///< Returns true if music is playing ///< 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; virtual void say(const MWWorld::ConstPtr& reference, VFS::Path::NormalizedView filename) = 0;
///< Make an actor say some text. ///< Make an actor say some text.
/// \param filename name of a sound file in the VFS /// \param filename name of a sound file in the VFS

@ -218,7 +218,7 @@ namespace MWGui
center(); center();
// Play LevelUp Music // 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) void LevelupDialog::onOkButtonClicked(MyGUI::Widget* sender)

@ -141,7 +141,7 @@ namespace MWLua
api["streamMusic"] = [](std::string_view fileName, const sol::optional<sol::table>& options) { api["streamMusic"] = [](std::string_view fileName, const sol::optional<sol::table>& options) {
auto args = getStreamMusicArgs(options); auto args = getStreamMusicArgs(options);
MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); 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"] api["say"]

@ -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 void Actors::predictAndAvoidCollisions(float duration) const
{ {
if (!MWBase::Environment::get().getMechanicsManager()->isAIActive()) if (!MWBase::Environment::get().getMechanicsManager()->isAIActive())
@ -1798,9 +1764,6 @@ namespace MWMechanics
{ {
// player's death animation is over // player's death animation is over
MWBase::Environment::get().getStateManager()->askLoadRecent(); 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 else
{ {

@ -162,8 +162,6 @@ namespace MWMechanics
bool isReadyToBlock(const MWWorld::Ptr& ptr) const; bool isReadyToBlock(const MWWorld::Ptr& ptr) const;
bool isAttackingOrSpell(const MWWorld::Ptr& ptr) const; bool isAttackingOrSpell(const MWWorld::Ptr& ptr) const;
bool playerHasHostiles() const;
int getGreetingTimer(const MWWorld::Ptr& ptr) const; int getGreetingTimer(const MWWorld::Ptr& ptr) const;
float getAngleToPlayer(const MWWorld::Ptr& ptr) const; float getAngleToPlayer(const MWWorld::Ptr& ptr) const;
GreetingState getGreetingState(const MWWorld::Ptr& ptr) const; GreetingState getGreetingState(const MWWorld::Ptr& ptr) const;

@ -250,7 +250,7 @@ namespace MWMechanics
, mClassSelected(false) , mClassSelected(false)
, mRaceSelected(false) , mRaceSelected(false)
, mAI(true) , 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 // 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); mActors.update(duration, paused);
mObjects.update(duration, paused); mObjects.update(duration, paused);
updateMusicState();
} }
void MechanicsManager::processChangedSettings(const Settings::CategorySettingVector& changed) void MechanicsManager::processChangedSettings(const Settings::CategorySettingVector& changed)
@ -1665,31 +1663,6 @@ namespace MWMechanics
return (Misc::Rng::roll0to99(prng) >= target); 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( void MechanicsManager::startCombat(
const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, const std::set<MWWorld::Ptr>* targetAllies) const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, const std::set<MWWorld::Ptr>* targetAllies)
{ {

@ -249,7 +249,6 @@ namespace MWMechanics
void setMusicType(MWSound::MusicType type) override { mMusicType = type; } void setMusicType(MWSound::MusicType type) override { mMusicType = type; }
private: private:
void updateMusicState();
bool canCommitCrimeAgainst(const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker); bool canCommitCrimeAgainst(const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker);
bool canReportCrime( bool canReportCrime(
const MWWorld::Ptr& actor, const MWWorld::Ptr& victim, std::set<MWWorld::Ptr>& playerFollowers); const MWWorld::Ptr& actor, const MWWorld::Ptr& victim, std::set<MWWorld::Ptr>& playerFollowers);

@ -67,7 +67,7 @@ namespace MWScript
runtime.pop(); runtime.pop();
MWBase::Environment::get().getSoundManager()->streamMusic( MWBase::Environment::get().getSoundManager()->streamMusic(
Misc::ResourceHelpers::correctMusicPath(music), MWSound::MusicType::Scripted); Misc::ResourceHelpers::correctMusicPath(music), MWSound::MusicType::MWScript);
} }
}; };

@ -262,7 +262,6 @@ namespace MWSound
return; return;
Log(Debug::Info) << "Playing \"" << filename << "\""; Log(Debug::Info) << "Playing \"" << filename << "\"";
mLastPlayedMusic = filename;
DecoderPtr decoder = getDecoder(); DecoderPtr decoder = getDecoder();
try try
@ -298,41 +297,6 @@ namespace MWSound
mMusic->setFadeout(fadeOut); 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<VFS::Path::Normalized>& 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() bool SoundManager::isMusicPlaying()
{ {
return mMusic && mOutput->isStreamPlaying(mMusic.get()); return mMusic && mOutput->isStreamPlaying(mMusic.get());
@ -343,45 +307,11 @@ namespace MWSound
const auto mechanicsManager = MWBase::Environment::get().getMechanicsManager(); const auto mechanicsManager = MWBase::Environment::get().getMechanicsManager();
// Can not interrupt scripted music by built-in playlists // Can not interrupt scripted music by built-in playlists
if (mechanicsManager->getMusicType() == MusicType::Scripted && type != MusicType::Scripted if (mechanicsManager->getMusicType() == MusicType::MWScript && type != MusicType::MWScript)
&& type != MusicType::Special)
return; return;
mechanicsManager->setMusicType(type); mechanicsManager->setMusicType(type);
advanceMusic(filename, fade); 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<VFS::Path::Normalized> 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) void SoundManager::say(const MWWorld::ConstPtr& ptr, VFS::Path::NormalizedView filename)
@ -1022,10 +952,6 @@ namespace MWSound
duration = mTimePassed; duration = mTimePassed;
mTimePassed = 0.0f; mTimePassed = 0.0f;
// Make sure music is still playing
if (!isMusicPlaying() && !mCurrentPlaylist.value().empty())
startRandomTitle();
Environment env = Env_Normal; Environment env = Env_Normal;
if (mListenerUnderwater) if (mListenerUnderwater)
env = Env_Underwater; env = Env_Underwater;
@ -1165,7 +1091,7 @@ namespace MWSound
if (isMainMenu && !isMusicPlaying()) if (isMainMenu && !isMusicPlaying())
{ {
if (mVFS->exists(MWSound::titleMusic)) if (mVFS->exists(MWSound::titleMusic))
streamMusic(MWSound::titleMusic, MWSound::MusicType::Special); streamMusic(MWSound::titleMusic, MWSound::MusicType::Normal);
} }
updateSounds(duration); updateSounds(duration);

@ -52,12 +52,6 @@ namespace MWSound
std::unique_ptr<Sound_Output> mOutput; std::unique_ptr<Sound_Output> mOutput;
// Caches available music tracks by <playlist name, (sound files) >
std::unordered_map<VFS::Path::Normalized, std::vector<VFS::Path::Normalized>, VFS::Path::Hash, std::equal_to<>>
mMusicFiles;
std::unordered_map<std::string, std::vector<int>> mMusicToPlay; // A list with music files not yet played
VFS::Path::Normalized mLastPlayedMusic; // The music file that was last played
WaterSoundUpdater mWaterSoundUpdater; WaterSoundUpdater mWaterSoundUpdater;
SoundBufferPool mSoundBuffers; SoundBufferPool mSoundBuffers;
@ -127,7 +121,6 @@ namespace MWSound
void streamMusicFull(VFS::Path::NormalizedView filename); void streamMusicFull(VFS::Path::NormalizedView filename);
void advanceMusic(VFS::Path::NormalizedView filename, float fadeOut = 1.f); void advanceMusic(VFS::Path::NormalizedView filename, float fadeOut = 1.f);
void startRandomTitle();
void cull3DSound(SoundBase* sound); void cull3DSound(SoundBase* sound);
@ -185,11 +178,6 @@ namespace MWSound
bool isMusicPlaying() override; bool isMusicPlaying() override;
///< Returns true if music is playing ///< 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; void say(const MWWorld::ConstPtr& reference, VFS::Path::NormalizedView filename) override;
///< Make an actor say some text. ///< Make an actor say some text.
/// \param filename name of a sound file in the VFS /// \param filename name of a sound file in the VFS

@ -396,7 +396,6 @@ namespace MWWorld
{ {
// Make sure that we do not continue to play a Title music after a new game video. // 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()->stopMusic();
MWBase::Environment::get().getSoundManager()->playPlaylist(MWSound::explorePlaylist);
MWBase::Environment::get().getWindowManager()->playVideo(video, true); MWBase::Environment::get().getWindowManager()->playVideo(video, true);
} }
} }

@ -6,6 +6,7 @@ paths=(
scripts/omw/mechanics/animationcontroller.lua scripts/omw/mechanics/animationcontroller.lua
scripts/omw/input/gamepadcontrols.lua scripts/omw/input/gamepadcontrols.lua
scripts/omw/camera/camera.lua scripts/omw/camera/camera.lua
scripts/omw/music/music.lua
scripts/omw/mwui/init.lua scripts/omw/mwui/init.lua
scripts/omw/settings/player.lua scripts/omw/settings/player.lua
scripts/omw/ui.lua scripts/omw/ui.lua

@ -41,6 +41,7 @@ Lua API reference
interface_controls interface_controls
interface_gamepadcontrols interface_gamepadcontrols
interface_item_usage interface_item_usage
interface_music
interface_mwui interface_mwui
interface_settings interface_settings
interface_skill_progression interface_skill_progression

@ -0,0 +1,8 @@
Interface Music
===============
.. include:: version.rst
.. raw:: html
:file: generated_html/scripts_omw_music_music.html

@ -39,6 +39,9 @@
* - :ref:`MWUI <Interface MWUI>` * - :ref:`MWUI <Interface MWUI>`
- by player scripts - by player scripts
- Morrowind-style UI templates. - Morrowind-style UI templates.
* - :ref:`Music <Interface Music>`
- by player scripts
- Provides access to music playlists.
* - :ref:`UI <Interface UI>` * - :ref:`UI <Interface UI>`
- by player scripts - by player scripts
- | High-level UI modes interface. Allows to override parts - | High-level UI modes interface. Allows to override parts

@ -48,6 +48,13 @@ set(BUILTIN_DATA_FILES
l10n/OMWEngine/sv.yaml l10n/OMWEngine/sv.yaml
l10n/OMWEngine/fr.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 for post-processing HUD and built-in shaders
l10n/OMWShaders/de.yaml l10n/OMWShaders/de.yaml
l10n/OMWShaders/en.yaml l10n/OMWShaders/en.yaml
@ -79,6 +86,10 @@ set(BUILTIN_DATA_FILES
scripts/omw/mechanics/animationcontroller.lua scripts/omw/mechanics/animationcontroller.lua
scripts/omw/mechanics/playercontroller.lua scripts/omw/mechanics/playercontroller.lua
scripts/omw/settings/menu.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/player.lua
scripts/omw/settings/global.lua scripts/omw/settings/global.lua
scripts/omw/settings/common.lua scripts/omw/settings/common.lua

@ -31,3 +31,8 @@ MENU: scripts/omw/console/menu.lua
PLAYER: scripts/omw/console/player.lua PLAYER: scripts/omw/console/player.lua
GLOBAL: scripts/omw/console/global.lua GLOBAL: scripts/omw/console/global.lua
CUSTOM: scripts/omw/console/local.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

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

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

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

@ -0,0 +1,7 @@
Music: "Музыка OpenMW"
settingsPageDescription: "Настройки музыки OpenMW"
musicSettings: "Настройки музыки"
CombatMusicEnabled: "Играть боевую музыку"
CombatMusicEnabledDescription: "Нужно ли переключаться на боевую музыку, когда есть сражающиеся персонажи."

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

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

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

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

@ -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,
}
},
})

@ -17,6 +17,9 @@
--- ---
-- @field [parent=#interfaces] scripts.omw.mwui.init#scripts.omw.mwui.init MWUI -- @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 -- @field [parent=#interfaces] scripts.omw.settings.player#scripts.omw.settings.player Settings

Loading…
Cancel
Save