From 1e9fa2aacaaf14769851b1e4eb0756638913610a Mon Sep 17 00:00:00 2001 From: andrew6180 <16847730+andrew6180@users.noreply.github.com> Date: Sun, 16 Apr 2023 00:31:14 -0700 Subject: [PATCH] init --- AI_VoiceOver/AI_VoiceOver.toc | 9 + AI_VoiceOver/Common/DataModules.lua | 293 + AI_VoiceOver/Common/Enums.lua | 113 + AI_VoiceOver/Common/FuzzySearch.lua | 40 + AI_VoiceOver/Common/Version.lua | 23 + AI_VoiceOver/Compatibility.lua | 523 + AI_VoiceOver/Debug.lua | 15 + AI_VoiceOver/Environment.lua | 18 + .../Libs/AceAddon-3.0/AceAddon-3.0.lua | 642 ++ .../Libs/AceAddon-3.0/AceAddon-3.0.xml | 4 + .../Libs/AceConfig-3.0/AceConfig-3.0.lua | 58 + .../Libs/AceConfig-3.0/AceConfig-3.0.xml | 8 + .../AceConfigCmd-3.0/AceConfigCmd-3.0.lua | 794 ++ .../AceConfigCmd-3.0/AceConfigCmd-3.0.xml | 4 + .../AceConfigDialog-3.0.lua | 1988 ++++ .../AceConfigDialog-3.0.xml | 4 + .../AceConfigRegistry-3.0.lua | 371 + .../AceConfigRegistry-3.0.xml | 4 + .../Libs/AceConsole-3.0/AceConsole-3.0.lua | 250 + .../Libs/AceConsole-3.0/AceConsole-3.0.xml | 4 + AI_VoiceOver/Libs/AceDB-3.0/AceDB-3.0.lua | 728 ++ AI_VoiceOver/Libs/AceDB-3.0/AceDB-3.0.xml | 4 + .../AceDBOptions-3.0/AceDBOptions-3.0.lua | 420 + .../AceDBOptions-3.0/AceDBOptions-3.0.xml | 4 + .../Libs/AceEvent-3.0/AceEvent-3.0.lua | 126 + .../Libs/AceEvent-3.0/AceEvent-3.0.xml | 4 + AI_VoiceOver/Libs/AceGUI-3.0/AceGUI-3.0.lua | 1034 ++ AI_VoiceOver/Libs/AceGUI-3.0/AceGUI-3.0.xml | 28 + .../AceGUIContainer-BlizOptionsGroup.lua | 138 + .../widgets/AceGUIContainer-DropDownGroup.lua | 157 + .../widgets/AceGUIContainer-Frame.lua | 316 + .../widgets/AceGUIContainer-InlineGroup.lua | 103 + .../widgets/AceGUIContainer-ScrollFrame.lua | 215 + .../widgets/AceGUIContainer-SimpleGroup.lua | 69 + .../widgets/AceGUIContainer-TabGroup.lua | 349 + .../widgets/AceGUIContainer-TreeGroup.lua | 705 ++ .../widgets/AceGUIContainer-Window.lua | 336 + .../widgets/AceGUIWidget-Button.lua | 103 + .../widgets/AceGUIWidget-CheckBox.lua | 296 + .../widgets/AceGUIWidget-ColorPicker.lua | 190 + .../widgets/AceGUIWidget-DropDown-Items.lua | 471 + .../widgets/AceGUIWidget-DropDown.lua | 745 ++ .../widgets/AceGUIWidget-EditBox.lua | 263 + .../widgets/AceGUIWidget-Heading.lua | 78 + .../AceGUI-3.0/widgets/AceGUIWidget-Icon.lua | 140 + .../widgets/AceGUIWidget-InteractiveLabel.lua | 94 + .../widgets/AceGUIWidget-Keybinding.lua | 249 + .../AceGUI-3.0/widgets/AceGUIWidget-Label.lua | 178 + .../widgets/AceGUIWidget-MultiLineEditBox.lua | 366 + .../widgets/AceGUIWidget-Slider.lua | 280 + .../Libs/AceTimer-3.0/AceTimer-3.0.lua | 327 + .../Libs/AceTimer-3.0/AceTimer-3.0.xml | 4 + .../CallbackHandler-1.0.lua | 238 + .../CallbackHandler-1.0.xml | 4 + .../Libs/LibDBIcon-1.0/LibDBIcon-1.0.lua | 300 + AI_VoiceOver/Libs/LibDBIcon-1.0/lib.xml | 7 + .../LibDataBroker-1.1/LibDataBroker-1.1.lua | 90 + .../Libs/LibDataBroker-1.1/README.textile | 13 + AI_VoiceOver/Libs/LibStub/LibStub.lua | 51 + AI_VoiceOver/Libs/LibStub/LibStub.toc | 9 + AI_VoiceOver/Libs/LibStub/tests/test.lua | 41 + AI_VoiceOver/Libs/LibStub/tests/test2.lua | 27 + AI_VoiceOver/Libs/LibStub/tests/test3.lua | 14 + AI_VoiceOver/Libs/LibStub/tests/test4.lua | 41 + AI_VoiceOver/Options.lua | 529 + AI_VoiceOver/QuestOverlayUI.lua | 140 + AI_VoiceOver/SoundQueue.lua | 176 + AI_VoiceOver/SoundQueueUI.lua | 669 ++ AI_VoiceOver/Sounds/silence.wav | Bin 0 -> 176444 bytes AI_VoiceOver/Textures/BackgroundGradient.blp | Bin 0 -> 6740 bytes AI_VoiceOver/Textures/MinimapButton.blp | Bin 0 -> 2564 bytes AI_VoiceOver/Textures/PortraitFrameAtlas.blp | Bin 0 -> 350724 bytes .../Textures/PortraitFrameBackground.blp | Bin 0 -> 44876 bytes AI_VoiceOver/Textures/SettingsButton.blp | Bin 0 -> 2564 bytes AI_VoiceOver/Textures/SizeGrabber-Down.blp | Bin 0 -> 1540 bytes .../Textures/SizeGrabber-Highlight.blp | Bin 0 -> 1540 bytes AI_VoiceOver/Textures/SizeGrabber-Up.blp | Bin 0 -> 1540 bytes .../Textures/SoundQueueBulletAccept.blp | Bin 0 -> 1540 bytes .../Textures/SoundQueueBulletComplete.blp | Bin 0 -> 1540 bytes .../Textures/SoundQueueBulletDelete.blp | Bin 0 -> 2564 bytes .../Textures/SoundQueueBulletGossip.blp | Bin 0 -> 1540 bytes .../Textures/SoundQueueBulletProgress.blp | Bin 0 -> 1540 bytes .../Textures/SoundQueueBulletQueue.blp | Bin 0 -> 6660 bytes AI_VoiceOver/Textures/StopGossip.blp | Bin 0 -> 2564 bytes AI_VoiceOver/Textures/StopGossipMore.blp | Bin 0 -> 2564 bytes AI_VoiceOver/Textures/spacer.blp | Bin 0 -> 1540 bytes AI_VoiceOver/Utils.lua | 178 + AI_VoiceOver/VoiceOver.lua | 225 + AI_VoiceOver/addon.xml | 16 + AI_VoiceOver/embeds.xml | 14 + .../AI_VoiceOverData_Vanilla.toc | 18 + AI_VoiceOverData_Vanilla/Module.lua | 14 + .../generated/gossip_file_lookups.lua | 7980 ++++++++++++++ .../npc_name_gossip_file_lookups.lua | 7977 ++++++++++++++ .../generated/npc_name_lookups.lua | 2098 ++++ .../generated/quest_id_lookups.lua | 8650 +++++++++++++++ .../generated/questlog_npc_lookups.lua | 3036 ++++++ .../generated/sound_length_table.lua | 9558 +++++++++++++++++ README.md | 4 +- 99 files changed, 55800 insertions(+), 2 deletions(-) create mode 100644 AI_VoiceOver/AI_VoiceOver.toc create mode 100644 AI_VoiceOver/Common/DataModules.lua create mode 100644 AI_VoiceOver/Common/Enums.lua create mode 100644 AI_VoiceOver/Common/FuzzySearch.lua create mode 100644 AI_VoiceOver/Common/Version.lua create mode 100644 AI_VoiceOver/Compatibility.lua create mode 100644 AI_VoiceOver/Debug.lua create mode 100644 AI_VoiceOver/Environment.lua create mode 100644 AI_VoiceOver/Libs/AceAddon-3.0/AceAddon-3.0.lua create mode 100644 AI_VoiceOver/Libs/AceAddon-3.0/AceAddon-3.0.xml create mode 100644 AI_VoiceOver/Libs/AceConfig-3.0/AceConfig-3.0.lua create mode 100644 AI_VoiceOver/Libs/AceConfig-3.0/AceConfig-3.0.xml create mode 100644 AI_VoiceOver/Libs/AceConfig-3.0/AceConfigCmd-3.0/AceConfigCmd-3.0.lua create mode 100644 AI_VoiceOver/Libs/AceConfig-3.0/AceConfigCmd-3.0/AceConfigCmd-3.0.xml create mode 100644 AI_VoiceOver/Libs/AceConfig-3.0/AceConfigDialog-3.0/AceConfigDialog-3.0.lua create mode 100644 AI_VoiceOver/Libs/AceConfig-3.0/AceConfigDialog-3.0/AceConfigDialog-3.0.xml create mode 100644 AI_VoiceOver/Libs/AceConfig-3.0/AceConfigRegistry-3.0/AceConfigRegistry-3.0.lua create mode 100644 AI_VoiceOver/Libs/AceConfig-3.0/AceConfigRegistry-3.0/AceConfigRegistry-3.0.xml create mode 100644 AI_VoiceOver/Libs/AceConsole-3.0/AceConsole-3.0.lua create mode 100644 AI_VoiceOver/Libs/AceConsole-3.0/AceConsole-3.0.xml create mode 100644 AI_VoiceOver/Libs/AceDB-3.0/AceDB-3.0.lua create mode 100644 AI_VoiceOver/Libs/AceDB-3.0/AceDB-3.0.xml create mode 100644 AI_VoiceOver/Libs/AceDBOptions-3.0/AceDBOptions-3.0.lua create mode 100644 AI_VoiceOver/Libs/AceDBOptions-3.0/AceDBOptions-3.0.xml create mode 100644 AI_VoiceOver/Libs/AceEvent-3.0/AceEvent-3.0.lua create mode 100644 AI_VoiceOver/Libs/AceEvent-3.0/AceEvent-3.0.xml create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/AceGUI-3.0.lua create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/AceGUI-3.0.xml create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/widgets/AceGUIContainer-BlizOptionsGroup.lua create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/widgets/AceGUIContainer-DropDownGroup.lua create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/widgets/AceGUIContainer-Frame.lua create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/widgets/AceGUIContainer-InlineGroup.lua create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/widgets/AceGUIContainer-ScrollFrame.lua create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/widgets/AceGUIContainer-SimpleGroup.lua create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/widgets/AceGUIContainer-TabGroup.lua create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/widgets/AceGUIContainer-TreeGroup.lua create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/widgets/AceGUIContainer-Window.lua create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/widgets/AceGUIWidget-Button.lua create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/widgets/AceGUIWidget-CheckBox.lua create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/widgets/AceGUIWidget-ColorPicker.lua create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/widgets/AceGUIWidget-DropDown-Items.lua create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/widgets/AceGUIWidget-DropDown.lua create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/widgets/AceGUIWidget-EditBox.lua create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/widgets/AceGUIWidget-Heading.lua create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/widgets/AceGUIWidget-Icon.lua create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/widgets/AceGUIWidget-InteractiveLabel.lua create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/widgets/AceGUIWidget-Keybinding.lua create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/widgets/AceGUIWidget-Label.lua create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/widgets/AceGUIWidget-MultiLineEditBox.lua create mode 100644 AI_VoiceOver/Libs/AceGUI-3.0/widgets/AceGUIWidget-Slider.lua create mode 100644 AI_VoiceOver/Libs/AceTimer-3.0/AceTimer-3.0.lua create mode 100644 AI_VoiceOver/Libs/AceTimer-3.0/AceTimer-3.0.xml create mode 100644 AI_VoiceOver/Libs/CallbackHandler-1.0/CallbackHandler-1.0.lua create mode 100644 AI_VoiceOver/Libs/CallbackHandler-1.0/CallbackHandler-1.0.xml create mode 100644 AI_VoiceOver/Libs/LibDBIcon-1.0/LibDBIcon-1.0.lua create mode 100644 AI_VoiceOver/Libs/LibDBIcon-1.0/lib.xml create mode 100644 AI_VoiceOver/Libs/LibDataBroker-1.1/LibDataBroker-1.1.lua create mode 100644 AI_VoiceOver/Libs/LibDataBroker-1.1/README.textile create mode 100644 AI_VoiceOver/Libs/LibStub/LibStub.lua create mode 100644 AI_VoiceOver/Libs/LibStub/LibStub.toc create mode 100644 AI_VoiceOver/Libs/LibStub/tests/test.lua create mode 100644 AI_VoiceOver/Libs/LibStub/tests/test2.lua create mode 100644 AI_VoiceOver/Libs/LibStub/tests/test3.lua create mode 100644 AI_VoiceOver/Libs/LibStub/tests/test4.lua create mode 100644 AI_VoiceOver/Options.lua create mode 100644 AI_VoiceOver/QuestOverlayUI.lua create mode 100644 AI_VoiceOver/SoundQueue.lua create mode 100644 AI_VoiceOver/SoundQueueUI.lua create mode 100644 AI_VoiceOver/Sounds/silence.wav create mode 100644 AI_VoiceOver/Textures/BackgroundGradient.blp create mode 100644 AI_VoiceOver/Textures/MinimapButton.blp create mode 100644 AI_VoiceOver/Textures/PortraitFrameAtlas.blp create mode 100644 AI_VoiceOver/Textures/PortraitFrameBackground.blp create mode 100644 AI_VoiceOver/Textures/SettingsButton.blp create mode 100644 AI_VoiceOver/Textures/SizeGrabber-Down.blp create mode 100644 AI_VoiceOver/Textures/SizeGrabber-Highlight.blp create mode 100644 AI_VoiceOver/Textures/SizeGrabber-Up.blp create mode 100644 AI_VoiceOver/Textures/SoundQueueBulletAccept.blp create mode 100644 AI_VoiceOver/Textures/SoundQueueBulletComplete.blp create mode 100644 AI_VoiceOver/Textures/SoundQueueBulletDelete.blp create mode 100644 AI_VoiceOver/Textures/SoundQueueBulletGossip.blp create mode 100644 AI_VoiceOver/Textures/SoundQueueBulletProgress.blp create mode 100644 AI_VoiceOver/Textures/SoundQueueBulletQueue.blp create mode 100644 AI_VoiceOver/Textures/StopGossip.blp create mode 100644 AI_VoiceOver/Textures/StopGossipMore.blp create mode 100644 AI_VoiceOver/Textures/spacer.blp create mode 100644 AI_VoiceOver/Utils.lua create mode 100644 AI_VoiceOver/VoiceOver.lua create mode 100644 AI_VoiceOver/addon.xml create mode 100644 AI_VoiceOver/embeds.xml create mode 100644 AI_VoiceOverData_Vanilla/AI_VoiceOverData_Vanilla.toc create mode 100644 AI_VoiceOverData_Vanilla/Module.lua create mode 100644 AI_VoiceOverData_Vanilla/generated/gossip_file_lookups.lua create mode 100644 AI_VoiceOverData_Vanilla/generated/npc_name_gossip_file_lookups.lua create mode 100644 AI_VoiceOverData_Vanilla/generated/npc_name_lookups.lua create mode 100644 AI_VoiceOverData_Vanilla/generated/quest_id_lookups.lua create mode 100644 AI_VoiceOverData_Vanilla/generated/questlog_npc_lookups.lua create mode 100644 AI_VoiceOverData_Vanilla/generated/sound_length_table.lua diff --git a/AI_VoiceOver/AI_VoiceOver.toc b/AI_VoiceOver/AI_VoiceOver.toc new file mode 100644 index 00000000..f491dbd1 --- /dev/null +++ b/AI_VoiceOver/AI_VoiceOver.toc @@ -0,0 +1,9 @@ +## Interface: 30300 +## Title: VoiceOver +## Notes: Adds voiceovers to npcs. +## Version: 1.3.0 +## SavedVariables: VoiceOverDB +## X-Part-Of: VoiceOver + +embeds.xml +addon.xml diff --git a/AI_VoiceOver/Common/DataModules.lua b/AI_VoiceOver/Common/DataModules.lua new file mode 100644 index 00000000..c7bce965 --- /dev/null +++ b/AI_VoiceOver/Common/DataModules.lua @@ -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 diff --git a/AI_VoiceOver/Common/Enums.lua b/AI_VoiceOver/Common/Enums.lua new file mode 100644 index 00000000..dc57ab67 --- /dev/null +++ b/AI_VoiceOver/Common/Enums.lua @@ -0,0 +1,113 @@ +setfenv(1, VoiceOver) +---@type table +Enums = {} + +---@enum SoundEvent +Enums.SoundEvent = +{ + QuestAccept = 1, + QuestProgress = 2, + QuestComplete = 3, + QuestGreeting = 4, + Gossip = 5, +} +function Enums.SoundEvent:IsQuestEvent(event) + return event == self.QuestAccept or event == self.QuestProgress or event == self.QuestComplete +end +function Enums.SoundEvent:IsGossipEvent(event) + return event == self.Gossip or event == self.QuestGreeting +end + +function Enums.SoundEvent:ToString(event) + if event == self.QuestAccept then + return "accept" + elseif event == self.QuestProgress then + return "progress" + elseif event == self.QuestComplete then + return "complete" + elseif event == self.QuestGreeting then + return "greeting" + elseif event == self.Gossip then + return "gossip" + else + return nil + end +end + +---@enum GossipFrequency +Enums.GossipFrequency = +{ + Always = 1, + OncePerQuestNPC = 2, + OncePerNPC = 3, + Never = 4, +} + +---@enum SoundChannel +Enums.SoundChannel = +{ + Master = 1, + Sound = 2, + Music = 3, + Ambience = 4, + Dialog = 5, +} + +---@enum GUID +Enums.GUID = +{ + Player = 2, + Item = 3, + Creature = 8, + Vehicle = 9, + GameObject = 11, +} +function Enums.GUID:IsCreature(type) + return type == self.Creature or type == self.Vehicle +end +function Enums.GUID:CanHaveID(type) + return type == self.Creature or type == self.Vehicle or type == self.GameObject +end + + + +---@class Enum +local Enum = {} +Enum.__index = Enum + +---@param value number Enum element value +---@return string|nil name Enum element name +function Enum:GetName(value) + for k, v in pairs(self) do + if v == value then + return k + end + end +end + +---@return table +function Enum:GetValueToNameMap() + local result = {} + for k, v in pairs(self) do + result[v] = k + end + return result +end + +for name, enum in pairs(Enums) do + if type(enum) == "table" then + local metatable + for k, v in pairs(enum) do + -- Move all functions from the enum to its metatable (which also "inherits" from Enum) + if type(v) == "function" then + if not metatable then + metatable = setmetatable({}, { __index = Enum }) + metatable.__index = metatable + end + metatable[k] = v + enum[k] = nil + end + end + Enums[name] = setmetatable(enum, metatable or Enum) + end +end diff --git a/AI_VoiceOver/Common/FuzzySearch.lua b/AI_VoiceOver/Common/FuzzySearch.lua new file mode 100644 index 00000000..684f1072 --- /dev/null +++ b/AI_VoiceOver/Common/FuzzySearch.lua @@ -0,0 +1,40 @@ +setfenv(1, VoiceOver) +local function jaccardSimilarity(a, b) + local tokens_a, tokens_b = {}, {} + for token in string.gmatch(a, "%S+") do tokens_a[token] = true end + for token in string.gmatch(b, "%S+") do tokens_b[token] = true end + + local intersection, union = 0, 0 + for token in pairs(tokens_a) do + union = union + 1 + if tokens_b[token] then + intersection = intersection + 1 + end + end + for token in pairs(tokens_b) do + if not tokens_a[token] then + union = union + 1 + end + end + + return intersection / union +end + +function FuzzySearchBestKeys(query, tableVar) + local best_result = nil + local max_similarity = -1 + + for entry, value in pairs(tableVar) do + local similarity = jaccardSimilarity(query, entry) + if similarity > max_similarity then + max_similarity = similarity + best_result = { + value = value, + text = entry, + similarity = similarity + } + end + end + + return best_result +end \ No newline at end of file diff --git a/AI_VoiceOver/Common/Version.lua b/AI_VoiceOver/Common/Version.lua new file mode 100644 index 00000000..f71a98ec --- /dev/null +++ b/AI_VoiceOver/Common/Version.lua @@ -0,0 +1,23 @@ +setfenv(1, VoiceOver) +Version = {} + +local CLIENT_VERSION, BUILD, _, INTERFACE_VERSION = GetBuildInfo() + +Version.Client = CLIENT_VERSION +Version.Build = BUILD +Version.Interface = INTERFACE_VERSION or 0 +Version.IsAnyLegacy = WOW_PROJECT_ID == nil or nil +Version.IsLegacyVanilla = Version.IsAnyLegacy and Version.Interface == 0 or nil +Version.IsLegacyWrath = Version.IsAnyLegacy and Version.Interface == 30300 or nil +Version.IsAnyRetail = not Version.IsAnyLegacy or nil +Version.IsRetailVanilla = Version.IsAnyRetail and WOW_PROJECT_ID == WOW_PROJECT_CLASSIC or nil +Version.IsRetailBurningCrusade = Version.IsAnyRetail and WOW_PROJECT_ID == WOW_PROJECT_BURNING_CRUSADE_CLASSIC or nil +Version.IsRetailWrath = Version.IsAnyRetail and WOW_PROJECT_ID == WOW_PROJECT_WRATH_CLASSIC or nil +Version.IsRetailMainline = Version.IsAnyRetail and WOW_PROJECT_ID == WOW_PROJECT_MAINLINE or nil + +function Version:IsBelowLegacyVersion(version) + return self.IsAnyLegacy and self.Interface < version or nil +end +function Version:IsRetailOrAboveLegacyVersion(version) + return self.IsAnyRetail or self.Interface >= version or nil +end diff --git a/AI_VoiceOver/Compatibility.lua b/AI_VoiceOver/Compatibility.lua new file mode 100644 index 00000000..9e54b5f6 --- /dev/null +++ b/AI_VoiceOver/Compatibility.lua @@ -0,0 +1,523 @@ +setfenv(1, VoiceOver) + +if not strsplit then + function strsplit(delimiter, text) + local result = {} + local from = 1 + local delim_from, delim_to = string.find(text, delimiter, from) + while delim_from do + table.insert(result, string.sub(text, from, delim_from - 1)) + from = delim_to + 1 + delim_from, delim_to = string.find(text, delimiter, from) + end + table.insert(result, string.sub(text, from)) + return unpack(result) + end +end + +if not GetAddOnEnableState then + function GetAddOnEnableState(character, addon) + addon = addon or character -- GetAddOnEnableState([character], addon) + local name, _, _, _, loadable, reason = _G.GetAddOnInfo(addon) + if not name or not loadable and reason == "DISABLED" then + return 0 + end + return 2 + end + + function GetAddOnInfo(indexOrName) + local name, title, notes, enabled, loadable, reason, security, newVersion = _G.GetAddOnInfo(indexOrName) + return name, title, notes, loadable, reason, security, newVersion + end +end + +if not GetQuestID then + local source, text + local old_QUEST_DETAIL = Addon.QUEST_DETAIL + local old_QUEST_PROGRESS = Addon.QUEST_PROGRESS + local old_QUEST_COMPLETE = Addon.QUEST_COMPLETE + function Addon:QUEST_DETAIL() source = "accept" text = GetQuestText() old_QUEST_DETAIL(self) end + function Addon:QUEST_PROGRESS() source = "progress" text = GetProgressText() old_QUEST_PROGRESS(self) end + function Addon:QUEST_COMPLETE() source = "complete" text = GetRewardText() old_QUEST_COMPLETE(self) end + function GetQuestID() + local npcName = Utils:GetNPCName() + if Utils:IsNPCPlayer() then + -- Can't do anything about quest sharing currently, because we need the original questgiver's name to obtain quest ID, and we need quest ID to obtain the questgiver's name + return 0 + end + + return DataModules:GetQuestID(source, GetTitleText(), npcName, text) or 0 + end +end + +if not QUESTS_DISPLAYED then + if QuestLogScrollFrame then + QUESTS_DISPLAYED = getn(QuestLogScrollFrame.buttons) + end +end + +-- Not sure when exactly were UI-Cursor-Move and UI-Cursor-SizeRight added, but the former was present in 6.0.1 +if Version:IsBelowLegacyVersion(60000) then + function SetCursor() end +end + +Enums.GUID.Player = tonumber("0000", 16) +Enums.GUID.Item = tonumber("4000", 16) +Enums.GUID.Creature = tonumber("F130", 16) +Enums.GUID.Vehicle = tonumber("F150", 16) +Enums.GUID.GameObject = tonumber("F110", 16) + +function Utils:GetGUIDType(guid) + return guid and tonumber(guid:sub(3, 3 + 4 - 1), 16) +end + +function Utils:GetIDFromGUID(guid) + if not guid then + return + end + local type = assert(self:GetGUIDType(guid), format([[Failed to determine the type of GUID "%s"]], guid)) + assert(Enums.GUID:GetName(type), format([[Unknown GUID type %d]], type)) + assert(Enums.GUID:CanHaveID(type), format([[GUID "%s" does not contain ID]], guid)) + return tonumber(guid:sub(7, 7 + 6 - 1), 16) +end + +function Utils:MakeGUID(type, id) + assert(Enums.GUID:CanHaveID(type), format("GUID of type %d (%s) cannot contain ID", type, Enums.GUID:GetName(type) or "Unknown")) + return format("0x%04X%06X%06X", type, id, 0) +end + +-- Patch 6.0.2 (2014-10-14): Removed returns 'questTag' and 'isDaily'. Added returns 'frequency', 'isOnMap', 'hasLocalPOI', 'isTask', and 'isStory'. +if Version:IsBelowLegacyVersion(60000) then + function GetQuestLogTitle(questIndex) + local title, level, questTag, suggestedGroup, isHeader, isCollapsed, isComplete, isDaily, questID, displayQuestID = _G.GetQuestLogTitle(questIndex) + local frequency = isDaily and 2 or 1 + return title, level, suggestedGroup, isHeader, isCollapsed, isComplete, frequency, questID + end +end + +local FrameMixins = {} +local ModelMixins = {} +local hookFrame +local hookModel +function CreateFrame(frameType, name, parent, template) + if UIParent.SetBackdrop and template == "BackdropTemplate" then + template = nil + end + + local frame = _G.CreateFrame(frameType, name, parent, template) + for k, v in pairs(FrameMixins) do + if not frame[k] then + frame[k] = v + end + end + if hookFrame then + hookFrame(frame) + end + if frameType == "Model" or frameType == "PlayerModel" or frameType == "DressUpModel" then + for k, v in pairs(ModelMixins) do + if not frame[k] then + frame[k] = v + end + end + if hookModel then + hookModel(frame) + end + end + return frame +end + +function FrameMixins:SetResizeBounds(minWidth, minHeight, maxWidth, maxHeight) + self:SetMinResize(minWidth, minHeight) + if maxWidth and maxHeight then + self:SetMaxResize(maxWidth, maxHeight) + end +end +function ModelMixins:SetAnimation(animation) + self:SetSequence(animation) +end +function ModelMixins:SetCustomCamera(camera) + self:SetCamera(camera) +end +-- Patch 7.0.3 (2016-07-19): Added. +if Version:IsBelowLegacyVersion(70000) then + local modelToFileID = { + ["Original"] = { + ["interface/buttons/talktomequestion_white"] = 130737, + + ["character/bloodelf/female/bloodelffemale"] = 116921, + ["character/bloodelf/male/bloodelfmale"] = 117170, + ["character/broken/female/brokenfemale"] = 117400, + ["character/broken/male/brokenmale"] = 117412, + ["character/draenei/female/draeneifemale"] = 117437, + ["character/draenei/male/draeneimale"] = 117721, + ["character/dwarf/female/dwarffemale"] = 118135, + ["character/dwarf/female/dwarffemale_hd"] = 950080, + ["character/dwarf/female/dwarffemale_npc"] = 950080, + ["character/dwarf/male/dwarfmale"] = 118355, + ["character/dwarf/male/dwarfmale_hd"] = 878772, + ["character/dwarf/male/dwarfmale_npc"] = 878772, + ["character/felorc/female/felorcfemale"] = 118652, + ["character/felorc/male/felorcmale"] = 118653, + ["character/felorc/male/felorcmaleaxe"] = 118654, + ["character/felorc/male/felorcmalesword"] = 118667, + ["character/foresttroll/male/foresttrollmale"] = 118798, + ["character/gnome/female/gnomefemale"] = 119063, + ["character/gnome/female/gnomefemale_hd"] = 940356, + ["character/gnome/female/gnomefemale_npc"] = 940356, + ["character/gnome/male/gnomemale"] = 119159, + ["character/gnome/male/gnomemale_hd"] = 900914, + ["character/gnome/male/gnomemale_npc"] = 900914, + ["character/goblin/female/goblinfemale"] = 119369, + ["character/goblin/male/goblinmale"] = 119376, + ["character/goblinold/male/goblinoldmale"] = 119376, + ["character/human/female/humanfemale"] = 119563, + ["character/human/female/humanfemale_hd"] = 1000764, + ["character/human/female/humanfemale_npc"] = 1000764, + ["character/human/male/humanmale"] = 119940, + ["character/human/male/humanmale_cata"] = 119940, + ["character/human/male/humanmale_hd"] = 1011653, + ["character/human/male/humanmale_npc"] = 1011653, + ["character/icetroll/male/icetrollmale"] = 232863, + ["character/naga_/female/naga_female"] = 120263, + ["character/naga_/male/naga_male"] = 120294, + ["character/nightelf/female/nightelffemale"] = 120590, + ["character/nightelf/female/nightelffemale_hd"] = 921844, + ["character/nightelf/female/nightelffemale_npc"] = 921844, + ["character/nightelf/male/nightelfmale"] = 120791, + ["character/nightelf/male/nightelfmale_hd"] = 974343, + ["character/nightelf/male/nightelfmale_npc"] = 974343, + ["character/northrendskeleton/male/northrendskeletonmale"] = 233367, + ["character/orc/female/orcfemale"] = 121087, + ["character/orc/female/orcfemale_npc"] = 121087, + ["character/orc/male/orcmale"] = 121287, + ["character/orc/male/orcmale_hd"] = 917116, + ["character/orc/male/orcmale_npc"] = 917116, + ["character/scourge/female/scourgefemale"] = 121608, + ["character/scourge/female/scourgefemale_hd"] = 997378, + ["character/scourge/female/scourgefemale_npc"] = 997378, + ["character/scourge/male/scourgemale"] = 121768, + ["character/scourge/male/scourgemale_hd"] = 959310, + ["character/scourge/male/scourgemale_npc"] = 959310, + ["character/skeleton/male/skeletonmale"] = 121942, + ["character/taunka/male/taunkamale"] = 233878, + ["character/tauren/female/taurenfemale"] = 121961, + ["character/tauren/female/taurenfemale_hd"] = 986648, + ["character/tauren/female/taurenfemale_npc"] = 986648, + ["character/tauren/male/taurenmale"] = 122055, + ["character/tauren/male/taurenmale_hd"] = 968705, + ["character/tauren/male/taurenmale_npc"] = 968705, + ["character/troll/female/trollfemale"] = 122414, + ["character/troll/female/trollfemale_hd"] = 1018060, + ["character/troll/female/trollfemale_npc"] = 1018060, + ["character/troll/male/trollmale"] = 122560, + ["character/troll/male/trollmale_hd"] = 1022938, + ["character/troll/male/trollmale_npc"] = 1022938, + ["character/tuskarr/male/tuskarrmale"] = 122738, + ["character/vrykul/male/vrykulmale"] = 122815, + }, + ["HD"] = { + ["character/scourge/female/scourgefemale"] = 997378, + }, + } + local function CleanupModelName(model) + return model:lower():gsub("\\", "/"):gsub("%.m2", ""):gsub("%.mdx", "") + end + function ModelMixins:GetModelFileID() + local model = self:GetModel() + if model and type(model) == "string" then + model = CleanupModelName(model) + local models = modelToFileID[Utils:GetCurrentModelSet()] or modelToFileID["Original"] + return models[model] or modelToFileID["Original"][model] + end + end +end + +if Version.IsLegacyWrath then + + function Utils:GetQuestLogScrollOffset() + return HybridScrollFrame_GetOffset(QuestLogScrollFrame) + end + + function Utils:GetQuestLogTitleFrame(index) + return _G["QuestLogScrollFrameButton" .. index] + end + + function Utils:GetQuestLogTitleNormalText(index) + return _G["QuestLogScrollFrameButton" .. index .. "NormalText"] + end + + function Utils:GetQuestLogTitleCheck(index) + return _G["QuestLogScrollFrameButton" .. index .. "Check"] + end + + local prefix + local QuestLogTitleButton_Resize = QuestLogTitleButton_Resize + function QuestOverlayUI:UpdateQuestTitle(questLogTitleFrame, playButton, normalText, questCheck) + if not prefix then + local text = normalText:GetText() + for i = 1, 20 do + normalText:SetText(string.rep(" ", i)) + if normalText:GetStringWidth() >= 24 then + prefix = normalText:GetText() + break + end + end + prefix = prefix or " " + normalText:SetText(text) + end + + playButton:SetPoint("LEFT", normalText, "LEFT", 4, 0) + normalText:SetText(prefix .. (normalText:GetText() or ""):trim()) + QuestLogTitleButton_Resize(questLogTitleFrame) + end + + hooksecurefunc(Addon, "OnInitialize", function() + QuestLogScrollFrame.update = QuestLog_Update + end) + + function Utils:WillSoundPlay(soundData) + return soundData.fileName and soundData.fileName ~= "missingSound" and soundData.length ~= nil + end + + function hookModel(self) + local function HasModelLoaded(self) + local model = self:GetModel() + return model and type(model) == "string" and self:GetModelFileID() ~= 130737 + end + self._sequence = 0 + hooksecurefunc(self, "ClearModel", function(self) + self._awaitingModel = nil + self._camera = nil + self._sequence = 0 + self._sequenceStart = nil + end) + local oldSetSequence = self.SetSequence + function self:SetSequence(sequence) + self._sequence = sequence + self._sequenceStart = GetTime() + if not self._awaitingModel then + oldSetSequence(self, sequence) + end + end + local oldSetCreature = self.SetCreature + function self:SetCreature(id) + self:ClearModel() + self:SetModel([[Interface\Buttons\TalkToMeQuestion_White.mdx]]) + oldSetCreature(self, id) + self._awaitingModel = not HasModelLoaded(self) + if self._awaitingModel then + self:SetPosition(5, 0, 2) + end + end + local oldSetCamera = self.SetCamera + function self:SetCamera(id) + self._camera = id + if not self._awaitingModel then + oldSetCamera(self, id) + end + end + self:HookScript("OnUpdate", function(self, elapsed) + if self._awaitingModel and HasModelLoaded(self) then + self._awaitingModel = nil + self:SetPosition(0, 0, 0) + + if self._sequence ~= 0 then + self:SetSequence(self._sequence) + end + end + if self._sequence ~= 0 and not self._awaitingModel then + self:SetSequenceTime(self._sequence, (GetTime() - self._sequenceStart) * 1000) + end + end) + end + + hooksecurefunc(SoundQueueUI, "InitPortrait", function(self) + self.frame.portrait.pause:HookScript("OnEnter", function() + if self.frame.portrait.model._awaitingModel then + GameTooltip:SetOwner(self.frame.portrait.pause, "ANCHOR_NONE") + GameTooltip:SetPoint("BOTTOMLEFT", self.frame.portrait.pause, "BOTTOMRIGHT", 4, -4) + GameTooltip:SetText("Uncached NPC", HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b) + GameTooltip:AddLine("Encounter this NPC in the world again to be able to see their model.", NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b, 1) + GameTooltip:Show() + end + end) + self.frame.portrait.pause:HookScript("OnLeave", GameTooltip_Hide) + end) + + --[[ + Here begins the code the plays the VO over music channel in order to support the ability to pause/stop the VO. + 3.3.5's PlaySound/PlaySoundFile cannot be stopped by any means short of restarting the whole sound system (freezes the client for a couple of seconds). + But PlayMusic can be stopped with StopMusic. This, however, causes the currently played script music to fade out instead of cutting, + which is a problem, because by letting this happen we'll hear the VO looping until it fully fades out. This can be worked around + by PlayMusic'ing another file (even one that doesn't exist), as that causes the script music to be instantly interrupted. + Toggling Sound_EnableMusic cvar off-and-on additionally allows us to interrupt the current in-game background music. + The whole process looks as follows: + 1. Sound queue requests to start playing the VO by calling Utils:PlaySound + 2. Music volume is smoothly lowered to 0 over the config.FadeOutMusic duration + 3. In-game background music is instantly stopped by toggling Sound_EnableMusic cvar off-and-on + 4. Music volume is instantly changed to config.Volume level + 5. VO sound file is played on the music channel + 6. Once the VO's duration has ran out (soundData.stopSoundTimer) - silence.wav is played as music to instantly stop the VO and prevent it from looping + 7. Sound queue requests to stop playing the VO by calling Utils:StopSound (either due to pause or soundData being removed from the queue) - silence.wav is played again to interrupt the VO in case it hasn't finished playing naturally + 8. Music volume is instantly changed to 0 + 9. Music volume is smoothly raised to back to the pre-VO level over the config.FadeOutMusic duration + 10. In-game background music is removed by calling StopMusic() + ]] + local function GetCurrentVolume() + return tonumber(GetCVar("Sound_MusicVolume")) or 1 + end + + -- Functions that deal with temporarily changing player's sound settings to utilize the music channel for VO playback + local prev_Sound_EnableMusic + local prev_Sound_MusicVolume + local function ReplaceCVars() + if prev_Sound_EnableMusic == nil then + prev_Sound_EnableMusic = GetCVar("Sound_EnableMusic") + prev_Sound_MusicVolume = GetCVar("Sound_MusicVolume") + SetCVar("Sound_EnableMusic", 1) + end + end + local function RestoreCVars() + if prev_Sound_EnableMusic ~= nil then + SetCVar("Sound_EnableMusic", prev_Sound_EnableMusic) + SetCVar("Sound_MusicVolume", prev_Sound_MusicVolume) + prev_Sound_EnableMusic = nil + prev_Sound_MusicVolume = nil + end + end + + -- Functions that deal with smoothly changing the music channel's volume to avoid abrupt changes + local slideVolumeTarget + local slideVolumeRate + local slideVolumeCallback + local EPS_VOLUME = 0.01 + local function GetMusicFadeOutDuration() + if tonumber(prev_Sound_EnableMusic) == 0 or tonumber(prev_Sound_MusicVolume) == 0 then + return 0 + end + return Addon.db.profile.LegacyWrath.PlayOnMusicChannel.FadeOutMusic or 0 + end + local function StopSlideVolume() + slideVolumeTarget = nil + slideVolumeRate = nil + slideVolumeCallback = nil + end + local function SlideVolume(target, callback) + local duration = GetMusicFadeOutDuration() + if duration <= 0 then + -- Instantly change the volume if the player had reduced the duration all the way to 0 + return false + end + local current = GetCurrentVolume() + if math.abs(target - current) <= EPS_VOLUME then + -- Instantly "change" the volume if it's already fuzzy-equal to the target volume, and cancel the ongoing slide volume ("remove currently played sound from queue" case) + StopSlideVolume() + return false + end + -- Interpolate towards the target volume over the configured duration + slideVolumeTarget = target + slideVolumeRate = (target - current) / duration + slideVolumeCallback = callback + return true + end + local volumeFrame = CreateFrame("Frame", "VoiceOverSlideVolumeFrame", UIParent) + volumeFrame:RegisterEvent("PLAYER_LOGOUT") + volumeFrame:HookScript("OnEvent", function(self, event) + if event == "PLAYER_LOGOUT" then + StopSlideVolume() + RestoreCVars() + end + end) + volumeFrame:HookScript("OnUpdate", function(self, elapsed) + if slideVolumeRate then + local current = GetCurrentVolume() + local target = slideVolumeTarget + local next = current + slideVolumeRate * elapsed + local finished = false + if math.abs(target - current) <= EPS_VOLUME or current < target and next >= target or current > target and next <= target then + next = target + finished = true + end + SetCVar("Sound_MusicVolume", next) + if finished then + if slideVolumeCallback then + slideVolumeCallback() + end + StopSlideVolume() + end + end + end) + + function Utils:PlaySound(soundData) + soundData.delay = nil + if not Addon.db.profile.LegacyWrath.PlayOnMusicChannel.Enabled then + -- Play VO as a sound, but have no ability to stop it + _G.PlaySoundFile(soundData.filePath) + return + end + + soundData.handle = 1 -- Just put something here to flag the sound as stoppable + + ReplaceCVars() + local function Play() + -- Hack to instantly interrupt the music + SetCVar("Sound_EnableMusic", 0) + SetCVar("Sound_EnableMusic", 1) + + SetCVar("Sound_MusicVolume", Addon.db.profile.LegacyWrath.PlayOnMusicChannel.Volume) + PlayMusic(soundData.filePath) + + soundData.stopSoundTimer = Addon:ScheduleTimer(function() + PlayMusic([[Interface\AddOns\AI_VoiceOver\Sounds\silence.wav]]) -- Instantly interrupt the VO sound + end, soundData.length) + end + if SlideVolume(0, Play) then + soundData.delay = GetMusicFadeOutDuration() + else + Play() + end + end + + function Utils:StopSound(soundData) + if not soundData.handle then + -- VO was played as a sound - we cannot stop it + return + end + + Addon:CancelTimer(soundData.stopSoundTimer) + soundData.stopSoundTimer = nil + + PlayMusic([[Interface\AddOns\AI_VoiceOver\Sounds\silence.wav]]) -- Instantly interrupt the VO sound + SetCVar("Sound_MusicVolume", 0) + + local function ResumeMusic() + StopMusic() + RestoreCVars() + end + if not SlideVolume(tonumber(prev_Sound_MusicVolume) or 1, ResumeMusic) then + ResumeMusic() + end + end + + -- Frame fade-in animation to help alleviate the UX damage caused by delaying the VO + hooksecurefunc(SoundQueueUI, "InitDisplay", function(self) + local fadeIn = self.frame:CreateAnimationGroup() + local animation = fadeIn:CreateAnimation("Alpha") + animation:SetOrder(1) + animation:SetDuration(0) + animation:SetChange(-1) + animation = fadeIn:CreateAnimation("Alpha") + animation:SetOrder(2) + animation:SetDuration(0.75) + animation:SetChange(1) + animation:SetSmoothing("OUT") + self.frame:HookScript("OnShow", function(self) + fadeIn:Stop() + local duration = Addon.db.profile.LegacyWrath.PlayOnMusicChannel.Enabled and GetMusicFadeOutDuration() or 0 + if duration > 0 then + animation:SetDuration(duration) + fadeIn:Play() + end + end) + end) +end diff --git a/AI_VoiceOver/Debug.lua b/AI_VoiceOver/Debug.lua new file mode 100644 index 00000000..2998b60f --- /dev/null +++ b/AI_VoiceOver/Debug.lua @@ -0,0 +1,15 @@ +setfenv(1, VoiceOver) +Debug = {} + +function Debug:Print(msg, header) + if Addon.db.profile.DebugEnabled then + if header then + print(Utils:ColorizeText("VoiceOver", NORMAL_FONT_COLOR_CODE) .. + Utils:ColorizeText(" (" .. header .. ")", GRAY_FONT_COLOR_CODE) .. + " - " .. msg) + else + print(Utils:ColorizeText("VoiceOver", NORMAL_FONT_COLOR_CODE) .. + " - " .. msg) + end + end +end diff --git a/AI_VoiceOver/Environment.lua b/AI_VoiceOver/Environment.lua new file mode 100644 index 00000000..8eb61d7c --- /dev/null +++ b/AI_VoiceOver/Environment.lua @@ -0,0 +1,18 @@ +--[[ +Use the addon's private table as an isolated environment. +By using { __index = _G } metatable we're allowing all global lookups to transparently fallback to the game-wide +globals table, while the private table itself will act as a thin layer on top of the game-wide globals table, +allowing us to have our own global variables isolated from the rest of the game. + +This accomplishes several goals: +1. Prevents addon-specific "globals" from leaking to game-wide global namespace _G +2. Optionally retains the ability to access these "globals" via the only exposed global variable "VoiceOver" +3. Allows us to make overrides for WoW API's global funtions and variables without actually touching + the real global namespace, making these overrides visible only to this addon. + This will be useful mainly for adding backwards-compatibility with older WoW clients. + +setfenv(1, VoiceOver) must be added to every .lua file to allow it to work within this environment, +and this Environment file must be loaded before all others +]] + +VoiceOver = setmetatable({}, { __index = _G }) diff --git a/AI_VoiceOver/Libs/AceAddon-3.0/AceAddon-3.0.lua b/AI_VoiceOver/Libs/AceAddon-3.0/AceAddon-3.0.lua new file mode 100644 index 00000000..6c89654b --- /dev/null +++ b/AI_VoiceOver/Libs/AceAddon-3.0/AceAddon-3.0.lua @@ -0,0 +1,642 @@ +--- **AceAddon-3.0** provides a template for creating addon objects. +-- It'll provide you with a set of callback functions that allow you to simplify the loading +-- process of your addon.\\ +-- Callbacks provided are:\\ +-- * **OnInitialize**, which is called directly after the addon is fully loaded. +-- * **OnEnable** which gets called during the PLAYER_LOGIN event, when most of the data provided by the game is already present. +-- * **OnDisable**, which is only called when your addon is manually being disabled. +-- @usage +-- -- A small (but complete) addon, that doesn't do anything, +-- -- but shows usage of the callbacks. +-- local MyAddon = LibStub("AceAddon-3.0"):NewAddon("MyAddon") +-- +-- function MyAddon:OnInitialize() +-- -- do init tasks here, like loading the Saved Variables, +-- -- or setting up slash commands. +-- end +-- +-- function MyAddon:OnEnable() +-- -- Do more initialization here, that really enables the use of your addon. +-- -- Register Events, Hook functions, Create Frames, Get information from +-- -- the game that wasn't available in OnInitialize +-- end +-- +-- function MyAddon:OnDisable() +-- -- Unhook, Unregister Events, Hide frames that you created. +-- -- You would probably only use an OnDisable if you want to +-- -- build a "standby" mode, or be able to toggle modules on/off. +-- end +-- @class file +-- @name AceAddon-3.0.lua +-- @release $Id: AceAddon-3.0.lua 895 2009-12-06 16:28:55Z nevcairiel $ + +local MAJOR, MINOR = "AceAddon-3.0", 5 +local AceAddon, oldminor = LibStub:NewLibrary(MAJOR, MINOR) + +if not AceAddon then return end -- No Upgrade needed. + +AceAddon.frame = AceAddon.frame or CreateFrame("Frame", "AceAddon30Frame") -- Our very own frame +AceAddon.addons = AceAddon.addons or {} -- addons in general +AceAddon.statuses = AceAddon.statuses or {} -- statuses of addon. +AceAddon.initializequeue = AceAddon.initializequeue or {} -- addons that are new and not initialized +AceAddon.enablequeue = AceAddon.enablequeue or {} -- addons that are initialized and waiting to be enabled +AceAddon.embeds = AceAddon.embeds or setmetatable({}, {__index = function(tbl, key) tbl[key] = {} return tbl[key] end }) -- contains a list of libraries embedded in an addon + +-- Lua APIs +local tinsert, tconcat, tremove = table.insert, table.concat, table.remove +local fmt, tostring = string.format, tostring +local select, pairs, next, type, unpack = select, pairs, next, type, unpack +local loadstring, assert, error = loadstring, assert, error +local setmetatable, getmetatable, rawset, rawget = setmetatable, getmetatable, rawset, rawget + +-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded +-- List them here for Mikk's FindGlobals script +-- GLOBALS: LibStub, IsLoggedIn, geterrorhandler + +--[[ + xpcall safecall implementation +]] +local xpcall = xpcall + +local function errorhandler(err) + return geterrorhandler()(err) +end + +local function CreateDispatcher(argCount) + local code = [[ + local xpcall, eh = ... + local method, ARGS + local function call() return method(ARGS) end + + local function dispatch(func, ...) + method = func + if not method then return end + ARGS = ... + return xpcall(call, eh) + end + + return dispatch + ]] + + local ARGS = {} + for i = 1, argCount do ARGS[i] = "arg"..i end + code = code:gsub("ARGS", tconcat(ARGS, ", ")) + return assert(loadstring(code, "safecall Dispatcher["..argCount.."]"))(xpcall, errorhandler) +end + +local Dispatchers = setmetatable({}, {__index=function(self, argCount) + local dispatcher = CreateDispatcher(argCount) + rawset(self, argCount, dispatcher) + return dispatcher +end}) +Dispatchers[0] = function(func) + return xpcall(func, errorhandler) +end + +local function safecall(func, ...) + -- we check to see if the func is passed is actually a function here and don't error when it isn't + -- this safecall is used for optional functions like OnInitialize OnEnable etc. When they are not + -- present execution should continue without hinderance + if type(func) == "function" then + return Dispatchers[select('#', ...)](func, ...) + end +end + +-- local functions that will be implemented further down +local Enable, Disable, EnableModule, DisableModule, Embed, NewModule, GetModule, GetName, SetDefaultModuleState, SetDefaultModuleLibraries, SetEnabledState, SetDefaultModulePrototype + +-- used in the addon metatable +local function addontostring( self ) return self.name end + +--- Create a new AceAddon-3.0 addon. +-- Any libraries you specified will be embeded, and the addon will be scheduled for +-- its OnInitialize and OnEnable callbacks. +-- The final addon object, with all libraries embeded, will be returned. +-- @paramsig [object ,]name[, lib, ...] +-- @param object Table to use as a base for the addon (optional) +-- @param name Name of the addon object to create +-- @param lib List of libraries to embed into the addon +-- @usage +-- -- Create a simple addon object +-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("MyAddon", "AceEvent-3.0") +-- +-- -- Create a Addon object based on the table of a frame +-- local MyFrame = CreateFrame("Frame") +-- MyAddon = LibStub("AceAddon-3.0"):NewAddon(MyFrame, "MyAddon", "AceEvent-3.0") +function AceAddon:NewAddon(objectorname, ...) + local object,name + local i=1 + if type(objectorname)=="table" then + object=objectorname + name=... + i=2 + else + name=objectorname + end + if type(name)~="string" then + error(("Usage: NewAddon([object,] name, [lib, lib, lib, ...]): 'name' - string expected got '%s'."):format(type(name)), 2) + end + if self.addons[name] then + error(("Usage: NewAddon([object,] name, [lib, lib, lib, ...]): 'name' - Addon '%s' already exists."):format(name), 2) + end + + object = object or {} + object.name = name + + local addonmeta = {} + local oldmeta = getmetatable(object) + if oldmeta then + for k, v in pairs(oldmeta) do addonmeta[k] = v end + end + addonmeta.__tostring = addontostring + + setmetatable( object, addonmeta ) + self.addons[name] = object + object.modules = {} + object.defaultModuleLibraries = {} + Embed( object ) -- embed NewModule, GetModule methods + self:EmbedLibraries(object, select(i,...)) + + -- add to queue of addons to be initialized upon ADDON_LOADED + tinsert(self.initializequeue, object) + return object +end + + +--- Get the addon object by its name from the internal AceAddon registry. +-- Throws an error if the addon object cannot be found (except if silent is set). +-- @param name unique name of the addon object +-- @param silent if true, the addon is optional, silently return nil if its not found +-- @usage +-- -- Get the Addon +-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon") +function AceAddon:GetAddon(name, silent) + if not silent and not self.addons[name] then + error(("Usage: GetAddon(name): 'name' - Cannot find an AceAddon '%s'."):format(tostring(name)), 2) + end + return self.addons[name] +end + +-- - Embed a list of libraries into the specified addon. +-- This function will try to embed all of the listed libraries into the addon +-- and error if a single one fails. +-- +-- **Note:** This function is for internal use by :NewAddon/:NewModule +-- @paramsig addon, [lib, ...] +-- @param addon addon object to embed the libs in +-- @param lib List of libraries to embed into the addon +function AceAddon:EmbedLibraries(addon, ...) + for i=1,select("#", ... ) do + local libname = select(i, ...) + self:EmbedLibrary(addon, libname, false, 4) + end +end + +-- - Embed a library into the addon object. +-- This function will check if the specified library is registered with LibStub +-- and if it has a :Embed function to call. It'll error if any of those conditions +-- fails. +-- +-- **Note:** This function is for internal use by :EmbedLibraries +-- @paramsig addon, libname[, silent[, offset]] +-- @param addon addon object to embed the library in +-- @param libname name of the library to embed +-- @param silent marks an embed to fail silently if the library doesn't exist (optional) +-- @param offset will push the error messages back to said offset, defaults to 2 (optional) +function AceAddon:EmbedLibrary(addon, libname, silent, offset) + local lib = LibStub:GetLibrary(libname, true) + if not lib and not silent then + error(("Usage: EmbedLibrary(addon, libname, silent, offset): 'libname' - Cannot find a library instance of %q."):format(tostring(libname)), offset or 2) + elseif lib and type(lib.Embed) == "function" then + lib:Embed(addon) + tinsert(self.embeds[addon], libname) + return true + elseif lib then + error(("Usage: EmbedLibrary(addon, libname, silent, offset): 'libname' - Library '%s' is not Embed capable"):format(libname), offset or 2) + end +end + +--- Return the specified module from an addon object. +-- Throws an error if the addon object cannot be found (except if silent is set) +-- @name //addon//:GetModule +-- @paramsig name[, silent] +-- @param name unique name of the module +-- @param silent if true, the module is optional, silently return nil if its not found (optional) +-- @usage +-- -- Get the Addon +-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon") +-- -- Get the Module +-- MyModule = MyAddon:GetModule("MyModule") +function GetModule(self, name, silent) + if not self.modules[name] and not silent then + error(("Usage: GetModule(name, silent): 'name' - Cannot find module '%s'."):format(tostring(name)), 2) + end + return self.modules[name] +end + +local function IsModuleTrue(self) return true end + +--- Create a new module for the addon. +-- The new module can have its own embeded libraries and/or use a module prototype to be mixed into the module.\\ +-- A module has the same functionality as a real addon, it can have modules of its own, and has the same API as +-- an addon object. +-- @name //addon//:NewModule +-- @paramsig name[, prototype|lib[, lib, ...]] +-- @param name unique name of the module +-- @param prototype object to derive this module from, methods and values from this table will be mixed into the module (optional) +-- @param lib List of libraries to embed into the addon +-- @usage +-- -- Create a module with some embeded libraries +-- MyModule = MyAddon:NewModule("MyModule", "AceEvent-3.0", "AceHook-3.0") +-- +-- -- Create a module with a prototype +-- local prototype = { OnEnable = function(self) print("OnEnable called!") end } +-- MyModule = MyAddon:NewModule("MyModule", prototype, "AceEvent-3.0", "AceHook-3.0") +function NewModule(self, name, prototype, ...) + if type(name) ~= "string" then error(("Usage: NewModule(name, [prototype, [lib, lib, lib, ...]): 'name' - string expected got '%s'."):format(type(name)), 2) end + if type(prototype) ~= "string" and type(prototype) ~= "table" and type(prototype) ~= "nil" then error(("Usage: NewModule(name, [prototype, [lib, lib, lib, ...]): 'prototype' - table (prototype), string (lib) or nil expected got '%s'."):format(type(prototype)), 2) end + + if self.modules[name] then error(("Usage: NewModule(name, [prototype, [lib, lib, lib, ...]): 'name' - Module '%s' already exists."):format(name), 2) end + + -- modules are basically addons. We treat them as such. They will be added to the initializequeue properly as well. + -- NewModule can only be called after the parent addon is present thus the modules will be initialized after their parent is. + local module = AceAddon:NewAddon(fmt("%s_%s", self.name or tostring(self), name)) + + module.IsModule = IsModuleTrue + module:SetEnabledState(self.defaultModuleState) + module.moduleName = name + + if type(prototype) == "string" then + AceAddon:EmbedLibraries(module, prototype, ...) + else + AceAddon:EmbedLibraries(module, ...) + end + AceAddon:EmbedLibraries(module, unpack(self.defaultModuleLibraries)) + + if not prototype or type(prototype) == "string" then + prototype = self.defaultModulePrototype or nil + end + + if type(prototype) == "table" then + local mt = getmetatable(module) + mt.__index = prototype + setmetatable(module, mt) -- More of a Base class type feel. + end + + safecall(self.OnModuleCreated, self, module) -- Was in Ace2 and I think it could be a cool thing to have handy. + self.modules[name] = module + + return module +end + +--- Returns the real name of the addon or module, without any prefix. +-- @name //addon//:GetName +-- @paramsig +-- @usage +-- print(MyAddon:GetName()) +-- -- prints "MyAddon" +function GetName(self) + return self.moduleName or self.name +end + +--- Enables the Addon, if possible, return true or false depending on success. +-- This internally calls AceAddon:EnableAddon(), thus dispatching a OnEnable callback +-- and enabling all modules of the addon (unless explicitly disabled).\\ +-- :Enable() also sets the internal `enableState` variable to true +-- @name //addon//:Enable +-- @paramsig +-- @usage +-- -- Enable MyModule +-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon") +-- MyModule = MyAddon:GetModule("MyModule") +-- MyModule:Enable() +function Enable(self) + self:SetEnabledState(true) + return AceAddon:EnableAddon(self) +end + +--- Disables the Addon, if possible, return true or false depending on success. +-- This internally calls AceAddon:DisableAddon(), thus dispatching a OnDisable callback +-- and disabling all modules of the addon.\\ +-- :Disable() also sets the internal `enableState` variable to false +-- @name //addon//:Disable +-- @paramsig +-- @usage +-- -- Disable MyAddon +-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon") +-- MyAddon:Disable() +function Disable(self) + self:SetEnabledState(false) + return AceAddon:DisableAddon(self) +end + +--- Enables the Module, if possible, return true or false depending on success. +-- Short-hand function that retrieves the module via `:GetModule` and calls `:Enable` on the module object. +-- @name //addon//:EnableModule +-- @paramsig name +-- @usage +-- -- Enable MyModule using :GetModule +-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon") +-- MyModule = MyAddon:GetModule("MyModule") +-- MyModule:Enable() +-- +-- -- Enable MyModule using the short-hand +-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon") +-- MyAddon:EnableModule("MyModule") +function EnableModule(self, name) + local module = self:GetModule( name ) + return module:Enable() +end + +--- Disables the Module, if possible, return true or false depending on success. +-- Short-hand function that retrieves the module via `:GetModule` and calls `:Disable` on the module object. +-- @name //addon//:DisableModule +-- @paramsig name +-- @usage +-- -- Disable MyModule using :GetModule +-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon") +-- MyModule = MyAddon:GetModule("MyModule") +-- MyModule:Disable() +-- +-- -- Disable MyModule using the short-hand +-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon") +-- MyAddon:DisableModule("MyModule") +function DisableModule(self, name) + local module = self:GetModule( name ) + return module:Disable() +end + +--- Set the default libraries to be mixed into all modules created by this object. +-- Note that you can only change the default module libraries before any module is created. +-- @name //addon//:SetDefaultModuleLibraries +-- @paramsig lib[, lib, ...] +-- @param lib List of libraries to embed into the addon +-- @usage +-- -- Create the addon object +-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("MyAddon") +-- -- Configure default libraries for modules (all modules need AceEvent-3.0) +-- MyAddon:SetDefaultModuleLibraries("AceEvent-3.0") +-- -- Create a module +-- MyModule = MyAddon:NewModule("MyModule") +function SetDefaultModuleLibraries(self, ...) + if next(self.modules) then + error("Usage: SetDefaultModuleLibraries(...): cannot change the module defaults after a module has been registered.", 2) + end + self.defaultModuleLibraries = {...} +end + +--- Set the default state in which new modules are being created. +-- Note that you can only change the default state before any module is created. +-- @name //addon//:SetDefaultModuleState +-- @paramsig state +-- @param state Default state for new modules, true for enabled, false for disabled +-- @usage +-- -- Create the addon object +-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("MyAddon") +-- -- Set the default state to "disabled" +-- MyAddon:SetDefaultModuleState(false) +-- -- Create a module and explicilty enable it +-- MyModule = MyAddon:NewModule("MyModule") +-- MyModule:Enable() +function SetDefaultModuleState(self, state) + if next(self.modules) then + error("Usage: SetDefaultModuleState(state): cannot change the module defaults after a module has been registered.", 2) + end + self.defaultModuleState = state +end + +--- Set the default prototype to use for new modules on creation. +-- Note that you can only change the default prototype before any module is created. +-- @name //addon//:SetDefaultModulePrototype +-- @paramsig prototype +-- @param prototype Default prototype for the new modules (table) +-- @usage +-- -- Define a prototype +-- local prototype = { OnEnable = function(self) print("OnEnable called!") end } +-- -- Set the default prototype +-- MyAddon:SetDefaultModulePrototype(prototype) +-- -- Create a module and explicitly Enable it +-- MyModule = MyAddon:NewModule("MyModule") +-- MyModule:Enable() +-- -- should print "OnEnable called!" now +-- @see NewModule +function SetDefaultModulePrototype(self, prototype) + if next(self.modules) then + error("Usage: SetDefaultModulePrototype(prototype): cannot change the module defaults after a module has been registered.", 2) + end + if type(prototype) ~= "table" then + error(("Usage: SetDefaultModulePrototype(prototype): 'prototype' - table expected got '%s'."):format(type(prototype)), 2) + end + self.defaultModulePrototype = prototype +end + +--- Set the state of an addon or module +-- This should only be called before any enabling actually happend, e.g. in/before OnInitialize. +-- @name //addon//:SetEnabledState +-- @paramsig state +-- @param state the state of an addon or module (enabled=true, disabled=false) +function SetEnabledState(self, state) + self.enabledState = state +end + + +--- Return an iterator of all modules associated to the addon. +-- @name //addon//:IterateModules +-- @paramsig +-- @usage +-- -- Enable all modules +-- for name, module in MyAddon:IterateModules() do +-- module:Enable() +-- end +local function IterateModules(self) return pairs(self.modules) end + +-- Returns an iterator of all embeds in the addon +-- @name //addon//:IterateEmbeds +-- @paramsig +local function IterateEmbeds(self) return pairs(AceAddon.embeds[self]) end + +--- Query the enabledState of an addon. +-- @name //addon//:IsEnabled +-- @paramsig +-- @usage +-- if MyAddon:IsEnabled() then +-- MyAddon:Disable() +-- end +local function IsEnabled(self) return self.enabledState end +local mixins = { + NewModule = NewModule, + GetModule = GetModule, + Enable = Enable, + Disable = Disable, + EnableModule = EnableModule, + DisableModule = DisableModule, + IsEnabled = IsEnabled, + SetDefaultModuleLibraries = SetDefaultModuleLibraries, + SetDefaultModuleState = SetDefaultModuleState, + SetDefaultModulePrototype = SetDefaultModulePrototype, + SetEnabledState = SetEnabledState, + IterateModules = IterateModules, + IterateEmbeds = IterateEmbeds, + GetName = GetName, +} +local function IsModule(self) return false end +local pmixins = { + defaultModuleState = true, + enabledState = true, + IsModule = IsModule, +} +-- Embed( target ) +-- target (object) - target object to embed aceaddon in +-- +-- this is a local function specifically since it's meant to be only called internally +function Embed(target) + for k, v in pairs(mixins) do + target[k] = v + end + for k, v in pairs(pmixins) do + target[k] = target[k] or v + end +end + + +-- - Initialize the addon after creation. +-- This function is only used internally during the ADDON_LOADED event +-- It will call the **OnInitialize** function on the addon object (if present), +-- and the **OnEmbedInitialize** function on all embeded libraries. +-- +-- **Note:** Do not call this function manually, unless you're absolutely sure that you know what you are doing. +-- @param addon addon object to intialize +function AceAddon:InitializeAddon(addon) + safecall(addon.OnInitialize, addon) + + local embeds = self.embeds[addon] + for i = 1, #embeds do + local lib = LibStub:GetLibrary(embeds[i], true) + if lib then safecall(lib.OnEmbedInitialize, lib, addon) end + end + + -- we don't call InitializeAddon on modules specifically, this is handled + -- from the event handler and only done _once_ +end + +-- - Enable the addon after creation. +-- Note: This function is only used internally during the PLAYER_LOGIN event, or during ADDON_LOADED, +-- if IsLoggedIn() already returns true at that point, e.g. for LoD Addons. +-- It will call the **OnEnable** function on the addon object (if present), +-- and the **OnEmbedEnable** function on all embeded libraries.\\ +-- This function does not toggle the enable state of the addon itself, and will return early if the addon is disabled. +-- +-- **Note:** Do not call this function manually, unless you're absolutely sure that you know what you are doing. +-- Use :Enable on the addon itself instead. +-- @param addon addon object to enable +function AceAddon:EnableAddon(addon) + if type(addon) == "string" then addon = AceAddon:GetAddon(addon) end + if self.statuses[addon.name] or not addon.enabledState then return false end + + -- set the statuses first, before calling the OnEnable. this allows for Disabling of the addon in OnEnable. + self.statuses[addon.name] = true + + safecall(addon.OnEnable, addon) + + -- make sure we're still enabled before continueing + if self.statuses[addon.name] then + local embeds = self.embeds[addon] + for i = 1, #embeds do + local lib = LibStub:GetLibrary(embeds[i], true) + if lib then safecall(lib.OnEmbedEnable, lib, addon) end + end + + -- enable possible modules. + for name, module in pairs(addon.modules) do + self:EnableAddon(module) + end + end + return self.statuses[addon.name] -- return true if we're disabled +end + +-- - Disable the addon +-- Note: This function is only used internally. +-- It will call the **OnDisable** function on the addon object (if present), +-- and the **OnEmbedDisable** function on all embeded libraries.\\ +-- This function does not toggle the enable state of the addon itself, and will return early if the addon is still enabled. +-- +-- **Note:** Do not call this function manually, unless you're absolutely sure that you know what you are doing. +-- Use :Disable on the addon itself instead. +-- @param addon addon object to enable +function AceAddon:DisableAddon(addon) + if type(addon) == "string" then addon = AceAddon:GetAddon(addon) end + if not self.statuses[addon.name] then return false end + + -- set statuses first before calling OnDisable, this allows for aborting the disable in OnDisable. + self.statuses[addon.name] = false + + safecall( addon.OnDisable, addon ) + + -- make sure we're still disabling... + if not self.statuses[addon.name] then + local embeds = self.embeds[addon] + for i = 1, #embeds do + local lib = LibStub:GetLibrary(embeds[i], true) + if lib then safecall(lib.OnEmbedDisable, lib, addon) end + end + -- disable possible modules. + for name, module in pairs(addon.modules) do + self:DisableAddon(module) + end + end + + return not self.statuses[addon.name] -- return true if we're disabled +end + +--- Get an iterator over all registered addons. +-- @usage +-- -- Print a list of all installed AceAddon's +-- for name, addon in AceAddon:IterateAddons() do +-- print("Addon: " .. name) +-- end +function AceAddon:IterateAddons() return pairs(self.addons) end + +--- Get an iterator over the internal status registry. +-- @usage +-- -- Print a list of all enabled addons +-- for name, status in AceAddon:IterateAddonStatus() do +-- if status then +-- print("EnabledAddon: " .. name) +-- end +-- end +function AceAddon:IterateAddonStatus() return pairs(self.statuses) end + +-- Following Iterators are deprecated, and their addon specific versions should be used +-- e.g. addon:IterateEmbeds() instead of :IterateEmbedsOnAddon(addon) +function AceAddon:IterateEmbedsOnAddon(addon) return pairs(self.embeds[addon]) end +function AceAddon:IterateModulesOfAddon(addon) return pairs(addon.modules) end + +-- Event Handling +local function onEvent(this, event, arg1) + if event == "ADDON_LOADED" or event == "PLAYER_LOGIN" then + -- if a addon loads another addon, recursion could happen here, so we need to validate the table on every iteration + while(#AceAddon.initializequeue > 0) do + local addon = tremove(AceAddon.initializequeue, 1) + -- this might be an issue with recursion - TODO: validate + if event == "ADDON_LOADED" then addon.baseName = arg1 end + AceAddon:InitializeAddon(addon) + tinsert(AceAddon.enablequeue, addon) + end + + if IsLoggedIn() then + while(#AceAddon.enablequeue > 0) do + local addon = tremove(AceAddon.enablequeue, 1) + AceAddon:EnableAddon(addon) + end + end + end +end + +AceAddon.frame:RegisterEvent("ADDON_LOADED") +AceAddon.frame:RegisterEvent("PLAYER_LOGIN") +AceAddon.frame:SetScript("OnEvent", onEvent) + +-- upgrade embeded +for name, addon in pairs(AceAddon.addons) do + Embed(addon) +end diff --git a/AI_VoiceOver/Libs/AceAddon-3.0/AceAddon-3.0.xml b/AI_VoiceOver/Libs/AceAddon-3.0/AceAddon-3.0.xml new file mode 100644 index 00000000..e6ad639f --- /dev/null +++ b/AI_VoiceOver/Libs/AceAddon-3.0/AceAddon-3.0.xml @@ -0,0 +1,4 @@ + +