Mercurial > wow > ouroloot
view core.lua @ 3:2753b9763882
Add bossmod selection UI to options. Widen the notes field a bit.
author | Farmbuyer of US-Kilrogg <farmbuyer@gmail.com> |
---|---|
date | Fri, 22 Apr 2011 02:13:18 +0000 |
parents | fe437e761ef8 |
children | 05caaf17b3ca |
line wrap: on
line source
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, ['bossmod'] = "DBM", ['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(.*)%.$" bossmod_registered = nil bossmods = {} 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 _register_bossmod 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" _register_bossmod(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" _register_bossmod(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 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, } 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 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) --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