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