farmbuyer@17: local nametag, addon = ... farmbuyer@1: farmbuyer@1: --[==[ farmbuyer@1: g_loot's numeric indices are loot entries (including titles, separators, farmbuyer@1: etc); its named indices are: farmbuyer@61: - forum saved text from forum markup window, default nil farmbuyer@61: - attend saved text from raid attendence window, default nil farmbuyer@61: - printed.FOO last loot index formatted into text window FOO, default 0 farmbuyer@61: - raiders accumulating raid roster data as we see raid members; indexed farmbuyer@57: by player name with subtable fields: farmbuyer@61: - class capitalized English codename ("WARRIOR", "DEATHKNIGHT", etc) farmbuyer@68: - subgroup 1-8 (NUM_RAID_GROUPS), NRG+1 if something's wrong farmbuyer@61: - race English codename ("BloodElf", etc) farmbuyer@61: - sex 1 = unknown/error, 2 = male, 3 = female farmbuyer@61: - level can be 0 if player was offline at the time farmbuyer@61: - guild guild name, or missing if unguilded farmbuyer@61: - online 1 = online, 2 = offline, 3 = no longer in raid farmbuyer@57: [both of these next two fields use time_t values:] farmbuyer@61: - join time player joined the raid (or first time we've seen them) farmbuyer@61: - leave time player left the raid (or time we've left the raid, if farmbuyer@61: 'online' is not 3) farmbuyer@50: farmbuyer@54: Common g_loot entry indices: farmbuyer@61: - kind time/boss/loot farmbuyer@61: - hour 0-23, on the *physical instance server*, not the realm server farmbuyer@61: - minute 0-59, ditto farmbuyer@61: - stamp time_t on the local computer farmbuyer@61: - cols graphical display data; cleared when logging out farmbuyer@50: farmbuyer@50: Time specific g_loot indices: farmbuyer@61: - startday table with month/day/year/text fields from makedate() farmbuyer@57: text is always "dd Month yyyy" farmbuyer@50: farmbuyer@50: Boss specific g_loot indices: farmbuyer@61: - bossname name of boss/encounter; farmbuyer@57: may be changed if "snarky boss names" option is enabled farmbuyer@61: - reason wipe/kill ("pull" does not generate an entry) farmbuyer@61: - instance name of instance, including size and difficulty farmbuyer@61: - maxsize max raid size: 5/10/25, presumably also 15 and 40 could show farmbuyer@61: up; can be 0 if we're outside an instance and the player farmbuyer@61: inside has an older version farmbuyer@61: - duration in seconds; may be missing (only present if local) farmbuyer@69: - raidersnap copy of g_loot.raiders at the time of the boss event; may be farmbuyer@69: empty for manual snapshots the player didn't want included farmbuyer@69: (not necessarily an "error" if this is missing entirely) farmbuyer@50: farmbuyer@50: Loot specific g_loot indices: farmbuyer@61: - person recipient farmbuyer@61: - person_class class of recipient if available; may be missing; farmbuyer@57: will be classID-style (e.g., DEATHKNIGHT) farmbuyer@61: - itemname not including square brackets farmbuyer@61: - id itemID as number farmbuyer@61: - itemlink full clickable link farmbuyer@61: - itexture icon path (e.g., Interface\Icons\INV_Misc_Rune_01) farmbuyer@61: - quality ITEM_QUALITY_* number farmbuyer@71: - unique an almost-certainly-unique string, content meaningless farmbuyer@61: - disposition offspec/gvault/shard; missing otherwise; can be set from farmbuyer@57: the extratext field farmbuyer@61: - count e.g., "x3"; missing otherwise; can be set/removed from farmbuyer@57: extratext; triggers only for a stack of items, not "the boss farmbuyer@57: dropped double axes today" farmbuyer@66: - variant 1 = heroic item, 2 = LFR item; missing otherwise farmbuyer@61: - cache_miss if GetItemInfo failed; SHOULD be missing (changes other fields) farmbuyer@65: - bcast_from player's name if received rebroadcast from that player; farmbuyer@65: missing otherwise; can be deleted as a result of in-game farmbuyer@65: fiddling of loot data farmbuyer@61: - extratext text in Note column, including disposition and rebroadcasting farmbuyer@61: - extratext_byhand true if text edited by player directly; missing otherwise farmbuyer@50: farmbuyer@1: farmbuyer@1: Functions arranged like this, with these lables (for jumping to). As a farmbuyer@1: rule, member functions with UpperCamelCase names are called directly by farmbuyer@1: user-facing code, ones with lowercase names are "one step removed", and farmbuyer@1: names with leading underscores are strictly internal helper functions. farmbuyer@1: ------ Saved variables farmbuyer@1: ------ Constants farmbuyer@1: ------ Addon member data farmbuyer@6: ------ Globals farmbuyer@1: ------ Expiring caches farmbuyer@1: ------ Ace3 framework stuff farmbuyer@1: ------ Event handlers farmbuyer@1: ------ Slash command handler farmbuyer@1: ------ On/off farmbuyer@1: ------ Behind the scenes routines farmbuyer@1: ------ Saved texts farmbuyer@1: ------ Loot histories farmbuyer@1: ------ Player communication farmbuyer@1: farmbuyer@1: This started off as part of a raid addon package written by somebody else. farmbuyer@1: After he retired, I began modifying the code. Eventually I set aside the farmbuyer@1: entire package and rewrote the loot tracker module from scratch. Many of the farmbuyer@1: variable/function naming conventions (sv_*, g_*, and family) stayed across the farmbuyer@16: rewrite. farmbuyer@16: farmbuyer@73: Some variables are needlessly initialized to nil just to look uniform and farmbuyer@76: serve as a spelling reminder. farmbuyer@1: farmbuyer@1: ]==] farmbuyer@1: farmbuyer@1: ------ Saved variables farmbuyer@16: OuroLootSV = nil -- possible copy of g_loot farmbuyer@16: OuroLootSV_saved = nil -- table of copies of saved texts, default nil; keys farmbuyer@16: -- are numeric indices of tables, subkeys of those farmbuyer@16: -- are name/forum/attend/date farmbuyer@16: OuroLootSV_opts = nil -- same as option_defaults until changed farmbuyer@16: -- autoshard: optional name of disenchanting player, default nil farmbuyer@16: -- threshold: optional loot threshold, default nil farmbuyer@16: OuroLootSV_hist = nil farmbuyer@19: OuroLootSV_log = {} farmbuyer@1: farmbuyer@1: farmbuyer@1: ------ Constants farmbuyer@1: local option_defaults = { farmbuyer@71: ['datarev'] = 20, -- cheating, this isn't actually an option farmbuyer@1: ['popup_on_join'] = true, farmbuyer@1: ['register_slashloot'] = true, farmbuyer@1: ['scroll_to_bottom'] = true, farmbuyer@1: ['chatty_on_kill'] = false, farmbuyer@1: ['no_tracking_wipes'] = false, farmbuyer@1: ['snarky_boss'] = true, farmbuyer@1: ['keybinding'] = false, farmbuyer@2: ['bossmod'] = "DBM", farmbuyer@1: ['keybinding_text'] = 'CTRL-SHIFT-O', farmbuyer@1: ['forum'] = { farmbuyer@25: ['[url] Wowhead'] = '[url=http://www.wowhead.com/?item=$I]$N[/url]$X - $T', farmbuyer@25: ['[url] MMO/Wowstead'] = '[http://db.mmo-champion.com/i/$I]$X - $T', farmbuyer@1: ['[item] by name'] = '[item]$N[/item]$X - $T', farmbuyer@1: ['[item] by ID'] = '[item]$I[/item]$X - $T', farmbuyer@1: ['Custom...'] = '', farmbuyer@1: }, farmbuyer@1: ['forum_current'] = '[item] by name', farmbuyer@57: ['display_disabled_LODs'] = false, farmbuyer@65: ['display_bcast_from'] = true, farmbuyer@73: ['precache_history_uniques'] = false, farmbuyer@73: ['chatty_on_remote_changes'] = false, farmbuyer@73: ['chatty_on_remote_changes_frame'] = 1, farmbuyer@1: } farmbuyer@1: local virgin = "First time loaded? Hi! Use the /ouroloot or /loot command" farmbuyer@1: .." to show the main display. You should probably browse the instructions" farmbuyer@1: .." if you've never used this before; %s to display the help window. This" farmbuyer@1: .." welcome message will not intrude again." farmbuyer@27: local newer_warning = "A newer version has been released. You can %s to display" farmbuyer@27: .." a download URL for copy-and-pasting. You can %s to ping other raiders" farmbuyer@27: .." for their installed versions (same as '/ouroloot ping' or clicking the" farmbuyer@27: .." 'Ping!' button on the options panel)." farmbuyer@77: local unique_collision = "|cffff1010%s:|r|nItem '%s' was carrying unique tag <%s>, but that was already in use; tried to generate a new tag and failed!|n|nRemote sender was '%s', previous cache entry was <%s/%s>.|n|nThis may require a live human to figure out; the loot in question has not been stored." farmbuyer@73: local remote_chatty = "|cff00ff00%s|r changed %d/%s from %s%s|r to %s%s|r" farmbuyer@1: local qualnames = { farmbuyer@1: ['gray'] = 0, ['grey'] = 0, ['poor'] = 0, ['trash'] = 0, farmbuyer@1: ['white'] = 1, ['common'] = 1, farmbuyer@1: ['green'] = 2, ['uncommon'] = 2, farmbuyer@1: ['blue'] = 3, ['rare'] = 3, farmbuyer@1: ['epic'] = 4, ['purple'] = 4, farmbuyer@1: ['legendary'] = 5, ['orange'] = 5, farmbuyer@1: ['artifact'] = 6, farmbuyer@1: --['heirloom'] = 7, farmbuyer@1: } farmbuyer@1: local my_name = UnitName('player') farmbuyer@76: local comm_cleanup_ttl = 4 -- seconds in the communications cache farmbuyer@27: local revision_large = nil -- defaults to 1, possibly changed by revision farmbuyer@20: local g_LOOT_ITEM_ss, g_LOOT_ITEM_MULTIPLE_sss, g_LOOT_ITEM_SELF_s, g_LOOT_ITEM_SELF_MULTIPLE_ss farmbuyer@1: farmbuyer@1: farmbuyer@1: ------ Addon member data farmbuyer@1: local flib = LibStub("LibFarmbuyer") farmbuyer@1: addon.author_debug = flib.author_debug farmbuyer@1: farmbuyer@6: -- Play cute games with namespaces here just to save typing. WTB Lua 5.2 PST. farmbuyer@1: do local _G = _G setfenv (1, addon) farmbuyer@1: farmbuyer@71: commrev = '17' farmbuyer@27: revision = _G.GetAddOnMetadata(nametag,"Version") or "?" -- "x.yy.z", etc farmbuyer@1: ident = "OuroLoot2" farmbuyer@1: identTg = "OuroLoot2Tg" farmbuyer@1: status_text = nil farmbuyer@1: farmbuyer@45: tekdebug = nil farmbuyer@45: if _G.tekDebug then farmbuyer@45: local tdframe = _G.tekDebug:GetFrame("Ouro Loot") farmbuyer@45: function tekdebug (txt) farmbuyer@45: -- tekDebug notices "|r:" farmbuyer@45: tdframe:AddMessage('|cff17ff0dOuro Loot|r:'..txt,1,1,1) farmbuyer@45: end farmbuyer@45: end farmbuyer@45: farmbuyer@1: DEBUG_PRINT = false farmbuyer@1: debug = { farmbuyer@76: comm = false, farmbuyer@76: loot = false, farmbuyer@76: flow = false, farmbuyer@76: notraid = false, farmbuyer@76: cache = false, farmbuyer@76: alsolog = false, farmbuyer@1: } farmbuyer@77: --@debug@ farmbuyer@77: DEBUG_PRINT = true farmbuyer@77: debug.loot = true farmbuyer@77: debug.comm = true farmbuyer@77: --@end-debug@ farmbuyer@77: farmbuyer@45: -- This looks ugly, but it factors out the load-time decisions from farmbuyer@73: -- the run-time ones. Args to [dp]print are concatenated with spaces. farmbuyer@45: if tekdebug then farmbuyer@45: function dprint (t,...) farmbuyer@45: if DEBUG_PRINT and debug[t] then farmbuyer@45: local text = flib.safefprint(tekdebug,"<"..t.."> ",...) farmbuyer@45: if debug.alsolog then farmbuyer@45: addon:log_with_timestamp(text) farmbuyer@45: end farmbuyer@45: end farmbuyer@45: end farmbuyer@45: else farmbuyer@45: function dprint (t,...) farmbuyer@45: if DEBUG_PRINT and debug[t] then farmbuyer@45: local text = flib.safeprint("<"..t.."> ",...) farmbuyer@45: if debug.alsolog then farmbuyer@45: addon:log_with_timestamp(text) farmbuyer@45: end farmbuyer@19: end farmbuyer@19: end farmbuyer@1: end farmbuyer@1: farmbuyer@45: if author_debug and tekdebug then farmbuyer@45: function pprint (t,...) farmbuyer@45: local text = flib.safefprint(tekdebug,"<<"..t..">> ",...) farmbuyer@19: if debug.alsolog then farmbuyer@19: addon:log_with_timestamp(text) farmbuyer@19: end farmbuyer@1: end farmbuyer@1: else farmbuyer@1: pprint = flib.nullfunc farmbuyer@1: end farmbuyer@1: farmbuyer@76: -- The same observable behavior as the Lua builtins, but with slightly farmbuyer@76: -- different hardcoded strings and, more importantly, implicit logging. farmbuyer@76: function error(txt,lvl) farmbuyer@76: pprint('ERROR()', txt) farmbuyer@76: pprint('DEBUGSTACK()', _G.debugstack()) farmbuyer@76: _G.error(txt,lvl) farmbuyer@76: end farmbuyer@76: function assert(cond,msg,...) farmbuyer@76: if cond then farmbuyer@76: return cond,msg,... farmbuyer@76: else farmbuyer@76: error('ASSERT() FAILED: '..tostring(msg or 'nil')) farmbuyer@76: end farmbuyer@76: end farmbuyer@76: farmbuyer@1: enabled = false farmbuyer@1: rebroadcast = false farmbuyer@76: display = nil -- reference to display frame iff visible farmbuyer@1: loot_clean = nil -- index of last GUI entry with known-current visual data farmbuyer@1: threshold = debug.loot and 0 or 3 -- rare by default farmbuyer@1: sharder = nil -- name of person whose loot is marked as shards farmbuyer@1: farmbuyer@1: -- The rest is also used in the GUI: farmbuyer@1: farmbuyer@76: sender_list = {active={},names={}} -- this should be reworked farmbuyer@1: popped = nil -- non-nil when reminder has been shown, actual value unimportant farmbuyer@1: farmbuyer@2: bossmod_registered = nil farmbuyer@76: bossmods = {} farmbuyer@2: farmbuyer@76: requesting = nil -- prompting for additional rebroadcasters farmbuyer@1: farmbuyer@76: -- don't use NUM_ITEM_QUALITIES as the upper loop bound unless we expect farmbuyer@76: -- heirlooms to show up farmbuyer@76: thresholds = {} farmbuyer@1: for i = 0,6 do farmbuyer@11: thresholds[i] = _G.ITEM_QUALITY_COLORS[i].hex .. _G["ITEM_QUALITY"..i.."_DESC"] .. "|r" farmbuyer@1: end farmbuyer@1: farmbuyer@1: _G.setfenv (1, _G) farmbuyer@1: end farmbuyer@1: farmbuyer@1: addon = LibStub("AceAddon-3.0"):NewAddon(addon, "Ouro Loot", farmbuyer@1: "AceTimer-3.0", "AceComm-3.0", "AceConsole-3.0", "AceEvent-3.0") farmbuyer@1: farmbuyer@67: -- if given, MSG should be a complete-ish sentence farmbuyer@67: function addon:load_assert (cond, msg, ...) farmbuyer@67: if cond then farmbuyer@67: return cond, msg, ... farmbuyer@67: end farmbuyer@67: msg = msg or "load-time assertion failed!" farmbuyer@67: self.NOLOAD = msg farmbuyer@67: 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) farmbuyer@67: SLASH_ACECONSOLE_OUROLOOT1 = nil farmbuyer@67: SLASH_ACECONSOLE_OUROLOOT2 = nil farmbuyer@76: self.error (msg, --[[level=]]2) farmbuyer@67: end farmbuyer@67: farmbuyer@67: -- Seriously? ORLY? farmbuyer@67: -- YARLY. Go ahead and guess what was involved in tracking this down. If farmbuyer@67: -- more such effects are added in the future, the "id==xxxxx" will need to farmbuyer@67: -- change into a probe of a table of known-problematic IDs. farmbuyer@67: for i = 1, 40 do -- BUFF_MAX_DISPLAY==32, enh farmbuyer@67: local id = select(11,UnitAura('player', i, 'HELPFUL')) farmbuyer@67: if id == 88715 then farmbuyer@67: -- What I really want to do is pause until the thing is clicked off, farmbuyer@67: -- then continue with the rest of the file. No can do. Could also farmbuyer@67: -- just set some hooks and then re-OnInit/OnEnable after the aura farmbuyer@67: -- expires, but that's a hassle. GAH. Punt. farmbuyer@67: local text = UnitAura('player', i, 'HELPFUL') farmbuyer@67: text = ([[Cannot initialize while |cff71d5ff|Hspell:88715|h[%s]|h|cff00ff00 is active!]]): farmbuyer@67: format(text) farmbuyer@67: addon:load_assert(nil,text) farmbuyer@67: return -- were this C code running through lint, I'd put NOTREACHED farmbuyer@67: end farmbuyer@67: end farmbuyer@67: farmbuyer@1: farmbuyer@6: ------ Globals farmbuyer@1: local g_loot = nil farmbuyer@1: local g_restore_p = nil farmbuyer@76: local g_wafer_thin = nil -- prompting for additional rebroadcasters farmbuyer@1: local g_today = nil -- "today" entry in g_loot farmbuyer@16: local g_boss_signpost = nil farmbuyer@73: local g_seeing_oldsigs = nil farmbuyer@73: local g_uniques = nil -- memoization of unique loot events farmbuyer@76: local g_unique_replace = nil farmbuyer@1: local opts = nil farmbuyer@1: farmbuyer@76: local error = addon.error farmbuyer@76: local assert = addon.assert farmbuyer@76: farmbuyer@73: -- for speeding up local loads, not because I think _G will change farmbuyer@73: local _G = _G farmbuyer@73: local type = _G.type farmbuyer@73: local select = _G.select farmbuyer@73: local pairs = _G.pairs farmbuyer@73: local ipairs = _G.ipairs farmbuyer@73: local tinsert = _G.table.insert farmbuyer@73: local tremove = _G.table.remove farmbuyer@73: local tostring = _G.tostring farmbuyer@73: local tonumber = _G.tonumber farmbuyer@73: local wipe = _G.table.wipe farmbuyer@73: farmbuyer@1: local pprint, tabledump = addon.pprint, flib.tabledump farmbuyer@73: local CopyTable, GetNumRaidMembers = _G.CopyTable, _G.GetNumRaidMembers farmbuyer@1: -- En masse forward decls of symbols defined inside local blocks farmbuyer@41: local _register_bossmod, makedate, create_new_cache, _init, _log farmbuyer@76: local _history_by_loot_id, _notify_about_remote, _setup_unique_replace farmbuyer@77: local _unavoidable_collision farmbuyer@1: farmbuyer@27: -- Try to extract numbers from the .toc "Version" and munge them into an farmbuyer@27: -- integral form for comparison. The result doesn't need to be meaningful as farmbuyer@38: -- long as we can reliably feed two of them to "<" and get useful answers. farmbuyer@29: -- farmbuyer@29: -- This makes/reinforces an assumption that revision_large of release packages farmbuyer@29: -- (e.g., 2016001) will always be higher than those of development packages farmbuyer@29: -- (e.g., 87), due to the tagging system versus subversion file revs. This farmbuyer@29: -- is good, as local dev code will never trigger a false positive update farmbuyer@62: -- warning for other users. farmbuyer@27: do farmbuyer@27: local r = 0 farmbuyer@27: for d in addon.revision:gmatch("%d+") do farmbuyer@27: r = 1000*r + d farmbuyer@27: end farmbuyer@62: -- If it's a big enough number to obviously be a release, then make farmbuyer@65: -- sure it's big enough to overcome many small previous point releases. farmbuyer@62: while r > 2000 and r < 2000000 do farmbuyer@62: r = 1000*r farmbuyer@62: end farmbuyer@27: revision_large = math.max(r,1) farmbuyer@27: end farmbuyer@27: farmbuyer@1: -- Hypertext support, inspired by DBM broadcast pizza timers farmbuyer@1: do farmbuyer@76: local hypertext_format_str = "|HOuroLoot:%d|h%s[%s]|r|h" farmbuyer@76: local func_map = {} --_G.setmetatable({}, {__mode = 'k'}) farmbuyer@76: local text_map = {} --_G.setmetatable({}, {__mode = 'kv'}) farmbuyer@76: local base = _G.newproxy(true) farmbuyer@76: _G.getmetatable(base).__tostring = function(ud) return text_map[ud] end farmbuyer@76: --@debug@ farmbuyer@76: -- collecting these tokens is an interesting micro-optimization but not yet farmbuyer@76: _G.getmetatable(base).__gc = function(ud) farmbuyer@76: print("Collecting hyperlink object <",tostring(ud),">") farmbuyer@76: end farmbuyer@76: --@end-debug@ farmbuyer@1: farmbuyer@38: -- TEXT will automatically be surrounded by brackets farmbuyer@76: -- COLOR can be ITEM_QUALITY_* or a formatting string ("|cff...") farmbuyer@76: -- FUNC can be "MethodName", "tab_title", or a function farmbuyer@76: -- farmbuyer@78: -- Returns an opaque token and a matching number. Calling tostring() on farmbuyer@78: -- the token will yield a formatted clickable string that can be displayed farmbuyer@78: -- in chat. The MethodName and raw function callbacks will both be farmbuyer@78: -- passed the addon table and the same matching number. farmbuyer@78: -- farmbuyer@78: -- This is largely an excuse to fool around with Lua data constructs. farmbuyer@76: function addon.format_hypertext (text, color, func) farmbuyer@76: local ret = _G.newproxy(base) farmbuyer@76: local num = #text_map + 1 farmbuyer@76: text_map[ret] = hypertext_format_str:format (num, farmbuyer@76: type(color)=='number' and ITEM_QUALITY_COLORS[color].hex or color, farmbuyer@76: text) farmbuyer@76: text_map[num] = ret farmbuyer@76: func_map[ret] = func farmbuyer@78: return ret, num farmbuyer@1: end farmbuyer@1: farmbuyer@76: --[[ farmbuyer@76: link: OuroLoot:n farmbuyer@76: fullstring: |HOuroLoot:n|h|cff.....[foo]|r|h farmbuyer@76: mousebutton: "LeftButton", "MiddleButton", "RightButton" farmbuyer@76: farmbuyer@76: amusingly, print()'ing the fullstring below as a debugging aid yields farmbuyer@76: another clickable link, yay data reproducability farmbuyer@76: ]] farmbuyer@76: local strsplit = _G.strsplit farmbuyer@76: DEFAULT_CHAT_FRAME:HookScript("OnHyperlinkClick", function(self, link, fullstring, mousebutton) farmbuyer@1: local ltype, arg = strsplit(":",link) farmbuyer@76: if ltype ~= "OuroLoot" then return end farmbuyer@78: arg = tonumber(arg) farmbuyer@78: local f = func_map[text_map[arg]] farmbuyer@76: if type(f) == 'function' then farmbuyer@78: f (addon, arg) farmbuyer@76: elseif type(f) == 'string' then farmbuyer@76: if type(addon[f]) == 'function' then farmbuyer@78: addon[f](addon,arg) -- method name farmbuyer@76: else farmbuyer@76: addon:BuildMainDisplay(f) -- tab title fragment farmbuyer@1: end farmbuyer@1: end farmbuyer@1: end) farmbuyer@1: farmbuyer@1: local old = ItemRefTooltip.SetHyperlink farmbuyer@1: function ItemRefTooltip:SetHyperlink (link, ...) farmbuyer@76: if link:match("^OuroLoot") then return end farmbuyer@1: return old (self, link, ...) farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@1: do farmbuyer@1: -- copied here because it's declared local to the calendar ui, thanks blizz >< farmbuyer@1: local CALENDAR_FULLDATE_MONTH_NAMES = { farmbuyer@1: FULLDATE_MONTH_JANUARY, FULLDATE_MONTH_FEBRUARY, FULLDATE_MONTH_MARCH, farmbuyer@1: FULLDATE_MONTH_APRIL, FULLDATE_MONTH_MAY, FULLDATE_MONTH_JUNE, farmbuyer@1: FULLDATE_MONTH_JULY, FULLDATE_MONTH_AUGUST, FULLDATE_MONTH_SEPTEMBER, farmbuyer@1: FULLDATE_MONTH_OCTOBER, FULLDATE_MONTH_NOVEMBER, FULLDATE_MONTH_DECEMBER, farmbuyer@1: } farmbuyer@1: -- returns "dd Month yyyy", mm, dd, yyyy farmbuyer@1: function makedate() farmbuyer@1: Calendar_LoadUI() farmbuyer@1: local _, M, D, Y = CalendarGetDate() farmbuyer@1: local text = ("%d %s %d"):format(D, CALENDAR_FULLDATE_MONTH_NAMES[M], Y) farmbuyer@1: return text, M, D, Y farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@56: -- Returns an instance name or abbreviation, followed by the raid size farmbuyer@1: local function instance_tag() farmbuyer@61: -- possibly redo this with the new GetRaidDifficulty function farmbuyer@1: local name, typeof, diffcode, diffstr, _, perbossheroic, isdynamic = GetInstanceInfo() farmbuyer@56: local t, r farmbuyer@1: name = addon.instance_abbrev[name] or name farmbuyer@56: if typeof == "none" then return name, MAX_RAID_MEMBERS end farmbuyer@1: -- diffstr is "5 Player", "10 Player (Heroic)", etc. ugh. farmbuyer@35: if (GetLFGMode()) and (GetLFGModeType() == 'raid') then farmbuyer@56: t,r = 'LFR', 25 farmbuyer@35: elseif diffcode == 1 then farmbuyer@76: if GetNumRaidMembers() > 0 then farmbuyer@76: t,r = "10",10 farmbuyer@76: else farmbuyer@76: t,r = "5",5 farmbuyer@76: end farmbuyer@1: elseif diffcode == 2 then farmbuyer@76: if GetNumRaidMembers() > 0 then farmbuyer@76: t,r = "25",25 farmbuyer@76: else farmbuyer@76: t,r = "5h",5 farmbuyer@76: end farmbuyer@1: elseif diffcode == 3 then farmbuyer@56: t,r = "10h", 10 farmbuyer@1: elseif diffcode == 4 then farmbuyer@56: t,r = "25h", 25 farmbuyer@1: end farmbuyer@1: -- dynamic difficulties always return normal "codes" farmbuyer@1: if isdynamic and perbossheroic == 1 then farmbuyer@1: t = t .. "h" farmbuyer@1: end farmbuyer@56: return name .. "(" .. t .. ")", r farmbuyer@1: end farmbuyer@1: addon.instance_tag = instance_tag -- grumble farmbuyer@42: addon.latest_instance = nil -- spelling reminder, assigned elsewhere farmbuyer@1: farmbuyer@73: -- Memoizing cache of unique IDs as we generate or search for them. Keys are farmbuyer@73: -- the uniques, values are the following: farmbuyer@73: -- 'history' active index into self.history farmbuyer@73: -- 'history_may' index into player's uniques list, CAN QUICKLY BE OUTDATED farmbuyer@73: -- and will instantly be wrong after manual insertion farmbuyer@73: -- 'loot' active index into g_loot farmbuyer@73: -- with all but the history entry optional. Values of g_uniqes.NOTFOUND farmbuyer@73: -- indicate a known missing status. Use g_uniques:RESET() to wipe the cache farmbuyer@73: -- and return to searching mode. farmbuyer@73: do farmbuyer@73: local notfound = -1 farmbuyer@73: local notfound_ret = { history = notfound } farmbuyer@73: local mt farmbuyer@73: farmbuyer@73: -- This can either be its own function or a slightly redundant __index. farmbuyer@73: local function m_probe_only (t, k) farmbuyer@73: return rawget(t,k) or notfound_ret farmbuyer@73: end farmbuyer@73: farmbuyer@73: -- Expensive search. farmbuyer@73: local function m_full_search (t, k) farmbuyer@73: local L, H, HU, loot farmbuyer@73: -- Try active loot entries first farmbuyer@73: for i,e in addon:filtered_loot_iter('loot') do farmbuyer@73: if k == e.unique then farmbuyer@73: L,loot = i,e farmbuyer@73: break farmbuyer@73: end farmbuyer@73: end farmbuyer@73: -- If it's active, try looking through that player's history first. farmbuyer@73: if L then farmbuyer@73: local hi,h = addon:get_loot_history (loot.person) farmbuyer@73: for ui,u in ipairs(h.unique) do farmbuyer@73: if k == u then farmbuyer@73: H, HU = hi, ui farmbuyer@73: break farmbuyer@73: end farmbuyer@73: end farmbuyer@73: else farmbuyer@73: -- No luck? Ugh, may have been reassigned and we're probing from farmbuyer@73: -- older data. Search the rest of current realm's history. farmbuyer@73: for hi,h in ipairs(addon.history) do farmbuyer@73: for ui,u in ipairs(h.unique) do farmbuyer@73: if k == u then farmbuyer@73: H, HU = hi, ui farmbuyer@73: break farmbuyer@73: end farmbuyer@73: end farmbuyer@73: end farmbuyer@73: end farmbuyer@73: local ret = { loot = L, history = H or notfound, history_may = HU } farmbuyer@73: t[k] = ret farmbuyer@73: return ret farmbuyer@73: end farmbuyer@73: farmbuyer@73: local function m_setmode (self, mode) farmbuyer@73: mt.__index = (mode == 'probe') and m_probe_only or farmbuyer@73: (mode == 'search') and m_full_search or farmbuyer@73: nil -- maybe error() here? farmbuyer@73: end farmbuyer@73: farmbuyer@73: local function m_reset (self) farmbuyer@73: wipe(self) farmbuyer@73: self[''] = notfound_ret -- special case for receiving older broadcast farmbuyer@73: self.NOTFOUND = notfound farmbuyer@73: self.RESET = m_reset farmbuyer@73: self.SEARCH = m_full_search farmbuyer@73: self.TEST = m_probe_only farmbuyer@73: self.SETMODE = m_setmode farmbuyer@73: mt.__index = m_full_search farmbuyer@73: return self farmbuyer@73: end farmbuyer@73: farmbuyer@73: -- If unique keys ever change into objects instead of strings, change farmbuyer@73: -- this into a weakly-keyed table. farmbuyer@73: mt = { __metatable = 'Should be using setmode.' } farmbuyer@73: farmbuyer@73: g_uniques = setmetatable (m_reset{}, mt) farmbuyer@73: end farmbuyer@73: farmbuyer@1: farmbuyer@1: ------ Expiring caches farmbuyer@1: --[[ farmbuyer@73: cache = create_new_cache ("mycache", 15 [,cleanup]) farmbuyer@73: cache:add(foo) farmbuyer@73: cache:test(foo) -- returns true farmbuyer@73: ....5 seconds pass farmbuyer@73: cache:add(bar) farmbuyer@73: ....10 seconds pass farmbuyer@73: cache:test(foo) -- returns false farmbuyer@73: cache:test(bar) -- returns true farmbuyer@73: ....5 seconds pass farmbuyer@73: ....bar also gone, cleanup() called farmbuyer@1: ]] farmbuyer@1: do farmbuyer@1: local caches = {} farmbuyer@25: local cleanup_group = _G.AnimTimerFrame:CreateAnimationGroup() farmbuyer@10: local time = _G.time farmbuyer@1: cleanup_group:SetLooping("REPEAT") farmbuyer@1: cleanup_group:SetScript("OnLoop", function(cg) farmbuyer@1: addon.dprint('cache',"OnLoop firing") farmbuyer@10: local now = time() farmbuyer@1: local alldone = true farmbuyer@1: -- this is ass-ugly farmbuyer@25: for name,c in pairs(caches) do farmbuyer@25: local fifo = c.fifo farmbuyer@25: local active = #fifo > 0 farmbuyer@73: while (#fifo > 0) and (now > fifo[1].t) do farmbuyer@40: addon.dprint('cache', name, "cache removing", fifo[1].t, "<", fifo[1].m, ">") farmbuyer@25: tremove(fifo,1) farmbuyer@1: end farmbuyer@25: if active and #fifo == 0 and c.func then farmbuyer@25: addon.dprint('cache', name, "empty, firing cleanup") farmbuyer@25: c:func() farmbuyer@25: end farmbuyer@25: alldone = alldone and (#fifo == 0) farmbuyer@1: end farmbuyer@1: if alldone then farmbuyer@40: addon.dprint('cache',"OnLoop FINISHING animation group") farmbuyer@1: cleanup_group:Finish() farmbuyer@73: _G.collectgarbage() farmbuyer@40: else farmbuyer@40: addon.dprint('cache',"OnLoop done, not yet finished") farmbuyer@1: end farmbuyer@1: end) farmbuyer@1: farmbuyer@1: local function _add (cache, x) farmbuyer@73: local datum = { t=time()+cache.ttl, m=x } farmbuyer@25: cache.hash[x] = datum farmbuyer@25: tinsert (cache.fifo, datum) farmbuyer@1: if not cleanup_group:IsPlaying() then farmbuyer@41: addon.dprint('cache', cache.name, "with entry", datum.t, "<", datum.m, "> STARTING animation group") farmbuyer@40: cache.cleanup:SetDuration(1) -- hmmm farmbuyer@1: cleanup_group:Play() farmbuyer@1: end farmbuyer@1: end farmbuyer@1: local function _test (cache, x) farmbuyer@40: -- FIXME This can return false positives, if called after the onloop farmbuyer@40: -- fifo has been removed but before the GC has removed the weak entry. farmbuyer@76: -- What to do, what to do... try forcing a GC during alldone. farmbuyer@25: return cache.hash[x] ~= nil farmbuyer@1: end farmbuyer@25: farmbuyer@1: function create_new_cache (name, ttl, on_alldone) farmbuyer@25: -- setting OnFinished for cleanup fires at the end of each inner loop, farmbuyer@25: -- with no 'requested' argument to distinguish cases. thus, on_alldone. farmbuyer@76: -- FWIW, on_alldone is passed this table as its sole argument: farmbuyer@1: local c = { farmbuyer@1: ttl = ttl, farmbuyer@1: name = name, farmbuyer@1: add = _add, farmbuyer@1: test = _test, farmbuyer@1: cleanup = cleanup_group:CreateAnimation("Animation"), farmbuyer@1: func = on_alldone, farmbuyer@25: fifo = {}, farmbuyer@25: hash = setmetatable({}, {__mode='kv'}), farmbuyer@1: } farmbuyer@1: c.cleanup:SetOrder(1) farmbuyer@25: caches[name] = c farmbuyer@1: return c farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@1: farmbuyer@1: ------ Ace3 framework stuff farmbuyer@1: function addon:OnInitialize() farmbuyer@67: if self.author_debug then farmbuyer@67: _G.OL = self farmbuyer@73: _G.g_uniques = g_uniques farmbuyer@67: end farmbuyer@73: _log = _G.OuroLootSV_log farmbuyer@41: farmbuyer@1: -- VARIABLES_LOADED has fired by this point; test if we're doing something like farmbuyer@1: -- relogging during a raid and already have collected loot data farmbuyer@73: local OuroLootSV = _G.OuroLootSV farmbuyer@1: g_restore_p = OuroLootSV ~= nil farmbuyer@1: self.dprint('flow', "oninit sets restore as", g_restore_p) farmbuyer@1: farmbuyer@73: if _G.OuroLootSV_opts == nil then farmbuyer@73: _G.OuroLootSV_opts = {} farmbuyer@76: local vclick = self.format_hypertext ([[click here]], ITEM_QUALITY_UNCOMMON, 'help') farmbuyer@1: self:ScheduleTimer(function(s) farmbuyer@76: s:Print(virgin, tostring(vclick)) farmbuyer@1: virgin = nil farmbuyer@1: end,10,self) farmbuyer@65: else farmbuyer@65: virgin = nil farmbuyer@1: end farmbuyer@73: opts = _G.OuroLootSV_opts farmbuyer@66: local stored_datarev = opts.datarev or 14 farmbuyer@1: for opt,default in pairs(option_defaults) do farmbuyer@1: if opts[opt] == nil then farmbuyer@1: opts[opt] = default farmbuyer@1: end farmbuyer@1: end farmbuyer@56: opts.datarev = option_defaults.datarev farmbuyer@38: farmbuyer@25: -- transition&remove old options farmbuyer@25: opts['forum_use_itemid'] = nil farmbuyer@25: if opts['forum_format'] then farmbuyer@25: opts.forum['Custom...'] = opts['forum_format'] farmbuyer@25: opts['forum_format'] = nil farmbuyer@25: end farmbuyer@25: if opts.forum['[url]'] then farmbuyer@25: opts.forum['[url] Wowhead'] = opts.forum['[url]'] farmbuyer@25: opts.forum['[url]'] = nil farmbuyer@25: opts.forum['[url] MMO/Wowstead'] = option_defaults.forum['[url] MMO/Wowstead'] farmbuyer@25: if opts['forum_current'] == '[url]' then farmbuyer@25: opts['forum_current'] = '[url] Wowhead' farmbuyer@25: end farmbuyer@25: end farmbuyer@1: option_defaults = nil farmbuyer@16: if OuroLootSV then -- may not be the same as testing g_restore_p soon farmbuyer@16: if OuroLootSV.saved then farmbuyer@16: OuroLootSV_saved = OuroLootSV.saved; OuroLootSV.saved = nil farmbuyer@16: end farmbuyer@16: if OuroLootSV.threshold then farmbuyer@16: opts.threshold = OuroLootSV.threshold; OuroLootSV.threshold = nil farmbuyer@16: end farmbuyer@16: if OuroLootSV.autoshard then farmbuyer@16: opts.autoshard = OuroLootSV.autoshard; OuroLootSV.autoshard = nil farmbuyer@16: end farmbuyer@16: end farmbuyer@38: farmbuyer@1: -- get item filter table if needed farmbuyer@1: if opts.itemfilter == nil then farmbuyer@67: opts.itemfilter = self.default_itemfilter farmbuyer@1: end farmbuyer@67: self.default_itemfilter = nil farmbuyer@70: if opts.itemvault == nil then farmbuyer@70: opts.itemvault = self.default_itemvault farmbuyer@70: end farmbuyer@70: self.default_itemvault = nil farmbuyer@1: farmbuyer@1: self:RegisterChatCommand("ouroloot", "OnSlash") farmbuyer@1: if opts.register_slashloot then farmbuyer@50: -- NOTA BENE: do not use /loot in the LoadOn list, ChatTypeInfo gets confused farmbuyer@50: -- maybe try to detect if this command is already in use... farmbuyer@73: _G.SLASH_ACECONSOLE_OUROLOOT2 = "/loot" farmbuyer@1: end farmbuyer@1: farmbuyer@73: self.history_all = self.history_all or _G.OuroLootSV_hist or {} farmbuyer@73: local r = self:load_assert (_G.GetRealmName(), "how the freak does GetRealmName() fail?") farmbuyer@1: self.history_all[r] = self:_prep_new_history_category (self.history_all[r], r) farmbuyer@1: self.history = self.history_all[r] farmbuyer@71: farmbuyer@71: local histformat = self.history_all.HISTFORMAT farmbuyer@71: self.history_all.HISTFORMAT = nil -- don't keep this in live data farmbuyer@73: if _G.OuroLootSV_hist farmbuyer@73: and (histformat == nil or histformat < 4) farmbuyer@73: then -- some big honkin' loops farmbuyer@38: for rname,realm in pairs(self.history_all) do farmbuyer@38: for pk,player in ipairs(realm) do farmbuyer@73: if histformat == nil or histformat < 3 then farmbuyer@73: for lk,loot in ipairs(player) do farmbuyer@73: if loot.count == "" then farmbuyer@73: loot.count = nil farmbuyer@73: end farmbuyer@73: if not loot.unique then farmbuyer@73: loot.unique = loot.id .. ' ' .. loot.when farmbuyer@73: end farmbuyer@71: end farmbuyer@38: end farmbuyer@73: -- format 3 to format 4 was a major revamp of per-player data farmbuyer@76: self:_uplift_history_format(player) farmbuyer@38: end farmbuyer@38: end farmbuyer@38: end farmbuyer@73: self._uplift_history_format = nil farmbuyer@6: --OuroLootSV_hist = nil farmbuyer@1: farmbuyer@56: -- Handle changes to the stored data format in stages from oldest to newest. farmbuyer@73: -- bumpers[X] is responsible for updating from X to X+1. farmbuyer@73: -- (This is turning into a lot of loops over the same table. Consolidate?) farmbuyer@56: if OuroLootSV then farmbuyer@56: local dirty = false farmbuyer@66: local bumpers = {} farmbuyer@66: bumpers[14] = function() farmbuyer@56: for i,e in ipairs(OuroLootSV) do farmbuyer@56: if e.bosskill then farmbuyer@56: e.bossname, e.bosskill = e.bosskill, nil farmbuyer@56: end farmbuyer@55: end farmbuyer@55: end farmbuyer@66: farmbuyer@66: bumpers[15] = function() farmbuyer@56: for i,e in ipairs(OuroLootSV) do farmbuyer@56: if e.kind == 'boss' then farmbuyer@56: e.maxsize, e.raiderlist, e.raidersnap = 0, nil, {} farmbuyer@56: end farmbuyer@56: end farmbuyer@56: OuroLootSV.raiders = OuroLootSV.raiders or {} farmbuyer@56: for name,r in pairs(OuroLootSV.raiders) do farmbuyer@56: r.subgroup = 0 farmbuyer@56: end farmbuyer@56: end farmbuyer@66: farmbuyer@66: bumpers[16] = function() farmbuyer@61: for i,e in ipairs(OuroLootSV) do farmbuyer@61: if e.kind == 'boss' then -- brown paper bag bugs farmbuyer@61: e.raidersnap = e.raidersnap or {} farmbuyer@61: e.maxsize = e.maxsize or 0 farmbuyer@61: end farmbuyer@61: end farmbuyer@66: end farmbuyer@66: farmbuyer@66: bumpers[17] = function() farmbuyer@66: for i,e in ipairs(OuroLootSV) do farmbuyer@66: if e.kind == 'loot' and e.is_heroic then farmbuyer@66: e.variant, e.is_heroic = 1, nil farmbuyer@66: -- Could try detecting any previous LFR loot here, but... gah farmbuyer@66: end farmbuyer@66: end farmbuyer@66: end farmbuyer@66: farmbuyer@69: bumpers[18] = bumpers[16] farmbuyer@69: -- In the not-very-many days between 16 and 19, I managed to break farmbuyer@69: -- the exact same data in the exact same way. At least they're farmbuyer@69: -- not actually running the same loop twice... probably... sigh. farmbuyer@69: farmbuyer@71: bumpers[19] = function() farmbuyer@73: local date = _G.date farmbuyer@71: for i,e in ipairs(OuroLootSV) do farmbuyer@73: if e.kind == 'loot' then farmbuyer@73: if e.history_unique then farmbuyer@73: e.unique, e.history_unique = e.history_unique, nil farmbuyer@73: end farmbuyer@73: if e.unique == nil or #e.unique == 0 then farmbuyer@73: e.unique = e.id .. ' ' .. date("%Y/%m/%d %H:%M",e.stamp) farmbuyer@73: end farmbuyer@71: end farmbuyer@71: end farmbuyer@71: end farmbuyer@71: farmbuyer@66: --[===[ farmbuyer@66: local real = bumpers farmbuyer@66: bumpers = newproxy(true) farmbuyer@66: local mt = getmetatable(bumpers) farmbuyer@66: mt.__index = real farmbuyer@66: mt.__gc = function() print"whadda ya know, garbage collection works" end ]===] farmbuyer@66: farmbuyer@66: while stored_datarev < opts.datarev do farmbuyer@66: self:Printf("Transitioning saved data format to %d...", stored_datarev+1) farmbuyer@66: dirty = true farmbuyer@66: bumpers[stored_datarev]() farmbuyer@66: stored_datarev = stored_datarev + 1 farmbuyer@61: end farmbuyer@56: if dirty then self:Print("Saved data has been massaged into shape.") end farmbuyer@55: end farmbuyer@55: farmbuyer@1: _init(self) farmbuyer@27: self.dprint('flow', "version strings:", revision_large, self.status_text) farmbuyer@67: self.load_assert = nil farmbuyer@66: self.OnInitialize = nil -- free up ALL the things! farmbuyer@1: end farmbuyer@1: farmbuyer@1: function addon:OnEnable() farmbuyer@10: self:RegisterEvent("PLAYER_LOGOUT") farmbuyer@10: self:RegisterEvent("RAID_ROSTER_UPDATE") farmbuyer@1: farmbuyer@73: -- Cribbed from Talented. I like the way jerry thinks: the first string farmbuyer@73: -- argument can be a format spec for the remainder of the arguments. farmbuyer@1: -- AceConsole:Printf isn't used because we can't specify a prefix without farmbuyer@73: -- jumping through ridonkulous hoops. The part about overriding :Print farmbuyer@1: -- with a version using prefix hyperlinks is my fault. farmbuyer@37: -- farmbuyer@73: -- CFPrint added instead of the usual Print testing of the first arg for farmbuyer@73: -- frame-ness, which would slow down all printing and only rarely be useful. farmbuyer@73: -- farmbuyer@37: -- There is no ITEM_QUALITY_LEGENDARY constant. Sigh. farmbuyer@1: do farmbuyer@1: local AC = LibStub("AceConsole-3.0") farmbuyer@76: local chat_prefix = self.format_hypertext ("Ouro Loot", --[[legendary]]5, farmbuyer@76: --[[empty -> nil -> main tab]]'') farmbuyer@76: local chat_prefix_s = tostring(chat_prefix) farmbuyer@1: function addon:Print (str, ...) farmbuyer@1: if type(str) == 'string' and str:find("%", nil, --[[plainmatch=]]true) then farmbuyer@76: return AC:Print (chat_prefix_s, str:format(...)) farmbuyer@1: else farmbuyer@76: return AC:Print (chat_prefix_s, str, ...) farmbuyer@1: end farmbuyer@1: end farmbuyer@73: function addon:CFPrint (frame, str, ...) farmbuyer@73: assert(type(frame)=='table' and frame.AddMessage) farmbuyer@73: if type(str) == 'string' and str:find("%", nil, --[[plainmatch=]]true) then farmbuyer@76: return AC:Print (frame, chat_prefix_s, str:format(...)) farmbuyer@73: else farmbuyer@76: return AC:Print (frame, chat_prefix_s, str, ...) farmbuyer@73: end farmbuyer@73: end farmbuyer@1: end farmbuyer@1: farmbuyer@51: while opts.keybinding do farmbuyer@51: if InCombatLockdown() then farmbuyer@76: local reload = self.format_hypertext ([[the options tab]], farmbuyer@76: ITEM_QUALITY_UNCOMMON, 'opt') farmbuyer@51: self:Print("Cannot create '%s' as a keybinding while in combat!", farmbuyer@51: opts.keybinding_text) farmbuyer@76: 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.", farmbuyer@76: tostring(reload)) farmbuyer@51: break farmbuyer@51: end farmbuyer@51: farmbuyer@15: KeyBindingFrame_LoadUI() farmbuyer@1: local btn = CreateFrame("Button", "OuroLootBindingOpen", nil, "SecureActionButtonTemplate") farmbuyer@1: btn:SetAttribute("type", "macro") farmbuyer@1: btn:SetAttribute("macrotext", "/ouroloot toggle") farmbuyer@1: if SetBindingClick(opts.keybinding_text, "OuroLootBindingOpen") then farmbuyer@73: -- a simple SaveBindings(GetCurrentBindingSet()) occasionally fails when farmbuyer@73: -- GCBS() decides to return neither 1 nor 2 during load, for reasons nobody farmbuyer@73: -- has ever learned farmbuyer@15: local c = GetCurrentBindingSet() farmbuyer@15: if c == ACCOUNT_BINDINGS or c == CHARACTER_BINDINGS then farmbuyer@15: SaveBindings(c) farmbuyer@15: end farmbuyer@1: else farmbuyer@51: self:Print("Error registering '%s' as a keybinding, check spelling!", farmbuyer@51: opts.keybinding_text) farmbuyer@1: end farmbuyer@51: break farmbuyer@1: end farmbuyer@1: farmbuyer@20: --[[ farmbuyer@20: The four loot format patterns of interest, changed into relatively tight farmbuyer@20: string match patterns. Done at enable-time rather than load-time against farmbuyer@20: the slim chance that one of the non-US "delocalizers" needs to mess with farmbuyer@20: the global patterns before we transform them. farmbuyer@20: farmbuyer@20: The SELF variants can be replaced with LOOT_ITEM_PUSHED_SELF[_MULTIPLE] to farmbuyer@20: trigger on 'receive item' instead, which would detect extracting stuff farmbuyer@20: from mail, or s/PUSHED/CREATED/ for things like healthstones and guild farmbuyer@20: cauldron flasks. farmbuyer@76: farmbuyer@76: ??? do something with LOOT_ITEM_WHILE_PLAYER_INELIGIBLE for locked LFRs? farmbuyer@20: ]] farmbuyer@20: farmbuyer@20: -- LOOT_ITEM = "%s receives loot: %s." --> (.+) receives loot: (.+)%. farmbuyer@20: g_LOOT_ITEM_ss = _G.LOOT_ITEM:gsub('%.$','%%.'):gsub('%%s','(.+)') farmbuyer@20: farmbuyer@20: -- LOOT_ITEM_MULTIPLE = "%s receives loot: %sx%d." --> (.+) receives loot: (.+)(x%d+)%. farmbuyer@20: g_LOOT_ITEM_MULTIPLE_sss = _G.LOOT_ITEM_MULTIPLE:gsub('%.$','%%.'):gsub('%%s','(.+)'):gsub('x%%d','(x%%d+)') farmbuyer@20: farmbuyer@20: -- LOOT_ITEM_SELF = "You receive loot: %s." --> You receive loot: (.+)%. farmbuyer@20: g_LOOT_ITEM_SELF_s = _G.LOOT_ITEM_SELF:gsub('%.$','%%.'):gsub('%%s','(.+)') farmbuyer@20: farmbuyer@20: -- LOOT_ITEM_SELF_MULTIPLE = "You receive loot: %sx%d." --> You receive loot: (.+)(x%d+)%. farmbuyer@20: g_LOOT_ITEM_SELF_MULTIPLE_ss = _G.LOOT_ITEM_SELF_MULTIPLE:gsub('%.$','%%.'):gsub('%%s','(.+)'):gsub('x%%d','(x%%d+)') farmbuyer@20: farmbuyer@44: --[[ farmbuyer@44: Stick something in the Blizzard addons options list, where most users farmbuyer@44: will probably look these days. Try to be conservative about needless farmbuyer@44: frame creation. farmbuyer@44: ]] farmbuyer@44: local bliz = CreateFrame("Frame") farmbuyer@44: bliz.name = "Ouro Loot" farmbuyer@44: bliz:SetScript("OnShow", function(_b) farmbuyer@44: local button = CreateFrame("Button",nil,_b,"UIPanelButtonTemplate") farmbuyer@44: button:SetWidth(150) farmbuyer@44: button:SetHeight(22) farmbuyer@44: button:SetScript("OnClick", function() farmbuyer@44: _G.InterfaceOptionsFrameCancel:Click() farmbuyer@44: _G.HideUIPanel(GameMenuFrame) farmbuyer@44: addon:OpenMainDisplayToTab"Options" farmbuyer@44: end) farmbuyer@44: button:SetText('"/ouroloot opt"') farmbuyer@44: button:SetPoint("TOPLEFT",20,-20) farmbuyer@44: _b:SetScript("OnShow",nil) farmbuyer@44: end) farmbuyer@44: _G.InterfaceOptions_AddCategory(bliz) farmbuyer@44: farmbuyer@73: -- Maybe load up g_uniques now? farmbuyer@73: if opts.precache_history_uniques then farmbuyer@73: self:_cache_history_uniques() farmbuyer@73: end farmbuyer@73: farmbuyer@49: self:_scan_LOD_modules() farmbuyer@49: farmbuyer@73: self:_set_remote_change_chatframe (opts.chatty_on_remote_changes_frame, --[[silent_p=]]true) farmbuyer@73: farmbuyer@1: if self.debug.flow then self:Print"is in control-flow debug mode." end farmbuyer@1: end farmbuyer@1: --function addon:OnDisable() end farmbuyer@1: farmbuyer@58: do farmbuyer@58: local prototype = {} farmbuyer@58: local function module_OnEnable (plugin) farmbuyer@58: if plugin.option_defaults then farmbuyer@58: local SVname = 'OuroLoot'..plugin:GetName()..'_opts' farmbuyer@58: if not _G[SVname] then farmbuyer@58: _G[SVname] = {} farmbuyer@58: if type(plugin.OnFirstTime) == 'function' then farmbuyer@58: plugin:OnFirstTime() farmbuyer@58: end farmbuyer@58: end farmbuyer@58: plugin.opts = _G[SVname] farmbuyer@58: for option,default in pairs(plugin.option_defaults) do farmbuyer@58: if plugin.opts[option] == nil then farmbuyer@58: plugin.opts[option] = default farmbuyer@58: end farmbuyer@58: end farmbuyer@58: plugin.option_defaults = nil farmbuyer@58: end farmbuyer@58: end farmbuyer@58: farmbuyer@58: -- By default, no plugins. First plugin to use the special registration farmbuyer@58: -- sets up code for any subsequent plugins. farmbuyer@58: addon.is_plugin = flib.nullfunc farmbuyer@58: local function module_rtg (plugin, text_type, ...) farmbuyer@58: local registry = { [text_type]=plugin } farmbuyer@58: addon.is_plugin = function(a,t) return registry[t] end farmbuyer@58: prototype.register_text_generator = function(p,t,...) farmbuyer@58: registry[t] = p farmbuyer@58: return addon:register_text_generator(t,...) farmbuyer@58: end farmbuyer@58: return addon:register_text_generator(text_type,...) farmbuyer@58: end farmbuyer@58: farmbuyer@58: prototype.OnEnable = module_OnEnable farmbuyer@58: prototype.default_OnEnable = module_OnEnable farmbuyer@58: prototype.register_text_generator = module_rtg farmbuyer@58: farmbuyer@58: addon:SetDefaultModuleLibraries("AceConsole-3.0") farmbuyer@58: addon:SetDefaultModulePrototype(prototype) farmbuyer@58: -- Fires before the plugin's own OnEnable (inherited or otherwise). farmbuyer@58: --function addon:OnModuleCreated (plugin) farmbuyer@58: -- print("created plugin", plugin:GetName()) farmbuyer@58: --end farmbuyer@63: farmbuyer@63: local olrev = tonumber("@project-revision@") or 0 farmbuyer@64: local err = [[Module '%s' cannot register itself because it failed a required condition: '%s']] farmbuyer@63: function addon:ConstrainedNewModule (modname, minrev, mincomm, mindata) farmbuyer@63: if not addon.author_debug then farmbuyer@64: if minrev and minrev > olrev then farmbuyer@63: self:Print(err,modname, farmbuyer@63: "revision "..olrev.." older than minimum "..minrev) farmbuyer@63: return false farmbuyer@63: end farmbuyer@64: if mincomm and mincomm > tonumber(self.commrev) then farmbuyer@63: self:Print(err,modname, farmbuyer@63: "commrev "..self.commrev.." older than minimum "..mincomm) farmbuyer@63: return false farmbuyer@63: end farmbuyer@64: if mindata and mindata > opts.datarev then farmbuyer@63: self:Print(err,modname, farmbuyer@63: "datarev "..opts.datarev.." older than minimum "..mindata) farmbuyer@63: return false farmbuyer@63: end farmbuyer@63: end farmbuyer@63: return self:NewModule(modname) farmbuyer@63: end farmbuyer@58: end farmbuyer@58: farmbuyer@1: farmbuyer@1: ------ Event handlers farmbuyer@1: function addon:_clear_SVs() farmbuyer@1: g_loot = {} -- not saved, just fooling PLAYER_LOGOUT tests farmbuyer@73: _G.OuroLootSV = nil farmbuyer@73: _G.OuroLootSV_saved = nil farmbuyer@73: _G.OuroLootSV_opts = nil farmbuyer@73: _G.OuroLootSV_hist = nil farmbuyer@73: _G.OuroLootSV_log = nil farmbuyer@73: _G.ReloadUI() farmbuyer@1: end farmbuyer@1: function addon:PLAYER_LOGOUT() farmbuyer@16: self:UnregisterEvent("RAID_ROSTER_UPDATE") farmbuyer@16: self:UnregisterEvent("PLAYER_ENTERING_WORLD") farmbuyer@16: farmbuyer@73: local worth_saving = #g_loot > 0 or _G.next(g_loot.raiders) farmbuyer@16: if not worth_saving then for text in self:registered_textgen_iter() do farmbuyer@16: worth_saving = worth_saving or g_loot.printed[text] > 0 farmbuyer@16: end end farmbuyer@16: if worth_saving then farmbuyer@16: opts.autoshard = self.sharder farmbuyer@16: opts.threshold = self.threshold farmbuyer@1: for i,e in ipairs(g_loot) do farmbuyer@1: e.cols = nil farmbuyer@1: end farmbuyer@73: _G.OuroLootSV = g_loot farmbuyer@16: else farmbuyer@73: _G.OuroLootSV = nil farmbuyer@1: end farmbuyer@16: farmbuyer@38: worth_saving = false farmbuyer@6: for r,t in pairs(self.history_all) do if type(t) == 'table' then farmbuyer@8: if #t == 0 then farmbuyer@8: self.history_all[r] = nil farmbuyer@8: else farmbuyer@38: worth_saving = true farmbuyer@8: t.realm = nil farmbuyer@8: t.st = nil farmbuyer@8: t.byname = nil farmbuyer@8: end farmbuyer@6: end end farmbuyer@38: if worth_saving then farmbuyer@73: _G.OuroLootSV_hist = self.history_all farmbuyer@73: _G.OuroLootSV_hist.HISTFORMAT = 4 farmbuyer@38: else farmbuyer@73: _G.OuroLootSV_hist = nil farmbuyer@38: end farmbuyer@73: _G.OuroLootSV_log = #_G.OuroLootSV_log > 0 and _G.OuroLootSV_log or nil farmbuyer@1: end farmbuyer@1: farmbuyer@10: do farmbuyer@67: local IsInInstance, UnitIsConnected, UnitClass, UnitRace, UnitSex, farmbuyer@56: UnitLevel, UnitInRaid, UnitIsVisible, GetGuildInfo, GetRaidRosterInfo = farmbuyer@67: IsInInstance, UnitIsConnected, UnitClass, UnitRace, UnitSex, farmbuyer@56: UnitLevel, UnitInRaid, UnitIsVisible, GetGuildInfo, GetRaidRosterInfo farmbuyer@10: local time, difftime = time, difftime farmbuyer@10: local R_ACTIVE, R_OFFLINE, R_LEFT = 1, 2, 3 farmbuyer@10: farmbuyer@10: local lastevent, now = 0, 0 farmbuyer@16: local redo_count = 0 farmbuyer@16: local redo, timer_handle farmbuyer@10: farmbuyer@10: function addon:CheckRoster (leaving_p, now_a) farmbuyer@10: if not g_loot.raiders then return end -- bad transition farmbuyer@10: farmbuyer@10: now = now_a or time() farmbuyer@10: farmbuyer@10: if leaving_p then farmbuyer@16: if timer_handle then farmbuyer@16: self:CancelTimer(timer_handle) farmbuyer@16: timer_handle = nil farmbuyer@16: end farmbuyer@10: for name,r in pairs(g_loot.raiders) do farmbuyer@10: r.leave = r.leave or now farmbuyer@10: end farmbuyer@10: return farmbuyer@10: end farmbuyer@10: farmbuyer@10: for name,r in pairs(g_loot.raiders) do farmbuyer@10: if r.online ~= R_LEFT and not UnitInRaid(name) then farmbuyer@10: r.online = R_LEFT farmbuyer@10: r.leave = now farmbuyer@10: end farmbuyer@10: end farmbuyer@10: farmbuyer@16: if redo then farmbuyer@16: redo_count = redo_count + 1 farmbuyer@16: end farmbuyer@16: redo = false farmbuyer@10: for i = 1, GetNumRaidMembers() do farmbuyer@10: local unit = 'raid'..i farmbuyer@67: -- We grab a bunch of return values here, but only pay attention to farmbuyer@67: -- them under specific circumstances. farmbuyer@67: local name, connected, subgroup, level, class, _ farmbuyer@67: name, _, subgroup, level, _, class, connected = GetRaidRosterInfo(i) farmbuyer@10: -- No, that's not my typo, it really is "uknownbeing" in Blizzard's code. farmbuyer@10: if name and name ~= UNKNOWN and name ~= UNKNOWNOBJECT and name ~= UKNOWNBEING then farmbuyer@10: if not g_loot.raiders[name] then farmbuyer@10: g_loot.raiders[name] = { needinfo=true } farmbuyer@10: end farmbuyer@10: local r = g_loot.raiders[name] farmbuyer@68: r.subgroup = subgroup or (NUM_RAID_GROUPS+1) farmbuyer@10: if r.needinfo and UnitIsVisible(unit) then farmbuyer@10: r.needinfo = nil farmbuyer@56: r.class = class --select(2,UnitClass(unit)) farmbuyer@10: r.race = select(2,UnitRace(unit)) farmbuyer@10: r.sex = UnitSex(unit) farmbuyer@56: r.level = level --UnitLevel(unit) farmbuyer@10: r.guild = GetGuildInfo(unit) farmbuyer@10: end farmbuyer@56: --local connected = UnitIsConnected(unit) farmbuyer@10: if connected and r.online ~= R_ACTIVE then farmbuyer@10: r.join = r.join or now farmbuyer@10: r.online = R_ACTIVE farmbuyer@10: elseif (not connected) and r.online ~= R_OFFLINE then farmbuyer@10: r.leave = now farmbuyer@10: r.online = R_OFFLINE farmbuyer@10: end farmbuyer@10: redo = redo or r.needinfo farmbuyer@10: end farmbuyer@10: end farmbuyer@16: if redo then -- XXX test redo_count here and eventually give up? farmbuyer@16: if not timer_handle then farmbuyer@16: timer_handle = self:ScheduleRepeatingTimer("RAID_ROSTER_UPDATE", 60) farmbuyer@16: end farmbuyer@16: else farmbuyer@16: redo_count = 0 farmbuyer@16: if timer_handle then farmbuyer@16: self:CancelTimer(timer_handle) farmbuyer@16: timer_handle = nil farmbuyer@16: end farmbuyer@10: end farmbuyer@10: end farmbuyer@10: farmbuyer@10: function addon:RAID_ROSTER_UPDATE (event) farmbuyer@10: if GetNumRaidMembers() == 0 then farmbuyer@16: -- Leaving a raid group. farmbuyer@10: -- Because of PLAYER_ENTERING_WORLD, this code also executes on load farmbuyer@10: -- screens while soloing and in regular groups. Take care. farmbuyer@16: self.dprint('flow', "GetNumRaidMembers == 0") farmbuyer@16: if self.enabled and not self.debug.notraid then farmbuyer@16: self.dprint('flow', "enabled, leaving raid") farmbuyer@10: self.popped = nil farmbuyer@16: self:Deactivate() -- self:UnregisterEvent("CHAT_MSG_LOOT") farmbuyer@10: self:CheckRoster(--[[leaving raid]]true) farmbuyer@10: end farmbuyer@10: return farmbuyer@10: end farmbuyer@10: farmbuyer@1: local inside,whatkind = IsInInstance() farmbuyer@1: if inside and (whatkind == "pvp" or whatkind == "arena") then farmbuyer@41: self.dprint('flow', "got RRU event but in pvp zone, bailing") farmbuyer@41: return farmbuyer@1: end farmbuyer@10: farmbuyer@10: local docheck = self.enabled farmbuyer@1: if event == "Activate" then farmbuyer@1: -- dispatched manually from Activate farmbuyer@10: self:RegisterEvent("CHAT_MSG_LOOT") farmbuyer@2: _register_bossmod(self) farmbuyer@10: docheck = true farmbuyer@1: elseif event == "RAID_ROSTER_UPDATE" then farmbuyer@10: -- hot code path, be careful farmbuyer@10: farmbuyer@1: -- event registration from onload, joined a raid, maybe show popup farmbuyer@16: self.dprint('flow', "RRU check:", self.popped, opts.popup_on_join) farmbuyer@10: if (not self.popped) and opts.popup_on_join then farmbuyer@1: self.popped = StaticPopup_Show "OUROL_REMIND" farmbuyer@1: self.popped.data = self farmbuyer@10: return farmbuyer@1: end farmbuyer@1: end farmbuyer@11: -- Throttle the checks fired by common events. farmbuyer@10: if docheck and not InCombatLockdown() then farmbuyer@10: now = time() farmbuyer@10: if difftime(now,lastevent) > 45 then farmbuyer@10: lastevent = now farmbuyer@10: self:CheckRoster(false,now) farmbuyer@10: end farmbuyer@10: end farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@73: --[=[ CHAT_MSG_LOOT handler and its helpers. farmbuyer@73: Situations for "unique tag" generation, given N people seeing local loot farmbuyer@73: events, M people seeing remote rebroadcasts, and player Z adding manually: farmbuyer@73: farmbuyer@73: + Local tracking: All LOCALs should see the same itemstring, thus the same farmbuyer@73: unique ID stripped out of field #9. LOCALn includes this in the broadcast farmbuyer@73: to REMOTEm. Tag is a large number, meaningless for clients and players. farmbuyer@73: farmbuyer@73: + Local broadcasting, remote tracking: same as local tracking. Possibly farmbuyer@73: some weirdness if all local versions are significantly older than the remote farmbuyer@73: versions; in this case each REMOTEn will generate their own tags of the form farmbuyer@73: itemID+formatted_date, which will not be "unique" for the next 60 seconds. farmbuyer@73: As long as at least one LOCALn is recent enough to strip and broadcast a farmbuyer@73: proper ID, multiple items of the same visible name will not be "lost". farmbuyer@73: farmbuyer@73: + Z manually inserts a loot entry: Z generates a tag, preserved locally. farmbuyer@73: If Z rebrodcasts that entry, all REMOTEs will see it. Tag is of the form farmbuyer@73: "n" followed by a random number. farmbuyer@73: ]=] farmbuyer@1: do farmbuyer@73: local counter, _do_loot farmbuyer@73: do farmbuyer@73: local count = 0 farmbuyer@73: function counter() count = count + 1; return count; end farmbuyer@73: end farmbuyer@73: farmbuyer@42: local function maybe_trash_kill_entry() farmbuyer@42: -- this is set on various boss interactions, so we've got a kill/wipe farmbuyer@73: -- entry already -- XXX maybe clear it after a delay, so that loot farmbuyer@73: -- from trash after a boss isn't grouped with that boss? farmbuyer@42: if addon.latest_instance then return end farmbuyer@69: --addon.latest_instance = instance_tag() farmbuyer@69: local ss, max, inst = addon:snapshot_raid() farmbuyer@69: addon.latest_instance = inst farmbuyer@69: addon:_mark_boss_kill (addon._addBossEntry{ farmbuyer@61: kind='boss', reason='kill', bossname=[[trash]], farmbuyer@61: instance=addon.latest_instance, duration=0, farmbuyer@61: raidersnap=ss, maxsize=max farmbuyer@42: }) farmbuyer@42: end farmbuyer@42: farmbuyer@76: -- Alert other trackers that unique tag EXISTING in subsequent 'casts farmbuyer@76: -- should be replaced by REPLACE instead. If multiple players all saw farmbuyer@76: -- the same loot event, this will cause a flurry of cross-improvs. farmbuyer@76: local function _announce_unique_improvisation (existing, replace) farmbuyer@76: if not g_unique_replace then _setup_unique_replace() end farmbuyer@76: g_unique_replace.new_entry (g_unique_replace.me, existing, replace, 'improv') farmbuyer@76: addon:vbroadcast('improv', g_unique_replace.me, existing, replace) farmbuyer@76: end farmbuyer@76: farmbuyer@73: local random = _G.math.random farmbuyer@76: local function _many_uniques_handle_it (u, prefix) farmbuyer@76: if u then farmbuyer@73: -- Check and alert for an existing value. farmbuyer@73: u = tostring(u) farmbuyer@73: if g_uniques[u].history ~= g_uniques.NOTFOUND then farmbuyer@76: if not g_unique_replace then _setup_unique_replace() end farmbuyer@76: local maybe = g_unique_replace.get_previous_replacement (u) farmbuyer@76: if maybe then farmbuyer@76: addon.dprint('loot',"previous replaced tag ("..u farmbuyer@76: ..") with ("..maybe.."), using that instead") farmbuyer@76: return false, u, maybe farmbuyer@76: end farmbuyer@76: local can_replace_p,improv = _many_uniques_handle_it (nil, 'c') farmbuyer@76: if can_replace_p then farmbuyer@76: _announce_unique_improvisation (u, improv) farmbuyer@76: return false, u, improv farmbuyer@76: end farmbuyer@76: return false, u farmbuyer@73: end farmbuyer@73: addon.dprint('loot',"verified unique tag ("..u..")") farmbuyer@73: else farmbuyer@73: -- Need to *find* an unused value. For now use a range of farmbuyer@73: -- J*10^4 where J is Jenny's Constant. Thank you, xkcd.com/1047. farmbuyer@76: prefix = prefix or 'n' farmbuyer@73: repeat farmbuyer@76: u = prefix .. random(8675309) farmbuyer@76: until g_uniques:TEST(u).history == g_uniques.NOTFOUND farmbuyer@76: addon.dprint('loot',"created unique tag ("..u..")") farmbuyer@73: end farmbuyer@73: return true, u farmbuyer@73: end farmbuyer@73: farmbuyer@1: -- Recent loot cache farmbuyer@25: local candidates = {} farmbuyer@73: local sigmap = {} farmbuyer@73: local function preempt_older_signature (oldersig, newersig) farmbuyer@76: --pprint("preempt", oldersig, "::", newersig) farmbuyer@76: local origin = candidates[oldersig] and candidates[oldersig].bcast_from farmbuyer@76: --pprint("preempt", "candidate", candidates[oldersig], "bcast:", origin) farmbuyer@73: if origin and g_seeing_oldsigs[origin] then farmbuyer@73: -- replace entry from older client with this newer one farmbuyer@73: candidates[oldersig] = nil farmbuyer@76: addon.dprint('loot', "preempting signature <", oldersig, "> from", origin) farmbuyer@73: end farmbuyer@73: return false farmbuyer@73: end farmbuyer@73: farmbuyer@25: local function prefer_local_loots (cache) farmbuyer@25: -- The function name is a bit of a misnomer, as local entries overwrite farmbuyer@25: -- remote entries as the candidate table is populated. This routine is farmbuyer@39: -- here to extract the final results once the cache timers have expired. farmbuyer@38: -- farmbuyer@34: -- Keep this sync'd with the local_override branch below. farmbuyer@25: for i,sig in ipairs(candidates) do farmbuyer@25: addon.dprint('loot', "processing candidate entry", i, sig) farmbuyer@25: local loot = candidates[sig] farmbuyer@25: if loot then farmbuyer@42: maybe_trash_kill_entry() -- Generate *some* kind of boss/location entry farmbuyer@25: candidates[sig] = nil farmbuyer@25: local looti = addon._addLootEntry(loot) farmbuyer@73: addon.dprint('loot', "entry", i, "was found, added at index", looti) farmbuyer@25: if (loot.disposition ~= 'shard') farmbuyer@25: and (loot.disposition ~= 'gvault') farmbuyer@25: and (not addon.history_suppress) farmbuyer@25: then farmbuyer@25: addon:_addHistoryEntry(looti) farmbuyer@76: elseif #loot.unique > 0 then farmbuyer@76: g_uniques[loot.unique] = -- stub entry farmbuyer@76: { loot = looti, history = g_uniques.NOTFOUND } farmbuyer@25: end farmbuyer@25: end farmbuyer@25: end farmbuyer@25: farmbuyer@25: if addon.display then farmbuyer@25: addon:redisplay() farmbuyer@25: end farmbuyer@47: wipe(candidates) farmbuyer@73: wipe(sigmap) farmbuyer@25: end farmbuyer@73: local recent_loot = create_new_cache ('loot', comm_cleanup_ttl+3, prefer_local_loots) farmbuyer@1: farmbuyer@73: local strsplit, GetItemInfo, GetItemIcon, UnitClass = farmbuyer@73: _G.strsplit, _G.GetItemInfo, _G.GetItemIcon, _G.UnitClass farmbuyer@1: farmbuyer@71: -- 'from' only present if this is triggered by a broadcast farmbuyer@73: function _do_loot (self, local_override, recipient, unique, itemid, count, from, extratext) farmbuyer@73: local prefix = "_do_loot[" .. counter() .. "]" farmbuyer@6: local itexture = GetItemIcon(itemid) farmbuyer@6: local iname, ilink, iquality = GetItemInfo(itemid) farmbuyer@73: local cache_miss farmbuyer@6: if (not iname) or (not itexture) then farmbuyer@73: cache_miss = true farmbuyer@6: iname, ilink, iquality, itexture = farmbuyer@6: UNKNOWN..': '..itemid, 'item:6948', ITEM_QUALITY_COMMON, [[ICONS\INV_Misc_QuestionMark]] farmbuyer@6: end farmbuyer@73: self.dprint('loot',">>"..prefix, "R:", recipient, "U:", unique, "I:", farmbuyer@71: itemid, "C:", count, "frm:", from, "ex:", extratext, "q:", iquality) farmbuyer@1: farmbuyer@19: itemid = tonumber(ilink:match("item:(%d+)") or 0) farmbuyer@73: farmbuyer@73: -- This is only a 'while' to make jumping out of it easy. farmbuyer@76: local i, unique_okay, replacement, ret1, ret2 farmbuyer@73: while local_override farmbuyer@73: or ((iquality >= self.threshold) and not opts.itemfilter[itemid]) farmbuyer@73: do farmbuyer@76: unique_okay, unique, replacement = farmbuyer@76: _many_uniques_handle_it ((not local_override) and unique) farmbuyer@73: if not unique_okay then farmbuyer@76: if replacement then farmbuyer@76: -- collision, but we've generated a placeholder for now farmbuyer@76: -- and broadcast the fact farmbuyer@76: self.dprint('loot', "substituting", unique, "with", replacement) farmbuyer@76: else farmbuyer@76: i = g_uniques[unique] farmbuyer@76: local err = unique_collision:format (ERROR_CAPS, iname, unique, farmbuyer@76: tostring(from), tostring(i.loot), tostring(i.history)) farmbuyer@77: _unavoidable_collision (err) farmbuyer@76: -- Make sure this is logged one way or another farmbuyer@76: ;(self.debug.loot and self.dprint or pprint)('loot', "COLLISION", prefix, err); farmbuyer@76: ret1, ret2 = nil, err farmbuyer@76: break farmbuyer@76: end farmbuyer@73: end farmbuyer@73: farmbuyer@1: if (self.rebroadcast and (not from)) and not local_override then farmbuyer@71: self:vbroadcast('loot', recipient, unique, itemid, count) farmbuyer@1: end farmbuyer@25: if (not self.enabled) and (not local_override) then break end farmbuyer@73: farmbuyer@73: local oldersig = recipient .. iname .. (count or "") farmbuyer@73: local signature, seenit farmbuyer@73: if #unique > 0 then farmbuyer@73: -- newer case farmbuyer@73: signature = unique .. oldersig farmbuyer@76: --pprint("newer", "mapping older <", oldersig, "> to newer <", signature, ">") farmbuyer@73: sigmap[oldersig] = signature farmbuyer@76: --pprint("newer", "testing recent for", signature, "yields", recent_loot:test(signature)) farmbuyer@76: seenit = (from and recent_loot:test(signature)) farmbuyer@73: -- The following clause is what handles older 'casts arriving farmbuyer@73: -- earlier. All this is tested inside-out to maximize short farmbuyer@76: -- circuit evaluation; the preempt function always returns farmbuyer@76: -- false to force seenit off. farmbuyer@76: or (g_seeing_oldsigs and preempt_older_signature(oldersig,signature)) farmbuyer@25: else farmbuyer@73: -- older case, only remote farmbuyer@73: assert(from) farmbuyer@73: signature = sigmap[oldersig] or oldersig farmbuyer@76: --pprint("older", "testing signature will be", signature) farmbuyer@73: seenit = recent_loot:test(signature) farmbuyer@73: end farmbuyer@73: farmbuyer@73: if seenit then farmbuyer@73: self.dprint('cache', "remote", prefix, "<", signature, farmbuyer@73: "> already in cache, skipping from", from) farmbuyer@73: break farmbuyer@73: end farmbuyer@73: farmbuyer@73: -- There is some redundancy in all this, in the interests of ease-of-coding farmbuyer@73: i = { farmbuyer@73: kind = 'loot', farmbuyer@73: person = recipient, farmbuyer@73: person_class= select(2,UnitClass(recipient)), farmbuyer@73: cache_miss = cache_miss, farmbuyer@73: quality = iquality, farmbuyer@73: itemname = iname, farmbuyer@73: id = itemid, farmbuyer@73: itemlink = ilink, farmbuyer@73: itexture = itexture, farmbuyer@76: unique = replacement or unique, farmbuyer@73: count = (count and count ~= "") and count or nil, farmbuyer@73: bcast_from = from, farmbuyer@73: extratext = extratext, farmbuyer@73: variant = self:is_variant_item(ilink), farmbuyer@73: } farmbuyer@73: if opts.itemvault[itemid] then farmbuyer@73: i.disposition = 'gvault' farmbuyer@73: elseif recipient == self.sharder then farmbuyer@73: i.disposition = 'shard' farmbuyer@73: end farmbuyer@73: if local_override then farmbuyer@73: -- player is adding loot by hand, don't wait for network cache timeouts farmbuyer@73: -- keep this sync'd with prefer_local_loots above farmbuyer@73: if i.extratext == 'shard' farmbuyer@73: or i.extratext == 'gvault' farmbuyer@73: or i.extratext == 'offspec' farmbuyer@73: then farmbuyer@73: i.disposition = i.extratext farmbuyer@70: end farmbuyer@73: local looti = self._addLootEntry(i) farmbuyer@73: if (i.disposition ~= 'shard') farmbuyer@73: and (i.disposition ~= 'gvault') farmbuyer@73: and (not self.history_suppress) farmbuyer@73: then farmbuyer@73: self:_addHistoryEntry(looti) farmbuyer@76: else farmbuyer@76: g_uniques[i.unique] = -- stub entry farmbuyer@76: { loot = looti, history = g_uniques.NOTFOUND } farmbuyer@34: end farmbuyer@73: ret1 = looti -- return value mostly for gui's manual entry farmbuyer@76: self.dprint('loot', "manual", looti) farmbuyer@73: else farmbuyer@73: recent_loot:add(signature) farmbuyer@73: candidates[signature] = i farmbuyer@73: tinsert (candidates, signature) farmbuyer@73: self.dprint('cache', prefix, "<", signature, farmbuyer@73: "> added to cache as candidate", #candidates) farmbuyer@1: end farmbuyer@25: break farmbuyer@1: end farmbuyer@73: self.dprint('loot',"<<"..prefix, "out") farmbuyer@73: return ret1, ret2 farmbuyer@1: end farmbuyer@1: farmbuyer@73: -- Returns the index of the resulting new loot entry, or nil after farmbuyer@73: -- displaying any errors. farmbuyer@1: function addon:CHAT_MSG_LOOT (event, ...) farmbuyer@1: if (not self.rebroadcast) and (not self.enabled) and (event ~= "manual") then return end farmbuyer@1: farmbuyer@1: --[[ farmbuyer@1: iname: Hearthstone farmbuyer@1: iquality: integer farmbuyer@1: ilink: clickable formatted link farmbuyer@1: itemstring: item:6948:.... farmbuyer@1: itexture: inventory icon texture farmbuyer@1: ]] farmbuyer@1: farmbuyer@1: if event == "CHAT_MSG_LOOT" then farmbuyer@1: local msg = ... farmbuyer@20: local person, itemstring, count farmbuyer@21: --ChatFrame2:AddMessage("original string: >"..(msg:gsub("\124","\124\124")).."<") farmbuyer@20: farmbuyer@20: -- test in most likely order: other people get more loot than "you" do farmbuyer@20: person, itemstring, count = msg:match(g_LOOT_ITEM_MULTIPLE_sss) farmbuyer@20: if not person then farmbuyer@20: person, itemstring = msg:match(g_LOOT_ITEM_ss) farmbuyer@20: end farmbuyer@20: if not person then farmbuyer@20: itemstring, count = msg:match(g_LOOT_ITEM_SELF_MULTIPLE_ss) farmbuyer@20: if not itemstring then farmbuyer@20: itemstring = msg:match(g_LOOT_ITEM_SELF_s) farmbuyer@20: end farmbuyer@20: end farmbuyer@20: farmbuyer@65: self.dprint('loot', "CHAT_MSG_LOOT, person is", person, farmbuyer@65: ", itemstring is", itemstring, ", count is", count) farmbuyer@73: if not itemstring then return end -- "PlayerX selected Greed", etc, not looting farmbuyer@1: farmbuyer@1: -- Name might be colorized, remove the highlighting farmbuyer@20: if person then farmbuyer@20: person = person:match("|c%x%x%x%x%x%x%x%x(%S+)") or person farmbuyer@20: else farmbuyer@38: person = my_name -- UNIT_YOU / You farmbuyer@20: end farmbuyer@1: farmbuyer@71: --local id = tonumber(itemstring:match('|Hitem:(%d+):')) farmbuyer@71: local id,unique,_ farmbuyer@71: _,id,_,_,_,_,_,_,unique = strsplit (":", itemstring) farmbuyer@71: if unique == 0 then unique = nil end farmbuyer@1: farmbuyer@73: return _do_loot (self, false, person, unique, id, count) farmbuyer@1: farmbuyer@1: elseif event == "broadcast" then farmbuyer@73: return _do_loot(self, false, ...) farmbuyer@1: farmbuyer@1: elseif event == "manual" then farmbuyer@1: local r,i,n = ... farmbuyer@73: return _do_loot(self, true, r, --[[unique=]]nil, i, farmbuyer@71: --[[count=]]nil, --[[from=]]nil, n) farmbuyer@1: end farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@1: farmbuyer@1: ------ Slash command handler farmbuyer@1: -- Thought about breaking this up into a table-driven dispatcher. But farmbuyer@1: -- that would result in a pile of teensy functions, most of which would farmbuyer@1: -- never be called. Too much overhead. (2.0: Most of these removed now farmbuyer@1: -- that GUI is in place.) farmbuyer@76: do farmbuyer@76: local green_help_link = addon.format_hypertext ([[Click here]], farmbuyer@76: ITEM_QUALITY_UNCOMMON, 'help') farmbuyer@76: function addon:OnSlash (txt) --, editbox) farmbuyer@76: txt = strtrim(txt:lower()) farmbuyer@76: local cmd, arg = "" farmbuyer@76: do farmbuyer@76: local s,e = txt:find("^%a+") farmbuyer@76: if s then farmbuyer@76: cmd = txt:sub(s,e) farmbuyer@76: s = txt:find("%S", e+2) farmbuyer@76: if s then arg = txt:sub(s,-1) end farmbuyer@76: end farmbuyer@1: end farmbuyer@1: farmbuyer@76: if cmd == "" then farmbuyer@76: if InCombatLockdown() then farmbuyer@76: return self:Print("Shouldn't display window in combat.") farmbuyer@76: else farmbuyer@76: return self:BuildMainDisplay() farmbuyer@76: end farmbuyer@1: farmbuyer@76: elseif cmd:find("^thre") then farmbuyer@76: self:SetThreshold(arg) farmbuyer@1: farmbuyer@76: elseif cmd == "on" then self:Activate(arg) farmbuyer@76: elseif cmd == "off" then self:Deactivate() farmbuyer@76: elseif cmd == "broadcast" or cmd == "bcast" then self:Activate(nil,true) farmbuyer@76: farmbuyer@76: elseif cmd == "toggle" then farmbuyer@76: if self.display then farmbuyer@76: self.display:Hide() farmbuyer@76: else farmbuyer@76: return self:BuildMainDisplay() farmbuyer@76: end farmbuyer@76: farmbuyer@76: elseif cmd == "fake" then -- maybe comment this out for real users farmbuyer@76: self:_mark_boss_kill (self._addBossEntry{ farmbuyer@76: kind='boss',reason='kill',bossname="Baron Steamroller",duration=0 farmbuyer@76: }) farmbuyer@76: self:CHAT_MSG_LOOT ('manual', my_name, 54797) farmbuyer@76: if self.display then farmbuyer@76: self:redisplay() farmbuyer@76: end farmbuyer@76: self:Print "Baron Steamroller has been slain. Congratulations on your rug." farmbuyer@76: farmbuyer@76: elseif cmd == "debug" then farmbuyer@76: if arg then farmbuyer@76: self.debug[arg] = not self.debug[arg] farmbuyer@76: _G.print(arg,self.debug[arg]) farmbuyer@76: if self.debug[arg] then self.DEBUG_PRINT = true end farmbuyer@76: else farmbuyer@76: self.DEBUG_PRINT = not self.DEBUG_PRINT farmbuyer@76: end farmbuyer@76: farmbuyer@76: elseif cmd == "save" and arg and arg:len() > 0 then farmbuyer@76: self:save_saveas(arg) farmbuyer@76: elseif cmd == "list" then farmbuyer@76: self:save_list() farmbuyer@76: elseif cmd == "restore" and arg and arg:len() > 0 then farmbuyer@76: self:save_restore(tonumber(arg)) farmbuyer@76: elseif cmd == "delete" and arg and arg:len() > 0 then farmbuyer@76: self:save_delete(tonumber(arg)) farmbuyer@76: farmbuyer@76: elseif cmd == "help" then farmbuyer@76: self:BuildMainDisplay('help') farmbuyer@76: elseif cmd == "ping" then farmbuyer@76: self:DoPing() farmbuyer@76: farmbuyer@76: elseif cmd == "fixcache" then farmbuyer@76: self:do_item_cache_fixup() farmbuyer@76: farmbuyer@23: else farmbuyer@76: if self:OpenMainDisplayToTab(cmd) then farmbuyer@76: return farmbuyer@76: end farmbuyer@76: self:Print("Unknown command '%s'. %s to see the help window.", farmbuyer@76: cmd, tostring(green_help_link)) farmbuyer@23: end farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@1: function addon:SetThreshold (arg, quiet_p) farmbuyer@1: local q = tonumber(arg) farmbuyer@1: if q then farmbuyer@76: q = math.floor(q+0.1) farmbuyer@1: if q<0 or q>6 then farmbuyer@1: return self:Print("Threshold must be 0-6.") farmbuyer@1: end farmbuyer@1: else farmbuyer@1: q = qualnames[arg] farmbuyer@1: if not q then farmbuyer@1: return self:Print("Unrecognized item quality argument.") farmbuyer@1: end farmbuyer@1: end farmbuyer@1: self.threshold = q farmbuyer@1: if not quiet_p then self:Print("Threshold now set to %s.", self.thresholds[q]) end farmbuyer@1: end farmbuyer@1: farmbuyer@1: farmbuyer@1: ------ On/off farmbuyer@1: function addon:Activate (opt_threshold, opt_bcast_only) farmbuyer@16: self.dprint('flow', ":Activate is running") farmbuyer@10: self:RegisterEvent("RAID_ROSTER_UPDATE") farmbuyer@16: self:RegisterEvent("PLAYER_ENTERING_WORLD", farmbuyer@16: function() self:ScheduleTimer("RAID_ROSTER_UPDATE", 5, "PLAYER_ENTERING_WORLD") end) farmbuyer@1: self.popped = true farmbuyer@1: if GetNumRaidMembers() > 0 then farmbuyer@16: self.dprint('flow', ">:Activate calling RRU") farmbuyer@1: self:RAID_ROSTER_UPDATE("Activate") farmbuyer@1: elseif self.debug.notraid then farmbuyer@76: self.dprint('flow', ">:(notraid) Activate registering loot and bossmods") farmbuyer@10: self:RegisterEvent("CHAT_MSG_LOOT") farmbuyer@2: _register_bossmod(self) farmbuyer@1: elseif g_restore_p then farmbuyer@1: g_restore_p = nil farmbuyer@16: self.popped = nil -- get the reminder if later joining a raid farmbuyer@16: if #g_loot == 0 then farmbuyer@16: -- only generated text and raider join/leave data, not worth verbage farmbuyer@16: self.dprint('flow', ">:Activate restored generated texts, un-popping") farmbuyer@16: return farmbuyer@16: end farmbuyer@78: self:Print("Restored previous data, but not in a raid", farmbuyer@48: "and 5-player mode not active. |cffff0505NOT tracking loot|r;", farmbuyer@1: "use 'enable' to activate loot tracking, or 'clear' to erase", farmbuyer@1: "previous data, or 'help' to read about saved-texts commands.") farmbuyer@1: return farmbuyer@1: end farmbuyer@1: self.rebroadcast = true -- hardcode to true; this used to be more complicated farmbuyer@1: self.enabled = not opt_bcast_only farmbuyer@73: g_seeing_oldsigs = nil farmbuyer@1: if opt_threshold then farmbuyer@1: self:SetThreshold (opt_threshold, --[[quiet_p=]]true) farmbuyer@1: end farmbuyer@1: self:Print("Ouro Raid Loot is %s. Threshold currently %s.", farmbuyer@1: self.enabled and "tracking" or "only broadcasting", farmbuyer@1: self.thresholds[self.threshold]) farmbuyer@27: self:broadcast('revcheck',revision_large) farmbuyer@1: end farmbuyer@1: farmbuyer@1: -- Note: running '/loot off' will also avoid the popup reminder when farmbuyer@1: -- joining a raid, but will not change the saved option setting. farmbuyer@1: function addon:Deactivate() farmbuyer@1: self.enabled = false farmbuyer@1: self.rebroadcast = false farmbuyer@10: self:UnregisterEvent("RAID_ROSTER_UPDATE") farmbuyer@10: self:UnregisterEvent("PLAYER_ENTERING_WORLD") farmbuyer@10: self:UnregisterEvent("CHAT_MSG_LOOT") farmbuyer@1: self:Print("Ouro Raid Loot deactivated.") farmbuyer@1: end farmbuyer@1: farmbuyer@1: function addon:Clear(verbose_p) farmbuyer@1: local repopup, st farmbuyer@1: if self.display then farmbuyer@1: -- in the new version, this is likely to always be the case farmbuyer@1: repopup = true farmbuyer@1: st = self.display:GetUserData("eoiST") farmbuyer@1: if not st then farmbuyer@1: self.dprint('flow', "Clear: display visible but eoiST not set??") farmbuyer@1: end farmbuyer@1: self.display:Hide() farmbuyer@1: end farmbuyer@1: g_restore_p = nil farmbuyer@73: _G.OuroLootSV = nil farmbuyer@1: self:_reset_timestamps() farmbuyer@1: if verbose_p then farmbuyer@73: if (_G.OuroLootSV_saved and #_G.OuroLootSV_saved>0) then farmbuyer@73: self:Print("Current loot data cleared, %d saved sets remaining.", #_G.OuroLootSV_saved) farmbuyer@1: else farmbuyer@1: self:Print("Current loot data cleared.") farmbuyer@1: end farmbuyer@1: end farmbuyer@1: _init(self,st) farmbuyer@1: if repopup then farmbuyer@1: addon:BuildMainDisplay() farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@1: farmbuyer@1: ------ Behind the scenes routines farmbuyer@19: -- Semi-experimental debugging aid. farmbuyer@19: do farmbuyer@76: -- Declaring _log as local to here can result in this sequence: farmbuyer@41: -- 1) logging happens, followed by reload or logout/login farmbuyer@41: -- 2) _log points to SV_log farmbuyer@41: -- 3) VARIABLES_LOADED replaces SV_log pointer with restored version farmbuyer@41: -- 4) logging happens to _log table (now with no other references) farmbuyer@41: -- 5) at logout, nothing new has been entered in the table being saved farmbuyer@19: local date = _G.date farmbuyer@19: function addon:log_with_timestamp (msg) farmbuyer@73: tinsert (_log, date('%m/%d %H:%M:%S ')..msg) farmbuyer@19: end farmbuyer@19: end farmbuyer@19: farmbuyer@49: -- Check for plugins which haven't already been loaded, and add hooks for farmbuyer@49: -- them. Credit to DBM for the approach here. farmbuyer@49: function addon:_scan_LOD_modules() farmbuyer@49: for i = 1, GetNumAddOns() do farmbuyer@49: if GetAddOnMetadata (i, "X-OuroLoot-Plugin") farmbuyer@49: and IsAddOnLoadOnDemand(i) farmbuyer@49: and not IsAddOnLoaded(i) farmbuyer@49: then farmbuyer@49: local folder, _, _, enabled, _, reason = GetAddOnInfo(i) farmbuyer@57: if enabled or opts.display_disabled_LODs then farmbuyer@57: local tabtitle = GetAddOnMetadata (i, "X-OuroLoot-Plugin") farmbuyer@57: self:_gui_add_LOD_tab (tabtitle, folder, i, enabled, reason) farmbuyer@57: end farmbuyer@49: end farmbuyer@49: end farmbuyer@49: end farmbuyer@49: farmbuyer@73: -- Routines for printing changes made by remote users. farmbuyer@73: do farmbuyer@73: local remote_change_chatframe farmbuyer@73: farmbuyer@73: function addon:_set_remote_change_chatframe (arg, silent_p) farmbuyer@73: local frame farmbuyer@73: if type(arg) == 'number' then farmbuyer@73: arg = _G.math.min (arg, _G.NUM_CHAT_WINDOWS) farmbuyer@73: frame = _G['ChatFrame'..arg] farmbuyer@73: elseif type(arg) == 'string' then farmbuyer@73: frame = _G[arg] farmbuyer@73: end farmbuyer@73: if type(frame) == 'table' and type(frame.AddMessage) == 'function' then farmbuyer@73: remote_change_chatframe = frame farmbuyer@73: if not silent_p then farmbuyer@73: local msg = "Now printing to chat frame " .. arg farmbuyer@73: if frame.GetName then farmbuyer@73: msg = msg .. " (" .. tostring(frame:GetName()) .. ")" farmbuyer@73: end farmbuyer@73: self:Print(msg) farmbuyer@73: if frame ~= _G.DEFAULT_CHAT_FRAME then farmbuyer@73: self:CFPrint (frame, msg) farmbuyer@73: end farmbuyer@73: end farmbuyer@73: return frame farmbuyer@73: else farmbuyer@73: self:Print("'%s' was not a valid chat frame number/name, no change has been made.", arg) farmbuyer@73: end farmbuyer@73: end farmbuyer@73: farmbuyer@73: function _notify_about_remote (sender, index, from_whom, olddisp) farmbuyer@73: local e = g_loot[index] farmbuyer@73: local from_color, from_text, to_color, to_text farmbuyer@73: if from_whom then farmbuyer@73: -- FIXME need to return previous name/class from reassign_loot farmbuyer@73: farmbuyer@73: else farmbuyer@73: if olddisp then farmbuyer@73: from_text = addon.disposition_colors[olddisp].text farmbuyer@73: else farmbuyer@73: olddisp = "normal" farmbuyer@73: from_text = "normal" farmbuyer@73: end farmbuyer@73: from_color = addon.disposition_colors[olddisp].hex farmbuyer@73: if e.disposition then farmbuyer@73: to_text = addon.disposition_colors[e.disposition].text farmbuyer@73: else farmbuyer@73: to_text = "normal" farmbuyer@73: end farmbuyer@73: to_color = addon.disposition_colors[e.disposition or "normal"].hex farmbuyer@73: end farmbuyer@73: farmbuyer@76: addon.dprint ('loot', "notifying:", sender, index, farmbuyer@76: e.itemlink, from_color, from_text, to_color, to_text) farmbuyer@73: addon:CFPrint (remote_change_chatframe, remote_chatty, sender, index, farmbuyer@73: e.itemlink, from_color, from_text, to_color, to_text) farmbuyer@73: end farmbuyer@73: end farmbuyer@73: farmbuyer@1: -- Adds indices to traverse the tables in a nice sorted order. farmbuyer@1: do farmbuyer@1: local byindex, temp = {}, {} farmbuyer@1: local function sort (src, dest) farmbuyer@1: for k in pairs(src) do farmbuyer@1: temp[#temp+1] = k farmbuyer@1: end farmbuyer@73: _G.table.sort(temp) farmbuyer@47: wipe(dest) farmbuyer@1: for i = 1, #temp do farmbuyer@1: dest[i] = src[temp[i]] farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@1: function addon.sender_list.sort() farmbuyer@1: sort (addon.sender_list.active, byindex) farmbuyer@47: wipe(temp) farmbuyer@1: addon.sender_list.activeI = #byindex farmbuyer@1: sort (addon.sender_list.names, byindex) farmbuyer@47: wipe(temp) farmbuyer@1: end farmbuyer@1: addon.sender_list.namesI = byindex farmbuyer@1: end farmbuyer@1: farmbuyer@23: function addon:DoPing() farmbuyer@23: self:Print("Give me a ping, Vasili. One ping only, please.") farmbuyer@23: self.sender_list.active = {} farmbuyer@23: self.sender_list.names = {} farmbuyer@23: self:broadcast('ping') farmbuyer@27: self:broadcast('revcheck',revision_large) farmbuyer@27: end farmbuyer@27: farmbuyer@27: function addon:_check_revision (otherrev) farmbuyer@27: self.dprint('comm', "revchecking against", otherrev) farmbuyer@27: otherrev = tonumber(otherrev) farmbuyer@27: if otherrev == revision_large then farmbuyer@27: -- normal case farmbuyer@27: farmbuyer@27: elseif otherrev < revision_large then farmbuyer@27: self.dprint('comm', "ours is newer, notifying") farmbuyer@27: self:broadcast('revcheck',revision_large) farmbuyer@27: farmbuyer@27: else farmbuyer@76: self.dprint('comm', "ours is older, (possibly) yammering") farmbuyer@27: if newer_warning then farmbuyer@76: local pop = addon.format_hypertext ([[click here]], ITEM_QUALITY_UNCOMMON, farmbuyer@76: function() farmbuyer@76: -- Sadly, this is not generated by the packager, so hardcode it for now. farmbuyer@76: -- The 'data' field is handled differently for onshow than for other callbacks. farmbuyer@76: StaticPopup_Show("OUROL_URL", --[[text_arg1=]]nil, --[[text_arg2=]]nil, farmbuyer@76: --[[data=]][[http://www.curse.com/addons/wow/ouroloot]]) farmbuyer@76: end) farmbuyer@76: local ping = addon.format_hypertext ([[click here]], ITEM_QUALITY_UNCOMMON, 'DoPing') farmbuyer@76: self:Print(newer_warning, tostring(pop), tostring(ping)) farmbuyer@27: newer_warning = nil farmbuyer@27: end farmbuyer@27: end farmbuyer@23: end farmbuyer@23: farmbuyer@1: -- Generic helpers farmbuyer@28: -- Returns index and entry at that index, or nil if not found. farmbuyer@1: function addon._find_next_after (kind, index) farmbuyer@1: index = index + 1 farmbuyer@1: while index <= #g_loot do farmbuyer@1: if g_loot[index].kind == kind then farmbuyer@1: return index, g_loot[index] farmbuyer@1: end farmbuyer@1: index = index + 1 farmbuyer@1: end farmbuyer@1: end farmbuyer@28: -- Essentially a _find_next_after('time'-or-'boss'), but if KIND is farmbuyer@28: -- 'boss', will also stop upon finding a timestamp. Returns nil if farmbuyer@28: -- appropriate fencepost is not found. farmbuyer@28: function addon._find_timeboss_fencepost (kind, index) farmbuyer@28: local fencepost farmbuyer@28: local closest_time = addon._find_next_after('time',index) farmbuyer@28: if kind == 'time' then farmbuyer@28: fencepost = closest_time farmbuyer@28: elseif kind == 'boss' then farmbuyer@28: local closest_boss = addon._find_next_after('boss',index) farmbuyer@28: if not closest_boss then farmbuyer@28: fencepost = closest_time farmbuyer@28: elseif not closest_time then farmbuyer@28: fencepost = closest_boss farmbuyer@28: else farmbuyer@73: fencepost = _G.math.min(closest_time,closest_boss) farmbuyer@28: end farmbuyer@28: end farmbuyer@28: return fencepost farmbuyer@28: end farmbuyer@1: farmbuyer@1: -- Iterate through g_loot entries according to the KIND field. Loop variables farmbuyer@1: -- are g_loot indices and the corresponding entries (essentially ipairs + some farmbuyer@1: -- conditionals). farmbuyer@1: function addon:filtered_loot_iter (filter_kind) farmbuyer@1: return self._find_next_after, filter_kind, 0 farmbuyer@1: end farmbuyer@1: farmbuyer@1: do farmbuyer@1: local itt farmbuyer@1: local function create() farmbuyer@1: local tip, lefts = CreateFrame("GameTooltip"), {} farmbuyer@1: for i = 1, 2 do -- scanning idea here also snagged from Talented farmbuyer@1: local L,R = tip:CreateFontString(), tip:CreateFontString() farmbuyer@1: L:SetFontObject(GameFontNormal) farmbuyer@1: R:SetFontObject(GameFontNormal) farmbuyer@1: tip:AddFontStrings(L,R) farmbuyer@1: lefts[i] = L farmbuyer@1: end farmbuyer@1: tip.lefts = lefts farmbuyer@1: return tip farmbuyer@1: end farmbuyer@66: function addon:is_variant_item(item) -- returns number or *nil* farmbuyer@1: itt = itt or create() farmbuyer@1: itt:SetOwner(UIParent,"ANCHOR_NONE") farmbuyer@1: itt:ClearLines() farmbuyer@1: itt:SetHyperlink(item) farmbuyer@1: local t = itt.lefts[2]:GetText() farmbuyer@1: itt:Hide() farmbuyer@66: return (t == ITEM_HEROIC and 1) farmbuyer@66: or (t == RAID_FINDER and 2) -- no ITEM_ for this, apparently farmbuyer@66: or nil farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@79: -- Called at the end of OnInit, and then also when a 'clear' is being farmbuyer@16: -- performed. If SV's are present then g_restore_p will be true. farmbuyer@1: function _init (self, possible_st) farmbuyer@1: self.dprint('flow',"_init running") farmbuyer@1: self.loot_clean = nil farmbuyer@1: self.hist_clean = nil farmbuyer@1: if g_restore_p then farmbuyer@73: g_loot = _G.OuroLootSV farmbuyer@16: self.popped = #g_loot > 0 farmbuyer@1: self.dprint('flow', "restoring", #g_loot, "entries") farmbuyer@16: self:ScheduleTimer("Activate", 12, opts.threshold) farmbuyer@1: -- FIXME printed could be too large if entries were deleted, how much do we care? farmbuyer@16: self.sharder = opts.autoshard farmbuyer@1: else farmbuyer@10: g_loot = { printed = {}, raiders = {} } farmbuyer@1: end farmbuyer@1: farmbuyer@16: self.threshold = opts.threshold or self.threshold -- in the case of restoring but not tracking farmbuyer@79: self:gui_init (g_loot, g_uniques) farmbuyer@16: opts.autoshard = nil farmbuyer@16: opts.threshold = nil farmbuyer@1: farmbuyer@1: if g_restore_p then farmbuyer@73: self:zero_printed_fenceposts() -- g_loot.printed.* = previous/safe values farmbuyer@1: else farmbuyer@73: self:zero_printed_fenceposts(0) -- g_loot.printed.* = 0 farmbuyer@1: end farmbuyer@1: if possible_st then farmbuyer@1: possible_st:SetData(g_loot) farmbuyer@1: end farmbuyer@1: farmbuyer@73: self.status_text = ("%s communicating as ident %s commrev %s"): farmbuyer@73: format (self.revision, self.ident, self.commrev) farmbuyer@1: self:RegisterComm(self.ident) farmbuyer@1: self:RegisterComm(self.identTg, "OnCommReceivedNocache") farmbuyer@1: farmbuyer@1: if self.author_debug then farmbuyer@1: _G.Oloot = g_loot farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@61: -- Raid roster snapshots farmbuyer@61: do farmbuyer@61: function addon:snapshot_raid (only_inraid_p) farmbuyer@61: local ss = CopyTable(g_loot.raiders) farmbuyer@61: local instance,maxsize = instance_tag() farmbuyer@61: if only_inraid_p then farmbuyer@73: for name,info in _G.next, ss do farmbuyer@61: if info.online == 3 then farmbuyer@61: ss[name] = nil farmbuyer@61: end farmbuyer@61: end farmbuyer@61: end farmbuyer@73: return ss, maxsize, instance, _G.time() farmbuyer@61: end farmbuyer@61: end farmbuyer@61: farmbuyer@25: -- Tie-in with Deadly Boss Mods (or other such addons) farmbuyer@1: do farmbuyer@25: local candidates = {} farmbuyer@25: local location farmbuyer@1: local function fixup_durations (cache) farmbuyer@1: local boss, bossi farmbuyer@1: boss = candidates[1] farmbuyer@1: if #candidates == 1 then farmbuyer@1: -- (1) or (2) farmbuyer@1: boss.duration = boss.duration or 0 farmbuyer@19: addon.dprint('loot', "only one boss candidate") farmbuyer@1: else farmbuyer@1: -- (3), should only be one 'cast entry and our local entry farmbuyer@1: if #candidates ~= 2 then farmbuyer@1: -- could get a bunch of 'cast entries on the heels of one another farmbuyer@1: -- before the local one ever fires, apparently... sigh farmbuyer@1: --addon:Print(" s3 cache has %d entries, does that seem right to you?", #candidates) farmbuyer@1: end farmbuyer@1: if candidates[2].duration == nil then farmbuyer@1: --addon:Print(" s3's second entry is not the local trigger, does that seem right to you?") farmbuyer@1: end farmbuyer@1: -- try and be generic anyhow farmbuyer@1: for i,c in ipairs(candidates) do farmbuyer@1: if c.duration then farmbuyer@1: boss = c farmbuyer@19: addon.dprint('loot', "fixup found boss candidate", i, "duration", c.duration) farmbuyer@1: break farmbuyer@1: end farmbuyer@1: end farmbuyer@1: end farmbuyer@69: bossi = addon._addBossEntry(boss) farmbuyer@19: -- addon. farmbuyer@19: bossi = addon._adjustBossOrder (bossi, g_boss_signpost) or bossi farmbuyer@16: g_boss_signpost = nil farmbuyer@42: addon.latest_instance = boss.instance farmbuyer@19: addon.dprint('loot', "added boss entry", bossi) farmbuyer@1: if boss.reason == 'kill' then farmbuyer@1: addon:_mark_boss_kill (bossi) farmbuyer@1: if opts.chatty_on_kill then farmbuyer@55: addon:Print("Registered kill for '%s' in %s!", boss.bossname, boss.instance) farmbuyer@1: end farmbuyer@1: end farmbuyer@47: wipe(candidates) farmbuyer@1: end farmbuyer@73: local recent_boss = create_new_cache ('boss', 10, fixup_durations) farmbuyer@1: farmbuyer@1: -- Similar to _do_loot, but duration+ parms only present when locally generated. farmbuyer@56: local function _do_boss (self, reason, bossname, intag, maxsize, duration) farmbuyer@56: self.dprint('loot',">>_do_boss, R:", reason, "B:", bossname, farmbuyer@56: "T:", intag, "MS:", maxsize, "D:", duration) farmbuyer@1: if self.rebroadcast and duration then farmbuyer@56: self:vbroadcast('boss', reason, bossname, intag, maxsize) farmbuyer@1: end farmbuyer@1: -- This is only a loop to make jumping out of it easy, and still do cleanup below. farmbuyer@1: while self.enabled do farmbuyer@1: if reason == 'wipe' and opts.no_tracking_wipes then break end farmbuyer@1: bossname = (opts.snarky_boss and self.boss_abbrev[bossname] or bossname) or bossname farmbuyer@1: local not_from_local = duration == nil farmbuyer@1: local signature = bossname .. reason farmbuyer@73: if not_from_local and recent_boss:test(signature) then farmbuyer@39: self.dprint('cache', "remote boss <",signature,"> already in cache, skipping") farmbuyer@1: else farmbuyer@73: recent_boss:add(signature) farmbuyer@16: g_boss_signpost = #g_loot + 1 farmbuyer@19: self.dprint('loot', "added boss signpost", g_boss_signpost) farmbuyer@1: -- Possible scenarios: (1) we don't see a boss event at all (e.g., we're farmbuyer@1: -- outside the instance) and so this only happens once as a non-local event, farmbuyer@1: -- (2) we see a local event first and all non-local events are filtered farmbuyer@1: -- by the cache, (3) we happen to get some non-local events before doing farmbuyer@1: -- our local event (not because of network weirdness but because our local farmbuyer@1: -- DBM might not trigger for a while). farmbuyer@1: local c = { farmbuyer@1: kind = 'boss', farmbuyer@55: bossname = bossname, farmbuyer@1: reason = reason, farmbuyer@1: instance = intag, farmbuyer@56: duration = duration, -- deliberately may be nil farmbuyer@61: raidersnap = self:snapshot_raid(), farmbuyer@56: maxsize = maxsize, farmbuyer@1: } farmbuyer@1: tinsert(candidates,c) farmbuyer@1: end farmbuyer@17: break farmbuyer@1: end farmbuyer@1: self.dprint('loot',"<<_do_boss out") farmbuyer@1: end farmbuyer@56: -- This exposes the function to OCR, and can be a wrapper layer later. farmbuyer@1: addon.on_boss_broadcast = _do_boss farmbuyer@1: farmbuyer@1: function addon:_mark_boss_kill (index) farmbuyer@1: local e = g_loot[index] farmbuyer@17: if not e then farmbuyer@17: self:Print("Something horribly wrong;", index, "is not a valid entry!") farmbuyer@17: return farmbuyer@17: end farmbuyer@55: if not e.bossname then farmbuyer@16: self:Print("Something horribly wrong;", index, "is not a boss entry!") farmbuyer@16: return farmbuyer@1: end farmbuyer@1: if e.reason ~= 'wipe' then farmbuyer@1: -- enh, bail farmbuyer@1: self.loot_clean = index-1 farmbuyer@1: end farmbuyer@1: local attempts = 1 farmbuyer@1: local first farmbuyer@1: farmbuyer@69: -- Maybe redo this to only collapse *contiguous* wipes...? farmbuyer@1: local i,d = 1,g_loot[1] farmbuyer@1: while d ~= e do farmbuyer@55: if d.bossname and farmbuyer@55: d.bossname == e.bossname and farmbuyer@53: d.instance == e.instance and farmbuyer@1: d.reason == 'wipe' farmbuyer@1: then farmbuyer@1: first = first or i farmbuyer@1: attempts = attempts + 1 farmbuyer@1: assert(tremove(g_loot,i)==d,"_mark_boss_kill screwed up data badly") farmbuyer@1: else farmbuyer@1: i = i + 1 farmbuyer@1: end farmbuyer@1: d = g_loot[i] farmbuyer@1: end farmbuyer@1: e.reason = 'kill' farmbuyer@1: e.attempts = attempts farmbuyer@1: self.loot_clean = first or index-1 farmbuyer@1: end farmbuyer@1: farmbuyer@2: function addon:register_boss_mod (name, registration_func, deregistration_func) farmbuyer@2: assert(type(name)=='string') farmbuyer@2: assert(type(registration_func)=='function') farmbuyer@76: if deregistration_func ~= nil then farmbuyer@76: assert(type(deregistration_func)=='function') farmbuyer@76: end farmbuyer@2: self.bossmods[#self.bossmods+1] = { farmbuyer@2: n = name, farmbuyer@2: r = registration_func, farmbuyer@2: d = deregistration_func, farmbuyer@2: } farmbuyer@5: self.bossmods[name] = self.bossmods[#self.bossmods] farmbuyer@5: assert(self.bossmods[name].n == self.bossmods[#self.bossmods].n) farmbuyer@2: end farmbuyer@1: farmbuyer@2: function _register_bossmod (self, force_p) farmbuyer@2: local x = self.bossmod_registered and self.bossmods[self.bossmod_registered] farmbuyer@2: if x then farmbuyer@2: if x.n == opts.bossmod and not force_p then farmbuyer@2: -- trying to register with already-registered boss mod farmbuyer@2: return farmbuyer@2: else farmbuyer@2: -- deregister farmbuyer@2: if x.d then x.d(self) end farmbuyer@2: end farmbuyer@1: end farmbuyer@1: farmbuyer@2: x = nil farmbuyer@2: for k,v in ipairs(self.bossmods) do farmbuyer@2: if v.n == opts.bossmod then farmbuyer@2: x = k farmbuyer@2: break farmbuyer@2: end farmbuyer@1: end farmbuyer@1: farmbuyer@2: if not x then farmbuyer@2: self.status_text = "|cffff1010No boss-mod found!|r" farmbuyer@2: self:Print(self.status_text) farmbuyer@2: return farmbuyer@1: end farmbuyer@1: farmbuyer@2: if self.bossmods[x].r (self, _do_boss) then farmbuyer@5: self.bossmod_registered = self.bossmods[x].n farmbuyer@1: else farmbuyer@2: self:Print("|cffff1010Boss mod registration failed|r") farmbuyer@1: end farmbuyer@1: end farmbuyer@2: end farmbuyer@1: farmbuyer@1: -- Adding entries to the loot record, and tracking the corresponding timestamp. farmbuyer@1: do farmbuyer@73: local rawget, setmetatable = _G.rawget, _G.setmetatable farmbuyer@73: farmbuyer@1: -- This shouldn't be required. /sadface farmbuyer@1: local loot_entry_mt = { farmbuyer@1: __index = function (e,key) farmbuyer@1: if key == 'cols' then farmbuyer@15: pprint('mt', e.kind, "key is", key) farmbuyer@1: --tabledump(e) -- not actually that useful farmbuyer@1: addon:_fill_out_eoi_data(1) farmbuyer@1: end farmbuyer@1: return rawget(e,key) farmbuyer@1: end farmbuyer@1: } farmbuyer@1: farmbuyer@1: -- Given a loot index, searches backwards for a timestamp. Returns that farmbuyer@1: -- index and the time entry, or nil if it falls off the beginning. Pass an farmbuyer@65: -- optional second index to search no earlier than that. farmbuyer@1: -- May also be able to make good use of this in forum-generation routine. farmbuyer@1: function addon:find_previous_time_entry(i,stop) farmbuyer@17: stop = stop or 0 farmbuyer@1: while i > stop do farmbuyer@1: if g_loot[i].kind == 'time' then farmbuyer@1: return i, g_loot[i] farmbuyer@1: end farmbuyer@1: i = i - 1 farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@1: -- format_timestamp (["format_string"], Day, [Loot]) farmbuyer@1: -- DAY is a loot entry with kind=='time', and controls the date printed. farmbuyer@1: -- LOOT may be any kind of entry in the g_loot table. If present, it farmbuyer@1: -- overrides the hour and minute printed; if absent, those values are farmbuyer@1: -- taken from the DAY entry. farmbuyer@1: -- FORMAT_STRING may contain $x (x in Y/M/D/h/m) tokens. farmbuyer@1: local format_timestamp_values, point2dee = {}, "%.2d" farmbuyer@1: function addon:format_timestamp (fmt_opt, day_entry, time_entry_opt) farmbuyer@1: if not time_entry_opt then farmbuyer@1: if type(fmt_opt) == 'table' then -- Two entries, default format farmbuyer@1: time_entry_opt, day_entry = day_entry, fmt_opt farmbuyer@1: fmt_opt = "$Y/$M/$D $h:$m" farmbuyer@41: --elseif type(fmt_opt) == "string" then -- Day entry only, caller-specified format farmbuyer@1: end farmbuyer@1: end farmbuyer@1: --format_timestamp_values.Y = point2dee:format (day_entry.startday.year % 100) farmbuyer@1: format_timestamp_values.Y = ("%.4d"):format (day_entry.startday.year) farmbuyer@1: format_timestamp_values.M = point2dee:format (day_entry.startday.month) farmbuyer@1: format_timestamp_values.D = point2dee:format (day_entry.startday.day) farmbuyer@1: format_timestamp_values.h = point2dee:format ((time_entry_opt or day_entry).hour) farmbuyer@1: format_timestamp_values.m = point2dee:format ((time_entry_opt or day_entry).minute) farmbuyer@1: return fmt_opt:gsub ('%$([YMDhm])', format_timestamp_values) farmbuyer@1: end farmbuyer@1: farmbuyer@1: local done_todays_date farmbuyer@1: function addon:_reset_timestamps() farmbuyer@1: done_todays_date = nil farmbuyer@1: end farmbuyer@1: local function do_todays_date() farmbuyer@1: local text, M, D, Y = makedate() farmbuyer@1: local found,ts = #g_loot+1 farmbuyer@1: repeat farmbuyer@1: found,ts = addon:find_previous_time_entry(found-1) farmbuyer@1: if found and ts.startday.text == text then farmbuyer@1: done_todays_date = true farmbuyer@1: end farmbuyer@1: until done_todays_date or (not found) farmbuyer@1: if done_todays_date then farmbuyer@1: g_today = ts farmbuyer@1: else farmbuyer@1: done_todays_date = true farmbuyer@1: g_today = g_loot[addon._addLootEntry{ farmbuyer@1: kind = 'time', farmbuyer@1: startday = { farmbuyer@1: text = text, month = M, day = D, year = Y farmbuyer@1: } farmbuyer@1: }] farmbuyer@1: end farmbuyer@1: addon:_fill_out_eoi_data(1) farmbuyer@1: end farmbuyer@1: farmbuyer@1: -- Adding anything original to g_loot goes through this routine. farmbuyer@1: function addon._addLootEntry (e) farmbuyer@1: setmetatable(e,loot_entry_mt) farmbuyer@1: farmbuyer@1: if not done_todays_date then do_todays_date() end farmbuyer@1: farmbuyer@73: local h, m = _G.GetGameTime() farmbuyer@10: --local localuptime = math.floor(GetTime()) farmbuyer@73: local time_t = _G.time() farmbuyer@1: e.hour = h farmbuyer@1: e.minute = m farmbuyer@10: e.stamp = time_t --localuptime farmbuyer@1: local index = #g_loot + 1 farmbuyer@1: g_loot[index] = e farmbuyer@1: return index farmbuyer@1: end farmbuyer@16: farmbuyer@69: -- Safety wrapper only. farmbuyer@69: -- XXX Maybe pprint something here. farmbuyer@69: function addon._addBossEntry (e) farmbuyer@69: local ret = addon._addLootEntry(e) farmbuyer@69: assert(e.kind=='boss') farmbuyer@69: local needSize = e.maxsize == nil farmbuyer@69: local needSnap = e.raidersnap == nil farmbuyer@69: local needInst = e.instance == nil farmbuyer@69: if needSize or needSnap then farmbuyer@69: local ss, max, inst = addon:snapshot_raid() farmbuyer@69: if needSize then e.maxsize = max end farmbuyer@69: if needSnap then e.raidersnap = ss end farmbuyer@69: if needInst then e.instance = inst end farmbuyer@69: end farmbuyer@69: return ret farmbuyer@69: end farmbuyer@69: farmbuyer@16: -- Problem: (1) boss kill happens, (2) fast looting happens, (3) boss farmbuyer@16: -- cache cleanup fires. Result: loot shows up before boss kill entry. farmbuyer@16: -- Solution: We need to shuffle the boss entry above any of the loot farmbuyer@16: -- from that boss. farmbuyer@16: function addon._adjustBossOrder (is, should_be) farmbuyer@16: --pprint('loot', is, should_be) farmbuyer@16: if is == should_be then --pprint('loot', "equal, yay") farmbuyer@16: return farmbuyer@16: end farmbuyer@17: if (type(is)~='number') or (type(should_be)~='number') or (is < should_be) then farmbuyer@19: pprint('loot', is, should_be, "...the hell? bailing") farmbuyer@16: return farmbuyer@16: end farmbuyer@16: if g_loot[should_be].kind == 'time' then farmbuyer@16: should_be = should_be + 1 farmbuyer@16: if is == should_be then farmbuyer@16: --pprint('loot', "needed to mark day entry, otherwise equal, yay") farmbuyer@16: return farmbuyer@16: end farmbuyer@16: end farmbuyer@16: farmbuyer@16: assert(g_loot[is].kind == 'boss') farmbuyer@16: local boss = tremove (g_loot, is) farmbuyer@55: --pprint('loot', "MOVING", boss.bossname) farmbuyer@16: tinsert (g_loot, should_be, boss) farmbuyer@16: return should_be farmbuyer@16: end farmbuyer@1: end farmbuyer@1: farmbuyer@19: -- In the rare case of items getting put into the loot table without current farmbuyer@19: -- item cache data (which will have arrived by now). farmbuyer@19: function addon:do_item_cache_fixup() farmbuyer@19: self:Print("Fixing up missing item cache data...") farmbuyer@19: farmbuyer@19: local numfound = 0 farmbuyer@19: local borkedpat = '^'..UNKNOWN..': (%S+)' farmbuyer@19: farmbuyer@73: -- 'while true' so that we can use (inner) break as (outer) continue farmbuyer@73: for i,e in self:filtered_loot_iter('loot') do while true do farmbuyer@73: if not e.cache_miss then break end farmbuyer@73: local borked_id = e.itemname:match(borkedpat) farmbuyer@73: if not borked_id then break end farmbuyer@73: numfound = numfound + 1 farmbuyer@73: -- Best to use the safest and most flexible API here, which is GII and farmbuyer@73: -- its assload of return values. farmbuyer@73: local iname, ilink, iquality, _,_,_,_,_,_, itexture = GetItemInfo(borked_id) farmbuyer@73: if iname then farmbuyer@73: local msg = [[ Entry %d patched up with %s.]] farmbuyer@73: e.quality = iquality farmbuyer@73: e.itemname = iname farmbuyer@73: e.id = tonumber(ilink:match("item:(%d+)")) farmbuyer@73: e.itemlink = ilink farmbuyer@73: e.itexture = itexture farmbuyer@73: e.cache_miss = nil farmbuyer@73: if e.unique then farmbuyer@73: local gu = g_uniques[e.unique] farmbuyer@73: local player_i, player_h, hist_i = _history_by_loot_id (e.unique, "fixcache") farmbuyer@73: if gu.loot ~= i then -- is this an actual problem? farmbuyer@76: pprint ('loot', farmbuyer@76: ("Unique value '%s' had iterator value %d but g_uniques index %s."): farmbuyer@76: format(e.unique,i,tostring(gu.loot))) farmbuyer@73: end farmbuyer@73: if player_i then farmbuyer@73: player_h.id[e.unique] = e.id farmbuyer@73: msg = [[ Entry %d (and history) patched up with %s.]] farmbuyer@19: end farmbuyer@19: end farmbuyer@73: self:Print(msg, i, ilink) farmbuyer@19: end farmbuyer@73: break farmbuyer@73: end end farmbuyer@19: farmbuyer@19: self:Print("...finished. Found %d |4entry:entries; with weird data.", numfound) farmbuyer@19: end farmbuyer@19: farmbuyer@76: do farmbuyer@76: local gur farmbuyer@76: farmbuyer@76: -- Strictly speaking, we'd want to handle individual 'exist' entries farmbuyer@76: -- as they expire, rather then waiting for all of them to expire and then farmbuyer@76: -- doing them as a group. But, if there're more than one of these per farmbuyer@76: -- boss, something else is funky anyhow and certainly not a hurry. farmbuyer@76: local function fixup_unique_replacements() farmbuyer@76: --print("replacements fixup happening!") farmbuyer@76: --tabledump(g_unique_replace.replacements) farmbuyer@76: --_G.GRR = g_unique_replace.replacements farmbuyer@76: for exist,info in pairs(gur.replacements) do farmbuyer@76: local winning_index = 1 farmbuyer@76: local winner = info[1] farmbuyer@76: info[1] = nil farmbuyer@76: pprint('improv', "fixup for", exist, "starting with", winner[1], farmbuyer@76: "with", winner[2], "out of", #info, "total entries") farmbuyer@76: -- Lowest player GUID wins. Seniority gotta count for something. farmbuyer@76: for i = 2, #info do farmbuyer@76: if winner[1] <= info[i][1] then farmbuyer@76: pprint('improv', "champ wins against", i, info[i][1]) farmbuyer@76: flib.del(info[i]) farmbuyer@76: else farmbuyer@76: pprint('improv', "challenger wins with", i, info[i][1]) farmbuyer@76: flib.del(winner) farmbuyer@76: winner = info[i] farmbuyer@76: winning_index = i farmbuyer@76: end farmbuyer@76: info[i] = nil farmbuyer@76: end farmbuyer@76: pprint('improv', "final:", winner[1], winner[2]) farmbuyer@76: --[[ farmbuyer@76: A: winner was generated locally farmbuyer@76: >g_loot and history already has the replacement value farmbuyer@76: >winning_index == 1 farmbuyer@76: B: winner was generated remotely farmbuyer@76: >need to scan and replace farmbuyer@76: ]] farmbuyer@76: if winning_index ~= 1 then farmbuyer@76: --XXX still needs to be debugged: farmbuyer@76: local cache = g_uniques[exist] farmbuyer@76: local looti = assert(cache.loot) -- can't possibly be missing... farmbuyer@76: if g_loot[looti].unique ~= exist then farmbuyer@76: pprint('improv', "WTF. entry", looti, farmbuyer@76: "does not match original unique tag! instead", farmbuyer@76: g_loot[looti].unique) farmbuyer@76: else farmbuyer@76: pprint('improv', "found and replaced loot entry", looti) farmbuyer@76: g_loot[looti].unique = winner[2] farmbuyer@76: end farmbuyer@76: local hi,ui = cache.history, cache.history_may farmbuyer@76: if hi ~= g_uniques.NOTFOUND then farmbuyer@76: local hist = addon.history[hi] farmbuyer@76: if ui and hist.unique[ui] == exist then farmbuyer@76: -- ui is valid farmbuyer@76: else farmbuyer@76: ui = nil farmbuyer@76: for i,ui2 in ipairs(hist.unique) do farmbuyer@76: if ui2 == exist then farmbuyer@76: ui = i farmbuyer@76: break farmbuyer@76: end farmbuyer@76: end farmbuyer@76: end farmbuyer@76: if ui then farmbuyer@76: pprint('improv', "found and replacing history entry", hi, farmbuyer@76: ui, hist.name) farmbuyer@76: hist.when[winner[2]] = hist.when[exist] farmbuyer@76: hist.id[winner[2]] = hist.id[exist] farmbuyer@76: hist.count[winner[2]] = hist.count[exist] farmbuyer@76: hist.unique[ui] = winner[2] farmbuyer@76: hist.when[exist] = nil farmbuyer@76: hist.id[exist] = nil farmbuyer@76: hist.count[exist] = nil farmbuyer@76: end farmbuyer@76: end farmbuyer@76: end farmbuyer@76: pprint('improv', "finished with", exist, "into", winner[2]) farmbuyer@76: flib.del(winner) farmbuyer@76: flib.del(info) farmbuyer@76: gur.replacements[exist] = nil farmbuyer@76: end farmbuyer@76: end farmbuyer@76: farmbuyer@76: local function new_entry (id, exist, repl, is_local) farmbuyer@76: pprint('improv', "new_entry", id, exist, repl, is_local) farmbuyer@76: gur.replacements[exist] = gur.replacements[exist] or flib.new() farmbuyer@76: tinsert (gur.replacements[exist], flib.new (tonumber(id), repl)) farmbuyer@76: if is_local then farmbuyer@76: gur.replacements[exist].LOCAL = repl farmbuyer@76: end farmbuyer@76: gur.cache:add (exist) farmbuyer@76: end farmbuyer@76: farmbuyer@76: local function get_previous_replacement (exist) farmbuyer@76: local l = gur.replacements[exist] farmbuyer@76: if l and l.LOCAL then farmbuyer@76: pprint('improv', "check for previous", exist, "returns valid", farmbuyer@76: l.LOCAL) farmbuyer@76: return l.LOCAL farmbuyer@76: end farmbuyer@76: pprint('improv', "check for previous", exist, "returns nil") farmbuyer@76: end farmbuyer@76: farmbuyer@76: function _setup_unique_replace () farmbuyer@76: gur = {} farmbuyer@76: gur.cache = create_new_cache ('improv', 10, fixup_unique_replacements) farmbuyer@76: gur.me = tonumber(_G.UnitGUID('player'):sub(-7),16) farmbuyer@76: gur.replacements = {} farmbuyer@76: gur.new_entry = new_entry farmbuyer@76: gur.get_previous_replacement = get_previous_replacement farmbuyer@76: g_unique_replace = gur farmbuyer@76: _setup_unique_replace = nil farmbuyer@76: end farmbuyer@76: end farmbuyer@76: farmbuyer@77: do farmbuyer@77: local clicky farmbuyer@77: function _unavoidable_collision (err) farmbuyer@77: -- This should happen so rarely that it's not worth moving into gui.lua farmbuyer@77: if not StaticPopupDialogs["OUROL_COLLISION"] then farmbuyer@77: StaticPopupDialogs["OUROL_COLLISION"] = flib.StaticPopup{ farmbuyer@77: button1 = OKAY, farmbuyer@77: } farmbuyer@77: clicky = addon.format_hypertext( farmbuyer@77: [[ SYSTEM FAILURE -- RELEASE RINZLER ]], "|cffff0000", farmbuyer@77: function() StaticPopup_Show "OUROL_COLLISION" end) farmbuyer@77: end farmbuyer@77: StaticPopupDialogs["OUROL_COLLISION"].text = err farmbuyer@77: _G.PlaySoundFile ([[Interface\AddOns\Ouro_Loot\sfrr.ogg]], "Master") farmbuyer@77: addon:Print (" ") farmbuyer@77: addon:Print (" ", clicky) farmbuyer@77: addon:Print (" ") farmbuyer@77: end farmbuyer@77: end farmbuyer@77: --function DOTEST() farmbuyer@77: -- local err = unique_collision:format (ERROR_CAPS, farmbuyer@77: -- "Codpiece of the Grimacing Lunatic", farmbuyer@77: -- 'n3183021', 'Farmbuyer', '14', '78') farmbuyer@77: -- _unavoidable_collision (err) farmbuyer@77: -- pprint('ohfuck', "COLLISION", err) farmbuyer@77: --end farmbuyer@77: farmbuyer@1: farmbuyer@1: ------ Saved texts farmbuyer@1: function addon:check_saved_table(silent_p) farmbuyer@73: local s = _G.OuroLootSV_saved farmbuyer@1: if s and (#s > 0) then return s end farmbuyer@73: _G.OuroLootSV_saved = nil farmbuyer@1: if not silent_p then self:Print("There are no saved loot texts.") end farmbuyer@1: end farmbuyer@1: farmbuyer@1: function addon:save_list() farmbuyer@1: local s = self:check_saved_table(); if not s then return end; farmbuyer@1: for i,t in ipairs(s) do farmbuyer@1: self:Print("#%d %s %d entries %s", i, t.date, t.count, t.name) farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@1: function addon:save_saveas(name) farmbuyer@73: _G.OuroLootSV_saved = _G.OuroLootSV_saved or {} farmbuyer@73: local SV = _G.OuroLootSV_saved farmbuyer@16: local n = #SV + 1 farmbuyer@1: local save = { farmbuyer@1: name = name, farmbuyer@1: date = makedate(), farmbuyer@1: count = #g_loot, farmbuyer@1: } farmbuyer@10: for text in self:registered_textgen_iter() do farmbuyer@10: save[text] = g_loot[text] farmbuyer@10: end farmbuyer@1: self:Print("Saving current loot texts to #%d '%s'", n, name) farmbuyer@16: SV[n] = save farmbuyer@1: return self:save_list() farmbuyer@1: end farmbuyer@1: farmbuyer@1: function addon:save_restore(num) farmbuyer@1: local s = self:check_saved_table(); if not s then return end; farmbuyer@1: if (not num) or (num > #s) then farmbuyer@1: return self:Print("Saved text number must be 1 - "..#s) farmbuyer@1: end farmbuyer@1: local save = s[num] farmbuyer@1: self:Print("Overwriting current loot data with saved text #%d '%s'", num, save.name) farmbuyer@1: self:Clear(--[[verbose_p=]]false) farmbuyer@1: -- Clear will already have displayed the window, and re-selected the first farmbuyer@1: -- tab. Set these up for when the text tabs are clicked. farmbuyer@10: for text in self:registered_textgen_iter() do farmbuyer@10: g_loot[text] = save[text] farmbuyer@10: end farmbuyer@1: end farmbuyer@1: farmbuyer@1: function addon:save_delete(num) farmbuyer@1: local s = self:check_saved_table(); if not s then return end; farmbuyer@1: if (not num) or (num > #s) then farmbuyer@1: return self:Print("Saved text number must be 1 - "..#s) farmbuyer@1: end farmbuyer@1: self:Print("Deleting saved text #"..num) farmbuyer@1: tremove(s,num) farmbuyer@1: return self:save_list() farmbuyer@1: end farmbuyer@1: farmbuyer@1: farmbuyer@1: ------ Loot histories farmbuyer@1: -- history_all = { farmbuyer@1: -- ["Kilrogg"] = { farmbuyer@1: -- ["realm"] = "Kilrogg", -- not saved farmbuyer@1: -- ["st"] = { lib-st display table }, -- not saved farmbuyer@1: -- ["byname"] = { -- not saved farmbuyer@1: -- ["OtherPlayer"] = 2, farmbuyer@1: -- ["Farmbuyer"] = 1, farmbuyer@1: -- } farmbuyer@1: -- [1] = { farmbuyer@1: -- ["name"] = "Farmbuyer", farmbuyer@73: -- ["person_class"] = "PRIEST", -- may be missing, used in display only farmbuyer@73: -- -- sorted array: farmbuyer@73: -- ["unique"] = { most_recent_tag, previous_tag, .... }, farmbuyer@73: -- -- these are indexed by unique tags, and 'count' may be missing: farmbuyer@73: -- ["when"] = { ["tag"] = "formatted timestamp for displaying loot" }, farmbuyer@73: -- ["id"] = { ["tag"] = 11111 }, farmbuyer@73: -- ["count"] = { ["tag"] = "x3", .... }, farmbuyer@1: -- }, farmbuyer@1: -- [2] = { farmbuyer@1: -- ["name"] = "OtherPlayer", farmbuyer@1: -- ...... farmbuyer@1: -- }, ...... farmbuyer@1: -- }, farmbuyer@1: -- ["OtherRealm"] = ...... farmbuyer@1: -- } farmbuyer@73: -- farmbuyer@73: -- Up through 2.81.4 (specifically through rev 95), an individual player's farmbuyer@73: -- table looked like this: farmbuyer@73: -- ["name"] = "Farmbuyer", farmbuyer@73: -- [1] = { id = nnnnn, when = "formatted timestamp for displaying" } -- most recent loot farmbuyer@73: -- [2] = { ......., [count = "x3"] } -- previous loot farmbuyer@73: -- which was much easier to manipulate, but had a ton of memory overhead. farmbuyer@1: do farmbuyer@73: -- Sorts a player's history from newest to oldest, according to the farmbuyer@73: -- formatted timestamp. This is expensive, and destructive for P.unique. farmbuyer@76: local function compare_timestamps (L, R) farmbuyer@76: return L > R -- reverse of normal order, newest first farmbuyer@76: end farmbuyer@73: local function sort_player (p) farmbuyer@73: local new_uniques, uniques_bywhen, when_array = {}, {}, {} farmbuyer@73: for u,tstamp in pairs(p.when) do farmbuyer@73: uniques_bywhen[tstamp] = u farmbuyer@73: when_array[#when_array+1] = tstamp farmbuyer@73: end farmbuyer@76: _G.table.sort (when_array, compare_timestamps) farmbuyer@73: for i,tstamp in ipairs(when_array) do farmbuyer@73: new_uniques[i] = uniques_bywhen[tstamp] farmbuyer@73: end farmbuyer@73: p.unique = new_uniques farmbuyer@73: end farmbuyer@73: farmbuyer@73: -- Possibly called during login. Cleared when no longer needed. farmbuyer@73: -- Rewrites a PLAYER table from format 3 to format 4. farmbuyer@76: function addon:_uplift_history_format (player) farmbuyer@73: local unique, when, id, count = {}, {}, {}, {} farmbuyer@73: local name = player.name farmbuyer@73: farmbuyer@73: for i,h in ipairs(player) do farmbuyer@73: local U = h.unique farmbuyer@73: unique[i] = U farmbuyer@73: when[U] = h.when farmbuyer@73: id[U] = h.id farmbuyer@73: count[U] = h.count farmbuyer@73: end farmbuyer@73: farmbuyer@73: wipe(player) farmbuyer@73: player.name = name farmbuyer@73: player.id, player.when, player.unique, player.count = farmbuyer@73: id, when, unique, count farmbuyer@73: end farmbuyer@76: farmbuyer@73: function addon:_cache_history_uniques() farmbuyer@73: UpdateAddOnMemoryUsage() farmbuyer@73: local before = GetAddOnMemoryUsage(nametag) farmbuyer@73: local trouble farmbuyer@73: local count = 0 farmbuyer@73: for hi,player in ipairs(self.history) do farmbuyer@73: for ui,u in ipairs(player.unique) do farmbuyer@73: g_uniques[u] = { history = hi, history_may = ui } farmbuyer@73: count = count + 1 farmbuyer@73: end farmbuyer@73: end farmbuyer@73: for i,e in self:filtered_loot_iter('loot') do farmbuyer@73: if e.unique and e.unique ~= "" then farmbuyer@73: local hmmm = _G.rawget(g_uniques,e.unique) farmbuyer@73: if hmmm then farmbuyer@73: hmmm.loot = i --;print("Active loot", i, "found with tag", e.unique) farmbuyer@73: elseif e.disposition == 'shard' or e.disposition == 'gvault' then farmbuyer@73: g_uniques[e.unique] = { loot = i, history = g_uniques.NOTFOUND } farmbuyer@73: count = count + 1 farmbuyer@73: --print("Active loot", i, "INSERTED with tag", e.unique, "as", e.disposition) farmbuyer@73: else farmbuyer@76: hmmm = "active data not found in history ("..i.."/"..tostring(e.unique) farmbuyer@76: ..") in precache loop! trying to fixup for this session" farmbuyer@76: pprint(hmmm) -- more? farmbuyer@73: -- try to simply fix up errors as we go farmbuyer@73: g_uniques[e.unique] = { loot = i, history = g_uniques.NOTFOUND } farmbuyer@76: --trouble = true farmbuyer@73: end farmbuyer@73: else farmbuyer@73: trouble = true farmbuyer@73: pprint('loot', "ERROR precache loop found bad unique tag!", farmbuyer@73: i, "tag", tostring(e.unique), "from?", tostring(e.bcast_from)) farmbuyer@73: end farmbuyer@73: end farmbuyer@73: UpdateAddOnMemoryUsage() farmbuyer@73: local after = GetAddOnMemoryUsage(nametag) farmbuyer@73: self:Print("Pre-scanning history for faster loot handling on %s used %.2f MB of memory across %d entries.", farmbuyer@73: self.history.realm, (after-before)/1024, count) farmbuyer@73: if trouble then farmbuyer@73: self:Print("Note that there were inconsistencies in the data;", farmbuyer@73: "you should consider submitting a bug report (including your", farmbuyer@73: "SavedVariables file), and regenerating or preening this", farmbuyer@73: "realm's loot history.") farmbuyer@73: end farmbuyer@73: g_uniques:SETMODE('probe') farmbuyer@73: self._cache_history_uniques = nil farmbuyer@73: end farmbuyer@6: farmbuyer@4: -- Builds the map of names to array indices, using passed table or farmbuyer@4: -- self.history, and stores the result into its 'byname' field. Also farmbuyer@4: -- called from the GUI code at least once. farmbuyer@1: function addon:_build_history_names (opt_hist) farmbuyer@1: local hist = opt_hist or self.history farmbuyer@1: local m = {} farmbuyer@1: for i = 1, #hist do farmbuyer@1: m[hist[i].name] = i farmbuyer@1: end farmbuyer@73: -- why yes, I *did* spend many years as a YP/NIS admin, how did you know? farmbuyer@1: hist.byname = m farmbuyer@1: end farmbuyer@1: farmbuyer@1: -- Prepares and returns table to be used as self.history. farmbuyer@1: function addon:_prep_new_history_category (prev_table, realmname) farmbuyer@1: local t = prev_table or { farmbuyer@1: --kind = 'realm', farmbuyer@6: --realm = realmname, farmbuyer@1: } farmbuyer@6: t.realm = realmname farmbuyer@1: farmbuyer@1: --[[ farmbuyer@1: t.cols = setmetatable({ farmbuyer@1: { value = realmname }, farmbuyer@1: }, self.time_column1_used_mt) farmbuyer@1: ]] farmbuyer@1: farmbuyer@1: if not t.byname then farmbuyer@1: self:_build_history_names (t) farmbuyer@1: end farmbuyer@1: farmbuyer@1: return t farmbuyer@1: end farmbuyer@1: farmbuyer@4: -- Maps a name to an array index, creating new tables if needed. Returns farmbuyer@6: -- the index and the table at that index. farmbuyer@4: function addon:get_loot_history (name) farmbuyer@4: local i farmbuyer@4: i = self.history.byname[name] farmbuyer@4: if not i then farmbuyer@4: i = #self.history + 1 farmbuyer@73: self.history[i] = { name=name, id={}, when={}, unique={}, count={} } farmbuyer@4: self.history.byname[name] = i farmbuyer@4: end farmbuyer@6: return i, self.history[i] farmbuyer@4: end farmbuyer@4: farmbuyer@73: -- Prepends data from the loot entry at LOOTINDEX to be the new most farmbuyer@73: -- recent history entry for that player. farmbuyer@1: function addon:_addHistoryEntry (lootindex) farmbuyer@1: local e = g_loot[lootindex] farmbuyer@6: if e.kind ~= 'loot' then return end farmbuyer@6: farmbuyer@6: local i,h = self:get_loot_history(e.person) farmbuyer@73: local when = self:format_timestamp (g_today, e) farmbuyer@73: farmbuyer@25: -- If any of these change, update the end of history_handle_disposition. farmbuyer@71: if (not e.unique) or (#e.unique==0) then farmbuyer@73: e.unique = e.id .. ' ' .. when farmbuyer@71: end farmbuyer@73: local U = e.unique farmbuyer@73: tinsert (h.unique, 1, U) farmbuyer@73: h.when[U] = when farmbuyer@73: h.id[U] = e.id farmbuyer@73: h.count[U] = e.count farmbuyer@73: farmbuyer@73: g_uniques[U] = { loot = lootindex, history = i } farmbuyer@6: end farmbuyer@6: farmbuyer@24: -- Create new history table based on current loot. farmbuyer@6: function addon:rewrite_history (realmname) farmbuyer@6: local r = assert(realmname) farmbuyer@6: self.history_all[r] = self:_prep_new_history_category (nil, r) farmbuyer@6: self.history = self.history_all[r] farmbuyer@73: g_uniques:RESET() farmbuyer@6: farmbuyer@6: local g_today_real = g_today farmbuyer@6: for i,e in ipairs(g_loot) do farmbuyer@6: if e.kind == 'time' then farmbuyer@6: g_today = e farmbuyer@6: elseif e.kind == 'loot' then farmbuyer@6: self:_addHistoryEntry(i) farmbuyer@6: end farmbuyer@6: end farmbuyer@6: g_today = g_today_real farmbuyer@6: self.hist_clean = nil farmbuyer@6: farmbuyer@6: -- safety measure: resort players' tables based on formatted timestamp farmbuyer@6: for i,h in ipairs(self.history) do farmbuyer@73: sort_player(h) farmbuyer@6: end farmbuyer@6: end farmbuyer@6: farmbuyer@76: -- Clears all but the most recent HOWMANY (optional, default 1) entries farmbuyer@76: -- for each player on REALMNAME. farmbuyer@76: -- This function's name is the legacy of the orignal fsck(8) "-p" option, farmbuyer@76: -- which has a similar feel. farmbuyer@76: function addon:preen_history (realmname, howmany) farmbuyer@6: local r = assert(realmname) farmbuyer@76: howmany = tonumber(howmany) or 1 farmbuyer@76: if type(self.history_all[r]) ~= 'table' then farmbuyer@76: return farmbuyer@76: end farmbuyer@73: g_uniques:RESET() farmbuyer@76: for i,h in ipairs(self.history_all[r]) do farmbuyer@73: -- This is going to do horrible things to memory. The subtables farmbuyer@73: -- after this step would be large and sparse, with no good way farmbuyer@73: -- of shrinking the allocation... farmbuyer@73: sort_player(h) farmbuyer@73: -- ...so it's better in the long run to discard them. farmbuyer@76: local new_unique, new_id, new_when, new_count = {}, {}, {}, {} farmbuyer@76: for ui = 1, howmany do farmbuyer@76: local U = h.unique[ui] farmbuyer@76: if not U then break end farmbuyer@76: new_unique[ui] = U farmbuyer@76: new_id[U] = h.id[U] farmbuyer@76: new_when[U] = h.when[U] farmbuyer@76: new_count[U] = h.count[U] farmbuyer@76: end farmbuyer@76: h.unique, h.id, h.when, h.count = farmbuyer@76: new_unique, new_id, new_when, new_count farmbuyer@6: end farmbuyer@1: end farmbuyer@24: farmbuyer@73: -- Given a unique tag OR an entry in a g_loot table, looks up the farmbuyer@73: -- corresponding history entry. Returns the player's index and history farmbuyer@73: -- table (as in get_loot_history) and the index into that table of the farmbuyer@73: -- loot entry. On failure, returns nil and an error message ready to be farmbuyer@73: -- formatted with the loot's name/itemlink. farmbuyer@73: function _history_by_loot_id (needle, operation_text) farmbuyer@73: local errtxt farmbuyer@73: if type(needle) == 'string' then farmbuyer@73: -- unique tag farmbuyer@73: elseif type(needle) == 'table' then farmbuyer@73: if needle.kind ~= 'loot' then farmbuyer@73: error("trying to "..operation_text.." something that isn't loot") farmbuyer@73: end farmbuyer@73: needle = needle.unique farmbuyer@73: if not needle then farmbuyer@73: return nil, --[[errtxt=]]"Entry for %s is missing a history tag!" farmbuyer@73: end farmbuyer@73: else farmbuyer@73: error("'"..tostring(needle).."' is neither unique string nor loot entry!") farmbuyer@25: end farmbuyer@24: farmbuyer@73: local player_i, player_h farmbuyer@73: local cache = g_uniques[needle] farmbuyer@24: farmbuyer@73: if cache.history == g_uniques.NOTFOUND then farmbuyer@73: -- 1) loot an item, 2) clear old history, 3) reassign from current loot farmbuyer@73: -- Bah. Anybody that tricky is already recoding the tables directly anyhow. farmbuyer@73: errtxt = "There is no record of %s ever having been assigned!" farmbuyer@24: else farmbuyer@73: player_i = cache.history farmbuyer@73: player_h = addon.history[player_i] farmbuyer@73: if cache.history_may farmbuyer@73: and needle == player_h.unique[cache.history_may] farmbuyer@73: then farmbuyer@73: return player_i, player_h, cache.history_may farmbuyer@73: end farmbuyer@73: for i,u in ipairs(player_h.unique) do farmbuyer@73: if needle == u then farmbuyer@73: cache.history_may = i -- might help, might not farmbuyer@73: return player_i, player_h, i farmbuyer@24: end farmbuyer@24: end farmbuyer@24: end farmbuyer@24: farmbuyer@73: if not errtxt then farmbuyer@73: -- The cache finder got a hit, but now it's gone? WTF? farmbuyer@73: 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." farmbuyer@24: end farmbuyer@73: return nil, errtxt farmbuyer@25: end farmbuyer@24: farmbuyer@25: function addon:reassign_loot (index, to_name) farmbuyer@25: assert(type(to_name)=='string' and to_name:len()>0) farmbuyer@25: local e = g_loot[index] farmbuyer@73: local from_i, from_h, hist_i = _history_by_loot_id (e, "reassign") farmbuyer@25: local from_name = e.person farmbuyer@25: local to_i,to_h = self:get_loot_history(to_name) farmbuyer@25: farmbuyer@73: if not from_i then -- from_h here is the formatted error text farmbuyer@25: self:Print(from_h .. " Loot will be reassigned, but history will NOT be updated.", e.itemlink) farmbuyer@25: else farmbuyer@73: local U = tremove (from_h.unique, hist_i) farmbuyer@73: -- The loot master giveth... farmbuyer@73: to_h.unique[#to_h.unique+1] = U farmbuyer@73: to_h.when[U] = from_h.when[U] farmbuyer@73: to_h.id[U] = from_h.id[U] farmbuyer@73: to_h.count[U] = from_h.count[U] farmbuyer@73: sort_player(to_h) farmbuyer@73: -- ...and the loot master taketh away. farmbuyer@73: from_h.when[U] = nil farmbuyer@73: from_h.id[U] = nil farmbuyer@73: from_h.count[U] = nil farmbuyer@73: -- Blessed be the lookup cache of the loot master. farmbuyer@73: g_uniques[U] = { loot = index, history = to_i } farmbuyer@25: end farmbuyer@25: e.person = to_name farmbuyer@73: e.person_class = select(2,_G.UnitClass(to_name)) farmbuyer@25: self.hist_clean = nil farmbuyer@25: farmbuyer@25: self:Print("Reassigned entry %d/%s from '%s' to '%s'.", index, e.itemlink, from_name, to_name) farmbuyer@25: end farmbuyer@25: farmbuyer@36: -- Similar to _addHistoryEntry. The second arg may be a loot entry farmbuyer@36: -- (which used to be at LOOTINDEX), or nil (and the loot entry will farmbuyer@36: -- be pulled from LOOTINDEX instead). farmbuyer@36: function addon:_delHistoryEntry (lootindex, opt_e) farmbuyer@36: local e = opt_e or g_loot[lootindex] farmbuyer@36: if e.kind ~= 'loot' then return end farmbuyer@36: farmbuyer@73: local from_i, from_h, hist_i = _history_by_loot_id (e, "delete") farmbuyer@36: if not from_i then farmbuyer@36: -- from_h is the formatted error text farmbuyer@36: self:Print(from_h .. " Loot will be deleted, but history will NOT be updated.", e.itemlink) farmbuyer@36: return farmbuyer@36: end farmbuyer@36: farmbuyer@73: local hist_u = tremove (from_h.unique, hist_i) farmbuyer@73: from_h.when[hist_u] = nil farmbuyer@73: from_h.id[hist_u] = nil farmbuyer@73: from_h.count[hist_u] = nil farmbuyer@73: g_uniques[hist_u] = nil farmbuyer@36: self.hist_clean = nil farmbuyer@36: farmbuyer@36: self:Print("Removed history entry %d/%s from '%s'.", lootindex, e.itemlink, e.person) farmbuyer@36: end farmbuyer@36: farmbuyer@25: -- Any extra work for the "Mark as " dropdown actions. The farmbuyer@25: -- corresponding will already have been assigned in the loot entry. farmbuyer@73: local deleted_cache = {} farmbuyer@25: function addon:history_handle_disposition (index, olddisp) farmbuyer@25: local e = g_loot[index] farmbuyer@25: -- Standard disposition has a nil entry, but that's tedious in debug farmbuyer@25: -- output, so force to a string instead. farmbuyer@25: olddisp = olddisp or 'normal' farmbuyer@25: local newdisp = e.disposition or 'normal' farmbuyer@25: -- Ignore misclicks and the like farmbuyer@25: if olddisp == newdisp then return end farmbuyer@25: farmbuyer@25: local name = e.person farmbuyer@25: farmbuyer@25: if (newdisp == 'shard' or newdisp == 'gvault') then farmbuyer@73: local name_i, name_h, hist_i = _history_by_loot_id (e, "mark") farmbuyer@73: -- remove history entry if it exists farmbuyer@25: if hist_i then farmbuyer@73: local c = flib.new() farmbuyer@73: local hist_u = tremove (name_h.unique, hist_i) farmbuyer@73: c.when = name_h.when[hist_u] farmbuyer@73: c.id = name_h.id[hist_u] farmbuyer@73: c.count = name_h.count[hist_u] farmbuyer@73: deleted_cache[hist_u] = c farmbuyer@73: name_h.when[hist_u] = nil farmbuyer@73: name_h.id[hist_u] = nil farmbuyer@73: name_h.count[hist_u] = nil farmbuyer@25: self.hist_clean = nil farmbuyer@25: elseif (olddisp == 'shard' or olddisp == 'gvault') then farmbuyer@25: -- Sharding a vault item, or giving the auto-sharder something to bank, farmbuyer@25: -- etc, wouldn't necessarily have had a history entry to begin with. farmbuyer@73: -- So this isn't treated as an error. farmbuyer@25: else farmbuyer@25: self:Print(name_h .. " Loot has been marked, but history will NOT be updated.", e.itemlink) farmbuyer@25: end farmbuyer@25: return farmbuyer@25: end farmbuyer@25: farmbuyer@25: if (olddisp == 'shard' or olddisp == 'gvault') farmbuyer@25: and (newdisp == 'normal' or newdisp == 'offspec') farmbuyer@25: then farmbuyer@25: local name_i, name_h = self:get_loot_history(name) farmbuyer@25: farmbuyer@25: -- Must create a new history entry. Could call '_addHistoryEntry(index)' farmbuyer@25: -- but that would duplicate a lot of effort. To start with, check the farmbuyer@25: -- cache of stuff we've already deleted; if it's not there then just do farmbuyer@25: -- the same steps as _addHistoryEntry. farmbuyer@73: -- FIXME The deleted cache isn't nearly as useful now with the new data structures. farmbuyer@73: local when farmbuyer@73: if (not e.unique) or (#e.unique==0) then farmbuyer@73: when = g_today and self:format_timestamp (g_today, e) or date("%Y/%m/%d %H:%M",e.stamp) farmbuyer@73: e.unique = e.id .. ' ' .. when farmbuyer@25: end farmbuyer@73: local U = e.unique farmbuyer@73: local c = deleted_cache[U] farmbuyer@73: deleted_cache[U] = nil farmbuyer@73: name_h.unique[#name_h.unique+1] = U farmbuyer@73: name_h.when[U] = c and c.when or when or date("%Y/%m/%d %H:%M",e.stamp) farmbuyer@73: name_h.id[U] = e.id -- c.id farmbuyer@73: name_h.count[U] = c and c.count or e.count farmbuyer@73: sort_player(name_h) farmbuyer@73: g_uniques[U] = { loot = index, history = name_i } farmbuyer@25: self.hist_clean = nil farmbuyer@73: farmbuyer@73: if c then flib.del(c) end farmbuyer@73: farmbuyer@25: return farmbuyer@25: end farmbuyer@24: end farmbuyer@73: farmbuyer@73: -- This is not entirely "history" but not completely anything else either. farmbuyer@73: -- Handles the primary "Mark as " action. Arguments depend on who's farmbuyer@73: -- calling it: farmbuyer@73: -- "local", row_index, new_disposition farmbuyer@73: -- "remote", sender, unique_id, item_id, old_disposition, new_disposition farmbuyer@73: -- In the local case, must also broadcast a trigger. In the remote case, farmbuyer@73: -- must figure out the corresponding loot entry (if it exists). In both farmbuyer@73: -- cases, must update history appropriately. Returns nil if anything odd farmbuyer@73: -- happens (not necessarily an error!); returns the affected loot index farmbuyer@73: -- on success. farmbuyer@73: function addon:loot_mark_disposition (how, ...) farmbuyer@73: -- This must all be filled out in all cases: farmbuyer@73: local e, index, olddisp, newdisp, unique, id farmbuyer@73: -- Only set in remote case: farmbuyer@73: local sender farmbuyer@73: farmbuyer@73: if how == "local" then farmbuyer@73: index, newdisp = ... farmbuyer@73: index = assert(tonumber(index)) farmbuyer@73: e = g_loot[index] farmbuyer@73: id = e.id farmbuyer@73: unique = e.unique -- can potentially still be nil at this step farmbuyer@73: olddisp = e.disposition farmbuyer@73: farmbuyer@73: elseif how == "remote" then farmbuyer@73: sender, unique, id, olddisp, newdisp = ... farmbuyer@73: local cache = g_uniques[unique] farmbuyer@73: if cache.loot then farmbuyer@73: index = tonumber(cache.loot) farmbuyer@73: e = g_loot[index] farmbuyer@73: end farmbuyer@73: farmbuyer@73: else farmbuyer@76: return -- silently ignore future cases from future clients farmbuyer@73: end farmbuyer@73: farmbuyer@73: if self.debug.loot then farmbuyer@73: local m = ("Re-mark index %d(pre-unique %s) with id %d from '%s' to '%s'."): farmbuyer@73: format(index, unique, id, tostring(olddisp), tostring(newdisp)) farmbuyer@73: self.dprint('loot', m) farmbuyer@73: if sender == my_name then farmbuyer@76: self.dprint('loot',"(Returning early from debug mode's double self-mark.)") farmbuyer@73: return index farmbuyer@73: end farmbuyer@73: end farmbuyer@73: farmbuyer@73: if not e then farmbuyer@73: -- say something? farmbuyer@73: return farmbuyer@73: end farmbuyer@73: farmbuyer@73: e.disposition = newdisp farmbuyer@73: e.bcast_from = nil -- I actually don't remember now why this gets cleared... farmbuyer@73: e.extratext = nil farmbuyer@73: self:history_handle_disposition (index, olddisp) farmbuyer@73: -- A unique tag has been set by this point. farmbuyer@73: if how == "local" then farmbuyer@73: unique = assert(e.unique) farmbuyer@73: self:vbroadcast('mark', unique, id, olddisp, newdisp) farmbuyer@73: end farmbuyer@73: return index farmbuyer@73: end farmbuyer@1: end farmbuyer@1: farmbuyer@1: farmbuyer@1: ------ Player communication farmbuyer@1: do farmbuyer@73: local select, tconcat, strsplit, unpack = select, table.concat, strsplit, unpack farmbuyer@56: --[[ old way: repeated string concatenations, BAD farmbuyer@76: new way: new table on every broadcast, BAD farmbuyer@56: local msg = ... farmbuyer@56: for i = 2, select('#',...) do farmbuyer@56: msg = msg .. '\a' .. (select(i,...) or "") farmbuyer@56: end farmbuyer@56: return msg farmbuyer@56: ]] farmbuyer@56: local function assemble(t,...) farmbuyer@73: local n = select('#',...) farmbuyer@73: if n > 0 then farmbuyer@56: local msg = {t,...} farmbuyer@56: -- tconcat requires strings, but T is known to be one already farmbuyer@73: -- can't use #msg since there might be holes farmbuyer@73: for i = 2, n+1 do farmbuyer@71: msg[i] = tostring(msg[i] or "") farmbuyer@56: end farmbuyer@56: return tconcat (msg, '\a') farmbuyer@56: end farmbuyer@56: return t farmbuyer@56: end farmbuyer@56: farmbuyer@56: -- broadcast('tag', ) farmbuyer@56: -- vbroadcast('tag', ) farmbuyer@56: function addon:vbroadcast(tag,...) farmbuyer@56: return self:broadcast(self.commrev..tag,...) farmbuyer@56: end farmbuyer@56: function addon:broadcast(tag,...) farmbuyer@56: local msg = assemble(tag,...) farmbuyer@56: self.dprint('comm', ":", msg) farmbuyer@56: -- the "GUILD" here is just so that we can also pick up on it farmbuyer@56: self:SendCommMessage(self.ident, msg, self.debug.comm and "GUILD" or "RAID") farmbuyer@56: end farmbuyer@56: -- whispercast(, 'tag', ) farmbuyer@56: function addon:whispercast(to,...) farmbuyer@56: local msg = assemble(...) farmbuyer@56: self.dprint('comm', "@", to, ":", msg) farmbuyer@56: self:SendCommMessage(self.identTg, msg, "WHISPER", to) farmbuyer@56: end farmbuyer@56: farmbuyer@1: local function adduser (name, status, active) farmbuyer@1: if status then addon.sender_list.names[name] = status end farmbuyer@1: if active then addon.sender_list.active[name] = active end farmbuyer@1: end farmbuyer@1: farmbuyer@1: -- Incoming handler functions. All take the sender name and the incoming farmbuyer@1: -- tag as the first two arguments. All of these are active even when the farmbuyer@1: -- player is not tracking loot, so test for that when appropriate. farmbuyer@1: local OCR_funcs = {} farmbuyer@1: farmbuyer@1: OCR_funcs.ping = function (sender) farmbuyer@1: pprint('comm', "incoming ping from", sender) farmbuyer@1: addon:whispercast (sender, 'pong', addon.revision, farmbuyer@1: addon.enabled and "tracking" or (addon.rebroadcast and "broadcasting" or "disabled")) farmbuyer@1: end farmbuyer@1: OCR_funcs.pong = function (sender, _, rev, status) farmbuyer@17: local s = ("|cff00ff00%s|r %s is |cff00ffff%s|r"):format(sender,rev,status) farmbuyer@1: addon:Print("Echo: ", s) farmbuyer@1: adduser (sender, s, status=="tracking" or status=="broadcasting" or nil) farmbuyer@1: end farmbuyer@27: OCR_funcs.revcheck = function (sender, _, revlarge) farmbuyer@27: addon.dprint('comm', "revcheck, sender", sender) farmbuyer@27: addon:_check_revision (revlarge) farmbuyer@27: end farmbuyer@1: farmbuyer@76: OCR_funcs['17improv'] = function (sender, _, senderid, existing, replace) farmbuyer@76: addon.dprint('comm', "DOTimprov/17, sender", sender, "id", senderid, farmbuyer@76: "existing", existing, "replace", replace) farmbuyer@76: if not g_unique_replace then _setup_unique_replace() end farmbuyer@76: g_unique_replace.new_entry (senderid, existing, replace) farmbuyer@76: end farmbuyer@76: farmbuyer@73: OCR_funcs['17mark'] = function (sender, _, unique, item, old, new) farmbuyer@73: addon.dprint('comm', "DOTmark/17, sender", sender, "unique", unique, farmbuyer@73: "item", item, "from old", old, "to new", new) farmbuyer@73: local index = addon:loot_mark_disposition ("remote", sender, unique, item, old, new) farmbuyer@73: --if not addon.enabled then return end -- hmm farmbuyer@73: if index and opts.chatty_on_remote_changes then farmbuyer@73: _notify_about_remote (sender, index, --[[from_whom=]]nil, old) farmbuyer@73: end farmbuyer@73: end farmbuyer@73: farmbuyer@71: OCR_funcs['16loot'] = function (sender, _, recip, item, count, extratext) farmbuyer@71: addon.dprint('comm', "DOTloot/16, sender", sender, "recip", recip, "item", item, "count", count) farmbuyer@1: if not addon.enabled then return end farmbuyer@1: adduser (sender, nil, true) farmbuyer@73: -- Empty unique string will pass through all of the loot handling, farmbuyer@73: -- and then be rewritten by the history routine (into older string farmbuyer@73: -- of ID+date). farmbuyer@73: g_seeing_oldsigs = g_seeing_oldsigs or {} farmbuyer@73: g_seeing_oldsigs[sender] = true farmbuyer@71: addon:CHAT_MSG_LOOT ("broadcast", recip, --[[unique=]]"", item, count, sender, extratext) farmbuyer@1: end farmbuyer@71: OCR_funcs.loot = OCR_funcs['16loot'] -- old unversioned stuff goes to 16 farmbuyer@71: OCR_funcs['17loot'] = function (sender, _, recip, unique, item, count, extratext) farmbuyer@73: addon.dprint('comm', "DOTloot/17, sender", sender, "recip", recip, farmbuyer@73: "unique", unique, "item", item, "count", count, "extratext", extratext) farmbuyer@71: if not addon.enabled then return end farmbuyer@71: adduser (sender, nil, true) farmbuyer@71: addon:CHAT_MSG_LOOT ("broadcast", recip, unique, item, count, sender, extratext) farmbuyer@71: end farmbuyer@1: farmbuyer@1: OCR_funcs.boss = function (sender, _, reason, bossname, instancetag) farmbuyer@56: addon.dprint('comm', "DOTboss, sender", sender, "reason", reason, farmbuyer@56: "name", bossname, "it", instancetag) farmbuyer@1: if not addon.enabled then return end farmbuyer@1: adduser (sender, nil, true) farmbuyer@56: addon:on_boss_broadcast (reason, bossname, instancetag, --[[maxsize=]]0) farmbuyer@56: end farmbuyer@56: OCR_funcs['16boss'] = function (sender, _, reason, bossname, instancetag, maxsize) farmbuyer@73: addon.dprint('comm', "DOTboss/16,17, sender", sender, "reason", reason, farmbuyer@56: "name", bossname, "it", instancetag, "size", maxsize) farmbuyer@56: if not addon.enabled then return end farmbuyer@56: adduser (sender, nil, true) farmbuyer@56: addon:on_boss_broadcast (reason, bossname, instancetag, maxsize) farmbuyer@1: end farmbuyer@71: OCR_funcs['17boss'] = OCR_funcs['16boss'] farmbuyer@1: farmbuyer@76: local bcast_on = addon.format_hypertext ([[the red pill]], '|cffff4040', farmbuyer@78: function (self) farmbuyer@78: if not self.rebroadcast then farmbuyer@78: self:Activate(nil,true) farmbuyer@76: end farmbuyer@78: self:broadcast('bcast_responder') farmbuyer@76: end) farmbuyer@76: local waferthin = addon.format_hypertext ([[the blue pill]], '|cff0070dd', farmbuyer@78: function (self) farmbuyer@76: g_wafer_thin = true -- mint? it's wafer thin! farmbuyer@78: self:broadcast('bcast_denied') -- fuck off, I'm full farmbuyer@76: end) farmbuyer@1: OCR_funcs.bcast_req = function (sender) farmbuyer@1: if addon.debug.comm or ((not g_wafer_thin) and (not addon.rebroadcast)) farmbuyer@1: then farmbuyer@38: addon:Print("%s has requested additional broadcasters! Choose %s to enable rebroadcasting, or %s to remain off and also ignore rebroadcast requests for as long as you're logged in.", farmbuyer@1: sender, farmbuyer@76: tostring(bcast_on), farmbuyer@76: tostring(waferthin)) farmbuyer@1: end farmbuyer@18: addon.popped = true farmbuyer@1: end farmbuyer@1: farmbuyer@1: OCR_funcs.bcast_responder = function (sender) farmbuyer@1: if addon.debug.comm or addon.requesting or farmbuyer@1: ((not g_wafer_thin) and (not addon.rebroadcast)) farmbuyer@1: then farmbuyer@1: addon:Print(sender, "has answered the call and is now broadcasting loot.") farmbuyer@1: end farmbuyer@1: end farmbuyer@1: -- remove this tag once it's all tested farmbuyer@1: OCR_funcs.bcast_denied = function (sender) farmbuyer@1: if addon.requesting then addon:Print(sender, "declines futher broadcast requests.") end farmbuyer@1: end farmbuyer@1: farmbuyer@73: -- Incoming message disassembler and dispatcher. The static weak table farmbuyer@73: -- is not my favorite approach to handling ellipses, but it lets me loop farmbuyer@73: -- through potential nils easily without creating a ton of garbage. farmbuyer@73: local OCR_data = setmetatable({}, {__mode='v'}) farmbuyer@1: local function dotdotdot (sender, tag, ...) farmbuyer@1: local f = OCR_funcs[tag] farmbuyer@73: if f then farmbuyer@73: --wipe(OCR_data) costs more than its worth here farmbuyer@73: local n = select('#',...) farmbuyer@73: for i = 1, n do farmbuyer@73: local d = select(i,...) farmbuyer@73: OCR_data[i] = (d ~= "") and d or nil farmbuyer@73: end farmbuyer@73: addon.dprint('comm', ":... processing", tag, "from", sender, "with arg count", n) farmbuyer@73: return f(sender,tag,unpack(OCR_data,1,n)) farmbuyer@73: end farmbuyer@73: addon.dprint('comm', "unknown comm message", tag, "from", sender) farmbuyer@1: end farmbuyer@73: -- Recent message cache (this can be accessed via advanced options panel) farmbuyer@1: addon.recent_messages = create_new_cache ('comm', comm_cleanup_ttl) farmbuyer@1: farmbuyer@1: function addon:OnCommReceived (prefix, msg, distribution, sender) farmbuyer@1: if prefix ~= self.ident then return end farmbuyer@1: if not self.debug.comm then farmbuyer@1: if distribution ~= "RAID" and distribution ~= "WHISPER" then return end farmbuyer@1: if sender == my_name then return end farmbuyer@1: end farmbuyer@1: self.dprint('comm', ":OCR from", sender, "message is", msg) farmbuyer@1: farmbuyer@1: if self.recent_messages:test(msg) then farmbuyer@40: self.dprint('cache', "OCR message <",msg,"> already in cache, skipping") farmbuyer@40: return farmbuyer@1: end farmbuyer@1: self.recent_messages:add(msg) farmbuyer@1: farmbuyer@1: -- Nothing is actually returned, just (ab)using tail calls. farmbuyer@1: return dotdotdot(sender,strsplit('\a',msg)) farmbuyer@1: end farmbuyer@1: farmbuyer@1: function addon:OnCommReceivedNocache (prefix, msg, distribution, sender) farmbuyer@1: if prefix ~= self.identTg then return end farmbuyer@1: if not self.debug.comm then farmbuyer@1: if distribution ~= "WHISPER" then return end farmbuyer@1: if sender == my_name then return end farmbuyer@1: end farmbuyer@1: self.dprint('comm', ":OCRN from", sender, "message is", msg) farmbuyer@1: return dotdotdot(sender,strsplit('\a',msg)) farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@1: -- vim:noet