init
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
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
|
||||
Reference in New Issue
Block a user