Mercurial > wow > ouroloot
view core.lua @ 156:1e2ee3f52bc8 beta-l10n-2
Stub files to test server-side localization generator. These are NOT LOADED YET.
author | Farmbuyer of US-Kilrogg <farmbuyer@gmail.com> |
---|---|
date | Sat, 21 Feb 2015 17:04:00 -0500 |
parents | 42dd3076baaf |
children |
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: - fname fully-qualified name; "PlayerName" or "PlayerName-RealmName" - 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 - realm realm name, or missing if same realm as player at the time - online 'online', 'offline', 'no_longer' [no longer in raid group] [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 'no_longer') - needinfo true if haven't yet gotten close enough to player to request unit data; SHOULD be missing 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, possibly tweaked for uniqueness 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) - person_realm recipient's realm if different from the player's; missing otherwise - itemname not including square brackets - id itemID as number - itemstring "item:nnnnn" string - itemlink full clickable link - itexture icon path (e.g., Interface\Icons\INV_Misc_Rune_01) - quality [LE_]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 XXX CHANGED FOR WoD: for now, if present at all, some kind of heroic/warforged/something-extra; 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; missing otherwise - 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 (callback 'events', search for LCALLBACK) ------ 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_hist = nil OuroLootSV_log = {} ------ Constants local option_defaults = { profile = { --['datarev'] = 20, -- cheating, this isn't actually an option ['popup_on_join'] = true, ['register_slash_synonyms'] = false, ['slash_synonyms'] = '/ol,/oloot', ['scroll_to_bottom'] = true, ['history_suppress_LFR'] = false, ['history_ignore_xrealm'] = true, ['gui_noob'] = 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_unusable_LODs'] = false, ['display_bcast_from'] = true, ['precache_history_uniques'] = false, ['chatty_on_remote_changes'] = false, ['chatty_on_local_changes'] = false, ['chatty_on_changes_frame'] = 1, ['itemfilter'] = {}, ['itemvault'] = {}, ['track_bonusrolls'] = true, ['track_nonloot'] = false, } } local virgin = "First time loaded? Hi! Use the /ouroloot 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 horrible_error_text = [[|cffff1010]] .. ERROR_CAPS ..[[:|n|cffffff00Something unrecoverable has happened. The error message]] ..[[ which was provided follows in white:|r|n|n%s|n|n|cffffff00Ouro Loot]] ..[[ will not display a window until this situation is corrected. ]] ..[[ You can try typing|n|cff00ff40/ouroloot fix ?|n]] ..[[|cffffff00to see what can be done by software alone. You may still]] ..[[ need to do a "/reload" afterwards, or even restart the game client.]] local unique_collision = "Item '%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 new_profile_warning = [[Be aware that profiles only store addon & plugin settings from the <Options> tab; loot and generated text is account-wide data, unrelated to your current profile.]] local remote_chatty = "|cff00ff00%s|r changed %d/%s from %s to %s" 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 version_large = nil -- defaults to 1, possibly changed by version local timestamp_fmt_unique = '%Y/%m/%dT%H:%M:%S' local timestamp_fmt_history = '%Y/%m/%d %H:%M:%S' ------ 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' version = _G.GetAddOnMetadata(nametag,"Version") or "?" -- "x.yy.z", etc ident = "OuroLoot2" identTg = "OuroLoot2Tg" status_text = nil revision = "@project-revision@" --@debug@ revision = "DEVEL" --@end-debug@ 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, callback = false, alsolog = false, } --@debug@ DEBUG_PRINT = true debug.loot = true debug.comm = true is_guilded = _G.IsInGuild() --@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: '.._G.tostring(msg or 'nil'),3) 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 lootjumps = {} -- maps hypertext idents to EOI line numbers -- 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 addon.FILES_LOADED = 0 -- 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 -- Class color support. Do the expensive string.format calls up front, and -- the cheap all-string-all-at-once single-op concatenation as needed. do local cc = {} local function extract (color_info) local hex if color_info.colorStr then -- MoP hex = "|c" .. color_info.colorStr else -- pre-MoP hex = ("|cff%.2x%.2x%.2x"):format(255*color_info.r, 255*color_info.g, 255*color_info.b) end return { r=color_info.r, g=color_info.g, b=color_info.b, a=1, hex=hex } end local function fill_out_class_colors() for class,color in pairs(CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS) do cc[class] = extract(color) end cc.DEFAULT = extract(_G.NORMAL_FONT_COLOR) end if CUSTOM_CLASS_COLORS and CUSTOM_CLASS_COLORS.RegisterCallback then CUSTOM_CLASS_COLORS:RegisterCallback(fill_out_class_colors) end addon.class_colors = cc -- this stays around addon.fill_out_class_colors = fill_out_class_colors -- this doesn't -- What I really want is to have the hooked :Print understand a special -- format specifier like "%Cs" and do the colorizing automatically. function addon:colorize (text, class) return ((class and cc[class]) or cc.DEFAULT).hex .. text .. "|r" 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 g_gui = nil local error = addon.error local assert = addon.assert local type, select, next, pairs, ipairs, tinsert, tremove, tostring, tonumber, wipe = type, select, next, pairs, ipairs, table.insert, table.remove, tostring, tonumber, table.wipe local pprint, tabledump = addon.pprint, flib.tabledump local strsplit, CopyTable = strsplit, CopyTable local GetNumGroupMembers = GetNumGroupMembers local IsInRaid = IsInRaid -- En masse forward decls of symbols defined inside local blocks local _register_bossmod, makedate, create_new_cache, _init, _log, _do_loot_metas local _history_by_loot_id, _setup_unique_replace, _unavoidable_collision local _notify_about_change, _LFR_suppressing, _add_loot_disposition, _grok -- 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 version_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.version: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 version_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 = setmetatable({}, {__mode = 'v'}) local base = newproxy(true) getmetatable(base).__tostring = function(ud) return text_map[ud] end --@debug@ --[[ auto collecting these tokens is an interesting micro-optimization but not yet getmetatable(base).__index = { ['done'] = function (ud) text_map[ud] = nil func_map[ud] = nil end, } getmetatable(base).__gc = function(ud) print("Collecting hyperlink object <",tostring(ud),">") end --]] --@end-debug@ -- TEXT will automatically be surrounded by brackets -- COLOR can be LE_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 local chat. Clicking a tab_title hyperlink opens the GUI to that -- tab; the MethodName and raw function callbacks will be passed the addon -- table, the same matching number, and the mouse button (ala OnClick) as -- arguments. -- -- This is largely an excuse to fool around with Lua data constructs. function addon.format_hypertext (text, color, func) local ret = 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 ]] 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]] local t = type(f) if t == 'string' then if type(addon[f]) == 'function' then addon[f](addon,arg,mousebutton) -- method else addon:BuildMainDisplay(f) -- tab title fragment end elseif t == 'function' then f (addon, arg, mousebutton) 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 instance_tag do local codemap = { --[0] = "?", -- should already be caught, avoid needless hash part [1] = "5", -- normal or scenario [2] = "5h", [3] = "10", [4] = "25", [5] = "10h", [6] = "25h", [7] = "LFR", [8] = "C", -- challenge mode [9] = "40", } function instance_tag() -- Return values of GetInstanceInfo changed considerably, now much -- more useful and consistent. local name, typeof, diffcode, _, raidsize, _, _, mapid, currsize = GetInstanceInfo() local t name = addon.instance_abbrev[mapid] or addon.instance_abbrev[name] or name if typeof == nil or typeof == "none" or diffcode == 0 or diffcode == nil then -- either outdoors or in a scenario (revisit those maybe) return name, MAX_RAID_MEMBERS end t = codemap[diffcode] or ("?"..diffcode.."?") return name .. "(" .. t .. ")", raidsize end end addon.instance_tag = instance_tag -- grumble addon.latest_instance = nil -- spelling reminder, assigned elsewhere -- Whether we're recording anything at all in the loot histories local function _history_suppress() return _LFR_suppressing or addon.history_suppress end -- Memoizing cache of unique IDs as we generate or search for them. Keys are -- the uniques, values are the following: -- 'history' active player name in 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 = h.name, 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 = h.name, 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: function cleanup (expired_entries) for i = 1, #expired_entries do -- this table is in strict FIFO order print(i, expired_entries[i]) -- 1 foo -- 2 bar end end ]] do local AnimTimerFrame = CreateFrame('Frame',nil,nil) -- FIXME transition hack local caches = {} local cleanup_group = AnimTimerFrame:CreateAnimationGroup() local time, next = time, next local new, del = flib.new, flib.del cleanup_group:SetLooping("REPEAT") cleanup_group:SetScript("OnLoop", function(cg) addon.dprint('cache',"OnLoop firing") local now = time() local actives = 0 local expired = new() -- this is ass-ugly for name,c in next, caches do local fifo = c.fifo local active = #fifo > 0 actives = actives + (active and 1 or 0) while (#fifo > 0) and (now > fifo[1].t) do local datum = tremove(fifo,1) addon.dprint('cache', name, "cache removing", datum.t, "<", datum.m, ">") c.hash[datum.m] = nil tinsert(expired,datum.m) del(datum) end if active and #fifo == 0 and c.func then addon.dprint('cache', name, "empty, firing cleanup") c.func(expired) end wipe(expired) end del(expired) if actives == 0 then addon.dprint('cache',"OnLoop FINISHING animation group") cleanup_group:Finish() else addon.dprint('cache',"OnLoop done, not yet finished") end end) local function _add (cache, x) assert(type(x)~='number') local datum = new() datum.m = x datum.t = time() + cache.ttl 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") cleanup_group:Play() end end local function _test (cache, x) 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. local c = { ttl = ttl, name = name, add = _add, test = _test, cleanup = cleanup_group:CreateAnimation("Animation"), func = on_alldone, -- Testing merging these two (_add's 'x' must not be numeric) fifo = {}, --hash = {}, } c.hash = c.fifo c.cleanup:SetOrder(1) -- [1,100] range within parent animation c.cleanup:SetDuration(0.8) -- hmmm caches[name] = c return c end end ------ Ace3 framework stuff function addon:DBProfileRefresh() opts = self.db.profile end function addon:OnInitialize() if self.author_debug then _G.OL = self _G.g_uniques = g_uniques end -- This kludgy-looking thing is because if there are serious errors in -- loading, some of the actions during PLAYER_LOGOUT can destroy data. if self.FILES_LOADED ~= 7 then print('|cffaee3ffouroloot reports load count of',self.FILES_LOADED,'fml|r') self:SetEnabledState(false) return end self.FILES_LOADED = nil _log = 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 = OuroLootSV g_restore_p = OuroLootSV ~= nil self.dprint('flow', "oninit sets restore as", g_restore_p) -- Primarily for plugins, but can be of use to me also... LCALLBACK self.callbacks = LibStub("CallbackHandler-1.0"):New(self) --function self.callbacks:OnUsed (target_aka_self, eventname) end --function self.callbacks:OnUnused (target_aka_self, eventname) end if _G.OuroLootOptsDB == nil then local vclick = self.format_hypertext ([[click here]], LE_ITEM_QUALITY_UNCOMMON, 'help') self:ScheduleTimer(function(s) for id in pairs(self.default_itemfilter) do opts.itemfilter[id] = true end for id in pairs(self.default_itemvault) do opts.itemvault[id] = true end s:Print(virgin, tostring(vclick)) virgin = nil end,10,self) else virgin = nil end self.db = LibStub("AceDB-3.0"):New("OuroLootOptsDB", option_defaults , --[[Default=]]true) self.db.RegisterCallback (self, "OnNewProfile", function() self:Print(new_profile_warning) end) self.db.RegisterCallback (self, "OnProfileChanged", "DBProfileRefresh") self.db.RegisterCallback (self, "OnProfileCopied", "DBProfileRefresh") self.db.RegisterCallback (self, "OnProfileReset", "DBProfileRefresh") self:DBProfileRefresh() --[[ 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]] self:RegisterChatCommand("ouroloot", "OnSlash") if opts.register_slash_synonyms then -- Maybe use %w here for non-English locales? local n = 2 for s in opts.slash_synonyms:gmatch("/%a+") do _G['SLASH_ACECONSOLE_OUROLOOT'..n] = s n = n + 1 end end self.history_all = self.history_all or _G.OuroLootSV_hist or {} local r = self:load_assert (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 false and OuroLootSV then local dirty = false local bumpers = {} --bumpers[14] = function() start --bumpers[19] = function() latest --[===[ 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 self:FINISH_SPECIAL_TABS() _init(self) self.dprint('flow', "version strings:", version_large, self.revision, self.status_text) g_gui = self.gui_state_pointer self.gui_state_pointer = nil self.load_assert = nil --[[ The loot format patterns of interest, changed into relatively tight string match patterns. Done during initialization rather than at load-time against the slim chance that one of the non-US "delocalizers" needs to adjust the global patterns before we transform them. ]] local function ss (m) return m:gsub('%.$','%%.'):gsub('%%s','(.+)') end local function ssd (m) return m:gsub('%.$','%%.'):gsub('%%s','(.+)'):gsub('x%%d','(x%%d+)') end local function s (m) return m:gsub('%.$','%%.'):gsub('%%s','(.+)') end local function sd (m) return m:gsub('%.$','%%.'):gsub('%%s','(.+)'):gsub('x%%d','(x%%d+)') end local loot_patterns = { -- LOOT_ITEM = "%s receives loot: %s." --> (.+) receives loot: (.+)%. -- LOOT_ITEM_BONUS_ROLL = "%s receives bonus loot: %s." --> (.+) receives bonus loot: (.+)%. -- LOOT_ITEM_PUSHED = "%s receives item: %s." --> (.+) receives item: (.+)%. ['LOOT_ss'] = ss(_G.LOOT_ITEM), ['BONUS_ss'] = ss(_G.LOOT_ITEM_BONUS_ROLL), ['ITEM_ss'] = ss(_G.LOOT_ITEM_PUSHED), -- LOOT_ITEM_MULTIPLE = "%s receives loot: %sx%d." --> (.+) receives loot: (.+)(x%d+)%. -- LOOT_ITEM_BONUS_ROLL_MULTIPLE = "%s receives bonus loot: %sx%d." --> (.+) receives bonus loot: (.+)(x%d+)%. -- LOOT_ITEM_PUSHED_MULTIPLE = "%s receives item: %sx%d." --> (.+) receives item: (.+)(x%d+)%. ['LOOT_MULTIPLE_ssd'] = ssd(_G.LOOT_ITEM_MULTIPLE), ['BONUS_MULTIPLE_ssd'] = ssd(_G.LOOT_ITEM_BONUS_ROLL_MULTIPLE), ['ITEM_MULTIPLE_ssd'] = ssd(_G.LOOT_ITEM_PUSHED_MULTIPLE), -- LOOT_ITEM_SELF = "You receive loot: %s." --> You receive loot: (.+)%. -- LOOT_ITEM_BONUS_ROLL_SELF = "You receive bonus loot: %s." --> You receive bonus loot: (.+)%. -- LOOT_ITEM_PUSHED_SELF = "You receive item: %s." --> You receive item: (.+)%. ['LOOT_SELF_s'] = s(_G.LOOT_ITEM_SELF), ['BONUS_SELF_s'] = s(_G.LOOT_ITEM_BONUS_ROLL_SELF), ['ITEM_SELF_s'] = s(_G.LOOT_ITEM_PUSHED_SELF), -- LOOT_ITEM_SELF_MULTIPLE = "You receive loot: %sx%d." --> You receive loot: (.+)(x%d+)%. -- LOOT_ITEM_BONUS_ROLL_SELF_MULTIPLE = "You receive bonus loot: %sx%d." --> You receive bonus loot: (.+)(x%d+)%. -- LOOT_ITEM_PUSHED_SELF_MULTIPLE = "You receive item: %sx%d." --> You receive item: (.+)(x%d+)%. ['LOOT_SELF_MULTIPLE_sd'] = sd(_G.LOOT_ITEM_SELF_MULTIPLE), ['BONUS_SELF_MULTIPLE_sd'] = sd(_G.LOOT_ITEM_BONUS_ROLL_SELF_MULTIPLE), ['ITEM_SELF_MULTIPLE_sd'] = sd(_G.LOOT_ITEM_PUSHED_SELF_MULTIPLE), -- LOOT_ITEM_WHILE_PLAYER_INELIGIBLE is mostly the same as LOOT_ITEM with -- an inline texture and no full stop. The punctuation in the texture -- path causes fits while matching, so just make that a wildcard rather -- than trying to escape it all. ['LOOT_WHILE_PLAYER_INELIGIBLE_ss'] = _G.LOOT_ITEM_WHILE_PLAYER_INELIGIBLE:gsub('\124T%S*\124t','\124T%%S*\124t'):gsub('%%s','(.+)'), } _grok = setfenv (_grok, loot_patterns) for k,v in pairs(loot_patterns) do self.dprint('loot', "Loot pattern", k, "set to", v) end self.OnInitialize = nil -- free up ALL the things! end function addon:OnEnable() self:RegisterEvent("PLAYER_LOGOUT") self:RegisterEvent("GROUP_ROSTER_UPDATE") -- This Print 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. -- -- Everything dealing with a prefix hyperlink 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. do local AC = LibStub("AceConsole-3.0") function addon.chatprefix (code, arg) local f = '' -- empty -> BuildMainDisplay(empty) -> main tab if code == "GoToLootLine" then f = code --elseif ..... end local ret, num = self.format_hypertext ("Ouro Loot", LE_ITEM_QUALITY_LEGENDARY, f) if code == "GoToLootLine" then self.lootjumps[num] = arg end return ret, num end --local chat_prefix = self.format_hypertext ("Ouro Loot", --[[legendary]]5, '') --local chat_prefix_s = tostring(chat_prefix) local chat_prefix_s = tostring((addon.chatprefix())) 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 function addon:PCFPrint (frame, prefix, str, ...) assert(type(frame)=='table' and frame.AddMessage) if type(str) == 'string' and str:find("%", nil, --[[plainmatch=]]true) then return AC:Print (frame, tostring(prefix), str:format(...)) else return AC:Print (frame, tostring(prefix), str, ...) end end end -- Copy these over once, now that other addons have mostly loaded. Any -- future tweaks via CUSTOM_CLASS_COLORS will trigger the same callback. if addon.fill_out_class_colors then addon.fill_out_class_colors() addon.fill_out_class_colors = nil end while opts.keybinding do if InCombatLockdown() then local reload = self.format_hypertext ([[the options tab]], LE_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 == _G.ACCOUNT_BINDINGS or c == _G.CHARACTER_BINDINGS then SaveBindings(c) end else self:Print("Error registering '%s' as a keybinding, check spelling!", opts.keybinding_text) end break end --[[ Throw in the default disposition types. This could be called during load were it not for the need to talk to the GUI data (not set up yet). Args: code, rhex, ghex, bhex, text_notes, opt_text_menu (uses notes if nil), opt_tooltip_txt, can_reassign_p, do_history_p, from_notes_text_p ]] local norm = _add_loot_disposition (self, 'normal', "ff","ff","ff", "", "normal/mainspec", [[This is the default. Selecting any 'Mark as <x>' action blanks out extra notes about who broadcast this entry, etc.]], true, true, false) _add_loot_disposition (self, 'offspec', "c6","9b","6d", "offspec", nil, nil, true, true, true) _add_loot_disposition (self, 'shard', "a3","35","ee", "shard", "disenchanted", nil, false, false, true) _add_loot_disposition (self, 'gvault', "33","ff","99", _G.GUILD_BANK:lower(), nil, nil, false, false, true) -- fix up the odd(!) standard case of having a nil disposition field norm.arg2 = nil --[[ 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" -- must match X-LoadOn-InterfaceOptions if AddonLoader then AddonLoader:RemoveInterfaceOptions(bliz.name) end bliz:SetScript("OnShow", function(_b) local button = CreateFrame("Button",nil,_b,"UIPanelButtonTemplate") button:SetWidth(150) button:SetHeight(22) button:SetScript("OnClick", function() InterfaceOptionsFrameCancel:Click() HideUIPanel(GameMenuFrame) addon:BuildMainDisplay('opt') end) button:SetText('"/ouroloot options"') button:SetPoint("TOPLEFT",20,-20) _b:SetScript("OnShow",nil) end) InterfaceOptions_AddCategory(bliz) -- Maybe load up g_uniques now? -- Calling the memory-querying APIs while in combat instantly triggers -- the "script ran too long" error, see -- http://www.wowinterface.com/forums/showthread.php?t=44812 if opts.precache_history_uniques and not InCombatLockdown() then self:_cache_history_uniques() end self._cache_history_uniques = nil -- This will be nil if there are no such modules. Used by the GUI -- in _gui_add_disabled_LOD_tabs self._disabled_LOD_modules = self:_scan_LOD_modules() self:_set_chatty_change_chatframe (opts.chatty_on_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 --[[ Module support (aka plugins). Field names with special meanings: - option_defaults: (IN) Standard AceDB-style table. Use a profiles key! - db: (OUT) AceDB object, set during init. - opts: (OUT) Pointer to plugin's "db.profile" subtable. Inherited unchanged: - _add_loot_disposition Inherited module variations: - OnInitialize, OnEnable - register_text_generator, register_tab_control: also flag plugin as a text-generating module in main addon ]] local prototype = {} local textgen_registry, chat_prefixes, chat_codes -- By default, no plugins. First one in sets up code for any after. addon.get_textgen_plugin = flib.nullfunc -- Called as part of NewModule, after embedding and metas are done. -- Mostly this is one-time setup stuff that we don't need until a plugin -- is actually built. function addon:OnModuleCreated (plugin) textgen_registry, chat_prefixes, chat_codes = {}, {}, {} addon.get_textgen_plugin = function(a,t) return textgen_registry[t] end prototype.register_text_generator = function(p,t,...) textgen_registry[t] = p return addon:register_text_generator(t,...) end prototype.register_tab_control = function(p,t,...) textgen_registry[t] = p return addon:register_tab_control(t,...) end function addon:ModulePrefixClick (codenum, mousebutton) local plugin = assert(chat_codes[codenum]) if not plugin:IsEnabled() then return end if mousebutton == 'LeftButton' then if plugin.PrefixLeftClick then plugin:PrefixLeftClick(codenum) else self:BuildMainDisplay() end elseif mousebutton == 'RightButton' then local uniqueval = plugin.name if plugin.PrefixRightClick then uniqueval = plugin:PrefixRightClick(codenum) end self:BuildMainDisplay('opt',uniqueval) end end function addon:OnModuleCreated (plugin) local token, code = self.format_hypertext (plugin.moduleName, LE_ITEM_QUALITY_LEGENDARY, "ModulePrefixClick") chat_prefixes[plugin] = token chat_codes[code] = plugin -- remove the libraries' embedded pointers so that the prototype -- can be inherited plugin.Print = nil end return self:OnModuleCreated(plugin) end function prototype.OnInitialize (plugin) if plugin.option_defaults then plugin.db = addon.db:RegisterNamespace (plugin.moduleName, plugin.option_defaults) plugin.opts = plugin.db.profile --plugin:SetEnabledState(plugin.db.profile.enabled) if that flag is needed later end end --function prototype.OnEnable (plugin) --end function prototype.GetOption (plugin, info) local name = info[#info] return plugin.db.profile[name] end function prototype.SetOption (plugin, info, value) local name = info[#info] plugin.db.profile[name] = value local arg = info.arg if type(arg) == 'function' then plugin[arg](plugin,info) end end -- may eventually just rework the main print routines and inherit all 3 function prototype.Print (plugin, str, ...) local AC = LibStub("AceConsole-3.0") local ps = tostring(chat_prefixes[plugin]) if type(str) == 'string' and str:find("%", nil, --[[plainmatch=]]true) then return AC:Print (ps, str:format(...)) else return AC:Print (ps, str, ...) end end -- For references that would be nil at load time, and I don't feel like -- rearranging the entire file. function addon:MODULE_PROTOTYPE_POINTERS() prototype.add_loot_disposition = _add_loot_disposition self.MODULE_PROTOTYPE_POINTERS = nil end addon:SetDefaultModuleLibraries("AceConsole-3.0") addon:SetDefaultModulePrototype(prototype) 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 tonumber(minrev) > (tonumber(self.revision) or math.huge) then self:Print(err,modname, "revision "..self.revision.." older than minimum "..minrev) return false end if mincomm and tonumber(mincomm) > tonumber(self.commrev) then self:Print(err,modname, "commrev "..self.commrev.." older than minimum "..mincomm) return false end --[[if mindata and tonumber(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 --[[ LCALLBACK Standard ace3-style callback registration and dispatching. All player names are simple (uncolored) strings. The "uniqueID" always refers to the unique tag string stored as 'unique' in loot entries and used as keys in history. Item IDs are always of numeric type. 'Activate', enabled_p, rebroadcast_p, threshold The two boolean predicates are self-explanatory. The threshold is an LE_ITEM_QUALITY_* constant integer. 'Deactivate', raiderdata After all system events have been unregistered. Argument is a holder of the current g_loot.raiders table (in 'raidersnap'). 'Reset' Clicking "Clear Loot", after all data manipulation is finished. 'NewBoss', boss Boss event triggered by a local bossmod (DBM, etc) or a remote OL tracker. Argument is a g_loot table entry of kind=='boss'. 'NewBossEntry', boss New row in primary EOI table of kind=='boss'. Includes all 'NewBoss' occasions, plus manual boss additions, testing, etc. Arg same as NewBoss. 'NewLootEntry', loot, row_index 'DelLootEntry', loot New or removed row in primary EOI table of kind=='loot'. Argument is a g_loot table entry of kind=='loot', and the index into g_loot of where the entry "is" (read: "will be" by the time the event fires). 'NewEOIEntry', entry, row_index 'DelEOIEntry', entry New or removed row in primary EOI table, of any kind. Argument is the g_loot entry, already inserted into or removed from g_loot. Note that boss entries may shift around after this event (if loot has happened and needs to be re-sorted). 'NewHistory', player_name, uniqueID 'DelHistory', player_name, uniqueID New or removed entry in player history. Name argument self-explanatory. ID is the corresponding loot, already inserted into or removed from the history structures. 'Reassign', uniqueID, itemID, loot, from_player_name, to_player_name Loot reassigned from one player to another. Loot represented by the unique & item IDs, and the g_loot entry of kind=='loot'. The player names are self-explanatory. 'MarkAs', uniqueID, itemID, loot, old_disposition, new_disposition The "Mark as <x>" action (as if from the item right-click menu, possibly from a remote tracker) has finished. ID & loot arguments are as in 'Reassign'. The old/new dispositions are those of the g_loot index "disposition" (described at the top of core.lua), with the added possible value of "normal" meaning exactly that. ]] do -- We don't want to trigger plugins or other addons as soon as something -- interesting happens, because a nontrivial amount of work happens "soon" -- after the interesting event: cleanups/fixups, improvs from network, -- etc. So firing a callback is delayed ever so briefly by human scales. -- -- For data safety, we replace any table arguments with read-only proxies -- before passing them to the callbacks. It's not supposed to be securing -- the data against tampering; the goal is to prevent accidents, not fraud. local unpack, setmetatable = unpack, setmetatable local mtnewindex = function() --[[local]]error("This table is read-only", 3) end local function make_readonly (t) return setmetatable({}, { __newindex = mtnewindex, __index = t, __metatable = false, __tostring = getmetatable(t) and getmetatable(t).__tostring, }) end local queue = create_new_cache ('callbacks', 1.2, function (allcbs) for _,c in ipairs(allcbs) do addon.callbacks:Fire (unpack(c)) flib.del(c) end end) function addon:Fire (...) self.dprint('callback', ...) local capture = flib.new(...) for k,v in ipairs(capture) do if type(v) == 'table' then capture[k] = make_readonly(v) end end queue:add(capture) 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.OuroLootOptsDB = nil _G.OuroLootSV_hist = nil _G.OuroLootSV_log = nil ReloadUI() end function addon:PLAYER_LOGOUT() -- Can these still fire at the very last instant? --self:UnregisterEvent("GROUP_ROSTER_UPDATE") --self:UnregisterEvent("PLAYER_ENTERING_WORLD") self.LOGGING_OUT = true -- kludge this instead local worth_saving = #g_loot > 0 or next(g_loot.raiders) if not worth_saving then for text in self:registered_textgen_iter() do worth_saving = worth_saving or g_loot.printed[text] > 0 end end if worth_saving then opts.autoshard = self.sharder opts.threshold = self.threshold _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 = 'online', 'offline', 'no_longer' local was_in_raid local lastevent, now = 0, 0 local redo_count = 0 local redo, timer_handle, my_realm 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 -- XXX somewhere in here, we could fire a useful callback event if redo then redo_count = redo_count + 1 end redo = false for i = 1, GetNumGroupMembers() do local unit = 'raid'..i -- We grab a bunch of return values here, but only pay attention to -- them under specific circumstances. Try to use as many of these -- values as possible rather than multiple Unit* calls. local fname, connected, subgroup, level, class, _ fname, _, subgroup, level, _, class, connected = GetRaidRosterInfo(i) -- No, that's not my typo, it really is "uknownbeing" in Blizzard's code. if fname and fname ~= UNKNOWN and fname ~= UNKNOWNOBJECT and fname ~= UKNOWNBEING then local name,realm = fname:match("(%S+)-(%S+)") if realm and realm == my_realm then -- shouldn't happen realm = nil end if not name then assert(realm == nil) name = fname end 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.fname = fname 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) r.realm = realm 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("GROUP_ROSTER_UPDATE", 60) end else redo_count = 0 if timer_handle then self:CancelTimer(timer_handle) timer_handle = nil end end end function addon:GROUP_ROSTER_UPDATE (event) if self.LOGGING_OUT then return end if not IsInRaid() then if was_in_raid then -- Leaving a raid group. self.dprint('flow', "no longer in raid group") was_in_raid = false if self.enabled and not self.debug.notraid then self.dprint('flow', "enabled, leaving raid") self.popped = nil self:Deactivate() self:CheckRoster(--[[leaving raid]]true) end _LFR_suppressing = nil end -- Flow for 5-player groups goes right to here. 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 == "GROUP_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",nil,nil,self) return end elseif event == "Activate" then -- dispatched from Activate if opts.history_suppress_LFR and GetLFGMode(LE_LFG_CATEGORY_RF) then _LFR_suppressing = true end my_realm = self.history.realm _register_bossmod(self) self:RegisterEvent("CHAT_MSG_LOOT") was_in_raid = true docheck = true 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 field is set on various boss interactions, so we've got a -- kill/wipe entry already. if addon.latest_instance then return end 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 = 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) local origin = candidates[oldersig] and candidates[oldersig].bcast_from 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 addon:_test_disposition (loot.disposition, 'affects_history') and not _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 GetItemInfo, GetItemIcon, UnitClass = GetItemInfo, GetItemIcon, UnitClass -- 'item' can be any of things passable to GetItemInfo, so we make no -- assumptions but rather base all information off of returns from -- GetItemInfo, including re-extracting the itemstring -- (in the case of local messages, 'item' is an itemstring) -- 'from' only present if this is triggered by a broadcast function _do_loot (self, local_override, recipient, unique, item, count, from, extratext) if unique == 0 then unique = nil end local prefix = "_do_loot[" .. counter() .. "]" --[[ iname: Hearthstone iquality: integer ilink: clickable formatted link itemstring: item:6948:.... ]] local cache_miss, itemid local iname, ilink, iquality, -- ilvl, minlvl, type, subtype, stacksize, equiploc, texture, sellprice _, _, _, _, _, _, itexture = GetItemInfo(item) if not itexture then -- Fun fact, GetItemIcon behaves oddly on a nil argument, and may -- or may not always work on a non-numerical-itemID argument itemid = tonumber(item) if (itemid) then itexture = GetItemIcon(itemid) end end if (not iname) or (not itexture) then cache_miss = true iname, ilink, iquality, itexture = UNKNOWN..': '..item, 'item:6948', LE_ITEM_QUALITY_COMMON, [[Interface\ICONS\INV_Misc_QuestionMark]] end self.dprint('loot',">>"..prefix, "R:", recipient, "U:", unique, "I:", item, "C:", count, "frm:", from, "ex:", extratext, "q:", iquality, "tex:", itexture) -- Get a known-good string and integer out of the returned link. local itemstring = ilink:match("item[%-?%d:]+") itemid = tonumber(itemstring: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 (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, itemstring, count, extratext) 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 sigmap[oldersig] = 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 seenit = recent_loot:test(signature) end if seenit then self.dprint('loot', "remote", prefix, "<", signature, "> already in cache, skipping from", from) break end -- Most of the time this will already be in place and filled out, -- but there are situations where not. Even if we can't get data -- back, at least avoid errors (or the need for existence checks). if not g_loot.raiders[recipient] then g_loot.raiders[recipient] = { needinfo=true } end -- There is some redundancy in all this, in the interests of ease-of-coding i = { kind = 'loot', person = recipient, person_class= g_loot.raiders[recipient].class, person_realm= g_loot.raiders[recipient].realm, cache_miss = cache_miss, quality = iquality, itemname = iname, id = itemid, itemstring = itemstring, itemlink = ilink, -- determines the GUI tooltip itexture = itexture, unique = replacement or unique, count = (count and count ~= "") and count or nil, bcast_from = from, extratext = extratext, variant = self:is_variant_item(itemstring), } 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 then for disp,text in self:_iter_dispositions('from_notes_text') do if text == i.extratext then i.disposition = disp break end end end local looti = self._addLootEntry(i) if self:_test_disposition (i.disposition, 'affects_history') and not _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('loot', prefix, "<", signature, "> added to cache as candidate", #candidates) end break end self.dprint('loot',"<<"..prefix, "out") return ret1, ret2 -- FIXME mostly garbage end local match = string.match _grok = function (msg, bonus_p, item_p) local person, itemlink, count, extra while true do extra = "loot" -- test in most likely order: other people get more loot than you do person, itemlink, count = match (msg, LOOT_MULTIPLE_ssd); if person then break end person, itemlink = match (msg, LOOT_ss); if person then break end -- Could only do this if in an LFR... but the restriction -- might apply elsewhere soon enough. person, itemlink = match (msg, LOOT_WHILE_PLAYER_INELIGIBLE_ss); if person then break end -- other people get more bonus loot than you do, too if bonus_p then extra = "bonus" person, itemlink, count = match (msg, BONUS_MULTIPLE_ssd); if person then break end person, itemlink = match (msg, BONUS_ss); if person then break end end -- i can haz cheezburgr? extra = "loot" itemlink, count = match (msg, LOOT_SELF_MULTIPLE_sd); if itemlink then break end itemlink = match (msg, LOOT_SELF_s); if itemlink then break end -- getting stuff out of the mailbox, the salvage boxes, etc if item_p then extra = "item" person, itemlink, count = match (msg, ITEM_MULTIPLE_ssd); if person then break end person, itemlink = match (msg, ITEM_ss); if person then break end itemlink, count = match (msg, ITEM_SELF_MULTIPLE_sd); if itemlink then break end itemlink = match (msg, ITEM_SELF_s); if itemlink then break end end break end return person, itemlink, count, extra 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 if event == "CHAT_MSG_LOOT" then local msg = ... --ChatFrame2:AddMessage("original string: >"..(msg:gsub("\124","\124\124")).."<") self.dprint('loot', "CHAT_MSG_LOOT, message is >"..(msg:gsub("\124","\124\124")).."<") local person, itemlink, count, extra = _grok (msg, opts.track_bonusrolls, opts.track_nonloot) if not itemlink then return end -- "PlayerX selected Greed", etc, not looting self.dprint('loot', "CHAT_MSG_LOOT, person is", person, ", itemlink is", itemlink, ", count is", count, "extra is", extra) extra = (extra == "bonus") and [[Bonus roll!]] or ((extra == "item") and [[Non-loot]] or nil) or nil -- Name might be colorized, remove the highlighting if person then person = match(person,"|c%x%x%x%x%x%x%x%x(%S+)") or person else person = my_name -- UNIT_YOU / You end return _do_loot (self, --[[override=]]false, person, --[[unique=]]nil, match(itemlink,"item[%-?%d:]+"), count, --[[from=]]nil, extra) elseif event == "broadcast" then return _do_loot (self, --[[override=]]false, ...) elseif event == "manual" then local recip,item,text = ... return _do_loot (self, --[[override=]]true, recip, --[[unique=]]nil, item, --[[count=]]nil, --[[from=]]nil, text) end end end -- This only triggers on entering combat after a registered boss kill. -- Clearing this field forces subsequent trash kills to generate an entry -- via maybe_trash_kill_entry. -- (Possibly what is wanted is to start a 3 or 5 minute timer, and *then* -- look for the next combat?) function addon:PLAYER_REGEN_DISABLED() self:UnregisterEvent ("PLAYER_REGEN_DISABLED") self.latest_instance = nil 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]], LE_ITEM_QUALITY_UNCOMMON, 'help') function addon:OnSlash (txt) --, editbox) txt = strtrim(txt:lower()) local cmd, arg = "" do local s,e = txt:find("^%w+") 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("cmdon",arg) elseif cmd == "off" then self:Deactivate() elseif cmd == "broadcast" or cmd == "bcast" then self:Activate("cmdbcast",nil,true) elseif cmd == "on5" then self.debug.notraid = true self:Activate("cmdon5",arg) 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) self:CHAT_MSG_LOOT ('manual', my_name, 'item:109948:0:0:0:0:0:0:0:100:0:2:2:499:524') 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.is_guilded = IsInGuild() self.debug[arg] = not self.debug[arg] 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:find("^ver") then self:Print(self.status_text) elseif cmd == "help" then self:BuildMainDisplay('help') elseif cmd == "ping" then self:DoPing() elseif cmd == "fix" then if arg == "?" then self:Print[['/ouroloot fix cache' updates loot that wasn't in the cache]] self:Print[['/ouroloot fix history' repairs inconsistent data on the History tab]] self:Print[['/ouroloot fix' changes no stored data, only allows the window to be displayed again (this is built into all fixes above)]] return elseif arg == "cache" then self:do_item_cache_fixup(--[[force_silent=]]false) elseif arg == "history" then self:repair_history_integrity() end self.NOLOAD = nil self:Print("Window unlocked, best of luck.") else if self:OpenMainDisplayToTab(cmd,arg) 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 -- Both of these need to be (effectively) idempotent. function addon:Activate (whence, opt_threshold, opt_bcast_only) self.dprint('flow', ":Activate called from", whence) self:RegisterEvent("GROUP_ROSTER_UPDATE") self:RegisterEvent("PLAYER_ENTERING_WORLD", function() self:ScheduleTimer("GROUP_ROSTER_UPDATE", 5, "PLAYER_ENTERING_WORLD") end) self.popped = true if self.DO_ITEMID_FIX then self.DO_ITEMID_FIX = nil self:do_item_cache_fixup(--[[force_silent=]]not self.author_debug) self.loot_clean = nil end if IsInRaid() then self.dprint('flow', ">:Activate calling RRU") self:GROUP_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 is 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.") if #g_loot > 200 then self:Print("|cffff0505Crikey!|r You are carrying around a lot of", "stored loot data. You should seriously consider clearing it", "out, as some of the text generation routines can choke the", "game client if they run for too long.") end 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:Fire ('Activate', self.enabled, self.rebroadcast, self.threshold) self:Print("Now %s; threshold currently %s.", self.enabled and "tracking" or "only broadcasting", self.thresholds[self.threshold]) self:broadcast('revcheck',version_large) self.dprint('flow', ":Activate from", whence, "finished") end -- Note: running '/ouroloot 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("GROUP_ROSTER_UPDATE") self:UnregisterEvent("PLAYER_ENTERING_WORLD") self:UnregisterEvent("CHAT_MSG_LOOT") _LFR_suppressing = nil -- Passing .raiders directly doesn't work with a proxy (again, WTB Lua -- 5.2 and its __pairs iterators). Give it the same structure as a boss -- entry instead. self:Fire ('Deactivate', { raidersnap = g_loot.raiders }) self:Print("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("GUI state").eoiST if not st then self.dprint('flow', "Clear: display visible but eoiST not set??") end self.display:Hide() end g_restore_p = nil OuroLootSV = nil self:_reset_timestamps() if verbose_p then if (OuroLootSV_saved and #OuroLootSV_saved>0) then self:Print("Current loot data cleared, %d saved sets remaining.", #OuroLootSV_saved) else self:Print("Current loot data cleared.") end end _init(self,st) self:Fire ('Reset') 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 = 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() local disabled for i = 1, GetNumAddOns() do if GetAddOnMetadata (i, "X-OuroLoot-Plugin") and IsAddOnLoadOnDemand(i) and not IsAddOnLoaded(i) then -- 'loadflag' will be false for LOD addons local folder, title, notes, loadflag, reason = GetAddOnInfo(i) local enabled = GetAddOnEnableState (my_name, i) > 0 local tabtitle = GetAddOnMetadata (i, "X-OuroLoot-Plugin") local decision = 0 self.dprint('flow', "scanning", folder, "loadflag is", loadflag, "enabled is", enabled, "reason-why-not is", reason) if enabled or opts.display_disabled_LODs then self:_gui_add_LOD_tab (tabtitle, folder, i, enabled, reason) decision = 1 elseif (not enabled) and reason == 'DISABLED' then disabled = disabled or flib.new() local t = flib.new() t.tabtitle = tabtitle t.folder = folder t.addon_index = i disabled[#disabled+1] = t decision = 2 elseif opts.display_unusable_LODs then self:_gui_add_LOD_tab (tabtitle, folder, i, enabled, reason) decision = 3 end self.dprint('flow', "...decision was", decision) end end return disabled end -- Routines for printing changes made by remote users. do local change_chatframe function addon:_set_chatty_change_chatframe (arg, silent_p) local frame if type(arg) == 'number' then arg = 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 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 local function _notify (chatframe, source, index, olddisp, from_whom, from_class) local e = g_loot[index] if not e then -- how did this happen? return end if source == my_name then source = _G.UNIT_YOU end local from_text, to_text if from_whom then if from_whom == my_name then from_whom = _G.UNIT_YOU end from_text = addon:colorize (from_whom, from_class) to_text = e.person == my_name and _G.UNIT_YOU or e.person to_text = addon:colorize (to_text, e.person_class) else if olddisp then from_text = addon.disposition_colors[olddisp].text else olddisp = "normal" from_text = "normal" end from_text = addon.disposition_colors[olddisp].hex .. from_text .. "|r" if e.disposition then to_text = addon.disposition_colors[e.disposition].text else to_text = "normal" end to_text = addon.disposition_colors[e.disposition].hex .. to_text .. "|r" end addon.dprint ('loot', "notification:", source, index, e.itemlink, from_text, to_text) local jumpprefix = addon.chatprefix ("GoToLootLine", index) addon:PCFPrint (chatframe, jumpprefix, remote_chatty, source, index, e.itemlink, from_text, to_text) end function _notify_about_change (sender, index, olddisp, from_whom, from_class) _notify (change_chatframe, sender, index, olddisp, from_whom, from_class) end end -- Adds indices to traverse the tables in a nice sorted order. do local byindex, temp = {}, {} local function sort (src, dest) for k in pairs(src) do temp[#temp+1] = k end table.sort(temp) 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',version_large) end function addon:_check_version (otherrev) self.dprint('comm', version_large, "revchecking against", otherrev) otherrev = tonumber(otherrev) if otherrev == version_large then -- normal case elseif otherrev < version_large then self.dprint('comm', "ours is newer, notifying") self:broadcast('revcheck',version_large) else self.dprint('comm', "ours is older, (possibly) yammering") if newer_warning then local pop = addon.format_hypertext ([[click here]], LE_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]], LE_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 = 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 -- Iterate through g_loot.raiders in sorted (alphabetical/collated) order. If -- USE_FULLNAME then the realmname is appended. If ONLINE_FILTER is present, -- then only raider entries with a matching 'online' key are included. Loop -- variables are a running count, the raider name, and the corresponding entry -- from g_loot.raiders. do local function nextr (list, index) index = index + 1 local name = list[index] if not name then flib.del(list) return nil end return index, name, list.__safety[name] end function addon:sorted_raiders_iter (use_fullname_p, opt_online_filter) local t = flib.new() for name,info in next, g_loot.raiders do if (not opt_online_filter) or (info.online == opt_online_filter) then -- this is not exactly "A?B:C" semantics, but it is exactly -- the behavior we want when fname is not present tinsert (t, use_fullname_p and info.fname or name) end end table.sort(t) t.__safety = g_loot.raiders return nextr, t, 0 end 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 ]] local bcodes = { [448] = "Warforged", -- and 499? [449] = "Heroic", -- 524? [450] = "Mythic", [451] = "LFR", [15] = "Epic", [171] = "Rare", } function addon:is_variant_item(istr) --flib.safeiprint(strsplit (":", istr)) --flib.safeiprint(select (13, strsplit (":", istr)) ) local bonuses = { select (13, strsplit (":", istr)) } if #bonuses < 1 then return -- nil for no bonus end local n = tonumber(bonuses[1]) or 0 if n < 1 then return -- nil for no bonus end --@debug@ print("bonuses has", #bonuses, "entries, size entry is", bonuses[1], "for", istr) table.remove (bonuses, 1) assert (n == #bonuses, "test for variant item on " .. istr .. " extracted count " .. (n or "nil?") .. " but table size is " .. #bonuses) for i = 1, #bonuses do local b = tonumber(bonuses[i]) print(" bonus ", b, bcodes[b]) bonuses[i] = b end --@end-debug@ return 42 -- will figure out meaningful value later end end -- Called at the end of OnInit, 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") -- paranoia: make sure the GUI isn't stumbling over these later local dofix, GetItemInfo = false, GetItemInfo for i,e in self:filtered_loot_iter('loot') do local missing_data = not GetItemInfo(e.id) e.cache_miss = (e.cache_miss or missing_data) or nil dofix = dofix or e.cache_miss end self.DO_ITEMID_FIX = dofix or nil _do_loot_metas() -- FIXME printed could be too large if entries were deleted, how much do we care? self.sharder = opts.autoshard if self.popped then -- Actual loot got restored, potentially re-activate. self:ScheduleTimer("Activate", 12, "_init-restore", opts.threshold) end else g_loot = {} end if type(g_loot.raiders) ~= 'table' then g_loot.raiders = {} end if type(g_loot.printed) ~= 'table' then g_loot.printed = {} end self.threshold = opts.threshold or self.threshold -- in the case of restoring but not tracking local g_loot_wrapper = self:gui_init (true, g_loot, g_uniques) 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_wrapper) end -- Make sure we have a current .raiders array, since other dropdowns and -- whatnot depend on that information. self:CheckRoster() self.status_text = ("%s(r%s) communicating as ident %s commrev %s"): format (self.version, self.revision, self.ident, self.commrev) self:RegisterComm(self.ident) self:RegisterComm(self.identTg, "OnCommReceivedNocache") 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 next, ss do if info.online == 3 then ss[name] = nil end end end return ss, maxsize, instance, 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:Fire ('NewBoss', boss) 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:RegisterEvent ("PLAYER_REGEN_DISABLED") addon:_mark_boss_kill (bossi) if opts.chatty_on_kill then local jumpprefix = addon.chatprefix ("GoToLootLine", bossi) addon:PCFPrint(_G.DEFAULT_CHAT_FRAME, jumpprefix, "Registered kill for '%s' in %s!", boss.bossname, boss.instance) end end wipe(candidates) end -- Ten seconds is a long time, but occasionally DBM takes for-EVAH to -- decide that a fight is over. 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(g_gui.g_dloot:remove(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 date, time, rawget, setmetatable = date, time, rawget, setmetatable --@debug@ local tos = {} tos.time = function (e) return e.startday.text end tos.boss = function (e) return e.bossname .. '/' .. e.reason end tos.loot = function (e) return e.itemname .. '/' .. e.person .. '/' .. e.unique .. '/' .. tostring(e.disposition) .. (e.extratext and ('/'..e.extratext) or '') end --@end-debug@ local loot_entry_mt = { __index = function (e,key) -- This shouldn't be required, as the refresh should be picking -- it up already. Sigh. if key == 'cols' then pprint('mt', e.kind, "key is", key) addon:_fill_out_eoi_data(1) end return rawget(e,key) end, --@debug@ __tostring = function (e) local k = e.kind if k then return ("<%s/%s>"):format(k, tos[k] and tos[k](e) or "?") end return "<unknown loot entry type>" end, --@end-debug@ } function _do_loot_metas() for i,e in ipairs(g_loot) do setmetatable(e,loot_entry_mt) end _do_loot_metas = nil 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]) -- FORMAT_STRING may contain $x (x in TmdHMS) tokens, with the same -- meanings as in Lua/strftime but restricted formatting: -- Y year, 4 digit -- m month, 2 digit -- d day, 2 digit -- H hour, 2 digit, 24-hour clock -- M minute -- S second -- 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 clock values printed; if absent, those values are -- taken from the DAY entry. 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 = ("%.4d"):format (day_entry.startday.year) format_timestamp_values.m = ("%.2d"):format (day_entry.startday.month) format_timestamp_values.d = ("%.2d"):format (day_entry.startday.day) format_timestamp_values.H = ("%.2d"):format ((time_entry_opt or day_entry).hour) format_timestamp_values.M = ("%.2d"):format ((time_entry_opt or day_entry).minute) format_timestamp_values.S = date ("%S", (time_entry_opt or day_entry).stamp) return fmt_opt:gsub ('%$([YmdHMS])', 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. -- More precisely, anything new on the EOI tab hits this; it does not -- necessarily need to be a looted item. function addon._addLootEntry (e) setmetatable(e,loot_entry_mt) if not done_todays_date then do_todays_date() end -- All kinds of things go awry (especially history preening) if two -- entries share the exact same timestamp, and we can't get any better -- than one second resolution. -- -- Well, the only API in-game with finer precision is GetTime but the -- computer uptime doesn't help us. Stripping the fractional part off -- and gluing it to time_t would be too risky as they don't cycle at -- the same time. We could do something involving a count incrementing -- with the refresh rate, but... blech. -- -- So we sort of cheat. New entries are pushed into the future one -- second at a time, if needed, so that no two time_t's are equal. local h, m = GetGameTime() local time_t = time() local index = #g_loot -- note, previous entry e.hour = h e.minute = m if index > 0 and g_loot[index].stamp >= time_t then time_t = g_loot[index].stamp + 1 addon.dprint('flow', "bumping timestamp to", time_t) end e.stamp = time_t index = index + 1 if e.kind == 'loot' then if (not e.unique) or (#e.unique==0) then e.unique = e.id .. (e.disposition or e.person) .. date (timestamp_fmt_unique, e.stamp) end addon:Fire ('NewLootEntry', e, index) end g_loot[index] = e g_gui.g_dloot[index] = nil addon:Fire ('NewEOIEntry', e, index) return index end -- Safety/convenience wrapper only. 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 addon:Fire ('NewBossEntry', e, ret) 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) if is == should_be then 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 return end end assert(g_loot[is].kind == 'boss') local boss = g_gui.g_dloot:remove(is) g_gui.g_dloot:insert (should_be, boss) return should_be end end -- Disposition control; more precisely, how various dispositions affect flow -- and displays elsewhere. do local default = { text = "", hex = "|cffffffff", r = 1.0, g = 1.0, b = 1.0, a = 1, can_reassign = true, affects_history = true, from_notes_text = false, } local mt = { __index = function (t,k) -- don't point index directly at default, need to catch nils return default end, } local alldisps = setmetatable({}, mt) addon.disposition_colors = setmetatable({}, mt) -- These two are clunky wrappers, probably rework this once gain some data. function addon:_test_disposition (code, attrib) return alldisps[code][attrib] end function addon:_iter_dispositions (attrib) local r = {} for code,disp in next, alldisps do if disp[attrib] then r[code] = disp.text end end return next, r end function _add_loot_disposition (self, code, rhex, ghex, bhex, text_notes, text_menu, tooltip_txt, can_reassign_p, do_history_p, from_notes_text_p ) assert(type(code)=='string' and #code>0) assert(type(text_notes)=='string') assert(type(rhex)=='string') assert(type(bhex)=='string') assert(type(ghex)=='string') addon.disposition_colors[code] = { text = text_notes, -- for chat output by core code hex = "|cff" .. rhex .. ghex .. bhex, -- for lib-st r = tonumber(rhex,16)/255, g = tonumber(ghex,16)/255, b = tonumber(bhex,16)/255, a = 1, } -- not not = poor man's "toboolean", don't potentially hold arg -- objects from garbage collection alldisps[code] = { text = text_notes, can_reassign = not not can_reassign_p, affects_history = not not do_history_p, from_notes_text = not not from_notes_text_p, } local dd = g_gui.add_dropdown_entry ('eoi_loot_mark', text_menu or text_notes, --[[function_table=]]nil, 'df_DISPOSITION', code, tooltip_txt) dd.colorCode = addon.disposition_colors[code].hex addon.dprint('flow', ("Source '%s' adds loot disposition '%s', flags"): format(self.name or tostring(self), code), can_reassign_p, do_history_p, from_notes_text_p) return dd 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 (silent_p) if not silent_p then self:Print("Fixing up missing item cache data...") end local numfound = 0 local earliest_fixed 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) or e.id 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.itemstring = ilink:match("item[%-?%d:]+") e.id = tonumber(e.itemstring:match("^item:(%d+)") or 0) 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 earliest_fixed = earliest_fixed or i if not silent_p then self:Print(msg, i, ilink) end end break end end if earliest_fixed then self.loot_clean = earliest_fixed-1 -- this works out even at i == 1 end if not silent_p then self:Print("...finished. Found %d |4entry:entries; with weird data.", numfound) end 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() 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 guid", winner[1], "with tag", 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 >winning_index == 1 >g_loot and history already has the replacement value B: winner was generated remotely >need to scan and replace Detecting A is strictly an optimization. We should be able to do this code safely in all cases. Important to note: a local winner will always be at index 1, but a winner at index 1 does not necessarily mean it was locally generated (e.g., if the local itemfilter drops it but a remote player does an improv). Just do the general case until/unless this becomes a problem. ]] local cache = g_uniques:SEARCH(exist) local looti,hi,ui = cache.loot, cache.history, cache.history_may -- Active loot if looti and g_loot[looti].unique == exist then pprint('improv', "found and replaced loot entry", looti) g_loot[looti].unique = winner[2] else -- If sharded, filtered, or the improv was done by the local -- player, then the "previous" unique would not have made it -- into the tables to begin with. So don't flag an error. pprint('improv', "No active loot found", looti, looti and g_loot[looti].unique, winning_index) end -- History if hi ~= g_uniques.NOTFOUND then hi = addon.history.byname[hi] 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) assert(exist ~= winner[2]) 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 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(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 addon:horrible_horrible_error (err_msg) if self.display then local d = self.display if d then -- Take this down a piece at a time, on the assumption that -- the main window won't be able to do so. local gui = d:GetUserData("GUI state") local eoist = gui.eoiST if eoist then eoist:Hide() end local histst = gui.histST if histst then histst:Hide() end d:Hide() end end self.NOLOAD = err_msg -- This should happen so rarely that it's not worth moving into gui.lua if not StaticPopupDialogs["OUROL_ARGH"] then StaticPopupDialogs["OUROL_ARGH"] = flib.StaticPopup{ button1 = OKAY, } clicky = addon.format_hypertext( [[ SYSTEM FAILURE -- RELEASE RINZLER ]], "|cffff0000", function() StaticPopup_Show "OUROL_ARGH" end) end StaticPopupDialogs["OUROL_ARGH"].text = horrible_error_text:format(err_msg) PlaySoundFile ([[Interface\AddOns\Ouro_Loot\sfrr.ogg]], "Master") addon:Print (" ") addon:Print (" ", clicky) addon:Print (" ") end function _unavoidable_collision (err) addon:horrible_horrible_error (err) -- we don't actually need to kill the GUI in this case addon.NOLOAD = nil end end ------ Saved texts function addon:check_saved_table(silent_p) local s = OuroLootSV_saved if s and (#s > 0) then return s end OuroLootSV_saved = nil if not silent_p then self:Print("There are no saved loot texts.") end end function addon:save_list() local s = self:check_saved_table(); if not s then return end for i,t in ipairs(s) do self:Print("#%d %s %d entries %s", i, t.date, t.count, t.name) end end function addon:save_saveas(name) OuroLootSV_saved = OuroLootSV_saved or {} local SV = OuroLootSV_saved local n = #SV + 1 local save = { name = name, date = makedate(), count = #g_loot, } for text in self:registered_textgen_iter() do save[text] = g_loot[text] end self:Print("Saving current loot texts to #%d '%s'", n, name) SV[n] = save return self:save_list() end function addon:save_restore(num) local s = self:check_saved_table(); if not s then return end if (not num) or (num > #s) then return self:Print("Saved text number must be 1 - "..#s) end local save = s[num] self:Print("Overwriting current loot data with saved text #%d '%s'",num,save.name) self:Clear(--[[verbose_p=]]false) -- Clear will already have displayed the window, and re-selected the first -- tab. Set these up for when the text tabs are clicked. for text in self:registered_textgen_iter() do g_loot[text] = save[text] end end function addon:save_delete(num) local s = self:check_saved_table(); if not s then return end if (not num) or (num > #s) then return self:Print("Saved text number must be 1 - "..#s) end self:Print("Deleting saved text #"..num) tremove(s,num) return self:save_list() end ------ Loot histories -- history_all = { -- ["Kilrogg"] = { -- ["realm"] = "Kilrogg", -- not saved -- ["st"] = { lib-st display table }, -- not saved -- ["byname"] = { -- not saved -- ["OtherPlayer"] = 2, -- ["Farmbuyer"] = 1, -- } -- [1] = { -- ["name"] = "Farmbuyer", -- ["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 history" }, -- ["id"] = { ["tag"] = 11111 }, -- ["count"] = { ["tag"] = "x3", .... }, -- }, -- [2] = { -- ["name"] = "OtherPlayer", -- ...... -- }, ...... -- }, -- ["OtherRealm"] = ...... -- } -- -- Up through 2.18.4 (specifically through rev 95), an individual player's -- table looked like this: -- ["name"] = "Farmbuyer", -- -- most recent loot: -- [1] = { id = nnnnn, when = "formatted timestamp for displaying" } -- -- previous loot: -- [2] = { ......., [count = "x3"] } -- which was much easier to manipulate, but had a ton of memory overhead. do local new, del, date = flib.new, flib.del, date -- 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 = {}, new(), new() for u,tstamp in pairs(p.when) do -- XXX multiple identical tstamps uniques_bywhen[tstamp] = u when_array[#when_array+1] = tstamp end 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 del(uniques_bywhen) del(when_array) end function addon:repair_history_integrity() local rcount, pcount, hcount, errors = 0, 0, 0, 0 local empties_to_delete = {} for rname,realm in pairs(self.history_all) do for pk,player in ipairs(realm) do local id, when, unique, count = {}, {}, {}, {} for i,h in ipairs(player.unique) do h = tostring(h) if player.when[h] and player.id[h] then unique[#unique+1] = h id[h] = player.id[h] when[h] = player.when[h] count[h] = player.count[h] else self:Print("> Realm %s, player %s, entry %d: tag <%s>, id <%s>, time <%s>, count <%s>", rname, player.name, i, h, tostring(player.id[h]), tostring(player.when[h]), tostring(player.count[h])) errors = errors + 1 end hcount = hcount + 1 end player.id, player.when, player.unique, player.count = id, when, unique, count player.person_class = player.person_class or (g_loot.raiders[player.name] and g_loot.raiders[player.name].class) if #player.unique > 1 then sort_player(player) elseif #player.unique == 0 then tinsert (empties_to_delete, 1, pk) end pcount = pcount + 1 end if #empties_to_delete > 0 then for _,pk in ipairs(empties_to_delete) do local player = tremove (realm, pk) self:Print("> Realm %s, player %s, is empty", rname, player.name) errors = errors + 1 end wipe(empties_to_delete) end if #realm == 0 then self.history_all[rname] = nil self:Print("> Realm %s is empty", rname) errors = errors + 1 end rcount = rcount + 1 end self:_build_history_names() if errors > 0 then self:Print("The listed entries have been erased from history.") end self:Print("Finished examining %d realms, %d players, %d history entries.", rcount, pcount, hcount) 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 = player.name, 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 = rawget(g_uniques,e.unique) if hmmm then hmmm.loot = i elseif not self:_test_disposition (e.disposition, 'affects_history') then g_uniques[e.unique] = { loot = i, history = g_uniques.NOTFOUND } count = count + 1 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 } end else -- The usual cause: when only source is from an older client -- and the disposition did not trigger addhistory, then not -- even a stub history entry happens. Code has now been added -- to try harder to prevent this, but it's still best to not -- simple ignore it. trouble = true pprint('loot', "ERROR precache loop found missing/outdated unique tag!", i, "tag <"..tostring(e.unique).."> from?", tostring(e.bcast_from)) end end UpdateAddOnMemoryUsage() local after = GetAddOnMemoryUsage(nametag) -- This displayed number is occasionally negative, if the GC happens -- to run during the loops. How much should we care? 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. If you keep seeing this message, type", "'/ouroloot fix ?' and try some of those actions.") end g_uniques:SETMODE('probe') 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 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 the history for that -- player, making it the "most recent" entry regardless of actual data. -- In the usual case, builds a formatted timestamp string from g_today and -- the loot entry's recorded time (thus the formatted string really *will* -- be the most recent entry). If g_today has not been set, then falls -- back on formatting LOOTINDEX's time_t 'stamp' field. -- -- If RESORT_P is true-valued, then re-sorts the player's history based on -- formatted timestmps, instead of leaving the new entry as the latest. local tfmt_hist = timestamp_fmt_history:gsub('%%','$') function addon:_addHistoryEntry (lootindex, resort_p) local e = g_loot[lootindex] if e.kind ~= 'loot' then return end if e.person_realm and opts.history_ignore_xrealm then return end local i,h = self:get_loot_history(e.person) -- If we've added anything at all into g_loot this session, g_today -- will be set. If we've logged on simply to manipulate history, then -- try and fake a timestamp (it'll be "close enough"). -- (WoD: This logic may be backwards now that loot entries track time_t.) local when = g_today and self:format_timestamp (tfmt_hist, g_today, e) or date (timestamp_fmt_history, e.stamp) assert(h.name==e.person) -- Should rarely happen anymore: if (not e.unique) or (#e.unique==0) then e.unique = e.id .. e.person .. 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 if resort_p then sort_player(h) end g_uniques[U] = { loot = lootindex, history = e.person } self:Fire ('NewHistory', e.person, U) 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 = addon.history.byname[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, with as many details as possible." end return nil, errtxt end -- Handles reassigning loot between players. Arguments depend on who's -- calling it: -- "local", row_index, new_recipient -- "remote", sender, unique_id, item_id, old_recipient, new_recipient -- 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; returns the affected loot index on success. function addon:reassign_loot (how, ...) -- This must all be filled out in all cases: local e, index, from_name, to_name, unique, id -- Only set in remote case: local sender if how == "local" then -- GUI doesn't allow reassignment unless the item is not-shard, -- so we can assume the presence of a unique tag in this function. index, to_name = ... assert(type(to_name)=='string' and to_name:len()>0) index = assert(tonumber(index)) e = g_loot[index] id = e.id unique = assert(e.unique) from_name = e.person elseif how == "remote" then sender, unique, id, from_name, to_name = ... id = tonumber(id) local cache local loop = 0 repeat -- wtb continue statement pst if loop > 1 then break end e = nil cache = cache and g_uniques:SEARCH(unique) or g_uniques[unique] index = tonumber(cache.loot) if index then e = g_loot[index] else end loop = loop + 1 until e and (e.id == id) else return -- silently ignore future cases from future clients end if self.debug.loot then local m = ("Reassign index %d (pre-unique %s) with id %d from '%s' to '%s'."): format(index, unique, id, tostring(from_name), tostring(to_name)) self.dprint('loot', m) if sender == my_name then self.dprint('loot',"(Returning early from debug mode's double self-reassign.)") return index end end if not e then -- say something? return end local from_i, from_h, hist_i = _history_by_loot_id (e, "reassign") local to_i,to_h = self:get_loot_history(to_name) if not from_i then if how == "local" 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) end else -- XXX do some sanity checks here? from_name == from_h.name, etc? -- If something were wrong at this point, what could we do? -- the Book of Job 1:21: "Naked I came from my faction capital -- city, and naked I shall return thither." if from_h ~= to_h then 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 end -- "Blessed be the lookup cache of the loot master." g_uniques[unique] = { loot = index, history = to_name } end local from_person_class = e.person_class or from_h.person_class or (g_loot.raiders[from_name] and g_loot.raiders[from_name].class) or select(2,UnitClass(from_name)) e.person = to_name e.person_class = to_h.person_class or (g_loot.raiders[to_name] and g_loot.raiders[to_name].class) or select(2,UnitClass(to_name)) self.hist_clean = nil self.loot_clean = nil if how == "local" then if opts.chatty_on_local_changes then _notify_about_change (_G.UNIT_YOU, index, nil, from_name, from_person_class) end self:vbroadcast('reassign', unique, id, from_name, to_name) elseif opts.chatty_on_remote_changes then _notify_about_change (sender, index, nil, from_name, from_person_class) end if self.display then self.display:GetUserData("GUI state").eoiST:OuroLoot_Refresh(index) self:redisplay() end self:Fire ('Reassign', unique, id, e, from_name, to_name) return index end local function expunge (player, index_or_unique) local i,u if type(index_or_unique) == 'string' then for u = 1, #player.unique do if player.unique[u] == index_or_unique then i = u break end end elseif type(index_or_unique) == 'number' then i = index_or_unique end if not i then return -- error here? end u = player.unique[i] assert(#u>0) tremove (player.unique, i) player.when[u], player.id[u], player.count[u] = nil, nil, nil g_uniques[u] = nil addon.hist_clean = nil addon:Fire ('DelHistory', player.name, u) return #player.unique end -- Mirror of _addHistoryEntry. Arguments are either: -- E - loot entry -- U,ITEM - unique tag, and a name/itemlink for errors -- If this entry was the only one for that player, will also remove that -- player's tables from the history array. -- -- On success, returns the number of remaining history entries for that -- player (potentially zero). On failure, returns nil+error. function addon:_delHistoryEntry (first, item) if type(first) == 'table' then item = first.itemlink or item --elseif type(first) == 'string' then end local from_i, from_h, hist_i = _history_by_loot_id (first, "delete") if not from_i then -- from_h is the formatted error text return nil, (from_h .." Loot will be deleted, but history will NOT be updated." ):format(item) end local remaining = expunge (from_h, hist_i) if not remaining then return nil, "Something bizarre happening trying to delete "..item elseif remaining > 0 then return remaining end tremove (self.history, from_i) self:_build_history_names() return 0 end -- Any extra work for the "Mark as <x>" dropdown actions. The -- corresponding <x> will already have been assigned in the loot entry. 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 not self:_test_disposition (newdisp, 'affects_history') then -- remove history entry if it exists local name_i, name_h, hist_i = _history_by_loot_id (e, "mark") if hist_i then -- clears g_uniques and fires DelHistory expunge (name_h, hist_i) elseif not self:_test_disposition (olddisp, 'affects_history') 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 (not self:_test_disposition (olddisp, 'affects_history')) and self:_test_disposition (newdisp, 'affects_history') then -- Must create a new history entry. local name_i, name_h = self:get_loot_history(name) -- puts entry into g_uniques and fires NewHistory self:_addHistoryEntry (index, --[[re-sort entries=]]true) self.hist_clean = nil 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 = ... id = tonumber(id) local cache local loop = 0 repeat -- wtb continue statement pst if loop > 1 then break end e = nil cache = cache and g_uniques:SEARCH(unique) or g_uniques[unique] index = tonumber(cache.loot) if index then e = g_loot[index] else end loop = loop + 1 until e and (e.id == id) 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) self.hist_clean = nil self.loot_clean = nil -- A unique tag has been set by this point. if how == 'local' then unique = assert(e.unique) if opts.chatty_on_local_changes then _notify_about_change (_G.UNIT_YOU, index, olddisp) end self:vbroadcast('mark', unique, id, olddisp, newdisp) end self:Fire ('MarkAs', unique, id, e, olddisp or 'normal', newdisp or 'normal') return index end end ------ Player communication do local select, tconcat, unpack = select, table.concat, unpack 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 nil 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) self:SendCommMessage(self.ident, msg, "RAID") -- this is what lets us debug our own message traffic: if self.debug.comm and self.is_guilded then self:SendCommMessage(self.ident, msg, "GUILD") end 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) local what = addon.enabled and "tracking" or (addon.rebroadcast and "broadcasting" or "disabled") addon:whispercast (sender, 'pong', addon.version, what, addon.revision) end OCR_funcs.pong = function (sender, _, ver, status, opt_rev) local s = ("|cff00ff00%s|r %s(r%s) is |cff00ffff%s|r"): format (sender, ver, opt_rev or "?", 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_version (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_change (sender, index, old) end end OCR_funcs['17reassign'] = function (sender, _, unique, item, from, to) addon.dprint('comm', "DOTreassign/17, sender", sender, "unique", unique, "item", item, "from", from, "to", to) --[[local index =]] addon:reassign_loot ("remote", sender, unique, item, from, to) -- Notification handled inside reassign_loot. 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("bcast-respond",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! Click %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 addon:MODULE_PROTOTYPE_POINTERS() addon.FILES_LOADED = 1 -- vim:noet