Mercurial > wow > ouroloot
diff core.lua @ 1:822b6ca3ef89
Import of 2.15, moving to wowace svn.
author | Farmbuyer of US-Kilrogg <farmbuyer@gmail.com> |
---|---|
date | Sat, 16 Apr 2011 06:03:29 +0000 |
parents | |
children | fe437e761ef8 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/core.lua Sat Apr 16 06:03:29 2011 +0000 @@ -0,0 +1,1328 @@ +local addon = select(2,...) + +--[==[ +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 index formatted into text window FOO, default 0 +- saved: table of copies of saved texts, default nil; keys are numeric + indices of tables, subkeys of those are name/forum/attend/date +- autoshard: optional name of disenchanting player, default nil +- threshold: optional loot threshold, default nil + +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 +------ Locals +------ 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_opts = nil -- same as option_defaults until changed +OuroLootSV_hist = nil + + +------ 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, + ['keybinding_text'] = 'CTRL-SHIFT-O', + ['forum'] = { + ['[url]'] = '[url=http://www.wowhead.com/?item=$I]$N[/url]$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 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 + + +------ Addon member data +local flib = LibStub("LibFarmbuyer") +addon.author_debug = flib.author_debug + +-- Play cute games with namespaces here just to save typing. +do local _G = _G setfenv (1, addon) + + revision = 15 + ident = "OuroLoot2" + identTg = "OuroLoot2Tg" + status_text = nil + + DEBUG_PRINT = false + debug = { + comm = false, + loot = false, + flow = false, + notraid = false, + cache = false, + } + function dprint (t,...) + if DEBUG_PRINT and debug[t] then return _G.print("<"..t.."> ",...) end + end + + if author_debug then + function pprint(t,...) + return _G.print("<<"..t..">> ",...) + 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(.*)%.$" + + dbm_registered = nil + requesting = nil -- for prompting for additional rebroadcasters + + thresholds, quality_hexes = {}, {} + for i = 0,6 do + local hex = _G.select(4,_G.GetItemQualityColor(i)) + local desc = _G["ITEM_QUALITY"..i.."_DESC"] + quality_hexes[i] = hex + thresholds[i] = hex .. 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") + + +------ Locals +local g_loot = nil +local g_restore_p = nil +local g_saved_tmp = nil -- restoring across a clear +local g_wafer_thin = nil -- for prompting for additional rebroadcasters +local g_today = nil -- "today" entry in g_loot +local opts = nil + +local pairs, ipairs, tinsert, tremove, tonumber = pairs, ipairs, table.insert, table.remove, tonumber + +local pprint, tabledump = addon.pprint, flib.tabledump + +-- En masse forward decls of symbols defined inside local blocks +local _registerDBM -- break out into separate file +local makedate, create_new_cache, _init + +-- Hypertext support, inspired by DBM broadcast pizza timers +do + local hypertext_format_str = "|HOuroRaid:%s|h%s[%s]|r|h" + + function addon.format_hypertext (code, text, color) + return hypertext_format_str:format (code, + type(color)=='number' and addon.quality_hexes[color] 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 == '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 = AnimTimerFrame:CreateAnimationGroup() + cleanup_group:SetLooping("REPEAT") + cleanup_group:SetScript("OnLoop", function(cg) + addon.dprint('cache',"OnLoop firing") + local now = GetTime() + local alldone = true + -- this is ass-ugly + for _,c in ipairs(caches) do + while (#c > 0) and (now - c[1].t > c.ttl) do + addon.dprint('cache', c.name, "cache removing",c[1].t, c[1].m) + tremove(c,1) + end + alldone = alldone and (#c == 0) + end + if alldone then + addon.dprint('cache',"OnLoop finishing animation group") + cleanup_group:Finish() + for _,c in ipairs(caches) do + if c.func then c:func() end + end + end + addon.dprint('cache',"OnLoop done") + end) + + local function _add (cache, x) + tinsert(cache, {t=GetTime(),m=x}) + 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) + for _,v in ipairs(cache) do + if v.m == x then return true end + end + end + function create_new_cache (name, ttl, on_alldone) + local c = { + ttl = ttl, + name = name, + add = _add, + test = _test, + cleanup = cleanup_group:CreateAnimation("Animation"), + func = on_alldone, + } + c.cleanup:SetOrder(1) + -- setting OnFinished for cleanup fires at the end of each inner loop, + -- with no 'requested' argument to distinguish cases. thus, on_alldone. + tinsert (caches, 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 + option_defaults = nil + -- 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 + -- 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 = GetRealmName() + self.history_all[r] = self:_prep_new_history_category (self.history_all[r], r) + self.history = self.history_all[r] + + _init(self) + 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 + local btn = CreateFrame("Button", "OuroLootBindingOpen", nil, "SecureActionButtonTemplate") + btn:SetAttribute("type", "macro") + btn:SetAttribute("macrotext", "/ouroloot toggle") + if SetBindingClick(opts.keybinding_text, "OuroLootBindingOpen") then + SaveBindings(GetCurrentBindingSet()) + else + self:Print("Error registering '%s' as a keybinding, check spelling!", + opts.keybinding_text) + end + end + + 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_opts = nil + OuroLootSV_hist = nil +end +function addon:PLAYER_LOGOUT() + if (#g_loot > 0) or g_loot.saved + or (g_loot.forum and g_loot.forum ~= "") + or (g_loot.attend and g_loot.attend ~= "") + then + g_loot.autoshard = self.sharder + g_loot.threshold = self.threshold + --OuroLootSV = g_loot + --for i,e in ipairs(OuroLootSV) do + for i,e in ipairs(g_loot) do + e.cols = nil + end + OuroLootSV = g_loot + end + self.history.kind = nil + self.history.st = nil + self.history.byname = nil + OuroLootSV_hist = self.history_all +end + +function addon:RAID_ROSTER_UPDATE (event) + if GetNumRaidMembers() > 0 then + 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 + if event == "Activate" then + -- dispatched manually from Activate + self:RegisterEvent "CHAT_MSG_LOOT" + _registerDBM(self) + elseif event == "RAID_ROSTER_UPDATE" then + -- event registration from onload, joined a raid, maybe show popup + if opts.popup_on_join and not self.popped then + self.popped = StaticPopup_Show "OUROL_REMIND" + self.popped.data = self + end + end + else + self:UnregisterEvent "CHAT_MSG_LOOT" + self.popped = nil + end +end + +-- helper for CHAT_MSG_LOOT handler +do + -- Recent loot cache + addon.recent_loot = create_new_cache ('loot', comm_cleanup_ttl) + + local GetItemInfo = GetItemInfo + + -- 'from' and onwards only present if this is triggered by a broadcast + function addon:_do_loot (local_override, recipient, itemid, count, from, extratext) + local iname, ilink, iquality, _,_,_,_,_,_, itexture = GetItemInfo(itemid) + if not iname then return end -- sigh + self.dprint('loot',">>_do_loot, R:", recipient, "I:", itemid, "C:", count, "frm:", from, "ex:", extratext) + + local i + itemid = tonumber(ilink:match("item:(%d+)")) + if local_override or ((iquality >= self.threshold) and not opts.itemfilter[itemid]) then + if (self.rebroadcast and (not from)) and not local_override then + self:broadcast('loot', recipient, itemid, count) + end + if self.enabled or local_override then + local signature = recipient .. iname .. (count or "") + if self.recent_loot:test(signature) then + self.dprint('cache', "loot <",signature,"> already in cache, skipping") + else + self.recent_loot:add(signature) + i = self._addLootEntry{ -- There is some redundancy here... + kind = 'loot', + person = recipient, + person_class= select(2,UnitClass(recipient)), + 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), + } + self.dprint('loot', "added entry", i) + self:_addHistoryEntry(i) + if self.display then + self:redisplay() + --[[ + local st = self.display:GetUserData("eoiST") + if st and st.frame:IsVisible() then + st:OuroLoot_Refresh() + end + ]] + end + end + end + 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 = ... + --ChatFrame2:AddMessage("original string: >"..(msg:gsub("\124","\124\124")).."<") + local person, itemstring, remainder = msg:match(self.loot_pattern) + self.dprint('loot', "CHAT_MSG_LOOT, person is", person, ", itemstring is", itemstring, ", rest is", remainder) + if not person 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 + local p = person:match("|c%x%x%x%x%x%x%x%x(%S+)") + person = p or person + person = (person == UNIT_YOU) and my_name or person + + local id = tonumber((select(2, strsplit(":", itemstring)))) + + 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 == "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 == "toggle" then + if self.display then + self.display:Hide() + else + return self:BuildMainDisplay() + end + + 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:RegisterEvent "RAID_ROSTER_UPDATE" + self.popped = true + if GetNumRaidMembers() > 0 then + self:RAID_ROSTER_UPDATE("Activate") + elseif self.debug.notraid then + self:RegisterEvent "CHAT_MSG_LOOT" + _registerDBM(self) + elseif g_restore_p then + g_restore_p = nil + if #g_loot == 0 then return end -- only saved texts, not worth verbage + 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.") + self.popped = nil -- get the reminder if later joining a raid + 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]) +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 "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() + g_saved_tmp = g_loot.saved + if verbose_p then + if (g_saved_tmp and #g_saved_tmp>0) then + self:Print("Current loot data cleared, %d saved sets remaining.", #g_saved_tmp) + else + self:Print("Current loot data cleared.") + end + end + _init(self,st) + if repopup then + addon:BuildMainDisplay() + end +end + + +------ Behind the scenes routines +-- 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 + +-- Generic helpers +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 + +-- 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 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 = true + self.dprint('flow', "restoring", #g_loot, "entries") + self:ScheduleTimer("Activate", 8, g_loot.threshold) + -- FIXME printed could be too large if entries were deleted, how much do we care? + self.sharder = g_loot.autoshard + else + g_loot = { printed = {} } + g_loot.saved = g_saved_tmp; g_saved_tmp = nil -- potentially restore across a clear + end + + self.threshold = g_loot.threshold or self.threshold -- in the case of restoring but not tracking + self:gui_init(g_loot) + + 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 = ("v2r%d communicating as ident %s"):format(self.revision,self.ident) + self:RegisterComm(self.ident) + self:RegisterComm(self.identTg, "OnCommReceivedNocache") + + if self.author_debug then + _G.OL = self + _G.Oloot = g_loot + end +end + +-- Tie-ins with Deadly Boss Mods +do + local candidates, location + local function fixup_durations (cache) + if candidates == nil then return end -- this is called for *all* cache expirations, including non-boss + local boss, bossi + boss = candidates[1] + if #candidates == 1 then + -- (1) or (2) + boss.duration = boss.duration or 0 + addon.dprint('loot', "only one 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 candidate", i, "duration", c.duration) + break + end + end + end + bossi = addon._addLootEntry(boss) + addon.dprint('loot', "added 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 + candidates = nil + 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) + -- 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, ", ") + } + candidates = candidates or {} + tinsert(candidates,c) + break + end + 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.bosskill then + return self:Print("Something horribly wrong;", index, "is not a boss entry!") + 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 + + local GetRaidRosterInfo = GetRaidRosterInfo + function addon:DBMBossCallback (reason, mod, ...) + if (not self.rebroadcast) and (not self.enabled) then return end + + local name + if mod.combatInfo and mod.combatInfo.name then + name = mod.combatInfo.name + elseif mod.id then + name = mod.id + else + name = "Unknown Boss" + end + + local it = location or instance_tag() + location = nil + + local duration = 0 + if mod.combatInfo and mod.combatInfo.pull then + duration = math.floor (GetTime() - mod.combatInfo.pull) + end + + -- attendance: maybe put people in groups 6,7,8 into a "backup/standby" + -- list? probably too specific to guild practices. + local raiders = {} + for i = 1, GetNumRaidMembers() do + tinsert(raiders, (GetRaidRosterInfo(i))) + end + table.sort(raiders) + + return _do_boss (self, reason, name, it, duration, raiders) + end + + local callback = function(...) addon:DBMBossCallback(...) end + function _registerDBM(self) + if DBM then + if not self.dbm_registered then + local rev = tonumber(DBM.Revision) or 0 + if rev < 1503 then + self.status_text = "|cffff1010Deadly Boss Mods must be version 1.26 or newer to work with Ouro Loot.|r" + return + end + local r = DBM:RegisterCallback("kill", callback) + DBM:RegisterCallback("wipe", callback) + DBM:RegisterCallback("pull", function() location = instance_tag() end) + self.dbm_registered = r > 0 + end + else + self.status_text = "|cffff1010Ouro Loot cannot find Deadly Boss Mods, loot will not be grouped by boss.|r" + end + end +end -- DBM tie-ins + +-- 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) + --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) + local 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()) + e.hour = h + e.minute = m + e.stamp = localuptime + local index = #g_loot + 1 + g_loot[index] = e + return index + end +end + + +------ Saved texts +function addon:check_saved_table(silent_p) + local s = g_loot.saved + if s and (#s > 0) then return s end + g_loot.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) + g_loot.saved = g_loot.saved or {} + local n = #(g_loot.saved) + 1 + local save = { + name = name, + date = makedate(), + count = #g_loot, + forum = g_loot.forum, + attend = g_loot.attend, + } + self:Print("Saving current loot texts to #%d '%s'", n, name) + g_loot.saved[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. + g_loot.forum = save.forum + g_loot.attend = save.attend +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 + -- Builds the map of names to array indices. + 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 + + -- Maps a name to an array index, creating new tables if needed. Returns + 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 self.history[i] + 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.cols = setmetatable({ + { value = realmname }, + }, self.time_column1_used_mt) + ]] + + if not t.byname then + self:_build_history_names (t) + end + + return t + end + + function addon:_addHistoryEntry (lootindex) + local e = g_loot[lootindex] + local h = self:get_loot_history(e.person) + local n = { + id = e.id, + when = self:format_timestamp (g_today, e), + count = e.count, + } + h[#h+1] = n + 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 v2r%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.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 + self.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