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