294 lines
11 KiB
Lua
294 lines
11 KiB
Lua
setfenv(1, VoiceOver)
|
|
|
|
local CURRENT_MODULE_VERSION = 1
|
|
local FORCE_ENABLE_DISABLED_MODULES = true
|
|
local LOAD_ALL_MODULES = true
|
|
|
|
DataModules =
|
|
{
|
|
availableModules = {}, -- To store the list of modules present in Interface\AddOns folder, whether they're loaded or not
|
|
availableModulesOrdered = {}, -- To store the list of modules present in Interface\AddOns folder, whether they're loaded or not
|
|
registeredModules = {}, -- To keep track of which module names were already registered
|
|
registeredModulesOrdered = {}, -- To have a consistent ordering of modules (which key-value hashmaps don't provide) to avoid bugs that can only be reproduced randomly
|
|
}
|
|
|
|
local function SortModules(a, b)
|
|
a = a.METADATA or a
|
|
b = b.METADATA or b
|
|
if a.ModulePriority ~= b.ModulePriority then
|
|
return a.ModulePriority > b.ModulePriority
|
|
end
|
|
return a.AddonName < b.AddonName
|
|
end
|
|
|
|
function DataModules:Register(name, module)
|
|
assert(not self.registeredModules[name], format([[Module "%s" already registered]], name))
|
|
|
|
local metadata = assert(self.availableModules[name],
|
|
format([[Module "%s" attempted to register but wasn't detected during addon enumeration]], name))
|
|
local moduleVersion = assert(tonumber(GetAddOnMetadata(name, "X-VoiceOver-DataModule-Version")),
|
|
format([[Module "%s" is missing data format version]], name))
|
|
|
|
-- Ideally if module format would ever change - there should be fallbacks in place to handle outdated formats
|
|
assert(moduleVersion == CURRENT_MODULE_VERSION,
|
|
format([[Module "%s" contains outdated data format (version %d, expected %d)]], name, moduleVersion,
|
|
CURRENT_MODULE_VERSION))
|
|
|
|
module.METADATA = metadata
|
|
|
|
self.registeredModules[name] = module
|
|
table.insert(self.registeredModulesOrdered, module)
|
|
|
|
-- Order the modules by priority (higher first) then by name (case-sensitive alphabetical)
|
|
-- Modules with higher priority will be iterated through first, so one can create a module with "overrides" for data in other modules by simply giving it a higher priority
|
|
table.sort(self.registeredModulesOrdered, SortModules)
|
|
end
|
|
|
|
function DataModules:GetModule(name)
|
|
return self.registeredModules[name]
|
|
end
|
|
|
|
function DataModules:GetModules()
|
|
return ipairs(self.registeredModulesOrdered)
|
|
end
|
|
|
|
function DataModules:GetAvailableModules()
|
|
return ipairs(self.availableModulesOrdered)
|
|
end
|
|
|
|
function DataModules:EnumerateAddons()
|
|
local playerName = UnitName("player")
|
|
for i = 1, GetNumAddOns() do
|
|
local moduleVersion = tonumber(GetAddOnMetadata(i, "X-VoiceOver-DataModule-Version"))
|
|
if moduleVersion and (FORCE_ENABLE_DISABLED_MODULES or GetAddOnEnableState(playerName, i) ~= 0) then
|
|
local name = GetAddOnInfo(i)
|
|
local mapsString = GetAddOnMetadata(i, "X-VoiceOver-DataModule-Maps")
|
|
local maps = {}
|
|
if mapsString then
|
|
for _, mapString in ipairs({ strsplit(",", mapsString) }) do
|
|
local map = tonumber(mapString)
|
|
if map then
|
|
maps[map] = true
|
|
end
|
|
end
|
|
end
|
|
local module =
|
|
{
|
|
AddonName = name,
|
|
LoadOnDemand = IsAddOnLoadOnDemand(name),
|
|
ModuleVersion = moduleVersion,
|
|
ModulePriority = tonumber(GetAddOnMetadata(name, "X-VoiceOver-DataModule-Priority")) or 0,
|
|
ContentVersion = GetAddOnMetadata(name, "Version"),
|
|
Title = GetAddOnMetadata(name, "Title") or name,
|
|
Maps = maps,
|
|
}
|
|
self.availableModules[name] = module
|
|
table.insert(self.availableModulesOrdered, module)
|
|
|
|
-- Maybe in the future we can load modules based on the map the player is in (select(8, GetInstanceInfo())), but for now - just load everything
|
|
if LOAD_ALL_MODULES and IsAddOnLoadOnDemand(name) then
|
|
DataModules:LoadModule(module)
|
|
end
|
|
end
|
|
end
|
|
|
|
table.sort(self.availableModulesOrdered, SortModules)
|
|
for order, module in self:GetAvailableModules() do
|
|
Options:AddDataModule(module, order)
|
|
end
|
|
end
|
|
|
|
function DataModules:LoadModule(module)
|
|
if not module.LoadOnDemand or self:GetModule(module.AddonName) or IsAddOnLoaded(module.AddonName) then
|
|
return false
|
|
end
|
|
|
|
if FORCE_ENABLE_DISABLED_MODULES and GetAddOnEnableState(UnitName("player"), module.AddonName) == 0 then
|
|
EnableAddOn(module.AddonName)
|
|
end
|
|
|
|
-- We deliberately use a high ##Interface version in TOC to ensure that all clients will load it.
|
|
-- Otherwise pre-classic-rerelease clients will refuse to load addons with version < 20000.
|
|
-- Here we temporarily enable "Load out of date AddOns" to load the module, and restore the user's setting afterwards.
|
|
local oldLoadOutOfDateAddons = GetCVar("checkAddonVersion")
|
|
SetCVar("checkAddonVersion", 0)
|
|
local loaded = LoadAddOn(module.AddonName)
|
|
SetCVar("checkAddonVersion", oldLoadOutOfDateAddons)
|
|
return loaded
|
|
end
|
|
|
|
function DataModules:GetNPCGossipTextHash(soundData)
|
|
local table = soundData.unitGUID and "GossipLookupByNPCID" or "GossipLookupByNPCName"
|
|
local npc = soundData.unitGUID and Utils:GetIDFromGUID(soundData.unitGUID) or soundData.name
|
|
local text = soundData.text
|
|
|
|
local text_entries = {}
|
|
|
|
for _, module in self:GetModules() do
|
|
local data = module[table]
|
|
if data then
|
|
local npc_gossip_table = data[npc]
|
|
if npc_gossip_table then
|
|
for text, hash in pairs(npc_gossip_table) do
|
|
text_entries[text] = text_entries[text] or
|
|
hash -- Respect module priority, don't overwrite the entry if there is already one
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local best_result = FuzzySearchBestKeys(text, text_entries)
|
|
return best_result and best_result.value
|
|
end
|
|
|
|
local function replaceDoubleQuotes(text)
|
|
return string.gsub(text, '"', "'")
|
|
end
|
|
|
|
local function getFirstNWords(text, n)
|
|
local firstNWords = {}
|
|
local count = 0
|
|
|
|
for word in string.gmatch(text, "%S+") do
|
|
count = count + 1
|
|
table.insert(firstNWords, word)
|
|
if count >= n then
|
|
break
|
|
end
|
|
end
|
|
|
|
return table.concat(firstNWords, " ")
|
|
end
|
|
|
|
local function getLastNWords(text, n)
|
|
local lastNWords = {}
|
|
local count = 0
|
|
|
|
for word in string.gmatch(text, "%S+") do
|
|
table.insert(lastNWords, word)
|
|
count = count + 1
|
|
end
|
|
|
|
local startIndex = math.max(1, count - n + 1)
|
|
local endIndex = count
|
|
|
|
return table.concat(lastNWords, " ", startIndex, endIndex)
|
|
end
|
|
|
|
function DataModules:GetQuestID(source, title, npcName, text)
|
|
local cleanedTitle = replaceDoubleQuotes(title)
|
|
local cleanedNPCName = replaceDoubleQuotes(npcName)
|
|
local cleanedText = replaceDoubleQuotes(getFirstNWords(text, 15)) ..
|
|
" " .. replaceDoubleQuotes(getLastNWords(text, 15))
|
|
local text_entries = {}
|
|
|
|
for _, module in self:GetModules() do
|
|
local data = module.QuestIDLookup
|
|
if data then
|
|
local titleLookup = data[source][cleanedTitle]
|
|
if titleLookup then
|
|
if type(titleLookup) == "number" then
|
|
return titleLookup
|
|
else
|
|
-- else titleLookup is a table and we need to search it further
|
|
local npcLookup = titleLookup[cleanedNPCName]
|
|
if npcLookup then
|
|
if type(npcLookup) == "number" then
|
|
return npcLookup
|
|
else
|
|
for text, ID in pairs(npcLookup) do
|
|
text_entries[text] = text_entries[text] or
|
|
ID -- Respect module priority, don't overwrite the entry if there is already one
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local best_result = FuzzySearchBestKeys(cleanedText, text_entries)
|
|
return best_result and best_result.value
|
|
end
|
|
|
|
function DataModules:GetQuestLogNPCID(questID)
|
|
for _, module in self:GetModules() do
|
|
local data = module.NPCIDLookupByQuestID
|
|
if data then
|
|
local npcID = data[questID]
|
|
if npcID then
|
|
return npcID
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function DataModules:GetNPCName(npcID)
|
|
for _, module in self:GetModules() do
|
|
local data = module.NPCNameLookupByNPCID
|
|
if data then
|
|
local npcName = data[npcID]
|
|
if npcName then
|
|
return npcName
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local getFileNameForEvent =
|
|
{
|
|
[Enums.SoundEvent.QuestAccept] = function(soundData) return format("%d-%s", soundData.questID, "accept") end,
|
|
[Enums.SoundEvent.QuestProgress] = function(soundData) return format("%d-%s", soundData.questID, "progress") end,
|
|
[Enums.SoundEvent.QuestComplete] = function(soundData) return format("%d-%s", soundData.questID, "complete") end,
|
|
[Enums.SoundEvent.QuestGreeting] = function(soundData) return DataModules:GetNPCGossipTextHash(soundData) end,
|
|
[Enums.SoundEvent.Gossip] = function(soundData) return DataModules:GetNPCGossipTextHash(soundData) end,
|
|
}
|
|
setmetatable(getFileNameForEvent,
|
|
{
|
|
__index = function(self, event)
|
|
error(format([[Unhandled VoiceOver sound event %d "%s"]], event,
|
|
Enums.SoundEvent:GetName(event) or "???"))
|
|
end
|
|
})
|
|
|
|
function DataModules:PrepareSound(soundData)
|
|
soundData.fileName = getFileNameForEvent[soundData.event](soundData)
|
|
|
|
if soundData.fileName == nil then
|
|
return false
|
|
end
|
|
|
|
for _, module in self:GetModules() do
|
|
local data = module.SoundLengthLookupByFileName
|
|
if data then
|
|
local playerGenderedFileName = DataModules:AddPlayerGenderToFilename(soundData.fileName)
|
|
if data[playerGenderedFileName] then
|
|
soundData.fileName = playerGenderedFileName
|
|
end
|
|
local length = data[soundData.fileName]
|
|
if length then
|
|
soundData.filePath = format([[Interface\AddOns\%s\%s]], module.METADATA.AddonName,
|
|
module.GetSoundPath and module:GetSoundPath(soundData.fileName, soundData.event) or
|
|
soundData.fileName)
|
|
soundData.length = length
|
|
soundData.module = module
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function DataModules:AddPlayerGenderToFilename(fileName)
|
|
local playerGender = UnitSex("player")
|
|
|
|
if playerGender == 2 then -- male
|
|
return "m-" .. fileName
|
|
elseif playerGender == 3 then -- female
|
|
return "f-" .. fileName
|
|
else -- unknown or error
|
|
return fileName
|
|
end
|
|
end
|