--AppAndIngame version of ZO_FormatUserFacingDisplayName for local use local function ZO_FormatUserFacingDisplayName(name) return IsConsoleUI() and UndecorateDisplayName(name) or name end ZO_VOICE_CHAT_CHANNEL_TO_COLOR = { [VOICE_CHANNEL_AREA] = VOICE_CHAT_COLORS_AREA, [VOICE_CHANNEL_GROUP] = VOICE_CHAT_COLORS_GROUP, [VOICE_CHANNEL_GUILD] = VOICE_CHAT_COLORS_GUILD, [VOICE_CHANNEL_BATTLEGROUP] = VOICE_CHAT_COLORS_GROUP, } ZO_VOICE_CHAT_CHANNEL_TO_ICON = { [VOICE_CHANNEL_AREA] = "EsoUI/Art/VOIP/voip-area.dds", [VOICE_CHANNEL_GROUP] = "EsoUI/Art/VOIP/voip-group.dds", [VOICE_CHANNEL_GUILD] = "EsoUI/Art/VOIP/voip-guild.dds", [VOICE_CHANNEL_BATTLEGROUP] = "EsoUI/Art/VOIP/voip-group.dds", } -------------------------------------------------------------------------------- -- Speaker List -- A helper class for generating hud speaker entries and anchoring them to -- form a list. Allows the VoiceChat HUD to only have to deal with adding -- to and clearing from a list. -------------------------------------------------------------------------------- local SpeakerList = {} function SpeakerList:Initialize(control) self.control = control self.freeList = {} self.activeList = {} self.nextControlId = 1 end function SpeakerList:AddLine(text, channelType) --Initialize control local newEntry = next(self.freeList) if not newEntry then -- can't just call CreateControlFromVirtual as that is defined in GlobalVars.lua which we don't load in the App GUI newEntry = GetWindowManager():CreateControlFromVirtual(self.nextControlId, self.control, "ZO_VoiceChatHUDEntry") self.nextControlId = self.nextControlId + 1 newEntry.textControl = newEntry:GetNamedChild("Text") newEntry.icon = newEntry:GetNamedChild("Icon") end newEntry:SetHidden(false) self.freeList[newEntry] = nil self.activeList[newEntry] = true --Set text local textControl = newEntry.textControl textControl:SetText(text) textControl:SetColor(GetInterfaceColor(INTERFACE_COLOR_TYPE_VOICE_CHAT_COLORS, ZO_VOICE_CHAT_CHANNEL_TO_COLOR[channelType])) --Set anchor if self.lastEntry then newEntry:SetAnchor(BOTTOMRIGHT, self.lastEntry, TOPRIGHT) else newEntry:SetAnchor(BOTTOMRIGHT, nil, BOTTOMRIGHT) end self.lastEntry = newEntry --Set icon local icon = newEntry.icon icon:SetTexture(ZO_VOICE_CHAT_CHANNEL_TO_ICON[channelType]) icon:SetColor(GetInterfaceColor(INTERFACE_COLOR_TYPE_VOICE_CHAT_COLORS, ZO_VOICE_CHAT_CHANNEL_TO_COLOR[channelType])) end function SpeakerList:Clear() self.lastEntry = nil for entry in pairs(self.activeList) do self.activeList[entry] = nil self.freeList[entry] = true entry:SetHidden(true) end end -------------------------------------------------------------------------------- -- VoiceChat HUD -- Class for displaying a list of current voice chat speakers. Templated -- outside the ingame UI layer so that we can also create one for the loading -- screen. -------------------------------------------------------------------------------- local LIST_ENTRY_LIMIT = 4 local CLEAR_DELAY_MS = 500 --after a user quits speaking, their HUD entry will persist for this duration before clearing local function ChannelDataFromName(channelName) local channelType, guildId, guildRoomNumber = VoiceChatGetChannelInfo(channelName) local channelData = { channelName = channelName, channelType = channelType, guildId = guildId, guildRoomNumber = guildRoomNumber } return channelData end ZO_VoiceChatHUD = {} function ZO_VoiceChatHUD:Initialize(control) self.control = control self.speakerData = {} --list of currently speaking users, including users who have stopped talking but their HUD entry is waiting to clear self.delayedClears = {} --table that maps users who have stopped talking to the time that their HUD entry should clear self.localPlayerName = "" self.speakerList = control:GetNamedChild("List") zo_mixin(self.speakerList, SpeakerList) self.speakerList:Initialize(self.speakerList) self:RegisterForEvents() end function ZO_VoiceChatHUD:Update() --Clear entries for users who haven't spoken recently local currentTime = GetFrameTimeMilliseconds() for displayName, clearTime in pairs(self.delayedClears) do if currentTime >= clearTime then self:RemoveSpeaker(displayName) self.delayedClears[displayName] = nil end end --Create the list self.speakerList:Clear() for i, speaker in ipairs(self.speakerData) do self.speakerList:AddLine(ZO_FormatUserFacingDisplayName(speaker.displayName), speaker.channelData.channelType) end end function ZO_VoiceChatHUD:InsertName(channelData, displayName) --The list is a stack with the users who spoke most recently on the bottom. The local player is an --exception to this and always shows at the bottom. local speakerDataEntry = { channelData = channelData, displayName = displayName, } --Remove any existing entry so it can be reinserted at the bottom self:RemoveSpeaker(displayName) local insertIndex = self:IsLocalPlayerFirstListEntry() and 2 or 1 table.insert(self.speakerData, insertIndex, speakerDataEntry) --Remove the oldest entry if we're over the limit if #self.speakerData > LIST_ENTRY_LIMIT then table.remove(self.speakerData) end self:Update() end function ZO_VoiceChatHUD:RemoveSpeaker(displayName) for i, speaker in ipairs(self.speakerData) do if speaker.displayName == displayName then table.remove(self.speakerData, i) self:Update() break end end end function ZO_VoiceChatHUD:RemoveSpeakersInChannel(channelData) for i = #self.speakerData, 1, -1 do local speaker = self.speakerData[i] if speaker.channelData.channelName == channelData.channelName then table.remove(self.speakerData, i) end end self:Update() end function ZO_VoiceChatHUD:IsUserLocalPlayer(displayName) if self.localPlayerName == "" then self.localPlayerName = GetDisplayName() --returns empty string "" if not yet set end return displayName == self.localPlayerName end function ZO_VoiceChatHUD:IsLocalPlayerFirstListEntry() local firstEntry = self.speakerData[1] if not firstEntry then return false end return self:IsUserLocalPlayer(firstEntry.displayName) end --Events function ZO_VoiceChatHUD:RegisterForEvents() self.control:RegisterForEvent(EVENT_VOICE_CHANNEL_LEFT, function(eventCode, ...) self:OnVoiceChannelLeft(...) end) self.control:RegisterForEvent(EVENT_VOICE_CHANNEL_UNAVAILABLE, function(eventCode, ...) self:OnVoiceChannelUnavailable(...) end) self.control:RegisterForEvent(EVENT_VOICE_USER_SPEAKING, function(eventCode, ...) self:OnUserSpeaking(...) end) self.control:RegisterForEvent(EVENT_VOICE_USER_JOINED_CHANNEL, function(eventCode, ...) self:OnVoiceUserJoinedChannel(...) end) self.control:RegisterForEvent(EVENT_VOICE_USER_LEFT_CHANNEL, function(eventCode, ...) self:OnVoiceUserLeftChannel(...) end) end function ZO_VoiceChatHUD:OnVoiceChannelLeft(channelName) local channelData = ChannelDataFromName(channelName) self:RemoveSpeakersInChannel(channelData) end function ZO_VoiceChatHUD:OnVoiceChannelUnavailable(channelName) local channelData = ChannelDataFromName(channelName) self:RemoveSpeakersInChannel(channelData) end function ZO_VoiceChatHUD:OnUserSpeaking(channelName, displayName, characterName, speaking) local channelData = ChannelDataFromName(channelName) if speaking then self.delayedClears[displayName] = nil self:InsertName(channelData, displayName) else self.delayedClears[displayName] = GetFrameTimeMilliseconds() + CLEAR_DELAY_MS end end function ZO_VoiceChatHUD:OnVoiceUserJoinedChannel(channelName, displayName, characterName, isSpeaking) if isSpeaking then self:OnUserSpeaking(channelName, displayName, characterName, isSpeaking) end end function ZO_VoiceChatHUD:OnVoiceUserLeftChannel(channelName, displayName) self:RemoveSpeaker(displayName) end