Mercurial > wow > ouroloot
view core.lua @ 30:47949f8eb783
Passing in data field for StaticPopup_Show must be done earlier.
author | Farmbuyer of US-Kilrogg <farmbuyer@gmail.com> |
---|---|
date | Fri, 21 Oct 2011 21:34:38 +0000 |
parents | 7d2742727869 |
children | f1d0a5d7b006 |
line wrap: on
line source
local nametag, addon = ... --[==[ g_loot's numeric indices are loot entries (including titles, separators, etc); its named indices are: - forum: saved text from forum markup window, default nil - attend: saved text from raid attendence window, default nil - printed.FOO: last loot index formatted into text window FOO, default 0 Functions arranged like this, with these lables (for jumping to). As a rule, member functions with UpperCamelCase names are called directly by user-facing code, ones with lowercase names are "one step removed", and names with leading underscores are strictly internal helper functions. ------ Saved variables ------ Constants ------ Addon member data ------ Globals ------ Expiring caches ------ Ace3 framework stuff ------ Event handlers ------ Slash command handler ------ On/off ------ Behind the scenes routines ------ Saved texts ------ Loot histories ------ Player communication This started off as part of a raid addon package written by somebody else. After he retired, I began modifying the code. Eventually I set aside the entire package and rewrote the loot tracker module from scratch. Many of the variable/function naming conventions (sv_*, g_*, and family) stayed across the rewrite. Some variables are needlessly initialized to nil just to look uniform. ]==] ------ Saved variables OuroLootSV = nil -- possible copy of g_loot OuroLootSV_saved = nil -- table of copies of saved texts, default nil; keys -- are numeric indices of tables, subkeys of those -- are name/forum/attend/date OuroLootSV_opts = nil -- same as option_defaults until changed -- autoshard: optional name of disenchanting player, default nil -- threshold: optional loot threshold, default nil OuroLootSV_hist = nil OuroLootSV_log = {} ------ Constants local option_defaults = { ['popup_on_join'] = true, ['register_slashloot'] = true, ['scroll_to_bottom'] = true, ['chatty_on_kill'] = false, ['no_tracking_wipes'] = false, ['snarky_boss'] = true, ['keybinding'] = false, ['bossmod'] = "DBM", ['keybinding_text'] = 'CTRL-SHIFT-O', ['forum'] = { ['[url] Wowhead'] = '[url=http://www.wowhead.com/?item=$I]$N[/url]$X - $T', ['[url] MMO/Wowstead'] = '[http://db.mmo-champion.com/i/$I]$X - $T', ['[item] by name'] = '[item]$N[/item]$X - $T', ['[item] by ID'] = '[item]$I[/item]$X - $T', ['Custom...'] = '', }, ['forum_current'] = '[item] by name', } local virgin = "First time loaded? Hi! Use the /ouroloot or /loot command" .." to show the main display. You should probably browse the instructions" .." if you've never used this before; %s to display the help window. This" .." welcome message will not intrude again." local newer_warning = "A newer version has been released. You can %s to display" .." a download URL for copy-and-pasting. You can %s to ping other raiders" .." for their installed versions (same as '/ouroloot ping' or clicking the" .." 'Ping!' button on the options panel)." local qualnames = { ['gray'] = 0, ['grey'] = 0, ['poor'] = 0, ['trash'] = 0, ['white'] = 1, ['common'] = 1, ['green'] = 2, ['uncommon'] = 2, ['blue'] = 3, ['rare'] = 3, ['epic'] = 4, ['purple'] = 4, ['legendary'] = 5, ['orange'] = 5, ['artifact'] = 6, --['heirloom'] = 7, } local my_name = UnitName('player') local comm_cleanup_ttl = 5 -- seconds in the cache local revision_large = nil -- defaults to 1, possibly changed by revision local g_LOOT_ITEM_ss, g_LOOT_ITEM_MULTIPLE_sss, g_LOOT_ITEM_SELF_s, g_LOOT_ITEM_SELF_MULTIPLE_ss ------ Addon member data local flib = LibStub("LibFarmbuyer") addon.author_debug = flib.author_debug -- Play cute games with namespaces here just to save typing. WTB Lua 5.2 PST. do local _G = _G setfenv (1, addon) commrev = 15 -- number revision = _G.GetAddOnMetadata(nametag,"Version") or "?" -- "x.yy.z", etc ident = "OuroLoot2" identTg = "OuroLoot2Tg" status_text = nil DEBUG_PRINT = false debug = { comm = false, loot = false, flow = false, notraid = false, cache = false, alsolog = false, } function dprint (t,...) if DEBUG_PRINT and debug[t] then local text = flib.safeprint("<"..t.."> ",...) if debug.alsolog then addon:log_with_timestamp(text) end end end if author_debug then function pprint(t,...) local text = flib.safeprint("<<"..t..">> ",...) if debug.alsolog then addon:log_with_timestamp(text) end end else pprint = flib.nullfunc end enabled = false rebroadcast = false display = nil -- display frame, when visible loot_clean = nil -- index of last GUI entry with known-current visual data sender_list = {active={},names={}} -- this should be reworked threshold = debug.loot and 0 or 3 -- rare by default sharder = nil -- name of person whose loot is marked as shards -- The rest is also used in the GUI: popped = nil -- non-nil when reminder has been shown, actual value unimportant -- This is an amalgamation of all four LOOT_ITEM_* patterns. -- Captures: 1 person/You, 2 itemstring, 3 rest of string after final |r until '.' -- Can change 'loot' to 'item' to trigger on, e.g., extracting stuff from mail. --loot_pattern = "(%S+) receives? loot:.*|cff%x+|H(.-)|h.*|r(.*)%.$" bossmod_registered = nil bossmods = {} requesting = nil -- for prompting for additional rebroadcasters thresholds = {} for i = 0,6 do thresholds[i] = _G.ITEM_QUALITY_COLORS[i].hex .. _G["ITEM_QUALITY"..i.."_DESC"] .. "|r" end _G.setfenv (1, _G) end addon = LibStub("AceAddon-3.0"):NewAddon(addon, "Ouro Loot", "AceTimer-3.0", "AceComm-3.0", "AceConsole-3.0", "AceEvent-3.0") ------ Globals local g_loot = nil local g_restore_p = nil local g_wafer_thin = nil -- for prompting for additional rebroadcasters local g_today = nil -- "today" entry in g_loot local g_boss_signpost = nil local opts = nil local pairs, ipairs, tinsert, tremove, tonumber = pairs, ipairs, table.insert, table.remove, tonumber local pprint, tabledump = addon.pprint, flib.tabledump local GetNumRaidMembers = GetNumRaidMembers -- En masse forward decls of symbols defined inside local blocks local _register_bossmod local makedate, create_new_cache, _init -- Try to extract numbers from the .toc "Version" and munge them into an -- integral form for comparison. The result doesn't need to be meaningful as -- long as we can reliably feed two of them to "<" and get useful results. -- -- This makes/reinforces an assumption that revision_large of release packages -- (e.g., 2016001) will always be higher than those of development packages -- (e.g., 87), due to the tagging system versus subversion file revs. This -- is good, as local dev code will never trigger a false positive update -- warning for other users. do local r = 0 for d in addon.revision:gmatch("%d+") do r = 1000*r + d end revision_large = math.max(r,1) end -- Hypertext support, inspired by DBM broadcast pizza timers do local hypertext_format_str = "|HOuroRaid:%s|h%s[%s]|r|h" -- text will automatically be surrounded by brackets function addon.format_hypertext (code, text, color) return hypertext_format_str:format (code, type(color)=='number' and ITEM_QUALITY_COLORS[color].hex or color, text) end DEFAULT_CHAT_FRAME:HookScript("OnHyperlinkClick", function(self, link, string, mousebutton) local ltype, arg = strsplit(":",link) if ltype ~= "OuroRaid" then return end if arg == 'openloot' then addon:BuildMainDisplay() elseif arg == 'popupurl' then -- Sadly, this is not generated by the packager, so hardcode it for now. -- The 'data' field is handled differently for onshow than for other callbacks. StaticPopup_Show("OUROL_URL", --[[text_arg1=]]nil, --[[text_arg2=]]nil, --[[data=]][[http://www.curse.com/addons/wow/ouroloot]]) elseif arg == 'doping' then addon:DoPing() elseif arg == 'help' then addon:BuildMainDisplay('help') elseif arg == 'bcaston' then if not addon.rebroadcast then addon:Activate(nil,true) end addon:broadcast('bcast_responder') elseif arg == 'waferthin' then -- mint? it's wafer thin! g_wafer_thin = true -- fuck off, I'm full addon:broadcast('bcast_denied') -- remove once tested end end) local old = ItemRefTooltip.SetHyperlink function ItemRefTooltip:SetHyperlink (link, ...) if link:match("^OuroRaid") then return end return old (self, link, ...) end end do -- copied here because it's declared local to the calendar ui, thanks blizz >< local CALENDAR_FULLDATE_MONTH_NAMES = { FULLDATE_MONTH_JANUARY, FULLDATE_MONTH_FEBRUARY, FULLDATE_MONTH_MARCH, FULLDATE_MONTH_APRIL, FULLDATE_MONTH_MAY, FULLDATE_MONTH_JUNE, FULLDATE_MONTH_JULY, FULLDATE_MONTH_AUGUST, FULLDATE_MONTH_SEPTEMBER, FULLDATE_MONTH_OCTOBER, FULLDATE_MONTH_NOVEMBER, FULLDATE_MONTH_DECEMBER, } -- returns "dd Month yyyy", mm, dd, yyyy function makedate() Calendar_LoadUI() local _, M, D, Y = CalendarGetDate() local text = ("%d %s %d"):format(D, CALENDAR_FULLDATE_MONTH_NAMES[M], Y) return text, M, D, Y end end -- Returns an instance name or abbreviation local function instance_tag() local name, typeof, diffcode, diffstr, _, perbossheroic, isdynamic = GetInstanceInfo() local t name = addon.instance_abbrev[name] or name if typeof == "none" then return name end -- diffstr is "5 Player", "10 Player (Heroic)", etc. ugh. if diffcode == 1 then t = ((GetNumRaidMembers()>0) and "10" or "5") elseif diffcode == 2 then t = ((GetNumRaidMembers()>0) and "25" or "5h") elseif diffcode == 3 then t = "10h" elseif diffcode == 4 then t = "25h" end -- dynamic difficulties always return normal "codes" if isdynamic and perbossheroic == 1 then t = t .. "h" end return name .. "(" .. t .. ")" end addon.instance_tag = instance_tag -- grumble ------ Expiring caches --[[ foo = create_new_cache("myfoo",15[,cleanup]) -- ttl foo:add("blah") foo:test("blah") -- returns true ]] do local caches = {} local cleanup_group = _G.AnimTimerFrame:CreateAnimationGroup() local time = _G.time cleanup_group:SetLooping("REPEAT") cleanup_group:SetScript("OnLoop", function(cg) addon.dprint('cache',"OnLoop firing") local now = time() local alldone = true -- this is ass-ugly for name,c in pairs(caches) do local fifo = c.fifo local active = #fifo > 0 while (#fifo > 0) and (now - fifo[1].t > c.ttl) do addon.dprint('cache', name, "cache removing",fifo[1].t, fifo[1].m) tremove(fifo,1) end if active and #fifo == 0 and c.func then addon.dprint('cache', name, "empty, firing cleanup") c:func() end alldone = alldone and (#fifo == 0) end if alldone then addon.dprint('cache',"OnLoop finishing animation group") cleanup_group:Finish() end addon.dprint('cache',"OnLoop done") end) local function _add (cache, x) local datum = { t=time(), m=x } cache.hash[x] = datum tinsert (cache.fifo, datum) if not cleanup_group:IsPlaying() then addon.dprint('cache', cache.name, "STARTING animation group") cache.cleanup:SetDuration(2) -- hmmm cleanup_group:Play() end end local function _test (cache, x) return cache.hash[x] ~= nil --[[for _,v in ipairs(cache) do if v.m == x then return true end end]] end function create_new_cache (name, ttl, on_alldone) -- setting OnFinished for cleanup fires at the end of each inner loop, -- with no 'requested' argument to distinguish cases. thus, on_alldone. local c = { ttl = ttl, name = name, add = _add, test = _test, cleanup = cleanup_group:CreateAnimation("Animation"), func = on_alldone, fifo = {}, hash = setmetatable({}, {__mode='kv'}), } c.cleanup:SetOrder(1) caches[name] = c return c end end ------ Ace3 framework stuff function addon:OnInitialize() -- VARIABLES_LOADED has fired by this point; test if we're doing something like -- relogging during a raid and already have collected loot data g_restore_p = OuroLootSV ~= nil self.dprint('flow', "oninit sets restore as", g_restore_p) if OuroLootSV_opts == nil then OuroLootSV_opts = {} self:ScheduleTimer(function(s) s:Print(virgin, s.format_hypertext('help',"click here",ITEM_QUALITY_UNCOMMON)) virgin = nil end,10,self) end opts = OuroLootSV_opts for opt,default in pairs(option_defaults) do if opts[opt] == nil then opts[opt] = default end end -- transition&remove old options opts['forum_use_itemid'] = nil if opts['forum_format'] then opts.forum['Custom...'] = opts['forum_format'] opts['forum_format'] = nil end if opts.forum['[url]'] then opts.forum['[url] Wowhead'] = opts.forum['[url]'] opts.forum['[url]'] = nil opts.forum['[url] MMO/Wowstead'] = option_defaults.forum['[url] MMO/Wowstead'] if opts['forum_current'] == '[url]' then opts['forum_current'] = '[url] Wowhead' end end option_defaults = nil if OuroLootSV then -- may not be the same as testing g_restore_p soon if OuroLootSV.saved then OuroLootSV_saved = OuroLootSV.saved; OuroLootSV.saved = nil end if OuroLootSV.threshold then opts.threshold = OuroLootSV.threshold; OuroLootSV.threshold = nil end if OuroLootSV.autoshard then opts.autoshard = OuroLootSV.autoshard; OuroLootSV.autoshard = nil end end -- get item filter table if needed if opts.itemfilter == nil then opts.itemfilter = addon.default_itemfilter end addon.default_itemfilter = nil self:RegisterChatCommand("ouroloot", "OnSlash") -- maybe try to detect if this command is already in use... if opts.register_slashloot then SLASH_ACECONSOLE_OUROLOOT2 = "/loot" end self.history_all = self.history_all or OuroLootSV_hist or {} local r = assert(GetRealmName()) self.history_all[r] = self:_prep_new_history_category (self.history_all[r], r) self.history = self.history_all[r] --OuroLootSV_hist = nil _init(self) self.dprint('flow', "version strings:", revision_large, self.status_text) self.OnInitialize = nil end function addon:OnEnable() self:RegisterEvent("PLAYER_LOGOUT") self:RegisterEvent("RAID_ROSTER_UPDATE") -- Cribbed from Talented. I like the way jerry thinks: the first argument -- can be a format spec for the remainder of the arguments. (The new -- AceConsole:Printf isn't used because we can't specify a prefix without -- jumping through ridonkulous hoops.) The part about overriding :Print -- with a version using prefix hyperlinks is my fault. do local AC = LibStub("AceConsole-3.0") local chat_prefix = self.format_hypertext('openloot',"Ouro Loot",--[[legendary]]5) function addon:Print (str, ...) if type(str) == 'string' and str:find("%", nil, --[[plainmatch=]]true) then return AC:Print (chat_prefix, str:format(...)) else return AC:Print (chat_prefix, str, ...) end end end if opts.keybinding then KeyBindingFrame_LoadUI() local btn = CreateFrame("Button", "OuroLootBindingOpen", nil, "SecureActionButtonTemplate") btn:SetAttribute("type", "macro") btn:SetAttribute("macrotext", "/ouroloot toggle") if SetBindingClick(opts.keybinding_text, "OuroLootBindingOpen") then -- a simple SaveBindings(GetCurrentBindingSet()) occasionally fails when GCBS -- decides to return neither 1 nor 2 during load local c = GetCurrentBindingSet() if c == ACCOUNT_BINDINGS or c == CHARACTER_BINDINGS then SaveBindings(c) end else self:Print("Error registering '%s' as a keybinding, check spelling!", opts.keybinding_text) end end --[[ The four loot format patterns of interest, changed into relatively tight string match patterns. Done at enable-time rather than load-time against the slim chance that one of the non-US "delocalizers" needs to mess with the global patterns before we transform them. The SELF variants can be replaced with LOOT_ITEM_PUSHED_SELF[_MULTIPLE] to trigger on 'receive item' instead, which would detect extracting stuff from mail, or s/PUSHED/CREATED/ for things like healthstones and guild cauldron flasks. ]] -- LOOT_ITEM = "%s receives loot: %s." --> (.+) receives loot: (.+)%. g_LOOT_ITEM_ss = _G.LOOT_ITEM:gsub('%.$','%%.'):gsub('%%s','(.+)') -- LOOT_ITEM_MULTIPLE = "%s receives loot: %sx%d." --> (.+) receives loot: (.+)(x%d+)%. g_LOOT_ITEM_MULTIPLE_sss = _G.LOOT_ITEM_MULTIPLE:gsub('%.$','%%.'):gsub('%%s','(.+)'):gsub('x%%d','(x%%d+)') -- LOOT_ITEM_SELF = "You receive loot: %s." --> You receive loot: (.+)%. g_LOOT_ITEM_SELF_s = _G.LOOT_ITEM_SELF:gsub('%.$','%%.'):gsub('%%s','(.+)') -- LOOT_ITEM_SELF_MULTIPLE = "You receive loot: %sx%d." --> You receive loot: (.+)(x%d+)%. g_LOOT_ITEM_SELF_MULTIPLE_ss = _G.LOOT_ITEM_SELF_MULTIPLE:gsub('%.$','%%.'):gsub('%%s','(.+)'):gsub('x%%d','(x%%d+)') if self.debug.flow then self:Print"is in control-flow debug mode." end end --function addon:OnDisable() end ------ Event handlers function addon:_clear_SVs() g_loot = {} -- not saved, just fooling PLAYER_LOGOUT tests OuroLootSV = nil OuroLootSV_saved = nil OuroLootSV_opts = nil OuroLootSV_hist = nil OuroLootSV_log = nil ReloadUI() end function addon:PLAYER_LOGOUT() self:UnregisterEvent("RAID_ROSTER_UPDATE") self:UnregisterEvent("PLAYER_ENTERING_WORLD") local worth_saving = #g_loot > 0 or next(g_loot.raiders) if not worth_saving then for text in self:registered_textgen_iter() do worth_saving = worth_saving or g_loot.printed[text] > 0 end end if worth_saving then opts.autoshard = self.sharder opts.threshold = self.threshold for i,e in ipairs(g_loot) do e.cols = nil end OuroLootSV = g_loot else OuroLootSV = nil end for r,t in pairs(self.history_all) do if type(t) == 'table' then if #t == 0 then self.history_all[r] = nil else t.realm = nil t.st = nil t.byname = nil end end end OuroLootSV_hist = self.history_all OuroLootSV_log = #OuroLootSV_log > 0 and OuroLootSV_log or nil end do local IsInInstance, UnitName, UnitIsConnected, UnitClass, UnitRace, UnitSex, UnitLevel, UnitInRaid, UnitIsVisible, GetGuildInfo = IsInInstance, UnitName, UnitIsConnected, UnitClass, UnitRace, UnitSex, UnitLevel, UnitInRaid, UnitIsVisible, GetGuildInfo local time, difftime = time, difftime local R_ACTIVE, R_OFFLINE, R_LEFT = 1, 2, 3 local lastevent, now = 0, 0 local redo_count = 0 local redo, timer_handle function addon:CheckRoster (leaving_p, now_a) if not g_loot.raiders then return end -- bad transition now = now_a or time() if leaving_p then if timer_handle then self:CancelTimer(timer_handle) timer_handle = nil end for name,r in pairs(g_loot.raiders) do r.leave = r.leave or now end return end for name,r in pairs(g_loot.raiders) do if r.online ~= R_LEFT and not UnitInRaid(name) then r.online = R_LEFT r.leave = now end end if redo then redo_count = redo_count + 1 end redo = false for i = 1, GetNumRaidMembers() do local unit = 'raid'..i local name = UnitName(unit) -- No, that's not my typo, it really is "uknownbeing" in Blizzard's code. if name and name ~= UNKNOWN and name ~= UNKNOWNOBJECT and name ~= UKNOWNBEING then if not g_loot.raiders[name] then g_loot.raiders[name] = { needinfo=true } end local r = g_loot.raiders[name] if r.needinfo and UnitIsVisible(unit) then r.needinfo = nil r.class = select(2,UnitClass(unit)) r.race = select(2,UnitRace(unit)) r.sex = UnitSex(unit) r.level = UnitLevel(unit) r.guild = GetGuildInfo(unit) end local connected = UnitIsConnected(unit) if connected and r.online ~= R_ACTIVE then r.join = r.join or now r.online = R_ACTIVE elseif (not connected) and r.online ~= R_OFFLINE then r.leave = now r.online = R_OFFLINE end redo = redo or r.needinfo end end if redo then -- XXX test redo_count here and eventually give up? if not timer_handle then timer_handle = self:ScheduleRepeatingTimer("RAID_ROSTER_UPDATE", 60) end else redo_count = 0 if timer_handle then self:CancelTimer(timer_handle) timer_handle = nil end end end function addon:RAID_ROSTER_UPDATE (event) if GetNumRaidMembers() == 0 then -- Leaving a raid group. -- Because of PLAYER_ENTERING_WORLD, this code also executes on load -- screens while soloing and in regular groups. Take care. self.dprint('flow', "GetNumRaidMembers == 0") if self.enabled and not self.debug.notraid then self.dprint('flow', "enabled, leaving raid") self.popped = nil self:Deactivate() -- self:UnregisterEvent("CHAT_MSG_LOOT") self:CheckRoster(--[[leaving raid]]true) end return end local inside,whatkind = IsInInstance() if inside and (whatkind == "pvp" or whatkind == "arena") then return self.dprint('flow', "got RRU event but in pvp zone, bailing") end local docheck = self.enabled if event == "Activate" then -- dispatched manually from Activate self:RegisterEvent("CHAT_MSG_LOOT") _register_bossmod(self) docheck = true elseif event == "RAID_ROSTER_UPDATE" then -- hot code path, be careful -- event registration from onload, joined a raid, maybe show popup self.dprint('flow', "RRU check:", self.popped, opts.popup_on_join) if (not self.popped) and opts.popup_on_join then self.popped = StaticPopup_Show "OUROL_REMIND" self.popped.data = self return end end -- Throttle the checks fired by common events. if docheck and not InCombatLockdown() then now = time() if difftime(now,lastevent) > 45 then lastevent = now self:CheckRoster(false,now) end end end end -- helper for CHAT_MSG_LOOT handler do -- Recent loot cache local candidates = {} local function prefer_local_loots (cache) -- The function name is a bit of a misnomer, as local entries overwrite -- remote entries as the candidate table is populated. This routine is -- to extract the results once the cache timers have expired. for i,sig in ipairs(candidates) do addon.dprint('loot', "processing candidate entry", i, sig) local loot = candidates[sig] if loot then addon.dprint('loot', i, "was found") candidates[sig] = nil local looti = addon._addLootEntry(loot) if (loot.disposition ~= 'shard') and (loot.disposition ~= 'gvault') and (not addon.history_suppress) then addon:_addHistoryEntry(looti) end end end if addon.display then addon:redisplay() end table.wipe(candidates) end addon.recent_loot = create_new_cache ('loot', comm_cleanup_ttl, prefer_local_loots) local GetItemInfo, GetItemIcon = GetItemInfo, GetItemIcon -- 'from' and onwards only present if this is triggered by a broadcast function addon:_do_loot (local_override, recipient, itemid, count, from, extratext) local itexture = GetItemIcon(itemid) local iname, ilink, iquality = GetItemInfo(itemid) local i if (not iname) or (not itexture) then i = true iname, ilink, iquality, itexture = UNKNOWN..': '..itemid, 'item:6948', ITEM_QUALITY_COMMON, [[ICONS\INV_Misc_QuestionMark]] end self.dprint('loot',">>_do_loot, R:", recipient, "I:", itemid, "C:", count, "frm:", from, "ex:", extratext, "q:", iquality) itemid = tonumber(ilink:match("item:(%d+)") or 0) -- This is only a loop to make jumping out of it easy, and still do cleanup below. while local_override or ((iquality >= self.threshold) and not opts.itemfilter[itemid]) do if (self.rebroadcast and (not from)) and not local_override then self:broadcast('loot', recipient, itemid, count) end if (not self.enabled) and (not local_override) then break end local signature = recipient .. iname .. (count or "") if from and self.recent_loot:test(signature) then self.dprint('cache', "loot <",signature,"> already in cache, skipping") else self.recent_loot:add(signature) -- There is some redundancy in all this, in the interests of ease-of-coding i = { kind = 'loot', person = recipient, person_class= select(2,UnitClass(recipient)), cache_miss = i and true or nil, quality = iquality, itemname = iname, id = itemid, itemlink = ilink, itexture = itexture, disposition = (recipient == self.sharder) and 'shard' or nil, count = count, bcast_from = from, extratext = extratext, is_heroic = self:is_heroic_item(ilink), } candidates[signature] = i tinsert (candidates, signature) self.dprint('cache', "loot <",signature,"> added to cache, candidate", #candidates) end break end self.dprint('loot',"<<_do_loot out") return i end function addon:CHAT_MSG_LOOT (event, ...) if (not self.rebroadcast) and (not self.enabled) and (event ~= "manual") then return end --[[ iname: Hearthstone iquality: integer ilink: clickable formatted link itemstring: item:6948:.... itexture: inventory icon texture ]] if event == "CHAT_MSG_LOOT" then local msg = ... local person, itemstring, count --ChatFrame2:AddMessage("original string: >"..(msg:gsub("\124","\124\124")).."<") --local person, itemstring, remainder = msg:match(self.loot_pattern) -- test in most likely order: other people get more loot than "you" do person, itemstring, count = msg:match(g_LOOT_ITEM_MULTIPLE_sss) if not person then person, itemstring = msg:match(g_LOOT_ITEM_ss) end if not person then itemstring, count = msg:match(g_LOOT_ITEM_SELF_MULTIPLE_ss) if not itemstring then itemstring = msg:match(g_LOOT_ITEM_SELF_s) end end --self.dprint('loot', "CHAT_MSG_LOOT, person is", person, ", itemstring is", itemstring, ", rest is", remainder) self.dprint('loot', "CHAT_MSG_LOOT, person is", person, ", itemstring is", itemstring, ", count is", count) if not itemstring then return end -- "So-and-So selected Greed", etc, not actual looting --local count = remainder and remainder:match(".*(x%d+)$") -- Name might be colorized, remove the highlighting if person then person = person:match("|c%x%x%x%x%x%x%x%x(%S+)") or person else -- UNIT_YOU / You person = my_name end --local id = tonumber((select(2, strsplit(":", itemstring)))) local id = tonumber(itemstring:match('|Hitem:(%d+):')) return self:_do_loot (false, person, id, count) elseif event == "broadcast" then return self:_do_loot(false, ...) elseif event == "manual" then local r,i,n = ... return self:_do_loot(true, r,i,nil,nil,n) end end end ------ Slash command handler -- Thought about breaking this up into a table-driven dispatcher. But -- that would result in a pile of teensy functions, most of which would -- never be called. Too much overhead. (2.0: Most of these removed now -- that GUI is in place.) function addon:OnSlash (txt) --, editbox) txt = strtrim(txt:lower()) local cmd, arg = "" do local s,e = txt:find("^%a+") if s then cmd = txt:sub(s,e) s = txt:find("%S", e+2) if s then arg = txt:sub(s,-1) end end end if cmd == "" then if InCombatLockdown() then return self:Print("Can't display window in combat.") else return self:BuildMainDisplay() end elseif cmd:find("^thre") then self:SetThreshold(arg) elseif cmd == "on" then self:Activate(arg) elseif cmd == "off" then self:Deactivate() elseif cmd == "broadcast" or cmd == "bcast" then self:Activate(nil,true) elseif cmd == "toggle" then if self.display then self.display:Hide() else return self:BuildMainDisplay() end elseif cmd == "fake" then -- maybe comment this out for real users self:_mark_boss_kill (self._addLootEntry{ kind='boss',reason='kill',bosskill="Baron Steamroller",instance=instance_tag(),duration=0 }) self:CHAT_MSG_LOOT ('manual', my_name, 54797) if self.display then self:redisplay() end self:Print "Baron Steamroller has been slain. Congratulations on your rug." elseif cmd == "debug" then if arg then self.debug[arg] = not self.debug[arg] _G.print(arg,self.debug[arg]) if self.debug[arg] then self.DEBUG_PRINT = true end else self.DEBUG_PRINT = not self.DEBUG_PRINT end elseif cmd == "save" and arg and arg:len() > 0 then self:save_saveas(arg) elseif cmd == "list" then self:save_list() elseif cmd == "restore" and arg and arg:len() > 0 then self:save_restore(tonumber(arg)) elseif cmd == "delete" and arg and arg:len() > 0 then self:save_delete(tonumber(arg)) elseif cmd == "help" then self:BuildMainDisplay('help') elseif cmd == "ping" then self:DoPing() elseif cmd == "fixcache" then self:do_item_cache_fixup() else if self:OpenMainDisplayToTab(cmd) then return end self:Print("Unknown command '%s'. %s to see the help window.", cmd, self.format_hypertext('help',"Click here",ITEM_QUALITY_UNCOMMON)) end end function addon:SetThreshold (arg, quiet_p) local q = tonumber(arg) if q then q = math.floor(q+0.001) if q<0 or q>6 then return self:Print("Threshold must be 0-6.") end else q = qualnames[arg] if not q then return self:Print("Unrecognized item quality argument.") end end self.threshold = q if not quiet_p then self:Print("Threshold now set to %s.", self.thresholds[q]) end end ------ On/off function addon:Activate (opt_threshold, opt_bcast_only) self.dprint('flow', ":Activate is running") self:RegisterEvent("RAID_ROSTER_UPDATE") self:RegisterEvent("PLAYER_ENTERING_WORLD", function() self:ScheduleTimer("RAID_ROSTER_UPDATE", 5, "PLAYER_ENTERING_WORLD") end) self.popped = true if GetNumRaidMembers() > 0 then self.dprint('flow', ">:Activate calling RRU") self:RAID_ROSTER_UPDATE("Activate") elseif self.debug.notraid then self.dprint('flow', ">:Activate registering loot and bossmods") self:RegisterEvent("CHAT_MSG_LOOT") _register_bossmod(self) elseif g_restore_p then g_restore_p = nil self.popped = nil -- get the reminder if later joining a raid if #g_loot == 0 then -- only generated text and raider join/leave data, not worth verbage self.dprint('flow', ">:Activate restored generated texts, un-popping") return end self:Print("Ouro Raid Loot restored previous data, but not in a raid", "and 5-person mode not active. |cffff0505NOT tracking loot|r;", "use 'enable' to activate loot tracking, or 'clear' to erase", "previous data, or 'help' to read about saved-texts commands.") return end self.rebroadcast = true -- hardcode to true; this used to be more complicated self.enabled = not opt_bcast_only if opt_threshold then self:SetThreshold (opt_threshold, --[[quiet_p=]]true) end self:Print("Ouro Raid Loot is %s. Threshold currently %s.", self.enabled and "tracking" or "only broadcasting", self.thresholds[self.threshold]) self:broadcast('revcheck',revision_large) end -- Note: running '/loot off' will also avoid the popup reminder when -- joining a raid, but will not change the saved option setting. function addon:Deactivate() self.enabled = false self.rebroadcast = false self:UnregisterEvent("RAID_ROSTER_UPDATE") self:UnregisterEvent("PLAYER_ENTERING_WORLD") self:UnregisterEvent("CHAT_MSG_LOOT") self:Print("Ouro Raid Loot deactivated.") end function addon:Clear(verbose_p) local repopup, st if self.display then -- in the new version, this is likely to always be the case repopup = true st = self.display:GetUserData("eoiST") if not st then self.dprint('flow', "Clear: display visible but eoiST not set??") end self.display:Hide() end g_restore_p = nil OuroLootSV = nil self:_reset_timestamps() if verbose_p then if (OuroLootSV_saved and #OuroLootSV_saved>0) then self:Print("Current loot data cleared, %d saved sets remaining.", #OuroLootSV_saved) else self:Print("Current loot data cleared.") end end _init(self,st) if repopup then addon:BuildMainDisplay() end end ------ Behind the scenes routines -- Semi-experimental debugging aid. do local date = _G.date local log = OuroLootSV_log function addon:log_with_timestamp (msg) tinsert (log, date('%m:%d %H:%M:%S ')..msg) end end -- Adds indices to traverse the tables in a nice sorted order. do local byindex, temp = {}, {} local function sort (src, dest) for k in pairs(src) do temp[#temp+1] = k end table.sort(temp) table.wipe(dest) for i = 1, #temp do dest[i] = src[temp[i]] end end function addon.sender_list.sort() sort (addon.sender_list.active, byindex) table.wipe(temp) addon.sender_list.activeI = #byindex sort (addon.sender_list.names, byindex) table.wipe(temp) end addon.sender_list.namesI = byindex end -- Message sending. -- See OCR_funcs.tag at the end of this file for incoming message treatment. do local function assemble(...) local msg = ... for i = 2, select('#',...) do msg = msg .. '\a' .. (select(i,...) or "") end return msg end -- broadcast('tag', <stuff>) function addon:broadcast(...) local msg = assemble(...) self.dprint('comm', "<broadcast>:", msg) -- the "GUILD" here is just so that we can also pick up on it self:SendCommMessage(self.ident, msg, self.debug.comm and "GUILD" or "RAID") end -- whispercast(<to>, 'tag', <stuff>) function addon:whispercast(to,...) local msg = assemble(...) self.dprint('comm', "<whispercast>@", to, ":", msg) self:SendCommMessage(self.identTg, msg, "WHISPER", to) end end function addon:DoPing() self:Print("Give me a ping, Vasili. One ping only, please.") self.sender_list.active = {} self.sender_list.names = {} self:broadcast('ping') self:broadcast('revcheck',revision_large) end function addon:_check_revision (otherrev) self.dprint('comm', "revchecking against", otherrev) otherrev = tonumber(otherrev) if otherrev == revision_large then -- normal case elseif otherrev < revision_large then self.dprint('comm', "ours is newer, notifying") self:broadcast('revcheck',revision_large) else self.dprint('comm', "ours is older, yammering") if newer_warning then self:Print(newer_warning, self.format_hypertext('popupurl',"click here",ITEM_QUALITY_UNCOMMON), self.format_hypertext('doping',"click here",ITEM_QUALITY_UNCOMMON)) newer_warning = nil end end end -- Generic helpers -- Returns index and entry at that index, or nil if not found. function addon._find_next_after (kind, index) index = index + 1 while index <= #g_loot do if g_loot[index].kind == kind then return index, g_loot[index] end index = index + 1 end end -- Essentially a _find_next_after('time'-or-'boss'), but if KIND is -- 'boss', will also stop upon finding a timestamp. Returns nil if -- appropriate fencepost is not found. function addon._find_timeboss_fencepost (kind, index) local fencepost local closest_time = addon._find_next_after('time',index) if kind == 'time' then fencepost = closest_time elseif kind == 'boss' then local closest_boss = addon._find_next_after('boss',index) if not closest_boss then fencepost = closest_time elseif not closest_time then fencepost = closest_boss else fencepost = math.min(closest_time,closest_boss) end end return fencepost end -- Iterate through g_loot entries according to the KIND field. Loop variables -- are g_loot indices and the corresponding entries (essentially ipairs + some -- conditionals). function addon:filtered_loot_iter (filter_kind) return self._find_next_after, filter_kind, 0 end do local itt local function create() local tip, lefts = CreateFrame("GameTooltip"), {} for i = 1, 2 do -- scanning idea here also snagged from Talented local L,R = tip:CreateFontString(), tip:CreateFontString() L:SetFontObject(GameFontNormal) R:SetFontObject(GameFontNormal) tip:AddFontStrings(L,R) lefts[i] = L end tip.lefts = lefts return tip end function addon:is_heroic_item(item) -- returns true or *nil* itt = itt or create() itt:SetOwner(UIParent,"ANCHOR_NONE") itt:ClearLines() itt:SetHyperlink(item) local t = itt.lefts[2]:GetText() itt:Hide() return (t == ITEM_HEROIC) or nil end end -- Called when first loading up, and then also when a 'clear' is being -- performed. If SV's are present then g_restore_p will be true. function _init (self, possible_st) self.dprint('flow',"_init running") self.loot_clean = nil self.hist_clean = nil if g_restore_p then g_loot = OuroLootSV self.popped = #g_loot > 0 self.dprint('flow', "restoring", #g_loot, "entries") self:ScheduleTimer("Activate", 12, opts.threshold) -- FIXME printed could be too large if entries were deleted, how much do we care? self.sharder = opts.autoshard else g_loot = { printed = {}, raiders = {} } end self.threshold = opts.threshold or self.threshold -- in the case of restoring but not tracking self:gui_init(g_loot) opts.autoshard = nil opts.threshold = nil if g_restore_p then self:zero_printed_fenceposts() -- g_loot.printed.* = previous/safe values else self:zero_printed_fenceposts(0) -- g_loot.printed.* = 0 end if possible_st then possible_st:SetData(g_loot) end self.status_text = ("%s communicating as ident %s commrev %d"):format(self.revision,self.ident,self.commrev) self:RegisterComm(self.ident) self:RegisterComm(self.identTg, "OnCommReceivedNocache") if self.author_debug then _G.OL = self _G.Oloot = g_loot end end -- Tie-in with Deadly Boss Mods (or other such addons) do local candidates = {} local location local function fixup_durations (cache) local boss, bossi boss = candidates[1] if #candidates == 1 then -- (1) or (2) boss.duration = boss.duration or 0 addon.dprint('loot', "only one boss candidate") else -- (3), should only be one 'cast entry and our local entry if #candidates ~= 2 then -- could get a bunch of 'cast entries on the heels of one another -- before the local one ever fires, apparently... sigh --addon:Print("<warning> s3 cache has %d entries, does that seem right to you?", #candidates) end if candidates[2].duration == nil then --addon:Print("<warning> s3's second entry is not the local trigger, does that seem right to you?") end -- try and be generic anyhow for i,c in ipairs(candidates) do if c.duration then boss = c addon.dprint('loot', "fixup found boss candidate", i, "duration", c.duration) break end end end bossi = addon._addLootEntry(boss) -- addon. bossi = addon._adjustBossOrder (bossi, g_boss_signpost) or bossi g_boss_signpost = nil addon.dprint('loot', "added boss entry", bossi) if boss.reason == 'kill' then addon:_mark_boss_kill (bossi) if opts.chatty_on_kill then addon:Print("Registered kill for '%s' in %s!", boss.bosskill, boss.instance) end end table.wipe(candidates) end addon.recent_boss = create_new_cache ('boss', 10, fixup_durations) -- Similar to _do_loot, but duration+ parms only present when locally generated. local function _do_boss (self, reason, bossname, intag, duration, raiders) self.dprint('loot',">>_do_boss, R:", reason, "B:", bossname, "T:", intag, "D:", duration, "RL:", (raiders and #raiders or 'nil')) if self.rebroadcast and duration then self:broadcast('boss', reason, bossname, intag) end -- This is only a loop to make jumping out of it easy, and still do cleanup below. while self.enabled do if reason == 'wipe' and opts.no_tracking_wipes then break end bossname = (opts.snarky_boss and self.boss_abbrev[bossname] or bossname) or bossname local not_from_local = duration == nil local signature = bossname .. reason if not_from_local and self.recent_boss:test(signature) then self.dprint('cache', "boss <",signature,"> already in cache, skipping") else self.recent_boss:add(signature) g_boss_signpost = #g_loot + 1 self.dprint('loot', "added boss signpost", g_boss_signpost) -- Possible scenarios: (1) we don't see a boss event at all (e.g., we're -- outside the instance) and so this only happens once as a non-local event, -- (2) we see a local event first and all non-local events are filtered -- by the cache, (3) we happen to get some non-local events before doing -- our local event (not because of network weirdness but because our local -- DBM might not trigger for a while). local c = { kind = 'boss', bosskill = bossname, -- minor misnomer, might not actually be a kill reason = reason, instance = intag, duration = duration, -- these two deliberately may be nil raiderlist = raiders and table.concat(raiders, ", ") } tinsert(candidates,c) end break end self.dprint('loot',"<<_do_boss out") end -- No wrapping layer for now addon.on_boss_broadcast = _do_boss function addon:_mark_boss_kill (index) local e = g_loot[index] if not e then self:Print("Something horribly wrong;", index, "is not a valid entry!") return end if not e.bosskill then self:Print("Something horribly wrong;", index, "is not a boss entry!") return end if e.reason ~= 'wipe' then -- enh, bail self.loot_clean = index-1 end local attempts = 1 local first local i,d = 1,g_loot[1] while d ~= e do if d.bosskill and d.bosskill == e.bosskill and d.reason == 'wipe' then first = first or i attempts = attempts + 1 assert(tremove(g_loot,i)==d,"_mark_boss_kill screwed up data badly") else i = i + 1 end d = g_loot[i] end e.reason = 'kill' e.attempts = attempts self.loot_clean = first or index-1 end function addon:register_boss_mod (name, registration_func, deregistration_func) assert(type(name)=='string') assert(type(registration_func)=='function') if deregistration_func ~= nil then assert(type(deregistration_func)=='function') end self.bossmods[#self.bossmods+1] = { n = name, r = registration_func, d = deregistration_func, } self.bossmods[name] = self.bossmods[#self.bossmods] assert(self.bossmods[name].n == self.bossmods[#self.bossmods].n) end function _register_bossmod (self, force_p) local x = self.bossmod_registered and self.bossmods[self.bossmod_registered] if x then if x.n == opts.bossmod and not force_p then -- trying to register with already-registered boss mod return else -- deregister if x.d then x.d(self) end end end x = nil for k,v in ipairs(self.bossmods) do if v.n == opts.bossmod then x = k break end end if not x then self.status_text = "|cffff1010No boss-mod found!|r" self:Print(self.status_text) return end if self.bossmods[x].r (self, _do_boss) then --self.bossmod_registered = x self.bossmod_registered = self.bossmods[x].n else self:Print("|cffff1010Boss mod registration failed|r") end end end -- Adding entries to the loot record, and tracking the corresponding timestamp. do -- This shouldn't be required. /sadface local loot_entry_mt = { __index = function (e,key) if key == 'cols' then pprint('mt', e.kind, "key is", key) --tabledump(e) -- not actually that useful addon:_fill_out_eoi_data(1) end return rawget(e,key) end } -- Given a loot index, searches backwards for a timestamp. Returns that -- index and the time entry, or nil if it falls off the beginning. Pass an -- optional second index to search no earlier than it. -- May also be able to make good use of this in forum-generation routine. function addon:find_previous_time_entry(i,stop) stop = stop or 0 while i > stop do if g_loot[i].kind == 'time' then return i, g_loot[i] end i = i - 1 end end -- format_timestamp (["format_string"], Day, [Loot]) -- DAY is a loot entry with kind=='time', and controls the date printed. -- LOOT may be any kind of entry in the g_loot table. If present, it -- overrides the hour and minute printed; if absent, those values are -- taken from the DAY entry. -- FORMAT_STRING may contain $x (x in Y/M/D/h/m) tokens. local format_timestamp_values, point2dee = {}, "%.2d" function addon:format_timestamp (fmt_opt, day_entry, time_entry_opt) if not time_entry_opt then if type(fmt_opt) == 'table' then -- Two entries, default format time_entry_opt, day_entry = day_entry, fmt_opt fmt_opt = "$Y/$M/$D $h:$m" --elseif type(fmt_opt) == "string" then -- Day entry only, specified format end end --format_timestamp_values.Y = point2dee:format (day_entry.startday.year % 100) format_timestamp_values.Y = ("%.4d"):format (day_entry.startday.year) format_timestamp_values.M = point2dee:format (day_entry.startday.month) format_timestamp_values.D = point2dee:format (day_entry.startday.day) format_timestamp_values.h = point2dee:format ((time_entry_opt or day_entry).hour) format_timestamp_values.m = point2dee:format ((time_entry_opt or day_entry).minute) return fmt_opt:gsub ('%$([YMDhm])', format_timestamp_values) end local done_todays_date function addon:_reset_timestamps() done_todays_date = nil end local function do_todays_date() local text, M, D, Y = makedate() local found,ts = #g_loot+1 repeat found,ts = addon:find_previous_time_entry(found-1) if found and ts.startday.text == text then done_todays_date = true end until done_todays_date or (not found) if done_todays_date then g_today = ts else done_todays_date = true g_today = g_loot[addon._addLootEntry{ kind = 'time', startday = { text = text, month = M, day = D, year = Y } }] end addon:_fill_out_eoi_data(1) end -- Adding anything original to g_loot goes through this routine. function addon._addLootEntry (e) setmetatable(e,loot_entry_mt) if not done_todays_date then do_todays_date() end local h, m = GetGameTime() --local localuptime = math.floor(GetTime()) local time_t = time() e.hour = h e.minute = m e.stamp = time_t --localuptime local index = #g_loot + 1 g_loot[index] = e return index end -- Problem: (1) boss kill happens, (2) fast looting happens, (3) boss -- cache cleanup fires. Result: loot shows up before boss kill entry. -- Solution: We need to shuffle the boss entry above any of the loot -- from that boss. function addon._adjustBossOrder (is, should_be) --pprint('loot', is, should_be) if is == should_be then --pprint('loot', "equal, yay") return end if (type(is)~='number') or (type(should_be)~='number') or (is < should_be) then pprint('loot', is, should_be, "...the hell? bailing") return end if g_loot[should_be].kind == 'time' then should_be = should_be + 1 if is == should_be then --pprint('loot', "needed to mark day entry, otherwise equal, yay") return end end assert(g_loot[is].kind == 'boss') local boss = tremove (g_loot, is) --pprint('loot', "MOVING", boss.bosskill) tinsert (g_loot, should_be, boss) return should_be end end -- In the rare case of items getting put into the loot table without current -- item cache data (which will have arrived by now). function addon:do_item_cache_fixup() self:Print("Fixing up missing item cache data...") local numfound = 0 local borkedpat = '^'..UNKNOWN..': (%S+)' for i,e in self:filtered_loot_iter('loot') do if e.cache_miss then local borked_id = e.itemname:match(borkedpat) if borked_id then numfound = numfound + 1 -- Best to use the safest and most flexible API here, which is GII and -- its assload of return values. local iname, ilink, iquality, _,_,_,_,_,_, itexture = GetItemInfo(borked_id) if iname then self:Print(" Entry %d patched up with %s.", i, ilink) e.quality = iquality e.itemname = iname e.id = tonumber(ilink:match("item:(%d+)")) e.itemlink = ilink e.itexture = itexture e.cache_miss = nil end end end end self:Print("...finished. Found %d |4entry:entries; with weird data.", numfound) end ------ Saved texts function addon:check_saved_table(silent_p) local s = OuroLootSV_saved if s and (#s > 0) then return s end OuroLootSV_saved = nil if not silent_p then self:Print("There are no saved loot texts.") end end function addon:save_list() local s = self:check_saved_table(); if not s then return end; for i,t in ipairs(s) do self:Print("#%d %s %d entries %s", i, t.date, t.count, t.name) end end function addon:save_saveas(name) OuroLootSV_saved = OuroLootSV_saved or {} local SV = OuroLootSV_saved local n = #SV + 1 local save = { name = name, date = makedate(), count = #g_loot, } for text in self:registered_textgen_iter() do save[text] = g_loot[text] end self:Print("Saving current loot texts to #%d '%s'", n, name) SV[n] = save return self:save_list() end function addon:save_restore(num) local s = self:check_saved_table(); if not s then return end; if (not num) or (num > #s) then return self:Print("Saved text number must be 1 - "..#s) end local save = s[num] self:Print("Overwriting current loot data with saved text #%d '%s'", num, save.name) self:Clear(--[[verbose_p=]]false) -- Clear will already have displayed the window, and re-selected the first -- tab. Set these up for when the text tabs are clicked. for text in self:registered_textgen_iter() do g_loot[text] = save[text] end end function addon:save_delete(num) local s = self:check_saved_table(); if not s then return end; if (not num) or (num > #s) then return self:Print("Saved text number must be 1 - "..#s) end self:Print("Deleting saved text #"..num) tremove(s,num) return self:save_list() end ------ Loot histories -- history_all = { -- ["Kilrogg"] = { -- ["realm"] = "Kilrogg", -- not saved -- ["st"] = { lib-st display table }, -- not saved -- ["byname"] = { -- not saved -- ["OtherPlayer"] = 2, -- ["Farmbuyer"] = 1, -- } -- [1] = { -- ["name"] = "Farmbuyer", -- [1] = { id = nnnnn, when = "formatted timestamp for displaying" } -- most recent loot -- [2] = { ......., [count = "x3"] } -- previous loot -- }, -- [2] = { -- ["name"] = "OtherPlayer", -- ...... -- }, ...... -- }, -- ["OtherRealm"] = ...... -- } do local tsort = table.sort local comp = function(L,R) return L.when > R.when end -- Builds the map of names to array indices, using passed table or -- self.history, and stores the result into its 'byname' field. Also -- called from the GUI code at least once. function addon:_build_history_names (opt_hist) local hist = opt_hist or self.history local m = {} for i = 1, #hist do m[hist[i].name] = i end hist.byname = m end -- Prepares and returns table to be used as self.history. function addon:_prep_new_history_category (prev_table, realmname) local t = prev_table or { --kind = 'realm', --realm = realmname, } t.realm = realmname --[[ t.cols = setmetatable({ { value = realmname }, }, self.time_column1_used_mt) ]] if not t.byname then self:_build_history_names (t) end return t end -- Maps a name to an array index, creating new tables if needed. Returns -- the index and the table at that index. function addon:get_loot_history (name) local i i = self.history.byname[name] if not i then i = #self.history + 1 self.history[i] = { name=name } self.history.byname[name] = i end return i, self.history[i] end function addon:_addHistoryEntry (lootindex) local e = g_loot[lootindex] if e.kind ~= 'loot' then return end local i,h = self:get_loot_history(e.person) -- If any of these change, update the end of history_handle_disposition. local n = { id = e.id, when = self:format_timestamp (g_today, e), count = e.count, } tinsert (h, 1, n) e.history_unique = n.id .. ' ' .. n.when end -- Create new history table based on current loot. function addon:rewrite_history (realmname) local r = assert(realmname) self.history_all[r] = self:_prep_new_history_category (nil, r) self.history = self.history_all[r] local g_today_real = g_today for i,e in ipairs(g_loot) do if e.kind == 'time' then g_today = e elseif e.kind == 'loot' then self:_addHistoryEntry(i) end end g_today = g_today_real self.hist_clean = nil -- safety measure: resort players' tables based on formatted timestamp for i,h in ipairs(self.history) do tsort (h, comp) end end -- Clears all but latest entry for each player. function addon:preen_history (realmname) local r = assert(realmname) for i,h in ipairs(self.history) do tsort (h, comp) while #h > 1 do tremove (h) end end end -- Given an entry in a g_loot table, looks up the corresponding history -- entry. Returns the player's index and history table (as in get_loot_history) -- and the index into that table of the loot entry. On failure, returns nil -- and an error message ready to be formatted with the loot's name/itemlink. function addon:_history_by_loot_id (loot, operation_text) -- Using assert() here would be concatenating error strings that probably -- wouldn't be used. Do more verbose testing instead. if type(loot) ~= 'table' then error("trying to "..operation_text.." nonexistant entry") end if loot.kind ~= 'loot' then error("trying to "..operation_text.." something that isn't loot") end local player = loot.person local tag = loot.history_unique local errtxt local player_i, player_h, hist_i if not tag then errtxt = "Entry for %s is missing a history tag!" else player_i,player_h = self:get_loot_history(player) for i,h in ipairs(player_h) do local unique = h.id .. ' ' .. h.when if unique == tag then hist_i = i break end end if not hist_i then -- 1) loot an item, 2) clear old history, 3) reassign from current loot -- Bah. Anybody that tricky is already recoding the tables directly anyhow. errtxt = "There is no record of %s ever having been assigned!" end end if errtxt then return nil, errtxt end return player_i, player_h, hist_i end function addon:reassign_loot (index, to_name) assert(type(to_name)=='string' and to_name:len()>0) local e = g_loot[index] local from_i, from_h, hist_i = self:_history_by_loot_id (e, "reassign") local from_name = e.person local to_i,to_h = self:get_loot_history(to_name) if not from_i then -- from_h is the formatted error text self:Print(from_h .. " Loot will be reassigned, but history will NOT be updated.", e.itemlink) else local hist_h = tremove (from_h, hist_i) tinsert (to_h, 1, hist_h) tsort (from_h, comp) tsort (to_h, comp) end e.person = to_name e.person_class = select(2,UnitClass(to_name)) self.hist_clean = nil self:Print("Reassigned entry %d/%s from '%s' to '%s'.", index, e.itemlink, from_name, to_name) end -- Any extra work for the "Mark as <x>" dropdown actions. The -- corresponding <x> will already have been assigned in the loot entry. local deleted_cache = {} --setmetatable({}, {__mode='k'}) function addon:history_handle_disposition (index, olddisp) local e = g_loot[index] -- Standard disposition has a nil entry, but that's tedious in debug -- output, so force to a string instead. olddisp = olddisp or 'normal' local newdisp = e.disposition or 'normal' -- Ignore misclicks and the like if olddisp == newdisp then return end local name = e.person if (newdisp == 'shard' or newdisp == 'gvault') then local name_i, name_h, hist_i = self:_history_by_loot_id (e, "mark") -- remove history entry if hist_i then local hist_h = tremove (name_h, hist_i) deleted_cache[e.history_unique] = hist_h self.hist_clean = nil elseif (olddisp == 'shard' or olddisp == 'gvault') then -- Sharding a vault item, or giving the auto-sharder something to bank, -- etc, wouldn't necessarily have had a history entry to begin with. else self:Print(name_h .. " Loot has been marked, but history will NOT be updated.", e.itemlink) end return end if (olddisp == 'shard' or olddisp == 'gvault') and (newdisp == 'normal' or newdisp == 'offspec') then local name_i, name_h = self:get_loot_history(name) -- Must create a new history entry. Could call '_addHistoryEntry(index)' -- but that would duplicate a lot of effort. To start with, check the -- cache of stuff we've already deleted; if it's not there then just do -- the same steps as _addHistoryEntry. local entry if e.history_unique and deleted_cache[e.history_unique] then entry = deleted_cache[e.history_unique] deleted_cache[e.history_unique] = nil end local when = g_today and self:format_timestamp (g_today, e) or tostring(e.stamp) entry = entry or { id = e.id, when = when, count = e.count, } tinsert (name_h, 1, entry) e.history_unique = e.history_unique or (entry.id .. ' ' .. entry.when) self.hist_clean = nil return end end end ------ Player communication do local function adduser (name, status, active) if status then addon.sender_list.names[name] = status end if active then addon.sender_list.active[name] = active end end -- Incoming handler functions. All take the sender name and the incoming -- tag as the first two arguments. All of these are active even when the -- player is not tracking loot, so test for that when appropriate. local OCR_funcs = {} OCR_funcs.ping = function (sender) pprint('comm', "incoming ping from", sender) addon:whispercast (sender, 'pong', addon.revision, addon.enabled and "tracking" or (addon.rebroadcast and "broadcasting" or "disabled")) end OCR_funcs.pong = function (sender, _, rev, status) local s = ("|cff00ff00%s|r %s is |cff00ffff%s|r"):format(sender,rev,status) addon:Print("Echo: ", s) adduser (sender, s, status=="tracking" or status=="broadcasting" or nil) end OCR_funcs.revcheck = function (sender, _, revlarge) addon.dprint('comm', "revcheck, sender", sender) addon:_check_revision (revlarge) end OCR_funcs.loot = function (sender, _, recip, item, count, extratext) addon.dprint('comm', "DOTloot, sender", sender, "recip", recip, "item", item, "count", count) if not addon.enabled then return end adduser (sender, nil, true) addon:CHAT_MSG_LOOT ("broadcast", recip, item, count, sender, extratext) end OCR_funcs.boss = function (sender, _, reason, bossname, instancetag) addon.dprint('comm', "DOTboss, sender", sender, "reason", reason, "name", bossname, "it", instancetag) if not addon.enabled then return end adduser (sender, nil, true) addon:on_boss_broadcast (reason, bossname, instancetag) end OCR_funcs.bcast_req = function (sender) if addon.debug.comm or ((not g_wafer_thin) and (not addon.rebroadcast)) then addon:Print("%s has requested additional broadcasters! Choose %s to enable rebroadcasting, or %s to remain off and also ignore rebroadcast requests for as long as you're logged in. Or do nothing for now to see if other requests arrive.", sender, addon.format_hypertext('bcaston',"the red pill",'|cffff4040'), addon.format_hypertext('waferthin',"the blue pill",'|cff0070dd')) end addon.popped = true end OCR_funcs.bcast_responder = function (sender) if addon.debug.comm or addon.requesting or ((not g_wafer_thin) and (not addon.rebroadcast)) then addon:Print(sender, "has answered the call and is now broadcasting loot.") end end -- remove this tag once it's all tested OCR_funcs.bcast_denied = function (sender) if addon.requesting then addon:Print(sender, "declines futher broadcast requests.") end end -- Incoming message dispatcher local function dotdotdot (sender, tag, ...) local f = OCR_funcs[tag] addon.dprint('comm', ":... processing",tag,"from",sender) if f then return f(sender,tag,...) end addon.dprint('comm', "unknown comm message",tag",from", sender) end -- Recent message cache addon.recent_messages = create_new_cache ('comm', comm_cleanup_ttl) function addon:OnCommReceived (prefix, msg, distribution, sender) if prefix ~= self.ident then return end if not self.debug.comm then if distribution ~= "RAID" and distribution ~= "WHISPER" then return end if sender == my_name then return end end self.dprint('comm', ":OCR from", sender, "message is", msg) if self.recent_messages:test(msg) then return self.dprint('cache', "message <",msg,"> already in cache, skipping") end self.recent_messages:add(msg) -- Nothing is actually returned, just (ab)using tail calls. return dotdotdot(sender,strsplit('\a',msg)) end function addon:OnCommReceivedNocache (prefix, msg, distribution, sender) if prefix ~= self.identTg then return end if not self.debug.comm then if distribution ~= "WHISPER" then return end if sender == my_name then return end end self.dprint('comm', ":OCRN from", sender, "message is", msg) return dotdotdot(sender,strsplit('\a',msg)) end end -- vim:noet