view CrossRealmAssist.lua @ 12:0deef890ec99

Updated version
author ShadowTheAge
date Wed, 11 Mar 2015 22:51:00 +0300
parents 421508c17712
children 1f68e8154947
line wrap: on
line source
CrossRealmAssist = LibStub("AceAddon-3.0"):NewAddon("CrossRealmAssist", "AceEvent-3.0", "AceConsole-3.0", "AceTimer-3.0")
local addon = CrossRealmAssist;
local wholib = LibStub:GetLibrary('LibWho-2.0'):Library()
local ScrollingTable = LibStub("ScrollingTable");

local homeRealm, realmSep, gui, btRefresh, btQuick, btManual, lbRealm, lbStatus, playerName, lfgui, lfgTabSelected, lfgTable, lbThrottle

local tabs = {}
local curRealmStat
local scanstate=0
local recentRealms={}
local recentgroups={}
local active=false

local lfgGroups={
    6, -- Custom
    10, -- Ashran
    1, -- Quests
    3 -- Raids
}

local StatusTable = {
    ["invited"]="Invited",
    ["declined"]="Declined",
    ["timedout"]="Timed out",
    ["applied"]="Applied",
    ["cancelled"]="Join cancelled",
    ["inviteaccepted"]="Joined",
    ["_init"]="Join request"
}

local curLfgGroup
local sheduledScan
local wasInGroup
local inInstance


-- Utils functions

local function PlayerName(fullname)
    fullname = fullname or "???-???"
    local name, realm = strsplit(realmSep, fullname)
    realm = realm or homeRealm;
    return name, realm;
end

local function canJoinGroup()
    return (not IsInGroup()) or (UnitIsGroupLeader('player') and not IsInRaid())
end

local function sortByWeight(a,b)
    return b.weight < a.weight
end


-- UI functions

local function createIcon(owner, icon, id)
    local image = owner:CreateTexture(nil, "BACKGROUND")
    image:SetWidth(16)
    image:SetHeight(16)
    image:SetTexture(icon)
    image:SetPoint("TOPLEFT",owner,"TOPLEFT",id*16,0)
    return image
end

local function setupWidget(widget, parameters, x, y)
    if parameters then
        for key,value in pairs(parameters) do widget[key](widget,value) end
    end
    if y then
        widget:SetPoint("TOPLEFT",widget:GetParent(),"TOPLEFT",x,-y)
    end
    return widget
end

local function ShowTooltip(widget)
    GameTooltip:SetOwner(widget, widget.TooltipAnchor)
    GameTooltip:ClearLines()
    widget:TooltipFactory()
    GameTooltip:Show()
end

local function HideTooltip(widget)
    GameTooltip:Hide()
end

local function setupTooltip(widget, anchor, factory)
    widget.TooltipAnchor = anchor;
    widget.TooltipFactory = factory;
    widget:SetScript("OnEnter", ShowTooltip)
    widget:SetScript("OnLeave", HideTooltip)
end

local function SelectLfgTab(tab)
    PanelTemplates_DeselectTab(lfgTabSelected)
    lfgTabSelected = tab
    addon.LfgScan(tab.searchID)
    PanelTemplates_SelectTab(lfgTabSelected)
end

local function realmToolip()
    if not curRealmStat then return end
    local players = curRealmStat.players;
    local threshold = curRealmStat.threshold
    GameTooltip:AddLine("Players in zone: "..players)
    if not curRealmStat.complete then
        GameTooltip:AppendText("+")
    end
    for i=1,curRealmStat.realms do
        local data = curRealmStat[i]
        local percent = math.ceil(100 * data.count / players - 0.5) .. "%";
        if data.count >= threshold then
            GameTooltip:AddDoubleLine(data.realm, percent, 0,1,0,0,1,0)
        else
            GameTooltip:AddDoubleLine(data.realm, percent, 0.5,0.5,0.5,0.5,0.5,0.5)
        end
    end
end







-- LFG list data providers, filters and tooltips

local lfgScanInProgress=false
local hasLfgListChanges=false

local function addTooltipLineIcon(predicat, text, icon, r, g, b, wrap)
    if predicat then
        GameTooltip:AddLine(text, r, g, b, wrap)
        if icon then GameTooltip:AddTexture(icon) end
    end
end

local function WeightLfgItem(id, forAutoJoin)
    local _, action, caption, desc, voice, ilvl, time, bnetfr, charfr, guild, delisted, fullname, pcount, autoinv = C_LFGList.GetSearchResultInfo(id)
    if delisted then return 0 end
    local name,realm = PlayerName(fullname);
    if forAutoJoin then
        if not autoinv or pcount >= 35 or pcount <= 5 or recentgroups[fullname] then return 0 end
    end

    local autoinvWeight = autoinv and 5 or 2

    local visCoef = 1
    if recentgroups[fullname] then
        local ago = GetTime() - recentgroups[fullname].time;
        visCoef = math.min(1,ago/600)
    end

    local leaderRealm = 3 -- recently visited realms
    if (realm ~= "???") then leaderRealm = (5 - (recentRealms[realm] or 0)) end

    local countWeight = 4 -- count weight
    if pcount >= 39 or pcount <= 1 then countWeight = 1
    elseif pcount >= 35 or pcount <= 4 then countWeight = 2
    elseif pcount >= 30 or pcount <= 10 then countWeight = 3
    else countWeight = 4 end

    local ageWeight = 2
    if time > 1200 then ageWeight = 4
    elseif time > 300 then ageWeight = 3 end

    return leaderRealm * countWeight * visCoef * autoinvWeight * ageWeight;
end

local function ShowLfgInfo(rowFrame, cellFrame, data, cols, row, realrow, column, scrollingTable, ...)
    if not realrow then return end
    local rowdata = scrollingTable:GetRow(realrow);
    local _, action, caption, desc, voice, ilvl, time, bnetfr, charfr, guild, delisted, fullname, pcount, autoinv = C_LFGList.GetSearchResultInfo(rowdata.id)
    local friends = bnetfr+charfr+guild
    GameTooltip:SetOwner(rowFrame, "ANCHOR_BOTTOMLEFT",-10,25)
    GameTooltip:ClearLines()
    GameTooltip:AddLine(caption,1,1,1)
    addTooltipLineIcon(desc ~= "", desc, nil, 0.5, 0.5, 0.5, true)
    local name, realm = PlayerName(fullname)
    GameTooltip:AddDoubleLine("Leader:",name)
    GameTooltip:AddDoubleLine("Leader realm:",realm)
    GameTooltip:AddLine(" ")
    GameTooltip:AddDoubleLine("Players:",pcount)
    addTooltipLineIcon(ilvl > 0, "Min. ilvl: "..ilvl, READY_CHECK_WAITING_TEXTURE, 1, 1, 0)
    addTooltipLineIcon(voice ~= "", "Voice: "..voice, READY_CHECK_WAITING_TEXTURE, 1, 1, 0)
    addTooltipLineIcon(friends > 0, "Friends: "..friends, READY_CHECK_WAITING_TEXTURE, 1, 1, 0)
    addTooltipLineIcon(autoinv, "Autoinvite!", READY_CHECK_READY_TEXTURE, 0, 1, 0)
    local visitinfo = recentgroups[fullname]
    if visitinfo then
        local ago = GetTime() - visitinfo.time;
        GameTooltip:AddDoubleLine(StatusTable[visitinfo.status] or visitinfo.status,SecondsToTime(ago, false, false, 1, false).." ago",1,0,0,1,0,0)
        GameTooltip:AddTexture(READY_CHECK_NOT_READY_TEXTURE)
    end
    GameTooltip:Show()
end

local function updateTableData(rowFrame, cellFrame, data, cols, row, realrow, column, fShow, table, ...)
    if fShow then
        local rowdata = table:GetRow(realrow);
        local icons = cellFrame.icons
        local _, action, caption, desc, voice, ilvl, time, bnetfr, charfr, guild, delisted, fullname, pcount, autoinv = C_LFGList.GetSearchResultInfo(rowdata.id)
        if not icons then
            local miscdata = createIcon(cellFrame,READY_CHECK_WAITING_TEXTURE,0)
            local autoinv = createIcon(cellFrame,READY_CHECK_READY_TEXTURE, 1)
            local visited = createIcon(cellFrame,READY_CHECK_NOT_READY_TEXTURE, 2)
            icons = {misc=miscdata, autoinv=autoinv, visited=visited}
            cellFrame.icons = icons
        end
        icons.misc:SetShown(voice ~= "" or bnetfr > 0 or charfr > 0 or guild > 0 or ilvl > 0)
        icons.autoinv:SetShown(autoinv)
        icons.visited:SetShown(recentgroups[fullname])
        if GameTooltip:GetOwner() == rowFrame then ShowLfgInfo(rowFrame, cellFrame, data, cols, row, realrow, column, table) end
    end
end

local function updateGroupName(id)
    return select(3, C_LFGList.GetSearchResultInfo(id))
end

local function updateGroupCount(id)
    return select(13, C_LFGList.GetSearchResultInfo(id))
end

local function updateGroupRealm(id)
    local name = select(12, C_LFGList.GetSearchResultInfo(id))
    local pname, realm = PlayerName(name);
    return realm
end

local function filterTable(self, row)
    local delisted = select(11, C_LFGList.GetSearchResultInfo(row.id))
    return not delisted;
end

function addon:UpdateResponseData(event, result)
    if select(11, C_LFGList.GetSearchResultInfo(result)) then
        lfgTable:SetFilter(filterTable)
    end
    hasLfgListChanges = true
end

local function refreshLFGList()
    if lfgScanInProgress or not lfgTable then return end
    local count, list = C_LFGList.GetSearchResults()
    local tableData = {}
    for i = 1,count do
        local rid = list[i];
        if not rid then break end
        local data = list[i];
        local cols = {}
        local row = {rid}
        cols[1] = {value=updateGroupName,args=row}
        cols[2] = {value=updateGroupCount,args=row}
        cols[3] = {value=updateGroupRealm,args=row}
        cols[4] = {}
        table.insert(tableData, {cols=cols,id=rid,weight=WeightLfgItem(rid)})
    end
    table.sort(tableData, sortByWeight);
    lfgTable:SetData(tableData)
end

local function JoinGroup(rowFrame, cellFrame, data, cols, row, realrow, column, scrollingTable, ...)
    if not realrow or not canJoinGroup() then return end
    local rowdata = scrollingTable:GetRow(realrow);
    local tank, heal, dd = C_LFGList.GetAvailableRoles()
    addon:SetGroupJoinStatus(rowdata.id, "_init")
    C_LFGList.ApplyToGroup(rowdata.id, "", tank, heal, dd)
end






-- Addon functions and gui constructors

function addon:OnEnable()
    local tabCount = table.getn(lfgGroups)
    realmSep = _G.REALM_SEPARATORS
    playerName = UnitName("player")
    homeRealm = GetRealmName()
    addon:RegisterChatCommand("cra", "Activate")
    addon:RegisterChatCommand("crossrealmassist", "Activate")

    addon:ScheduleTimer("Activate", 0.5)
end

function addon:OnDisable()
    addon:Deactivate()
end

function addon:Activate()
    if active then return end
    active=true
    wasInGroup = IsInGroup()
    if not gui then addon:CreateUI() end
    addon:updateCurrentRealm();
    addon:ForceRefreshZone();
    gui:Show()
    addon:RegisterEvent("ZONE_CHANGED_NEW_AREA", "ForceRefreshZone")
    addon:RegisterEvent("SCENARIO_UPDATE", "ForceRefreshZone")
    addon:RegisterEvent("LFG_LIST_SEARCH_RESULTS_RECEIVED", "LfgResponseData")
    addon:RegisterEvent("LFG_LIST_SEARCH_RESULT_UPDATED", "UpdateResponseData")
    addon:RegisterEvent("LFG_LIST_SEARCH_FAILED", "LfgScanFailed")
    addon:RegisterEvent("GROUP_ROSTER_UPDATE", "updatePartyInfo")
    addon:RegisterEvent("LFG_LIST_APPLICATION_STATUS_UPDATED", "updateAppStatus")
    addon.LfgScan(lfgGroups[1])
end

function addon:Deactivate()
    if not active then return end
    active = false
    gui:Hide()
    if lfgui then lfgui:Hide() end
    scanstate = 0
    lfgScanInProgress = false
    addon:UnregisterEvent("ZONE_CHANGED_NEW_AREA")
    addon:UnregisterEvent("PLAYER_REGEN_ENABLED")
    addon:UnregisterEvent("LFG_LIST_SEARCH_RESULTS_RECEIVED")
    addon:UnregisterEvent("LFG_LIST_SEARCH_RESULT_UPDATED")
    addon:UnregisterEvent("LFG_LIST_SEARCH_FAILED")
    addon:UnregisterEvent("GROUP_ROSTER_UPDATE")
    addon:UnregisterEvent("LFG_LIST_APPLICATION_STATUS_UPDATED")
end

function addon:SetStatus(status)
    if status then
        lbStatus:SetText(status)
    else
        lbStatus:SetText("") -- TODO
    end
end

function addon:CreateUI()
    gui = setupWidget(CreateFrame("Frame","CrossRealmAssistMainUI",nil,"InsetFrameTemplate3"), {SetFrameStrata="LOW",SetWidth=208,SetHeight=60,EnableMouse=true,SetMovable=true})
    local title = setupWidget(CreateFrame("Frame",nil,gui), {SetWidth=190,SetHeight=18,EnableMouse=true,RegisterForDrag="LeftButton"}, 0, 6);
    title:SetScript("OnDragStart", function() gui:StartMoving() end)
    title:SetScript("OnDragStop", function() gui:StopMovingOrSizing() end)
    gui:SetScript("OnHide", addon.Deactivate)
    setupTooltip(title, "ANCHOR_TOP", realmToolip)

    lbRealm = setupWidget(gui:CreateFontString(nil,"BACKGROUND", "GameFontNormal"), {SetWidth=200,SetHeight=18}, 0, 6)
    lbStatus = setupWidget(gui:CreateFontString(nil,"BACKGROUND", "GameFontHighlightSmallLeft"), {SetWidth=200,SetHeight=10}, 6, 23)

    btQuick = setupWidget(CreateFrame("Button",nil,gui,"UIMenuButtonStretchTemplate"),{SetWidth=90,SetHeight=20,SetText="Quick join"},4,36)
    btQuick:SetScript("OnClick",addon.DoAutoAction)
    btManual = setupWidget(CreateFrame("Button",nil,gui,"UIMenuButtonStretchTemplate"),{SetWidth=90,SetHeight=20,SetText="Manual join"},94,36)
    btManual:SetScript("OnClick",addon.ShowManualLfg)
    setupWidget(CreateFrame("Button",nil,gui,"UIPanelCloseButton"),{EnableMouse=true,SetWidth=20,SetHeight=20},188,0)

    btRefresh = setupWidget(CreateFrame("Button",nil,gui,"UIPanelSquareButton"),{SetWidth=20,SetHeight=20},184,36)
    btRefresh.icon:SetTexture("Interface/BUTTONS/UI-RefreshButton")
    btRefresh.icon:SetTexCoord(0,1,0,1);
    btRefresh:SetScript("OnClick",addon.RefreshZone)

    gui:SetPoint("CENTER",0,0)
    addon:UpdateAutoButtonStatus()
end

function addon:ShowManualLfg()
    if not lfgui then addon:CreateLFGUI() end
    lfgui:Show()
end

function addon.lfgUpdate()
    if hasLfgListChanges then
        lfgTable:Refresh()
        hasLfgListChanges = false
    end
end

function addon:CreateLFGUI()
    lfgui = setupWidget(CreateFrame("Frame","CrossRealmAssistJoinUI",nil,"UIPanelDialogTemplate"), {SetFrameStrata="DIALOG",SetWidth=405,SetHeight=300,EnableMouse=true,SetMovable=true})
    lfgui.title:SetText("Click to join group")
    lfgui:SetScript("OnUpdate",addon.lfgUpdate)
    local titlereg = lfgui:CreateTitleRegion()
    titlereg:SetAllPoints(lfgui.title)
    addon:CreateTabs()

    lfgTable = ScrollingTable:CreateST({
        {name="Title",width=160},
        {name="#",width=30,align="CENTER"},
        {name="Realm",width=120,align="RIGHT"},
        {name="",width=50,DoCellUpdate=updateTableData}
    },15,16,nil,lfgui);

    lfgTable:RegisterEvents({OnEnter=ShowLfgInfo,OnLeave=HideTooltip,OnClick=JoinGroup})

    setupWidget(lfgTable.frame, nil, 10, 45);
    lfgTable.frame:SetBackdrop(nil)
    lfgui:SetPoint("CENTER",0,0)
    refreshLFGList()

    lbThrottle = setupWidget(lfgui:CreateFontString(nil,"BACKGROUND", "GameFontHighlightSmall"), {SetWidth=300}, 50, 100)
    lbThrottle:Hide();

    local btRefresh = setupWidget(CreateFrame("Button",nil,lfgui,"UIPanelSquareButton"),{SetWidth=22,SetHeight=22},379,27)
    btRefresh.icon:SetTexture("Interface/BUTTONS/UI-RefreshButton")
    btRefresh.icon:SetTexCoord(0,1,0,1);
    btRefresh:SetScript("OnClick",addon.refreshLfgCurrent)
end

function addon:CreateTabs()
    local prevTab
    for i=1,#lfgGroups do
        local tab = CreateFrame("Button","$parentTab"..i,lfgui,"CharacterFrameTabButtonTemplate")
        tab:SetText((C_LFGList.GetCategoryInfo(lfgGroups[i])));
        tab.searchID = lfgGroups[i];
        tab:SetID(i);
        PanelTemplates_TabResize(tab, 0)
        if i == 1 then
            tab:SetPoint("TOPLEFT", lfgui, "BOTTOMLEFT", 10, 7)
            lfgTabSelected = tab
            PanelTemplates_SelectTab(tab)
        else
            tab:SetPoint("TOPLEFT", prevTab, "TOPRIGHT",-15,0)
            PanelTemplates_DeselectTab(tab)
        end
        tab:SetScript("OnClick", SelectLfgTab)
        tabs[i] = tab
        prevTab = tab
    end
end

-- LFG scanning routine

function addon.refreshLfgCurrent()
    addon.LfgScan(curLfgGroup)
end

function addon.LfgScan(group)
    if lfgTable then lfgTable:SetData({}) end
    lfgScanInProgress = true
    curLfgGroup = group
    C_LFGList.Search(curLfgGroup,"")
end

function addon:LfgScanFailed(event, reason)
    if reason == "throttled" then
        addon:ScheduleTimer(addon.LfgScan, 2, curLfgGroup)
        lbThrottle:SetText("LFG scan is delayed (throttled).\nThis page will update automatically...")
    else
        lbThrottle:SetText("Scan failed ("..reason..")")
    end
    lbThrottle:Show()
end

function addon:LfgResponseData()
    lfgScanInProgress = false;
    if lbThrottle then lbThrottle:Hide() end
    refreshLFGList()
end

function addon:updateAppStatus(event, id, status, oldstatus)
    if status == "invited" then
        LFGListInviteDialog_Accept(LFGListInviteDialog)
    end
    addon:UpdateAutoButtonStatus()
    addon:SetGroupJoinStatus(id, status)
end

function addon:SetGroupJoinStatus(id, status)
    local name = select(12, C_LFGList.GetSearchResultInfo(id))
    recentgroups[name] = {status=status, time=GetTime()}
end

function addon:updatePartyInfo()
    local realms = {}
    local inGroup = IsInGroup()
    if inGroup then
        if IsInRaid() then
            for i=1, MAX_RAID_MEMBERS do
                addon:AddUnitIdStat("raid"..i, realms)
            end
        else
            for i=1, MAX_PARTY_MEMBERS do
                addon:AddUnitIdStat("party"..i, realms)
            end
        end
    end
    if inGroup ~= wasInGroup  then
        addon:ForceRefreshZone()
        wasInGroup = inGroup
        addon:UpdateAutoButtonStatus()
    end
end




-- Zone scanning routine

function addon:LeaveCombat()
    addon:UnregisterEvent("PLAYER_REGEN_ENABLED")
    scanstate = 0
    addon:ScanRealm();
end

function addon:ForceRefreshZone()
    addon:RefreshZone(true)
end

function addon:RefreshZone(shedule)
    local inst, instType = IsInInstance()
    inInstance = (inst or instType ~= "none")
    if inInstance then
        lbRealm:SetText("Instanced zone")
        curRealmStat = nil
        inInstance = true
    else
        inInstance = false;
        if scanstate == 0 then
            addon:ScanRealm()
        elseif shedule == true and scanstate == 2 then
            addon:SetStatus("Rescan sheduled...")
            scanstate = 3
        end
    end
    addon:UpdateAutoButtonStatus()
end

function addon:ScanRealm()
    if scanstate > 0 then return end
    if InCombatLockdown() then
        addon:RegisterEvent("PLAYER_REGEN_ENABLED","LeaveCombat")
        scanstate = 1
        addon:SetStatus("Scan sheduled after combat...");
        return
    end
    scanstate = 2
    addon:SetStatus("Scanning current realm...");
    local searchString = _G.WHO_TAG_ZONE .. '"' .. GetZoneText() .. '"';
    wholib:Who(searchString, {callback = "ScanResult", handler=addon})
end

function addon:ScanResult(query, results, complete)
    local realms = {}
    for _, player in ipairs(results) do
        addon:AddRealmStat(player.Name, realms);
    end
    curRealmStat = addon:GetRealmStat(realms);
    curRealmStat.complete = complete
    addon:updateCurrentRealm();
    local rescan = scanstate == 3
    scanstate = 0;
    if rescan then addon:ScanRealm() end
end

function addon:AddRealmStat(name, realms)
    if (name == playerName) then return end
    local _, realm = strsplit(realmSep, name)
    realm = realm or homeRealm
    realms[realm] = (realms[realm] or 0) + 1
end

function addon:AddUnitIdStat(unitid, realms)
    local name, realm = UnitName(unitid);
    if not name then return end
    if realm == "" then realm = homeRealm end
    realm = realm or homeRealm
    realms[realm] = (realms[realm] or 0) + 1
end

function addon:GetRealmStat(realms)
    local rcount = 0;
    local stat = {};
    local pcount = 0;
    for realm,count in pairs(realms) do
        table.insert(stat,{realm=realm,count=count})
        rcount = rcount + 1;
        pcount = pcount + count;
    end
    if rcount > 1 then
        table.sort(stat, function(a,b) return a.count > b.count end)
        stat.max = stat[1].count
    else stat.max = 0 end
    stat.players = pcount;
    stat.realms = rcount;
    return stat;
end

function addon:updateCurrentRealm()
    addon:SetStatus();
    if not curRealmStat or curRealmStat.realms < 1 then
        lbRealm:SetText("Unknown realm (No players)")
        return;
    end

    local bestRealm = curRealmStat[1]
    local prevThreshold = math.min(bestRealm.count/3,10)
    local mixCount = 0
    for i=2,curRealmStat.realms do
        local data = curRealmStat[i]
        if data.count >= prevThreshold then
            mixCount = mixCount + 1
            prevThreshold = math.min(data.count/3,10)
        end
    end
    curRealmStat.threshold = prevThreshold

    if (mixCount > 0) then
        lbRealm:SetText("Mixed "..bestRealm.realm.." +"..mixCount)
    else
        lbRealm:SetText(bestRealm.realm)
    end
end






-- Auto action button

local action;

local function CancelJoin()
    local apps = C_LFGList.GetApplications();
    if apps[1] then
        C_LFGList.CancelApplication(apps[1])
    end
end

local function findGroupToJoin()
    local count, list = C_LFGList.GetSearchResults()
    local tableData = {}
    for i = 1,count do
        local rid = list[i];
        if not rid then break end
        local _, action, caption, desc, voice, ilvl, time, bnetfr, charfr, guild, delisted, fullname, pcount, autoinv = C_LFGList.GetSearchResultInfo(rid)

        local data = list[i];
        local cols = {}
        local row = {rid}
        cols[1] = {value=updateGroupName,args=row}
        cols[2] = {value=updateGroupCount,args=row}
        cols[3] = {value=updateGroupRealm,args=row}
        cols[4] = {}
        table.insert(tableData, {cols=cols,id=rid})

    end
end

local function QuickJoin()

end

function addon:DoAutoAction()
    if action then action() end
    addon:UpdateAutoButtonStatus()
end

function addon:UpdateAutoButtonStatus()
    btQuick:Enable()
    if IsInGroup() then
        action = LeaveParty;
        btQuick:SetText("Leave group")
    elseif C_LFGList.GetNumApplications() > 0 then
        action = CancelJoin;
        btQuick:SetText("Cancel join")
    else
        if inInstance then btQuick:Disable() end
        btQuick:SetText("Quick join")
    end
end