mirror of https://github.com/OpenMW/openmw.git
Implement Lua-based music
parent
c3d02c0b41
commit
5a1ec8ce87
@ -0,0 +1,8 @@
|
|||||||
|
Interface Music
|
||||||
|
===============
|
||||||
|
|
||||||
|
.. include:: version.rst
|
||||||
|
|
||||||
|
.. raw:: html
|
||||||
|
:file: generated_html/scripts_omw_music_music.html
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
Loading…
Reference in New Issue