Mercurial > wow > ouroloot
view core.lua @ 78:f8118aa5fbb8
Manually inserting loot entries now (1) uses autocomplete dropdowns for the recipient name (raid members if in a raid, online guild members otherwise), and (2) adds a rebroadcasting hyperlink to the text response. Clicking the link rebroadcasts just like the context menu.
author | Farmbuyer of US-Kilrogg <farmbuyer@gmail.com> |
---|---|
date | Tue, 12 Jun 2012 03:46:00 +0000 |
parents | a07c9dd79f3a |
children | 0235a1695b83 |
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 - raiders accumulating raid roster data as we see raid members; indexed by player name with subtable fields: - class capitalized English codename ("WARRIOR", "DEATHKNIGHT", etc) - subgroup 1-8 (NUM_RAID_GROUPS), NRG+1 if something's wrong - race English codename ("BloodElf", etc) - sex 1 = unknown/error, 2 = male, 3 = female - level can be 0 if player was offline at the time - guild guild name, or missing if unguilded - online 1 = online, 2 = offline, 3 = no longer in raid [both of these next two fields use time_t values:] - join time player joined the raid (or first time we've seen them) - leave time player left the raid (or time we've left the raid, if 'online' is not 3) Common g_loot entry indices: - kind time/boss/loot - hour 0-23, on the *physical instance server*, not the realm server - minute 0-59, ditto - stamp time_t on the local computer - cols graphical display data; cleared when logging out Time specific g_loot indices: - startday table with month/day/year/text fields from makedate() text is always "dd Month yyyy" Boss specific g_loot indices: - bossname name of boss/encounter; may be changed if "snarky boss names" option is enabled - reason wipe/kill ("pull" does not generate an entry) - instance name of instance, including size and difficulty - maxsize max raid size: 5/10/25, presumably also 15 and 40 could show up; can be 0 if we're outside an instance and the player inside has an older version - duration in seconds; may be missing (only present if local) - raidersnap copy of g_loot.raiders at the time of the boss event; may be empty for manual snapshots the player didn't want included (not necessarily an "error" if this is missing entirely) Loot specific g_loot indices: - person recipient - person_class class of recipient if available; may be missing; will be classID-style (e.g., DEATHKNIGHT) - itemname not including square brackets - id itemID as number - itemlink full clickable link - itexture icon path (e.g., Interface\Icons\INV_Misc_Rune_01) - quality ITEM_QUALITY_* number - unique an almost-certainly-unique string, content meaningless - disposition offspec/gvault/shard; missing otherwise; can be set from the extratext field - count e.g., "x3"; missing otherwise; can be set/removed from extratext; triggers only for a stack of items, not "the boss dropped double axes today" - variant 1 = heroic item, 2 = LFR item; missing otherwise - cache_miss if GetItemInfo failed; SHOULD be missing (changes other fields) - bcast_from player's name if received rebroadcast from that player; missing otherwise; can be deleted as a result of in-game fiddling of loot data - extratext text in Note column, including disposition and rebroadcasting - extratext_byhand true if text edited by player directly; missing otherwise 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 and serve as a spelling reminder. ]==] ------ 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 = { ['datarev'] = 20, -- cheating, this isn't actually an option ['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', ['display_disabled_LODs'] = false, ['display_bcast_from'] = true, ['precache_history_uniques'] = false, ['chatty_on_remote_changes'] = false, ['chatty_on_remote_changes_frame'] = 1, } 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 unique_collision = "|cffff1010%s:|r|nItem '%s' was carrying unique tag <%s>, but that was already in use; tried to generate a new tag and failed!|n|nRemote sender was '%s', previous cache entry was <%s/%s>.|n|nThis may require a live human to figure out; the loot in question has not been stored." local remote_chatty = "|cff00ff00%s|r changed %d/%s from %s%s|r to %s%s|r" 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 = 4 -- seconds in the communications 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 = '17' revision = _G.GetAddOnMetadata(nametag,"Version") or "?" -- "x.yy.z", etc ident = "OuroLoot2" identTg = "OuroLoot2Tg" status_text = nil tekdebug = nil if _G.tekDebug then local tdframe = _G.tekDebug:GetFrame("Ouro Loot") function tekdebug (txt) -- tekDebug notices "<name passed to getframe>|r:" tdframe:AddMessage('|cff17ff0dOuro Loot|r:'..txt,1,1,1) end end DEBUG_PRINT = false debug = { comm = false, loot = false, flow = false, notraid = false, cache = false, alsolog = false, } --@debug@ DEBUG_PRINT = true debug.loot = true debug.comm = true --@end-debug@ -- This looks ugly, but it factors out the load-time decisions from -- the run-time ones. Args to [dp]print are concatenated with spaces. if tekdebug then function dprint (t,...) if DEBUG_PRINT and debug[t] then local text = flib.safefprint(tekdebug,"<"..t.."> ",...) if debug.alsolog then addon:log_with_timestamp(text) end end end else 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 end if author_debug and tekdebug then function pprint (t,...) local text = flib.safefprint(tekdebug,"<<"..t..">> ",...) if debug.alsolog then addon:log_with_timestamp(text) end end else pprint = flib.nullfunc end -- The same observable behavior as the Lua builtins, but with slightly -- different hardcoded strings and, more importantly, implicit logging. function error(txt,lvl) pprint('ERROR()', txt) pprint('DEBUGSTACK()', _G.debugstack()) _G.error(txt,lvl) end function assert(cond,msg,...) if cond then return cond,msg,... else error('ASSERT() FAILED: '..tostring(msg or 'nil')) end end enabled = false rebroadcast = false display = nil -- reference to display frame iff visible loot_clean = nil -- index of last GUI entry with known-current visual data 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: sender_list = {active={},names={}} -- this should be reworked popped = nil -- non-nil when reminder has been shown, actual value unimportant bossmod_registered = nil bossmods = {} requesting = nil -- prompting for additional rebroadcasters -- don't use NUM_ITEM_QUALITIES as the upper loop bound unless we expect -- heirlooms to show up 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") -- if given, MSG should be a complete-ish sentence function addon:load_assert (cond, msg, ...) if cond then return cond, msg, ... end msg = msg or "load-time assertion failed!" self.NOLOAD = msg self:Printf([[|cffff1010ERROR:|r <|cff00ff00%s|r> Ouro Loot cannot finish loading. You will need to type |cff30adff%s|r once these problems are resolved, and try again.]], msg, _G.SLASH_RELOAD1) SLASH_ACECONSOLE_OUROLOOT1 = nil SLASH_ACECONSOLE_OUROLOOT2 = nil self.error (msg, --[[level=]]2) end -- Seriously? ORLY? -- YARLY. Go ahead and guess what was involved in tracking this down. If -- more such effects are added in the future, the "id==xxxxx" will need to -- change into a probe of a table of known-problematic IDs. for i = 1, 40 do -- BUFF_MAX_DISPLAY==32, enh local id = select(11,UnitAura('player', i, 'HELPFUL')) if id == 88715 then -- What I really want to do is pause until the thing is clicked off, -- then continue with the rest of the file. No can do. Could also -- just set some hooks and then re-OnInit/OnEnable after the aura -- expires, but that's a hassle. GAH. Punt. local text = UnitAura('player', i, 'HELPFUL') text = ([[Cannot initialize while |cff71d5ff|Hspell:88715|h[%s]|h|cff00ff00 is active!]]): format(text) addon:load_assert(nil,text) return -- were this C code running through lint, I'd put NOTREACHED end end ------ Globals local g_loot = nil local g_restore_p = nil local g_wafer_thin = nil -- prompting for additional rebroadcasters local g_today = nil -- "today" entry in g_loot local g_boss_signpost = nil local g_seeing_oldsigs = nil local g_uniques = nil -- memoization of unique loot events local g_unique_replace = nil local opts = nil local error = addon.error local assert = addon.assert -- for speeding up local loads, not because I think _G will change local _G = _G local type = _G.type local select = _G.select local pairs = _G.pairs local ipairs = _G.ipairs local tinsert = _G.table.insert local tremove = _G.table.remove local tostring = _G.tostring local tonumber = _G.tonumber local wipe = _G.table.wipe local pprint, tabledump = addon.pprint, flib.tabledump local CopyTable, GetNumRaidMembers = _G.CopyTable, _G.GetNumRaidMembers -- En masse forward decls of symbols defined inside local blocks local _register_bossmod, makedate, create_new_cache, _init, _log local _history_by_loot_id, _notify_about_remote, _setup_unique_replace local _unavoidable_collision -- 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 answers. -- -- 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 -- If it's a big enough number to obviously be a release, then make -- sure it's big enough to overcome many small previous point releases. while r > 2000 and r < 2000000 do r = 1000*r end revision_large = math.max(r,1) end -- Hypertext support, inspired by DBM broadcast pizza timers do local hypertext_format_str = "|HOuroLoot:%d|h%s[%s]|r|h" local func_map = {} --_G.setmetatable({}, {__mode = 'k'}) local text_map = {} --_G.setmetatable({}, {__mode = 'kv'}) local base = _G.newproxy(true) _G.getmetatable(base).__tostring = function(ud) return text_map[ud] end --@debug@ -- collecting these tokens is an interesting micro-optimization but not yet _G.getmetatable(base).__gc = function(ud) print("Collecting hyperlink object <",tostring(ud),">") end --@end-debug@ -- TEXT will automatically be surrounded by brackets -- COLOR can be ITEM_QUALITY_* or a formatting string ("|cff...") -- FUNC can be "MethodName", "tab_title", or a function -- -- Returns an opaque token and a matching number. Calling tostring() on -- the token will yield a formatted clickable string that can be displayed -- in chat. The MethodName and raw function callbacks will both be -- passed the addon table and the same matching number. -- -- This is largely an excuse to fool around with Lua data constructs. function addon.format_hypertext (text, color, func) local ret = _G.newproxy(base) local num = #text_map + 1 text_map[ret] = hypertext_format_str:format (num, type(color)=='number' and ITEM_QUALITY_COLORS[color].hex or color, text) text_map[num] = ret func_map[ret] = func return ret, num end --[[ link: OuroLoot:n fullstring: |HOuroLoot:n|h|cff.....[foo]|r|h mousebutton: "LeftButton", "MiddleButton", "RightButton" amusingly, print()'ing the fullstring below as a debugging aid yields another clickable link, yay data reproducability ]] local strsplit = _G.strsplit DEFAULT_CHAT_FRAME:HookScript("OnHyperlinkClick", function(self, link, fullstring, mousebutton) local ltype, arg = strsplit(":",link) if ltype ~= "OuroLoot" then return end arg = tonumber(arg) local f = func_map[text_map[arg]] if type(f) == 'function' then f (addon, arg) elseif type(f) == 'string' then if type(addon[f]) == 'function' then addon[f](addon,arg) -- method name else addon:BuildMainDisplay(f) -- tab title fragment end end end) local old = ItemRefTooltip.SetHyperlink function ItemRefTooltip:SetHyperlink (link, ...) if link:match("^OuroLoot") 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, followed by the raid size local function instance_tag() -- possibly redo this with the new GetRaidDifficulty function local name, typeof, diffcode, diffstr, _, perbossheroic, isdynamic = GetInstanceInfo() local t, r name = addon.instance_abbrev[name] or name if typeof == "none" then return name, MAX_RAID_MEMBERS end -- diffstr is "5 Player", "10 Player (Heroic)", etc. ugh. if (GetLFGMode()) and (GetLFGModeType() == 'raid') then t,r = 'LFR', 25 elseif diffcode == 1 then if GetNumRaidMembers() > 0 then t,r = "10",10 else t,r = "5",5 end elseif diffcode == 2 then if GetNumRaidMembers() > 0 then t,r = "25",25 else t,r = "5h",5 end elseif diffcode == 3 then t,r = "10h", 10 elseif diffcode == 4 then t,r = "25h", 25 end -- dynamic difficulties always return normal "codes" if isdynamic and perbossheroic == 1 then t = t .. "h" end return name .. "(" .. t .. ")", r end addon.instance_tag = instance_tag -- grumble addon.latest_instance = nil -- spelling reminder, assigned elsewhere -- Memoizing cache of unique IDs as we generate or search for them. Keys are -- the uniques, values are the following: -- 'history' active index into self.history -- 'history_may' index into player's uniques list, CAN QUICKLY BE OUTDATED -- and will instantly be wrong after manual insertion -- 'loot' active index into g_loot -- with all but the history entry optional. Values of g_uniqes.NOTFOUND -- indicate a known missing status. Use g_uniques:RESET() to wipe the cache -- and return to searching mode. do local notfound = -1 local notfound_ret = { history = notfound } local mt -- This can either be its own function or a slightly redundant __index. local function m_probe_only (t, k) return rawget(t,k) or notfound_ret end -- Expensive search. local function m_full_search (t, k) local L, H, HU, loot -- Try active loot entries first for i,e in addon:filtered_loot_iter('loot') do if k == e.unique then L,loot = i,e break end end -- If it's active, try looking through that player's history first. if L then local hi,h = addon:get_loot_history (loot.person) for ui,u in ipairs(h.unique) do if k == u then H, HU = hi, ui break end end else -- No luck? Ugh, may have been reassigned and we're probing from -- older data. Search the rest of current realm's history. for hi,h in ipairs(addon.history) do for ui,u in ipairs(h.unique) do if k == u then H, HU = hi, ui break end end end end local ret = { loot = L, history = H or notfound, history_may = HU } t[k] = ret return ret end local function m_setmode (self, mode) mt.__index = (mode == 'probe') and m_probe_only or (mode == 'search') and m_full_search or nil -- maybe error() here? end local function m_reset (self) wipe(self) self[''] = notfound_ret -- special case for receiving older broadcast self.NOTFOUND = notfound self.RESET = m_reset self.SEARCH = m_full_search self.TEST = m_probe_only self.SETMODE = m_setmode mt.__index = m_full_search return self end -- If unique keys ever change into objects instead of strings, change -- this into a weakly-keyed table. mt = { __metatable = 'Should be using setmode.' } g_uniques = setmetatable (m_reset{}, mt) end ------ Expiring caches --[[ cache = create_new_cache ("mycache", 15 [,cleanup]) cache:add(foo) cache:test(foo) -- returns true ....5 seconds pass cache:add(bar) ....10 seconds pass cache:test(foo) -- returns false cache:test(bar) -- returns true ....5 seconds pass ....bar also gone, cleanup() called ]] 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) 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() _G.collectgarbage() else addon.dprint('cache',"OnLoop done, not yet finished") end end) local function _add (cache, x) local datum = { t=time()+cache.ttl, m=x } cache.hash[x] = datum tinsert (cache.fifo, datum) if not cleanup_group:IsPlaying() then addon.dprint('cache', cache.name, "with entry", datum.t, "<", datum.m, "> STARTING animation group") cache.cleanup:SetDuration(1) -- hmmm cleanup_group:Play() end end local function _test (cache, x) -- FIXME This can return false positives, if called after the onloop -- fifo has been removed but before the GC has removed the weak entry. -- What to do, what to do... try forcing a GC during alldone. return cache.hash[x] ~= nil 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. -- FWIW, on_alldone is passed this table as its sole argument: 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() if self.author_debug then _G.OL = self _G.g_uniques = g_uniques end _log = _G.OuroLootSV_log -- VARIABLES_LOADED has fired by this point; test if we're doing something like -- relogging during a raid and already have collected loot data local OuroLootSV = _G.OuroLootSV g_restore_p = OuroLootSV ~= nil self.dprint('flow', "oninit sets restore as", g_restore_p) if _G.OuroLootSV_opts == nil then _G.OuroLootSV_opts = {} local vclick = self.format_hypertext ([[click here]], ITEM_QUALITY_UNCOMMON, 'help') self:ScheduleTimer(function(s) s:Print(virgin, tostring(vclick)) virgin = nil end,10,self) else virgin = nil end opts = _G.OuroLootSV_opts local stored_datarev = opts.datarev or 14 for opt,default in pairs(option_defaults) do if opts[opt] == nil then opts[opt] = default end end opts.datarev = option_defaults.datarev -- 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 = self.default_itemfilter end self.default_itemfilter = nil if opts.itemvault == nil then opts.itemvault = self.default_itemvault end self.default_itemvault = nil self:RegisterChatCommand("ouroloot", "OnSlash") if opts.register_slashloot then -- NOTA BENE: do not use /loot in the LoadOn list, ChatTypeInfo gets confused -- maybe try to detect if this command is already in use... _G.SLASH_ACECONSOLE_OUROLOOT2 = "/loot" end self.history_all = self.history_all or _G.OuroLootSV_hist or {} local r = self:load_assert (_G.GetRealmName(), "how the freak does GetRealmName() fail?") self.history_all[r] = self:_prep_new_history_category (self.history_all[r], r) self.history = self.history_all[r] local histformat = self.history_all.HISTFORMAT self.history_all.HISTFORMAT = nil -- don't keep this in live data if _G.OuroLootSV_hist and (histformat == nil or histformat < 4) then -- some big honkin' loops for rname,realm in pairs(self.history_all) do for pk,player in ipairs(realm) do if histformat == nil or histformat < 3 then for lk,loot in ipairs(player) do if loot.count == "" then loot.count = nil end if not loot.unique then loot.unique = loot.id .. ' ' .. loot.when end end end -- format 3 to format 4 was a major revamp of per-player data self:_uplift_history_format(player) end end end self._uplift_history_format = nil --OuroLootSV_hist = nil -- Handle changes to the stored data format in stages from oldest to newest. -- bumpers[X] is responsible for updating from X to X+1. -- (This is turning into a lot of loops over the same table. Consolidate?) if OuroLootSV then local dirty = false local bumpers = {} bumpers[14] = function() for i,e in ipairs(OuroLootSV) do if e.bosskill then e.bossname, e.bosskill = e.bosskill, nil end end end bumpers[15] = function() for i,e in ipairs(OuroLootSV) do if e.kind == 'boss' then e.maxsize, e.raiderlist, e.raidersnap = 0, nil, {} end end OuroLootSV.raiders = OuroLootSV.raiders or {} for name,r in pairs(OuroLootSV.raiders) do r.subgroup = 0 end end bumpers[16] = function() for i,e in ipairs(OuroLootSV) do if e.kind == 'boss' then -- brown paper bag bugs e.raidersnap = e.raidersnap or {} e.maxsize = e.maxsize or 0 end end end bumpers[17] = function() for i,e in ipairs(OuroLootSV) do if e.kind == 'loot' and e.is_heroic then e.variant, e.is_heroic = 1, nil -- Could try detecting any previous LFR loot here, but... gah end end end bumpers[18] = bumpers[16] -- In the not-very-many days between 16 and 19, I managed to break -- the exact same data in the exact same way. At least they're -- not actually running the same loop twice... probably... sigh. bumpers[19] = function() local date = _G.date for i,e in ipairs(OuroLootSV) do if e.kind == 'loot' then if e.history_unique then e.unique, e.history_unique = e.history_unique, nil end if e.unique == nil or #e.unique == 0 then e.unique = e.id .. ' ' .. date("%Y/%m/%d %H:%M",e.stamp) end end end end --[===[ local real = bumpers bumpers = newproxy(true) local mt = getmetatable(bumpers) mt.__index = real mt.__gc = function() print"whadda ya know, garbage collection works" end ]===] while stored_datarev < opts.datarev do self:Printf("Transitioning saved data format to %d...", stored_datarev+1) dirty = true bumpers[stored_datarev]() stored_datarev = stored_datarev + 1 end if dirty then self:Print("Saved data has been massaged into shape.") end end _init(self) self.dprint('flow', "version strings:", revision_large, self.status_text) self.load_assert = nil self.OnInitialize = nil -- free up ALL the things! end function addon:OnEnable() self:RegisterEvent("PLAYER_LOGOUT") self:RegisterEvent("RAID_ROSTER_UPDATE") -- Cribbed from Talented. I like the way jerry thinks: the first string -- argument can be a format spec for the remainder of the arguments. -- 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. -- -- CFPrint added instead of the usual Print testing of the first arg for -- frame-ness, which would slow down all printing and only rarely be useful. -- -- There is no ITEM_QUALITY_LEGENDARY constant. Sigh. do local AC = LibStub("AceConsole-3.0") local chat_prefix = self.format_hypertext ("Ouro Loot", --[[legendary]]5, --[[empty -> nil -> main tab]]'') local chat_prefix_s = tostring(chat_prefix) function addon:Print (str, ...) if type(str) == 'string' and str:find("%", nil, --[[plainmatch=]]true) then return AC:Print (chat_prefix_s, str:format(...)) else return AC:Print (chat_prefix_s, str, ...) end end function addon:CFPrint (frame, str, ...) assert(type(frame)=='table' and frame.AddMessage) if type(str) == 'string' and str:find("%", nil, --[[plainmatch=]]true) then return AC:Print (frame, chat_prefix_s, str:format(...)) else return AC:Print (frame, chat_prefix_s, str, ...) end end end while opts.keybinding do if InCombatLockdown() then local reload = self.format_hypertext ([[the options tab]], ITEM_QUALITY_UNCOMMON, 'opt') self:Print("Cannot create '%s' as a keybinding while in combat!", opts.keybinding_text) self:Print("The rest of the addon will continue to work, but you will need to reload out of combat to get the keybinding. Either type /reload or use the button on %s in the lower right.", tostring(reload)) break end 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, for reasons nobody -- has ever learned 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 break 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. ??? do something with LOOT_ITEM_WHILE_PLAYER_INELIGIBLE for locked LFRs? ]] -- 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+)') --[[ Stick something in the Blizzard addons options list, where most users will probably look these days. Try to be conservative about needless frame creation. ]] local bliz = CreateFrame("Frame") bliz.name = "Ouro Loot" bliz:SetScript("OnShow", function(_b) local button = CreateFrame("Button",nil,_b,"UIPanelButtonTemplate") button:SetWidth(150) button:SetHeight(22) button:SetScript("OnClick", function() _G.InterfaceOptionsFrameCancel:Click() _G.HideUIPanel(GameMenuFrame) addon:OpenMainDisplayToTab"Options" end) button:SetText('"/ouroloot opt"') button:SetPoint("TOPLEFT",20,-20) _b:SetScript("OnShow",nil) end) _G.InterfaceOptions_AddCategory(bliz) -- Maybe load up g_uniques now? if opts.precache_history_uniques then self:_cache_history_uniques() end self:_scan_LOD_modules() self:_set_remote_change_chatframe (opts.chatty_on_remote_changes_frame, --[[silent_p=]]true) if self.debug.flow then self:Print"is in control-flow debug mode." end end --function addon:OnDisable() end do local prototype = {} local function module_OnEnable (plugin) if plugin.option_defaults then local SVname = 'OuroLoot'..plugin:GetName()..'_opts' if not _G[SVname] then _G[SVname] = {} if type(plugin.OnFirstTime) == 'function' then plugin:OnFirstTime() end end plugin.opts = _G[SVname] for option,default in pairs(plugin.option_defaults) do if plugin.opts[option] == nil then plugin.opts[option] = default end end plugin.option_defaults = nil end end -- By default, no plugins. First plugin to use the special registration -- sets up code for any subsequent plugins. addon.is_plugin = flib.nullfunc local function module_rtg (plugin, text_type, ...) local registry = { [text_type]=plugin } addon.is_plugin = function(a,t) return registry[t] end prototype.register_text_generator = function(p,t,...) registry[t] = p return addon:register_text_generator(t,...) end return addon:register_text_generator(text_type,...) end prototype.OnEnable = module_OnEnable prototype.default_OnEnable = module_OnEnable prototype.register_text_generator = module_rtg addon:SetDefaultModuleLibraries("AceConsole-3.0") addon:SetDefaultModulePrototype(prototype) -- Fires before the plugin's own OnEnable (inherited or otherwise). --function addon:OnModuleCreated (plugin) -- print("created plugin", plugin:GetName()) --end local olrev = tonumber("@project-revision@") or 0 local err = [[Module '%s' cannot register itself because it failed a required condition: '%s']] function addon:ConstrainedNewModule (modname, minrev, mincomm, mindata) if not addon.author_debug then if minrev and minrev > olrev then self:Print(err,modname, "revision "..olrev.." older than minimum "..minrev) return false end if mincomm and mincomm > tonumber(self.commrev) then self:Print(err,modname, "commrev "..self.commrev.." older than minimum "..mincomm) return false end if mindata and mindata > opts.datarev then self:Print(err,modname, "datarev "..opts.datarev.." older than minimum "..mindata) return false end end return self:NewModule(modname) end end ------ Event handlers function addon:_clear_SVs() g_loot = {} -- not saved, just fooling PLAYER_LOGOUT tests _G.OuroLootSV = nil _G.OuroLootSV_saved = nil _G.OuroLootSV_opts = nil _G.OuroLootSV_hist = nil _G.OuroLootSV_log = nil _G.ReloadUI() end function addon:PLAYER_LOGOUT() self:UnregisterEvent("RAID_ROSTER_UPDATE") self:UnregisterEvent("PLAYER_ENTERING_WORLD") local worth_saving = #g_loot > 0 or _G.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 _G.OuroLootSV = g_loot else _G.OuroLootSV = nil end worth_saving = false for r,t in pairs(self.history_all) do if type(t) == 'table' then if #t == 0 then self.history_all[r] = nil else worth_saving = true t.realm = nil t.st = nil t.byname = nil end end end if worth_saving then _G.OuroLootSV_hist = self.history_all _G.OuroLootSV_hist.HISTFORMAT = 4 else _G.OuroLootSV_hist = nil end _G.OuroLootSV_log = #_G.OuroLootSV_log > 0 and _G.OuroLootSV_log or nil end do local IsInInstance, UnitIsConnected, UnitClass, UnitRace, UnitSex, UnitLevel, UnitInRaid, UnitIsVisible, GetGuildInfo, GetRaidRosterInfo = IsInInstance, UnitIsConnected, UnitClass, UnitRace, UnitSex, UnitLevel, UnitInRaid, UnitIsVisible, GetGuildInfo, GetRaidRosterInfo 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 -- We grab a bunch of return values here, but only pay attention to -- them under specific circumstances. local name, connected, subgroup, level, class, _ name, _, subgroup, level, _, class, connected = GetRaidRosterInfo(i) -- 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] r.subgroup = subgroup or (NUM_RAID_GROUPS+1) if r.needinfo and UnitIsVisible(unit) then r.needinfo = nil r.class = class --select(2,UnitClass(unit)) r.race = select(2,UnitRace(unit)) r.sex = UnitSex(unit) r.level = 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 self.dprint('flow', "got RRU event but in pvp zone, bailing") return 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 --[=[ CHAT_MSG_LOOT handler and its helpers. Situations for "unique tag" generation, given N people seeing local loot events, M people seeing remote rebroadcasts, and player Z adding manually: + Local tracking: All LOCALs should see the same itemstring, thus the same unique ID stripped out of field #9. LOCALn includes this in the broadcast to REMOTEm. Tag is a large number, meaningless for clients and players. + Local broadcasting, remote tracking: same as local tracking. Possibly some weirdness if all local versions are significantly older than the remote versions; in this case each REMOTEn will generate their own tags of the form itemID+formatted_date, which will not be "unique" for the next 60 seconds. As long as at least one LOCALn is recent enough to strip and broadcast a proper ID, multiple items of the same visible name will not be "lost". + Z manually inserts a loot entry: Z generates a tag, preserved locally. If Z rebrodcasts that entry, all REMOTEs will see it. Tag is of the form "n" followed by a random number. ]=] do local counter, _do_loot do local count = 0 function counter() count = count + 1; return count; end end local function maybe_trash_kill_entry() -- this is set on various boss interactions, so we've got a kill/wipe -- entry already -- XXX maybe clear it after a delay, so that loot -- from trash after a boss isn't grouped with that boss? if addon.latest_instance then return end --addon.latest_instance = instance_tag() local ss, max, inst = addon:snapshot_raid() addon.latest_instance = inst addon:_mark_boss_kill (addon._addBossEntry{ kind='boss', reason='kill', bossname=[[trash]], instance=addon.latest_instance, duration=0, raidersnap=ss, maxsize=max }) end -- Alert other trackers that unique tag EXISTING in subsequent 'casts -- should be replaced by REPLACE instead. If multiple players all saw -- the same loot event, this will cause a flurry of cross-improvs. local function _announce_unique_improvisation (existing, replace) if not g_unique_replace then _setup_unique_replace() end g_unique_replace.new_entry (g_unique_replace.me, existing, replace, 'improv') addon:vbroadcast('improv', g_unique_replace.me, existing, replace) end local random = _G.math.random local function _many_uniques_handle_it (u, prefix) if u then -- Check and alert for an existing value. u = tostring(u) if g_uniques[u].history ~= g_uniques.NOTFOUND then if not g_unique_replace then _setup_unique_replace() end local maybe = g_unique_replace.get_previous_replacement (u) if maybe then addon.dprint('loot',"previous replaced tag ("..u ..") with ("..maybe.."), using that instead") return false, u, maybe end local can_replace_p,improv = _many_uniques_handle_it (nil, 'c') if can_replace_p then _announce_unique_improvisation (u, improv) return false, u, improv end return false, u end addon.dprint('loot',"verified unique tag ("..u..")") else -- Need to *find* an unused value. For now use a range of -- J*10^4 where J is Jenny's Constant. Thank you, xkcd.com/1047. prefix = prefix or 'n' repeat u = prefix .. random(8675309) until g_uniques:TEST(u).history == g_uniques.NOTFOUND addon.dprint('loot',"created unique tag ("..u..")") end return true, u end -- Recent loot cache local candidates = {} local sigmap = {} local function preempt_older_signature (oldersig, newersig) --pprint("preempt", oldersig, "::", newersig) local origin = candidates[oldersig] and candidates[oldersig].bcast_from --pprint("preempt", "candidate", candidates[oldersig], "bcast:", origin) if origin and g_seeing_oldsigs[origin] then -- replace entry from older client with this newer one candidates[oldersig] = nil addon.dprint('loot', "preempting signature <", oldersig, "> from", origin) end return false end 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 -- here to extract the final results once the cache timers have expired. -- -- Keep this sync'd with the local_override branch below. for i,sig in ipairs(candidates) do addon.dprint('loot', "processing candidate entry", i, sig) local loot = candidates[sig] if loot then maybe_trash_kill_entry() -- Generate *some* kind of boss/location entry candidates[sig] = nil local looti = addon._addLootEntry(loot) addon.dprint('loot', "entry", i, "was found, added at index", looti) if (loot.disposition ~= 'shard') and (loot.disposition ~= 'gvault') and (not addon.history_suppress) then addon:_addHistoryEntry(looti) elseif #loot.unique > 0 then g_uniques[loot.unique] = -- stub entry { loot = looti, history = g_uniques.NOTFOUND } end end end if addon.display then addon:redisplay() end wipe(candidates) wipe(sigmap) end local recent_loot = create_new_cache ('loot', comm_cleanup_ttl+3, prefer_local_loots) local strsplit, GetItemInfo, GetItemIcon, UnitClass = _G.strsplit, _G.GetItemInfo, _G.GetItemIcon, _G.UnitClass -- 'from' only present if this is triggered by a broadcast function _do_loot (self, local_override, recipient, unique, itemid, count, from, extratext) local prefix = "_do_loot[" .. counter() .. "]" local itexture = GetItemIcon(itemid) local iname, ilink, iquality = GetItemInfo(itemid) local cache_miss if (not iname) or (not itexture) then cache_miss = true iname, ilink, iquality, itexture = UNKNOWN..': '..itemid, 'item:6948', ITEM_QUALITY_COMMON, [[ICONS\INV_Misc_QuestionMark]] end self.dprint('loot',">>"..prefix, "R:", recipient, "U:", unique, "I:", itemid, "C:", count, "frm:", from, "ex:", extratext, "q:", iquality) itemid = tonumber(ilink:match("item:(%d+)") or 0) -- This is only a 'while' to make jumping out of it easy. local i, unique_okay, replacement, ret1, ret2 while local_override or ((iquality >= self.threshold) and not opts.itemfilter[itemid]) do unique_okay, unique, replacement = _many_uniques_handle_it ((not local_override) and unique) if not unique_okay then if replacement then -- collision, but we've generated a placeholder for now -- and broadcast the fact self.dprint('loot', "substituting", unique, "with", replacement) else i = g_uniques[unique] local err = unique_collision:format (ERROR_CAPS, iname, unique, tostring(from), tostring(i.loot), tostring(i.history)) _unavoidable_collision (err) -- Make sure this is logged one way or another ;(self.debug.loot and self.dprint or pprint)('loot', "COLLISION", prefix, err); ret1, ret2 = nil, err break end end if (self.rebroadcast and (not from)) and not local_override then self:vbroadcast('loot', recipient, unique, itemid, count) end if (not self.enabled) and (not local_override) then break end local oldersig = recipient .. iname .. (count or "") local signature, seenit if #unique > 0 then -- newer case signature = unique .. oldersig --pprint("newer", "mapping older <", oldersig, "> to newer <", signature, ">") sigmap[oldersig] = signature --pprint("newer", "testing recent for", signature, "yields", recent_loot:test(signature)) seenit = (from and recent_loot:test(signature)) -- The following clause is what handles older 'casts arriving -- earlier. All this is tested inside-out to maximize short -- circuit evaluation; the preempt function always returns -- false to force seenit off. or (g_seeing_oldsigs and preempt_older_signature(oldersig,signature)) else -- older case, only remote assert(from) signature = sigmap[oldersig] or oldersig --pprint("older", "testing signature will be", signature) seenit = recent_loot:test(signature) end if seenit then self.dprint('cache', "remote", prefix, "<", signature, "> already in cache, skipping from", from) break end -- 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 = cache_miss, quality = iquality, itemname = iname, id = itemid, itemlink = ilink, itexture = itexture, unique = replacement or unique, count = (count and count ~= "") and count or nil, bcast_from = from, extratext = extratext, variant = self:is_variant_item(ilink), } if opts.itemvault[itemid] then i.disposition = 'gvault' elseif recipient == self.sharder then i.disposition = 'shard' end if local_override then -- player is adding loot by hand, don't wait for network cache timeouts -- keep this sync'd with prefer_local_loots above if i.extratext == 'shard' or i.extratext == 'gvault' or i.extratext == 'offspec' then i.disposition = i.extratext end local looti = self._addLootEntry(i) if (i.disposition ~= 'shard') and (i.disposition ~= 'gvault') and (not self.history_suppress) then self:_addHistoryEntry(looti) else g_uniques[i.unique] = -- stub entry { loot = looti, history = g_uniques.NOTFOUND } end ret1 = looti -- return value mostly for gui's manual entry self.dprint('loot', "manual", looti) else recent_loot:add(signature) candidates[signature] = i tinsert (candidates, signature) self.dprint('cache', prefix, "<", signature, "> added to cache as candidate", #candidates) end break end self.dprint('loot',"<<"..prefix, "out") return ret1, ret2 end -- Returns the index of the resulting new loot entry, or nil after -- displaying any errors. 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")).."<") -- 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, ", count is", count) if not itemstring then return end -- "PlayerX selected Greed", etc, not looting -- 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 person = my_name -- UNIT_YOU / You end --local id = tonumber(itemstring:match('|Hitem:(%d+):')) local id,unique,_ _,id,_,_,_,_,_,_,unique = strsplit (":", itemstring) if unique == 0 then unique = nil end return _do_loot (self, false, person, unique, id, count) elseif event == "broadcast" then return _do_loot(self, false, ...) elseif event == "manual" then local r,i,n = ... return _do_loot(self, true, r, --[[unique=]]nil, i, --[[count=]]nil, --[[from=]]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.) do local green_help_link = addon.format_hypertext ([[Click here]], ITEM_QUALITY_UNCOMMON, 'help') 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("Shouldn'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._addBossEntry{ kind='boss',reason='kill',bossname="Baron Steamroller",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, tostring(green_help_link)) end end end function addon:SetThreshold (arg, quiet_p) local q = tonumber(arg) if q then q = math.floor(q+0.1) 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', ">:(notraid) 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("Restored previous data, but not in a raid", "and 5-player 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 g_seeing_oldsigs = nil 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 _G.OuroLootSV = nil self:_reset_timestamps() if verbose_p then if (_G.OuroLootSV_saved and #_G.OuroLootSV_saved>0) then self:Print("Current loot data cleared, %d saved sets remaining.", #_G.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 -- Declaring _log as local to here can result in this sequence: -- 1) logging happens, followed by reload or logout/login -- 2) _log points to SV_log -- 3) VARIABLES_LOADED replaces SV_log pointer with restored version -- 4) logging happens to _log table (now with no other references) -- 5) at logout, nothing new has been entered in the table being saved local date = _G.date function addon:log_with_timestamp (msg) tinsert (_log, date('%m/%d %H:%M:%S ')..msg) end end -- Check for plugins which haven't already been loaded, and add hooks for -- them. Credit to DBM for the approach here. function addon:_scan_LOD_modules() for i = 1, GetNumAddOns() do if GetAddOnMetadata (i, "X-OuroLoot-Plugin") and IsAddOnLoadOnDemand(i) and not IsAddOnLoaded(i) then local folder, _, _, enabled, _, reason = GetAddOnInfo(i) if enabled or opts.display_disabled_LODs then local tabtitle = GetAddOnMetadata (i, "X-OuroLoot-Plugin") self:_gui_add_LOD_tab (tabtitle, folder, i, enabled, reason) end end end end -- Routines for printing changes made by remote users. do local remote_change_chatframe function addon:_set_remote_change_chatframe (arg, silent_p) local frame if type(arg) == 'number' then arg = _G.math.min (arg, _G.NUM_CHAT_WINDOWS) frame = _G['ChatFrame'..arg] elseif type(arg) == 'string' then frame = _G[arg] end if type(frame) == 'table' and type(frame.AddMessage) == 'function' then remote_change_chatframe = frame if not silent_p then local msg = "Now printing to chat frame " .. arg if frame.GetName then msg = msg .. " (" .. tostring(frame:GetName()) .. ")" end self:Print(msg) if frame ~= _G.DEFAULT_CHAT_FRAME then self:CFPrint (frame, msg) end end return frame else self:Print("'%s' was not a valid chat frame number/name, no change has been made.", arg) end end function _notify_about_remote (sender, index, from_whom, olddisp) local e = g_loot[index] local from_color, from_text, to_color, to_text if from_whom then -- FIXME need to return previous name/class from reassign_loot else if olddisp then from_text = addon.disposition_colors[olddisp].text else olddisp = "normal" from_text = "normal" end from_color = addon.disposition_colors[olddisp].hex if e.disposition then to_text = addon.disposition_colors[e.disposition].text else to_text = "normal" end to_color = addon.disposition_colors[e.disposition or "normal"].hex end addon.dprint ('loot', "notifying:", sender, index, e.itemlink, from_color, from_text, to_color, to_text) addon:CFPrint (remote_change_chatframe, remote_chatty, sender, index, e.itemlink, from_color, from_text, to_color, to_text) 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 _G.table.sort(temp) 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) wipe(temp) addon.sender_list.activeI = #byindex sort (addon.sender_list.names, byindex) wipe(temp) end addon.sender_list.namesI = byindex 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, (possibly) yammering") if newer_warning then local pop = addon.format_hypertext ([[click here]], ITEM_QUALITY_UNCOMMON, function() -- 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]]) end) local ping = addon.format_hypertext ([[click here]], ITEM_QUALITY_UNCOMMON, 'DoPing') self:Print(newer_warning, tostring(pop), tostring(ping)) 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 = _G.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_variant_item(item) -- returns number 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 and 1) or (t == RAID_FINDER and 2) -- no ITEM_ for this, apparently 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 = _G.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 %s"): format (self.revision, self.ident, self.commrev) self:RegisterComm(self.ident) self:RegisterComm(self.identTg, "OnCommReceivedNocache") if self.author_debug then _G.Oloot = g_loot end end -- Raid roster snapshots do function addon:snapshot_raid (only_inraid_p) local ss = CopyTable(g_loot.raiders) local instance,maxsize = instance_tag() if only_inraid_p then for name,info in _G.next, ss do if info.online == 3 then ss[name] = nil end end end return ss, maxsize, instance, _G.time() 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._addBossEntry(boss) -- addon. bossi = addon._adjustBossOrder (bossi, g_boss_signpost) or bossi g_boss_signpost = nil addon.latest_instance = boss.instance 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.bossname, boss.instance) end end wipe(candidates) end local 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, maxsize, duration) self.dprint('loot',">>_do_boss, R:", reason, "B:", bossname, "T:", intag, "MS:", maxsize, "D:", duration) if self.rebroadcast and duration then self:vbroadcast('boss', reason, bossname, intag, maxsize) 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 recent_boss:test(signature) then self.dprint('cache', "remote boss <",signature,"> already in cache, skipping") else 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', bossname = bossname, reason = reason, instance = intag, duration = duration, -- deliberately may be nil raidersnap = self:snapshot_raid(), maxsize = maxsize, } tinsert(candidates,c) end break end self.dprint('loot',"<<_do_boss out") end -- This exposes the function to OCR, and can be a wrapper layer later. 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.bossname 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 -- Maybe redo this to only collapse *contiguous* wipes...? local i,d = 1,g_loot[1] while d ~= e do if d.bossname and d.bossname == e.bossname and d.instance == e.instance 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 = 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 local rawget, setmetatable = _G.rawget, _G.setmetatable -- 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 that. -- 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, caller-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 = _G.GetGameTime() --local localuptime = math.floor(GetTime()) local time_t = _G.time() e.hour = h e.minute = m e.stamp = time_t --localuptime local index = #g_loot + 1 g_loot[index] = e return index end -- Safety wrapper only. -- XXX Maybe pprint something here. function addon._addBossEntry (e) local ret = addon._addLootEntry(e) assert(e.kind=='boss') local needSize = e.maxsize == nil local needSnap = e.raidersnap == nil local needInst = e.instance == nil if needSize or needSnap then local ss, max, inst = addon:snapshot_raid() if needSize then e.maxsize = max end if needSnap then e.raidersnap = ss end if needInst then e.instance = inst end end return ret 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.bossname) 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+)' -- 'while true' so that we can use (inner) break as (outer) continue for i,e in self:filtered_loot_iter('loot') do while true do if not e.cache_miss then break end local borked_id = e.itemname:match(borkedpat) if not borked_id then break end 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 local msg = [[ Entry %d patched up with %s.]] e.quality = iquality e.itemname = iname e.id = tonumber(ilink:match("item:(%d+)")) e.itemlink = ilink e.itexture = itexture e.cache_miss = nil if e.unique then local gu = g_uniques[e.unique] local player_i, player_h, hist_i = _history_by_loot_id (e.unique, "fixcache") if gu.loot ~= i then -- is this an actual problem? pprint ('loot', ("Unique value '%s' had iterator value %d but g_uniques index %s."): format(e.unique,i,tostring(gu.loot))) end if player_i then player_h.id[e.unique] = e.id msg = [[ Entry %d (and history) patched up with %s.]] end end self:Print(msg, i, ilink) end break end end self:Print("...finished. Found %d |4entry:entries; with weird data.", numfound) end do local gur -- Strictly speaking, we'd want to handle individual 'exist' entries -- as they expire, rather then waiting for all of them to expire and then -- doing them as a group. But, if there're more than one of these per -- boss, something else is funky anyhow and certainly not a hurry. local function fixup_unique_replacements() --print("replacements fixup happening!") --tabledump(g_unique_replace.replacements) --_G.GRR = g_unique_replace.replacements for exist,info in pairs(gur.replacements) do local winning_index = 1 local winner = info[1] info[1] = nil pprint('improv', "fixup for", exist, "starting with", winner[1], "with", winner[2], "out of", #info, "total entries") -- Lowest player GUID wins. Seniority gotta count for something. for i = 2, #info do if winner[1] <= info[i][1] then pprint('improv', "champ wins against", i, info[i][1]) flib.del(info[i]) else pprint('improv', "challenger wins with", i, info[i][1]) flib.del(winner) winner = info[i] winning_index = i end info[i] = nil end pprint('improv', "final:", winner[1], winner[2]) --[[ A: winner was generated locally >g_loot and history already has the replacement value >winning_index == 1 B: winner was generated remotely >need to scan and replace ]] if winning_index ~= 1 then --XXX still needs to be debugged: local cache = g_uniques[exist] local looti = assert(cache.loot) -- can't possibly be missing... if g_loot[looti].unique ~= exist then pprint('improv', "WTF. entry", looti, "does not match original unique tag! instead", g_loot[looti].unique) else pprint('improv', "found and replaced loot entry", looti) g_loot[looti].unique = winner[2] end local hi,ui = cache.history, cache.history_may if hi ~= g_uniques.NOTFOUND then local hist = addon.history[hi] if ui and hist.unique[ui] == exist then -- ui is valid else ui = nil for i,ui2 in ipairs(hist.unique) do if ui2 == exist then ui = i break end end end if ui then pprint('improv', "found and replacing history entry", hi, ui, hist.name) hist.when[winner[2]] = hist.when[exist] hist.id[winner[2]] = hist.id[exist] hist.count[winner[2]] = hist.count[exist] hist.unique[ui] = winner[2] hist.when[exist] = nil hist.id[exist] = nil hist.count[exist] = nil end end end pprint('improv', "finished with", exist, "into", winner[2]) flib.del(winner) flib.del(info) gur.replacements[exist] = nil end end local function new_entry (id, exist, repl, is_local) pprint('improv', "new_entry", id, exist, repl, is_local) gur.replacements[exist] = gur.replacements[exist] or flib.new() tinsert (gur.replacements[exist], flib.new (tonumber(id), repl)) if is_local then gur.replacements[exist].LOCAL = repl end gur.cache:add (exist) end local function get_previous_replacement (exist) local l = gur.replacements[exist] if l and l.LOCAL then pprint('improv', "check for previous", exist, "returns valid", l.LOCAL) return l.LOCAL end pprint('improv', "check for previous", exist, "returns nil") end function _setup_unique_replace () gur = {} gur.cache = create_new_cache ('improv', 10, fixup_unique_replacements) gur.me = tonumber(_G.UnitGUID('player'):sub(-7),16) gur.replacements = {} gur.new_entry = new_entry gur.get_previous_replacement = get_previous_replacement g_unique_replace = gur _setup_unique_replace = nil end end do local clicky function _unavoidable_collision (err) -- This should happen so rarely that it's not worth moving into gui.lua if not StaticPopupDialogs["OUROL_COLLISION"] then StaticPopupDialogs["OUROL_COLLISION"] = flib.StaticPopup{ button1 = OKAY, } clicky = addon.format_hypertext( [[ SYSTEM FAILURE -- RELEASE RINZLER ]], "|cffff0000", function() StaticPopup_Show "OUROL_COLLISION" end) end StaticPopupDialogs["OUROL_COLLISION"].text = err _G.PlaySoundFile ([[Interface\AddOns\Ouro_Loot\sfrr.ogg]], "Master") addon:Print (" ") addon:Print (" ", clicky) addon:Print (" ") end end --function DOTEST() -- local err = unique_collision:format (ERROR_CAPS, -- "Codpiece of the Grimacing Lunatic", -- 'n3183021', 'Farmbuyer', '14', '78') -- _unavoidable_collision (err) -- pprint('ohfuck', "COLLISION", err) --end ------ Saved texts function addon:check_saved_table(silent_p) local s = _G.OuroLootSV_saved if s and (#s > 0) then return s end _G.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) _G.OuroLootSV_saved = _G.OuroLootSV_saved or {} local SV = _G.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", -- ["person_class"] = "PRIEST", -- may be missing, used in display only -- -- sorted array: -- ["unique"] = { most_recent_tag, previous_tag, .... }, -- -- these are indexed by unique tags, and 'count' may be missing: -- ["when"] = { ["tag"] = "formatted timestamp for displaying loot" }, -- ["id"] = { ["tag"] = 11111 }, -- ["count"] = { ["tag"] = "x3", .... }, -- }, -- [2] = { -- ["name"] = "OtherPlayer", -- ...... -- }, ...... -- }, -- ["OtherRealm"] = ...... -- } -- -- Up through 2.81.4 (specifically through rev 95), an individual player's -- table looked like this: -- ["name"] = "Farmbuyer", -- [1] = { id = nnnnn, when = "formatted timestamp for displaying" } -- most recent loot -- [2] = { ......., [count = "x3"] } -- previous loot -- which was much easier to manipulate, but had a ton of memory overhead. do -- Sorts a player's history from newest to oldest, according to the -- formatted timestamp. This is expensive, and destructive for P.unique. local function compare_timestamps (L, R) return L > R -- reverse of normal order, newest first end local function sort_player (p) local new_uniques, uniques_bywhen, when_array = {}, {}, {} for u,tstamp in pairs(p.when) do uniques_bywhen[tstamp] = u when_array[#when_array+1] = tstamp end _G.table.sort (when_array, compare_timestamps) for i,tstamp in ipairs(when_array) do new_uniques[i] = uniques_bywhen[tstamp] end p.unique = new_uniques end -- Possibly called during login. Cleared when no longer needed. -- Rewrites a PLAYER table from format 3 to format 4. function addon:_uplift_history_format (player) local unique, when, id, count = {}, {}, {}, {} local name = player.name for i,h in ipairs(player) do local U = h.unique unique[i] = U when[U] = h.when id[U] = h.id count[U] = h.count end wipe(player) player.name = name player.id, player.when, player.unique, player.count = id, when, unique, count end function addon:_cache_history_uniques() UpdateAddOnMemoryUsage() local before = GetAddOnMemoryUsage(nametag) local trouble local count = 0 for hi,player in ipairs(self.history) do for ui,u in ipairs(player.unique) do g_uniques[u] = { history = hi, history_may = ui } count = count + 1 end end for i,e in self:filtered_loot_iter('loot') do if e.unique and e.unique ~= "" then local hmmm = _G.rawget(g_uniques,e.unique) if hmmm then hmmm.loot = i --;print("Active loot", i, "found with tag", e.unique) elseif e.disposition == 'shard' or e.disposition == 'gvault' then g_uniques[e.unique] = { loot = i, history = g_uniques.NOTFOUND } count = count + 1 --print("Active loot", i, "INSERTED with tag", e.unique, "as", e.disposition) else hmmm = "active data not found in history ("..i.."/"..tostring(e.unique) ..") in precache loop! trying to fixup for this session" pprint(hmmm) -- more? -- try to simply fix up errors as we go g_uniques[e.unique] = { loot = i, history = g_uniques.NOTFOUND } --trouble = true end else trouble = true pprint('loot', "ERROR precache loop found bad unique tag!", i, "tag", tostring(e.unique), "from?", tostring(e.bcast_from)) end end UpdateAddOnMemoryUsage() local after = GetAddOnMemoryUsage(nametag) self:Print("Pre-scanning history for faster loot handling on %s used %.2f MB of memory across %d entries.", self.history.realm, (after-before)/1024, count) if trouble then self:Print("Note that there were inconsistencies in the data;", "you should consider submitting a bug report (including your", "SavedVariables file), and regenerating or preening this", "realm's loot history.") end g_uniques:SETMODE('probe') self._cache_history_uniques = nil 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 -- why yes, I *did* spend many years as a YP/NIS admin, how did you know? 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, id={}, when={}, unique={}, count={} } self.history.byname[name] = i end return i, self.history[i] end -- Prepends data from the loot entry at LOOTINDEX to be the new most -- recent history entry for that player. 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) local when = self:format_timestamp (g_today, e) -- If any of these change, update the end of history_handle_disposition. if (not e.unique) or (#e.unique==0) then e.unique = e.id .. ' ' .. when end local U = e.unique tinsert (h.unique, 1, U) h.when[U] = when h.id[U] = e.id h.count[U] = e.count g_uniques[U] = { loot = lootindex, history = i } 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] g_uniques:RESET() 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 sort_player(h) end end -- Clears all but the most recent HOWMANY (optional, default 1) entries -- for each player on REALMNAME. -- This function's name is the legacy of the orignal fsck(8) "-p" option, -- which has a similar feel. function addon:preen_history (realmname, howmany) local r = assert(realmname) howmany = tonumber(howmany) or 1 if type(self.history_all[r]) ~= 'table' then return end g_uniques:RESET() for i,h in ipairs(self.history_all[r]) do -- This is going to do horrible things to memory. The subtables -- after this step would be large and sparse, with no good way -- of shrinking the allocation... sort_player(h) -- ...so it's better in the long run to discard them. local new_unique, new_id, new_when, new_count = {}, {}, {}, {} for ui = 1, howmany do local U = h.unique[ui] if not U then break end new_unique[ui] = U new_id[U] = h.id[U] new_when[U] = h.when[U] new_count[U] = h.count[U] end h.unique, h.id, h.when, h.count = new_unique, new_id, new_when, new_count end end -- Given a unique tag OR 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 _history_by_loot_id (needle, operation_text) local errtxt if type(needle) == 'string' then -- unique tag elseif type(needle) == 'table' then if needle.kind ~= 'loot' then error("trying to "..operation_text.." something that isn't loot") end needle = needle.unique if not needle then return nil, --[[errtxt=]]"Entry for %s is missing a history tag!" end else error("'"..tostring(needle).."' is neither unique string nor loot entry!") end local player_i, player_h local cache = g_uniques[needle] if cache.history == g_uniques.NOTFOUND 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!" else player_i = cache.history player_h = addon.history[player_i] if cache.history_may and needle == player_h.unique[cache.history_may] then return player_i, player_h, cache.history_may end for i,u in ipairs(player_h.unique) do if needle == u then cache.history_may = i -- might help, might not return player_i, player_h, i end end end if not errtxt then -- The cache finder got a hit, but now it's gone? WTF? errtxt = "ZOMG! %s was in history but now is gone. Possibly your history tables have been corrupted and should be recreated. This is likely a bug. Tell Farmbuyer what steps you took to cause this." end return nil, errtxt 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 = _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 here is the formatted error text self:Print(from_h .. " Loot will be reassigned, but history will NOT be updated.", e.itemlink) else local U = tremove (from_h.unique, hist_i) -- The loot master giveth... to_h.unique[#to_h.unique+1] = U to_h.when[U] = from_h.when[U] to_h.id[U] = from_h.id[U] to_h.count[U] = from_h.count[U] sort_player(to_h) -- ...and the loot master taketh away. from_h.when[U] = nil from_h.id[U] = nil from_h.count[U] = nil -- Blessed be the lookup cache of the loot master. g_uniques[U] = { loot = index, history = to_i } end e.person = to_name e.person_class = select(2,_G.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 -- Similar to _addHistoryEntry. The second arg may be a loot entry -- (which used to be at LOOTINDEX), or nil (and the loot entry will -- be pulled from LOOTINDEX instead). function addon:_delHistoryEntry (lootindex, opt_e) local e = opt_e or g_loot[lootindex] if e.kind ~= 'loot' then return end local from_i, from_h, hist_i = _history_by_loot_id (e, "delete") if not from_i then -- from_h is the formatted error text self:Print(from_h .. " Loot will be deleted, but history will NOT be updated.", e.itemlink) return end local hist_u = tremove (from_h.unique, hist_i) from_h.when[hist_u] = nil from_h.id[hist_u] = nil from_h.count[hist_u] = nil g_uniques[hist_u] = nil self.hist_clean = nil self:Print("Removed history entry %d/%s from '%s'.", lootindex, e.itemlink, e.person) 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 = {} 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 = _history_by_loot_id (e, "mark") -- remove history entry if it exists if hist_i then local c = flib.new() local hist_u = tremove (name_h.unique, hist_i) c.when = name_h.when[hist_u] c.id = name_h.id[hist_u] c.count = name_h.count[hist_u] deleted_cache[hist_u] = c name_h.when[hist_u] = nil name_h.id[hist_u] = nil name_h.count[hist_u] = nil 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. -- So this isn't treated as an error. 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. -- FIXME The deleted cache isn't nearly as useful now with the new data structures. local when if (not e.unique) or (#e.unique==0) then when = g_today and self:format_timestamp (g_today, e) or date("%Y/%m/%d %H:%M",e.stamp) e.unique = e.id .. ' ' .. when end local U = e.unique local c = deleted_cache[U] deleted_cache[U] = nil name_h.unique[#name_h.unique+1] = U name_h.when[U] = c and c.when or when or date("%Y/%m/%d %H:%M",e.stamp) name_h.id[U] = e.id -- c.id name_h.count[U] = c and c.count or e.count sort_player(name_h) g_uniques[U] = { loot = index, history = name_i } self.hist_clean = nil if c then flib.del(c) end return end end -- This is not entirely "history" but not completely anything else either. -- Handles the primary "Mark as <x>" action. Arguments depend on who's -- calling it: -- "local", row_index, new_disposition -- "remote", sender, unique_id, item_id, old_disposition, new_disposition -- In the local case, must also broadcast a trigger. In the remote case, -- must figure out the corresponding loot entry (if it exists). In both -- cases, must update history appropriately. Returns nil if anything odd -- happens (not necessarily an error!); returns the affected loot index -- on success. function addon:loot_mark_disposition (how, ...) -- This must all be filled out in all cases: local e, index, olddisp, newdisp, unique, id -- Only set in remote case: local sender if how == "local" then index, newdisp = ... index = assert(tonumber(index)) e = g_loot[index] id = e.id unique = e.unique -- can potentially still be nil at this step olddisp = e.disposition elseif how == "remote" then sender, unique, id, olddisp, newdisp = ... local cache = g_uniques[unique] if cache.loot then index = tonumber(cache.loot) e = g_loot[index] end else return -- silently ignore future cases from future clients end if self.debug.loot then local m = ("Re-mark index %d(pre-unique %s) with id %d from '%s' to '%s'."): format(index, unique, id, tostring(olddisp), tostring(newdisp)) self.dprint('loot', m) if sender == my_name then self.dprint('loot',"(Returning early from debug mode's double self-mark.)") return index end end if not e then -- say something? return end e.disposition = newdisp e.bcast_from = nil -- I actually don't remember now why this gets cleared... e.extratext = nil self:history_handle_disposition (index, olddisp) -- A unique tag has been set by this point. if how == "local" then unique = assert(e.unique) self:vbroadcast('mark', unique, id, olddisp, newdisp) end return index end end ------ Player communication do local select, tconcat, strsplit, unpack = select, table.concat, strsplit, unpack --[[ old way: repeated string concatenations, BAD new way: new table on every broadcast, BAD local msg = ... for i = 2, select('#',...) do msg = msg .. '\a' .. (select(i,...) or "") end return msg ]] local function assemble(t,...) local n = select('#',...) if n > 0 then local msg = {t,...} -- tconcat requires strings, but T is known to be one already -- can't use #msg since there might be holes for i = 2, n+1 do msg[i] = tostring(msg[i] or "") end return tconcat (msg, '\a') end return t end -- broadcast('tag', <stuff>) -- vbroadcast('tag', <stuff>) function addon:vbroadcast(tag,...) return self:broadcast(self.commrev..tag,...) end function addon:broadcast(tag,...) local msg = assemble(tag,...) 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 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['17improv'] = function (sender, _, senderid, existing, replace) addon.dprint('comm', "DOTimprov/17, sender", sender, "id", senderid, "existing", existing, "replace", replace) if not g_unique_replace then _setup_unique_replace() end g_unique_replace.new_entry (senderid, existing, replace) end OCR_funcs['17mark'] = function (sender, _, unique, item, old, new) addon.dprint('comm', "DOTmark/17, sender", sender, "unique", unique, "item", item, "from old", old, "to new", new) local index = addon:loot_mark_disposition ("remote", sender, unique, item, old, new) --if not addon.enabled then return end -- hmm if index and opts.chatty_on_remote_changes then _notify_about_remote (sender, index, --[[from_whom=]]nil, old) end end OCR_funcs['16loot'] = function (sender, _, recip, item, count, extratext) addon.dprint('comm', "DOTloot/16, sender", sender, "recip", recip, "item", item, "count", count) if not addon.enabled then return end adduser (sender, nil, true) -- Empty unique string will pass through all of the loot handling, -- and then be rewritten by the history routine (into older string -- of ID+date). g_seeing_oldsigs = g_seeing_oldsigs or {} g_seeing_oldsigs[sender] = true addon:CHAT_MSG_LOOT ("broadcast", recip, --[[unique=]]"", item, count, sender, extratext) end OCR_funcs.loot = OCR_funcs['16loot'] -- old unversioned stuff goes to 16 OCR_funcs['17loot'] = function (sender, _, recip, unique, item, count, extratext) addon.dprint('comm', "DOTloot/17, sender", sender, "recip", recip, "unique", unique, "item", item, "count", count, "extratext", extratext) if not addon.enabled then return end adduser (sender, nil, true) addon:CHAT_MSG_LOOT ("broadcast", recip, unique, 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, --[[maxsize=]]0) end OCR_funcs['16boss'] = function (sender, _, reason, bossname, instancetag, maxsize) addon.dprint('comm', "DOTboss/16,17, sender", sender, "reason", reason, "name", bossname, "it", instancetag, "size", maxsize) if not addon.enabled then return end adduser (sender, nil, true) addon:on_boss_broadcast (reason, bossname, instancetag, maxsize) end OCR_funcs['17boss'] = OCR_funcs['16boss'] local bcast_on = addon.format_hypertext ([[the red pill]], '|cffff4040', function (self) if not self.rebroadcast then self:Activate(nil,true) end self:broadcast('bcast_responder') end) local waferthin = addon.format_hypertext ([[the blue pill]], '|cff0070dd', function (self) g_wafer_thin = true -- mint? it's wafer thin! self:broadcast('bcast_denied') -- fuck off, I'm full 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.", sender, tostring(bcast_on), tostring(waferthin)) 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 disassembler and dispatcher. The static weak table -- is not my favorite approach to handling ellipses, but it lets me loop -- through potential nils easily without creating a ton of garbage. local OCR_data = setmetatable({}, {__mode='v'}) local function dotdotdot (sender, tag, ...) local f = OCR_funcs[tag] if f then --wipe(OCR_data) costs more than its worth here local n = select('#',...) for i = 1, n do local d = select(i,...) OCR_data[i] = (d ~= "") and d or nil end addon.dprint('comm', ":... processing", tag, "from", sender, "with arg count", n) return f(sender,tag,unpack(OCR_data,1,n)) end addon.dprint('comm', "unknown comm message", tag, "from", sender) end -- Recent message cache (this can be accessed via advanced options panel) 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 self.dprint('cache', "OCR message <",msg,"> already in cache, skipping") return 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