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@114: - fname fully-qualified name; "PlayerName" or "PlayerName-RealmName" 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@114: - realm realm name, or missing if same realm as player at the time farmbuyer@102: - online 'online', 'offline', 'no_longer' [no longer in raid group] 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@102: 'online' is not 'no_longer') farmbuyer@125: - needinfo true if haven't yet gotten close enough to player to request farmbuyer@125: unit data; SHOULD be missing 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@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@114: - person_realm recipient's realm if different from the player's; missing farmbuyer@114: otherwise 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@109: - extratext text in Note column, including disposition and rebroadcasting; farmbuyer@109: missing otherwise 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@102: ------ Ace3 framework stuff (callback 'events', search for LCALLBACK) 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@102: -- are name/forum/attend/date farmbuyer@16: OuroLootSV_hist = nil farmbuyer@19: OuroLootSV_log = {} farmbuyer@1: farmbuyer@1: farmbuyer@1: ------ Constants farmbuyer@99: local option_defaults = { profile = { farmbuyer@99: --['datarev'] = 20, -- cheating, this isn't actually an option farmbuyer@1: ['popup_on_join'] = true, farmbuyer@94: ['register_slash_synonyms'] = false, farmbuyer@94: ['slash_synonyms'] = '/ol,/oloot', farmbuyer@1: ['scroll_to_bottom'] = true, farmbuyer@114: ['history_suppress_LFR'] = false, farmbuyer@114: ['history_ignore_xrealm'] = true, farmbuyer@83: ['gui_noob'] = 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@104: ['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@101: ['chatty_on_local_changes'] = false, farmbuyer@101: ['chatty_on_changes_frame'] = 1, farmbuyer@97: ['itemfilter'] = {}, farmbuyer@97: ['itemvault'] = {}, farmbuyer@99: } } farmbuyer@95: local virgin = "First time loaded? Hi! Use the /ouroloot 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@81: local horrible_error_text = [[|cffff1010]] .. ERROR_CAPS farmbuyer@81: ..[[:|n|cffffff00Something unrecoverable has happened. The error message]] farmbuyer@81: ..[[ which was provided follows in white:|r|n|n%s|n|n|cffffff00Ouro Loot]] farmbuyer@81: ..[[ will not display a window until this situation is corrected. ]] farmbuyer@81: ..[[ You can try typing|n|cff00ff40/ouroloot fix ?|n]] farmbuyer@81: ..[[|cffffff00to see what can be done by software alone. You may still]] farmbuyer@81: ..[[ need to do a "/reload" afterwards, or even restart the game client.]] farmbuyer@81: local unique_collision = "Item '%s' was carrying unique tag <%s>, but that was already in use; tried to generate a new tag and failed!|n|nRemote sender was '%s', previous cache entry was <%s/%s>.|n|nThis may require a live human to figure out; the loot in question has not been stored." farmbuyer@99: local new_profile_warning = [[Be aware that profiles only store addon & plugin settings from the tab; loot and generated text is account-wide data, unrelated to your current profile.]] farmbuyer@86: local remote_chatty = "|cff00ff00%s|r changed %d/%s from %s to %s" 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@89: local version_large = nil -- defaults to 1, possibly changed by version farmbuyer@129: local g_LOOT_ITEM_ss, g_LOOT_ITEM_MULTIPLE_sss, g_LOOT_ITEM_SELF_s farmbuyer@129: local g_LOOT_ITEM_SELF_MULTIPLE_ss, g_LOOT_ITEM_WHILE_PLAYER_INELIGIBLE_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@89: version = _G.GetAddOnMetadata(nametag,"Version") or "?" -- "x.yy.z", etc farmbuyer@1: ident = "OuroLoot2" farmbuyer@1: identTg = "OuroLoot2Tg" farmbuyer@1: status_text = nil farmbuyer@89: revision = "@project-revision@" farmbuyer@89: --@debug@ farmbuyer@89: revision = "DEVEL" farmbuyer@89: --@end-debug@ 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@100: callback = 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@100: is_guilded = _G.IsInGuild() 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@109: error('ASSERT() FAILED: '.._G.tostring(msg or 'nil'),3) 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@103: lootjumps = {} -- maps hypertext idents to EOI line numbers farmbuyer@103: 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@109: addon.FILES_LOADED = 0 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@86: -- Class color support. Do the expensive string.format calls up front, and farmbuyer@86: -- the cheap all-string-all-at-once single-op concatenation as needed. farmbuyer@86: do farmbuyer@86: local cc = {} farmbuyer@86: local function extract (color_info) farmbuyer@93: local hex farmbuyer@93: if color_info.colorStr then -- MoP farmbuyer@93: hex = "|c" .. color_info.colorStr farmbuyer@93: else -- pre-MoP farmbuyer@93: hex = ("|cff%.2x%.2x%.2x"):format(255*color_info.r, farmbuyer@93: 255*color_info.g, 255*color_info.b) farmbuyer@93: end farmbuyer@92: return { r=color_info.r, g=color_info.g, b=color_info.b, a=1, hex=hex } farmbuyer@86: end farmbuyer@86: local function fill_out_class_colors() farmbuyer@86: for class,color in pairs(CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS) do farmbuyer@86: cc[class] = extract(color) farmbuyer@86: end farmbuyer@86: cc.DEFAULT = extract(_G.NORMAL_FONT_COLOR) farmbuyer@86: end farmbuyer@86: if CUSTOM_CLASS_COLORS and CUSTOM_CLASS_COLORS.RegisterCallback then farmbuyer@86: CUSTOM_CLASS_COLORS:RegisterCallback(fill_out_class_colors) farmbuyer@86: end farmbuyer@93: addon.class_colors = cc -- this stays around farmbuyer@93: addon.fill_out_class_colors = fill_out_class_colors -- this doesn't farmbuyer@93: farmbuyer@86: -- What I really want is to have the hooked :Print understand a special farmbuyer@86: -- format specifier like "%Cs" and do the colorizing automatically. farmbuyer@86: function addon:colorize (text, class) farmbuyer@86: return ((class and cc[class]) or cc.DEFAULT).hex .. text .. "|r" farmbuyer@86: end farmbuyer@86: end farmbuyer@86: 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@110: local g_gui = nil farmbuyer@1: farmbuyer@76: local error = addon.error farmbuyer@76: local assert = addon.assert farmbuyer@76: farmbuyer@107: local type, select, next, pairs, ipairs, tinsert, tremove, tostring, tonumber, wipe = farmbuyer@107: type, select, next, pairs, ipairs, table.insert, table.remove, tostring, tonumber, table.wipe farmbuyer@73: farmbuyer@1: local pprint, tabledump = addon.pprint, flib.tabledump farmbuyer@107: local CopyTable = CopyTable farmbuyer@135: local GetNumGroupMembers = GetNumGroupMembers farmbuyer@135: local IsInRaid = IsInRaid farmbuyer@1: -- En masse forward decls of symbols defined inside local blocks farmbuyer@100: local _register_bossmod, makedate, create_new_cache, _init, _log, _do_loot_metas farmbuyer@81: local _history_by_loot_id, _setup_unique_replace, _unavoidable_collision farmbuyer@128: local _notify_about_change, _LFR_suppressing, _add_loot_disposition 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@89: -- This makes/reinforces an assumption that version_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@89: for d in addon.version: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@89: version_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@107: local text_map = setmetatable({}, {__mode = 'v'}) farmbuyer@107: local base = newproxy(true) farmbuyer@107: getmetatable(base).__tostring = function(ud) return text_map[ud] end farmbuyer@76: --@debug@ farmbuyer@103: -- auto collecting these tokens is an interesting micro-optimization but not yet farmbuyer@107: getmetatable(base).__index = { farmbuyer@103: ['done'] = function (ud) farmbuyer@103: text_map[ud] = nil farmbuyer@103: func_map[ud] = nil farmbuyer@103: end, farmbuyer@103: } farmbuyer@107: 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@125: -- in local chat. Clicking a tab_title hyperlink opens the GUI to that farmbuyer@125: -- tab; the MethodName and raw function callbacks will be passed the addon farmbuyer@125: -- table, the same matching number, and the mouse button (ala OnClick) as farmbuyer@125: -- arguments. 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@107: local ret = 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@107: local strsplit = 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@125: local t = type(f) farmbuyer@125: if t == 'string' then farmbuyer@76: if type(addon[f]) == 'function' then farmbuyer@125: addon[f](addon,arg,mousebutton) -- method farmbuyer@76: else farmbuyer@125: addon:BuildMainDisplay(f) -- tab title fragment farmbuyer@1: end farmbuyer@125: elseif t == 'function' then farmbuyer@125: f (addon, arg, mousebutton) 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@133: if GetLFGMode(LE_LFG_CATEGORY_RF) then farmbuyer@56: t,r = 'LFR', 25 farmbuyer@35: elseif diffcode == 1 then farmbuyer@93: if IsInRaid() 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@93: if IsInRaid() 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@114: -- Whether we're recording anything at all in the loot histories farmbuyer@114: local function _history_suppress() farmbuyer@114: return _LFR_suppressing or addon.history_suppress farmbuyer@114: end farmbuyer@114: 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@87: -- 'history' active player name in 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@87: H, HU = h.name, 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@87: H, HU = h.name, 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@107: 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@102: ....bar also gone, cleanup() called: farmbuyer@102: function cleanup (expired_entries) farmbuyer@102: for i = 1, #expired_entries do -- this table is in strict FIFO order farmbuyer@102: print(i, expired_entries[i]) farmbuyer@102: -- 1 foo farmbuyer@102: -- 2 bar farmbuyer@102: end farmbuyer@102: end farmbuyer@1: ]] farmbuyer@1: do farmbuyer@1: local caches = {} farmbuyer@107: local cleanup_group = AnimTimerFrame:CreateAnimationGroup() farmbuyer@107: local time, next = time, next farmbuyer@102: local new, del = flib.new, flib.del 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@102: local actives = 0 farmbuyer@102: local expired = new() farmbuyer@1: -- this is ass-ugly farmbuyer@102: for name,c in next, caches do farmbuyer@25: local fifo = c.fifo farmbuyer@25: local active = #fifo > 0 farmbuyer@102: actives = actives + (active and 1 or 0) farmbuyer@73: while (#fifo > 0) and (now > fifo[1].t) do farmbuyer@102: local datum = tremove(fifo,1) farmbuyer@102: addon.dprint('cache', name, "cache removing", datum.t, "<", datum.m, ">") farmbuyer@102: c.hash[datum.m] = nil farmbuyer@102: tinsert(expired,datum.m) farmbuyer@102: del(datum) farmbuyer@1: end farmbuyer@25: if active and #fifo == 0 and c.func then farmbuyer@25: addon.dprint('cache', name, "empty, firing cleanup") farmbuyer@102: c.func(expired) farmbuyer@25: end farmbuyer@102: wipe(expired) farmbuyer@1: end farmbuyer@102: del(expired) farmbuyer@102: if actives == 0 then farmbuyer@40: addon.dprint('cache',"OnLoop FINISHING animation group") farmbuyer@1: cleanup_group:Finish() 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@102: assert(type(x)~='number') farmbuyer@102: local datum = new() farmbuyer@102: datum.m = x farmbuyer@102: datum.t = time() + cache.ttl 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@1: cleanup_group:Play() farmbuyer@1: end farmbuyer@1: end farmbuyer@1: local function _test (cache, x) 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@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@102: -- Testing merging these two (_add's 'x' must not be numeric) farmbuyer@25: fifo = {}, farmbuyer@102: --hash = {}, farmbuyer@1: } farmbuyer@102: c.hash = c.fifo farmbuyer@102: c.cleanup:SetOrder(1) -- [1,100] range within parent animation farmbuyer@102: c.cleanup:SetDuration(0.8) -- hmmm 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@98: function addon:DBProfileRefresh() farmbuyer@98: opts = self.db.profile farmbuyer@98: end farmbuyer@98: 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@109: -- This kludgy-looking thing is because if there are serious errors in farmbuyer@109: -- loading, some of the actions during PLAYER_LOGOUT can destroy data. farmbuyer@109: if self.FILES_LOADED ~= 7 then farmbuyer@109: print('|cffaee3ffouroloot reports load count of',self.FILES_LOADED,'fml|r') farmbuyer@109: self:SetEnabledState(false) farmbuyer@109: return farmbuyer@109: end farmbuyer@109: self.FILES_LOADED = nil farmbuyer@109: farmbuyer@107: _log = 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@107: local OuroLootSV = OuroLootSV farmbuyer@1: g_restore_p = OuroLootSV ~= nil farmbuyer@1: self.dprint('flow', "oninit sets restore as", g_restore_p) farmbuyer@1: farmbuyer@125: -- Primarily for plugins, but can be of use to me also... LCALLBACK farmbuyer@107: self.callbacks = LibStub("CallbackHandler-1.0"):New(self) farmbuyer@100: --function self.callbacks:OnUsed (target_aka_self, eventname) end farmbuyer@100: --function self.callbacks:OnUnused (target_aka_self, eventname) end farmbuyer@100: farmbuyer@97: if _G.OuroLootOptsDB == nil then farmbuyer@76: local vclick = self.format_hypertext ([[click here]], ITEM_QUALITY_UNCOMMON, 'help') farmbuyer@1: self:ScheduleTimer(function(s) farmbuyer@97: for id in pairs(self.default_itemfilter) do farmbuyer@97: opts.itemfilter[id] = true farmbuyer@97: end farmbuyer@97: for id in pairs(self.default_itemvault) do farmbuyer@97: opts.itemvault[id] = true farmbuyer@97: end 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@107: self.db = LibStub("AceDB-3.0"):New("OuroLootOptsDB", option_defaults , --[[Default=]]true) farmbuyer@99: self.db.RegisterCallback (self, "OnNewProfile", function() farmbuyer@99: self:Print(new_profile_warning) farmbuyer@99: end) farmbuyer@98: self.db.RegisterCallback (self, "OnProfileChanged", "DBProfileRefresh") farmbuyer@98: self.db.RegisterCallback (self, "OnProfileCopied", "DBProfileRefresh") farmbuyer@98: self.db.RegisterCallback (self, "OnProfileReset", "DBProfileRefresh") farmbuyer@98: self:DBProfileRefresh() farmbuyer@97: farmbuyer@97: --[[ 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@97: opts.datarev = option_defaults.datarev]] farmbuyer@1: farmbuyer@1: self:RegisterChatCommand("ouroloot", "OnSlash") farmbuyer@94: if opts.register_slash_synonyms then farmbuyer@94: -- Maybe use %w here for non-English locales? farmbuyer@94: local n = 2 farmbuyer@94: for s in opts.slash_synonyms:gmatch("/%a+") do farmbuyer@94: _G['SLASH_ACECONSOLE_OUROLOOT'..n] = s farmbuyer@94: n = n + 1 farmbuyer@94: end farmbuyer@1: end farmbuyer@1: farmbuyer@73: self.history_all = self.history_all or _G.OuroLootSV_hist or {} farmbuyer@107: local r = self:load_assert (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@97: if false and OuroLootSV then farmbuyer@56: local dirty = false farmbuyer@66: local bumpers = {} farmbuyer@97: --bumpers[14] = function() start farmbuyer@97: --bumpers[19] = function() latest 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@95: self:FINISH_SPECIAL_TABS() farmbuyer@1: _init(self) farmbuyer@89: self.dprint('flow', "version strings:", version_large, self.revision, self.status_text) farmbuyer@110: g_gui = self.gui_state_pointer farmbuyer@95: self.gui_state_pointer = nil 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@135: self:RegisterEvent("GROUP_ROSTER_UPDATE") farmbuyer@1: farmbuyer@125: -- This Print cribbed from Talented. I like the way jerry thinks: the farmbuyer@125: -- first string argument can be a format spec for the remainder of the farmbuyer@125: -- arguments. AceConsole:Printf isn't used because we can't specify a farmbuyer@125: -- prefix without jumping through ridonkulous hoops. farmbuyer@125: -- farmbuyer@125: -- Everything dealing with a prefix hyperlink 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@103: function addon.chatprefix (code, arg) farmbuyer@103: local f = '' -- empty -> BuildMainDisplay(empty) -> main tab farmbuyer@103: if code == "GoToLootLine" then farmbuyer@103: f = code farmbuyer@103: --elseif ..... farmbuyer@103: end farmbuyer@103: local ret, num = self.format_hypertext ("Ouro Loot", --[[legendary]]5, f) farmbuyer@103: if code == "GoToLootLine" then farmbuyer@103: self.lootjumps[num] = arg farmbuyer@103: end farmbuyer@103: return ret, num farmbuyer@103: end farmbuyer@103: --local chat_prefix = self.format_hypertext ("Ouro Loot", --[[legendary]]5, '') farmbuyer@103: --local chat_prefix_s = tostring(chat_prefix) farmbuyer@103: local chat_prefix_s = tostring((addon.chatprefix())) 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@103: function addon:PCFPrint (frame, prefix, str, ...) farmbuyer@103: assert(type(frame)=='table' and frame.AddMessage) farmbuyer@103: if type(str) == 'string' and str:find("%", nil, --[[plainmatch=]]true) then farmbuyer@103: return AC:Print (frame, tostring(prefix), str:format(...)) farmbuyer@103: else farmbuyer@103: return AC:Print (frame, tostring(prefix), str, ...) farmbuyer@103: end farmbuyer@103: end farmbuyer@1: end farmbuyer@1: farmbuyer@93: -- Copy these over once, now that other addons have mostly loaded. Any farmbuyer@93: -- future tweaks via CUSTOM_CLASS_COLORS will trigger the same callback. farmbuyer@93: if addon.fill_out_class_colors then farmbuyer@93: addon.fill_out_class_colors() farmbuyer@93: addon.fill_out_class_colors = nil farmbuyer@93: end farmbuyer@93: 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@129: The five 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@20: ]] farmbuyer@20: farmbuyer@20: -- LOOT_ITEM = "%s receives loot: %s." --> (.+) receives loot: (.+)%. farmbuyer@107: g_LOOT_ITEM_ss = LOOT_ITEM:gsub('%.$','%%.'):gsub('%%s','(.+)') farmbuyer@20: farmbuyer@20: -- LOOT_ITEM_MULTIPLE = "%s receives loot: %sx%d." --> (.+) receives loot: (.+)(x%d+)%. farmbuyer@107: g_LOOT_ITEM_MULTIPLE_sss = 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@107: g_LOOT_ITEM_SELF_s = 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@107: g_LOOT_ITEM_SELF_MULTIPLE_ss = LOOT_ITEM_SELF_MULTIPLE:gsub('%.$','%%.'):gsub('%%s','(.+)'):gsub('x%%d','(x%%d+)') farmbuyer@20: farmbuyer@129: -- LOOT_ITEM_WHILE_PLAYER_INELIGIBLE is mostly the same as LOOT_ITEM with farmbuyer@129: -- an inline texture and no full stop. The punctuation in the texture farmbuyer@129: -- path causes fits while matching, so just make that a wildcard rather farmbuyer@129: -- than trying to escape it all. farmbuyer@129: g_LOOT_ITEM_WHILE_PLAYER_INELIGIBLE_ss = LOOT_ITEM_WHILE_PLAYER_INELIGIBLE: farmbuyer@129: gsub('\124T%S*\124t','\124T%%S*\124t'):gsub('%%s','(.+)') farmbuyer@129: farmbuyer@44: --[[ farmbuyer@110: Throw in the default disposition types. This could be called during load farmbuyer@110: were it not for the need to talk to the GUI data (not set up yet). farmbuyer@110: farmbuyer@110: Args: code, rhex, ghex, bhex, farmbuyer@110: text_notes, opt_text_menu (uses notes if nil), opt_tooltip_txt, farmbuyer@110: can_reassign_p, do_history_p, from_notes_text_p farmbuyer@110: ]] farmbuyer@128: local norm = _add_loot_disposition (self, 'normal', "ff","ff","ff", "", "normal/mainspec", farmbuyer@110: [[This is the default. Selecting any 'Mark as ' action blanks out extra notes about who broadcast this entry, etc.]], farmbuyer@110: true, true, false) farmbuyer@128: _add_loot_disposition (self, 'offspec', "c6","9b","6d", "offspec", nil, nil, farmbuyer@110: true, true, true) farmbuyer@128: _add_loot_disposition (self, 'shard', "a3","35","ee", "shard", farmbuyer@110: "disenchanted", nil, false, false, true) farmbuyer@128: _add_loot_disposition (self, 'gvault', "33","ff","99", _G.GUILD_BANK:lower(), farmbuyer@110: nil, nil, false, false, true) farmbuyer@128: -- fix up the odd(!) standard case of having a nil disposition field farmbuyer@110: norm.arg2 = nil farmbuyer@110: farmbuyer@110: --[[ 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@125: bliz.name = "Ouro Loot" -- must match X-LoadOn-InterfaceOptions farmbuyer@125: if AddonLoader then farmbuyer@125: AddonLoader:RemoveInterfaceOptions(bliz.name) farmbuyer@125: end 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@107: InterfaceOptionsFrameCancel:Click() farmbuyer@107: HideUIPanel(GameMenuFrame) farmbuyer@125: addon:BuildMainDisplay('opt') farmbuyer@44: end) farmbuyer@125: button:SetText('"/ouroloot options"') farmbuyer@44: button:SetPoint("TOPLEFT",20,-20) farmbuyer@44: _b:SetScript("OnShow",nil) farmbuyer@44: end) farmbuyer@107: 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@101: self:_set_chatty_change_chatframe (opts.chatty_on_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@99: --[[ farmbuyer@99: Module support (aka plugins). Field names with special meanings: farmbuyer@99: - option_defaults: (IN) Standard AceDB-style table. Use a profiles key! farmbuyer@99: - db: (OUT) AceDB object, set during init. farmbuyer@99: - opts: (OUT) Pointer to plugin's "db.profile" subtable. farmbuyer@99: farmbuyer@125: Inherited unchanged: farmbuyer@128: - _add_loot_disposition farmbuyer@125: farmbuyer@125: Inherited module variants: farmbuyer@125: - OnInitialize, OnEnable farmbuyer@125: - register_text_generator, register_tab_control: also flag plugin as farmbuyer@125: a text-generating module in main addon farmbuyer@99: ]] farmbuyer@58: local prototype = {} farmbuyer@125: local textgen_registry, chat_prefixes, chat_codes farmbuyer@95: farmbuyer@95: -- By default, no plugins. First one in sets up code for any after. farmbuyer@125: addon.get_textgen_plugin = flib.nullfunc farmbuyer@125: farmbuyer@125: -- Called as part of NewModule, after embedding and metas are done. farmbuyer@125: -- Mostly this is one-time setup stuff that we don't need until a plugin farmbuyer@125: -- is actually built. farmbuyer@95: function addon:OnModuleCreated (plugin) farmbuyer@125: textgen_registry, chat_prefixes, chat_codes = {}, {}, {} farmbuyer@125: addon.get_textgen_plugin = function(a,t) farmbuyer@125: return textgen_registry[t] farmbuyer@125: end farmbuyer@125: prototype.register_text_generator = function(p,t,...) farmbuyer@125: textgen_registry[t] = p farmbuyer@125: return addon:register_text_generator(t,...) farmbuyer@125: end farmbuyer@125: prototype.register_tab_control = function(p,t,...) farmbuyer@125: textgen_registry[t] = p farmbuyer@125: return addon:register_tab_control(t,...) farmbuyer@125: end farmbuyer@125: farmbuyer@125: function addon:ModulePrefixClick (codenum, mousebutton) farmbuyer@125: local plugin = assert(chat_codes[codenum]) farmbuyer@125: if not plugin:IsEnabled() then return end farmbuyer@125: if mousebutton == 'LeftButton' then farmbuyer@125: if plugin.PrefixLeftClick then farmbuyer@125: plugin:PrefixLeftClick(codenum) farmbuyer@125: else farmbuyer@125: self:BuildMainDisplay() farmbuyer@125: end farmbuyer@125: elseif mousebutton == 'RightButton' then farmbuyer@125: local uniqueval = plugin.name farmbuyer@125: if plugin.PrefixRightClick then farmbuyer@125: uniqueval = plugin:PrefixRightClick(codenum) farmbuyer@125: end farmbuyer@125: self:BuildMainDisplay('opt',uniqueval) farmbuyer@95: end farmbuyer@95: end farmbuyer@125: function addon:OnModuleCreated (plugin) farmbuyer@125: local token, code = self.format_hypertext (plugin.moduleName, farmbuyer@125: --[[heirloom]]7, "ModulePrefixClick") farmbuyer@125: chat_prefixes[plugin] = token farmbuyer@125: chat_codes[code] = plugin farmbuyer@125: -- remove the libraries' embedded pointers so that the prototype farmbuyer@125: -- can be inherited farmbuyer@125: plugin.Print = nil farmbuyer@125: end farmbuyer@125: farmbuyer@125: return self:OnModuleCreated(plugin) farmbuyer@95: end farmbuyer@95: farmbuyer@125: function prototype.OnInitialize (plugin) farmbuyer@58: if plugin.option_defaults then farmbuyer@99: plugin.db = addon.db:RegisterNamespace (plugin.moduleName, plugin.option_defaults) farmbuyer@99: plugin.opts = plugin.db.profile farmbuyer@99: --plugin:SetEnabledState(plugin.db.profile.enabled) if that flag is needed later farmbuyer@58: end farmbuyer@58: end farmbuyer@58: farmbuyer@125: --function prototype.OnEnable (plugin) farmbuyer@125: --end farmbuyer@125: farmbuyer@125: function prototype.GetOption (plugin, info) farmbuyer@99: local name = info[#info] farmbuyer@99: return plugin.db.profile[name] farmbuyer@99: end farmbuyer@125: function prototype.SetOption (plugin, info, value) farmbuyer@99: local name = info[#info] farmbuyer@99: plugin.db.profile[name] = value farmbuyer@99: local arg = info.arg farmbuyer@99: if type(arg) == 'function' then farmbuyer@99: plugin[arg](plugin,info) farmbuyer@99: end farmbuyer@99: end farmbuyer@99: farmbuyer@125: -- may eventually just rework the main print routines and inherit all 3 farmbuyer@125: function prototype.Print (plugin, str, ...) farmbuyer@125: local AC = LibStub("AceConsole-3.0") farmbuyer@125: local ps = tostring(chat_prefixes[plugin]) farmbuyer@125: if type(str) == 'string' and str:find("%", nil, --[[plainmatch=]]true) then farmbuyer@125: return AC:Print (ps, str:format(...)) farmbuyer@125: else farmbuyer@125: return AC:Print (ps, str, ...) farmbuyer@125: end farmbuyer@125: end farmbuyer@58: farmbuyer@128: -- For references that would be nil at load time, and I don't feel like farmbuyer@128: -- rearranging the entire file. farmbuyer@128: function addon:MODULE_PROTOTYPE_POINTERS() farmbuyer@128: prototype.add_loot_disposition = _add_loot_disposition farmbuyer@128: self.MODULE_PROTOTYPE_POINTERS = nil farmbuyer@128: end farmbuyer@128: farmbuyer@58: addon:SetDefaultModuleLibraries("AceConsole-3.0") farmbuyer@58: addon:SetDefaultModulePrototype(prototype) farmbuyer@63: farmbuyer@64: local err = [[Module '%s' cannot register itself because it failed a required condition: '%s']] farmbuyer@133: function addon:ConstrainedNewModule (modname, minrev, mincomm, mindata, ...) farmbuyer@63: if not addon.author_debug then farmbuyer@89: if minrev and tonumber(minrev) > (tonumber(self.revision) or math.huge) then farmbuyer@63: self:Print(err,modname, farmbuyer@89: "revision "..self.revision.." older than minimum "..minrev) farmbuyer@63: return false farmbuyer@63: end farmbuyer@89: if mincomm and tonumber(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@99: --[[if mindata and tonumber(mindata) > opts.datarev then farmbuyer@63: self:Print(err,modname, farmbuyer@63: "datarev "..opts.datarev.." older than minimum "..mindata) farmbuyer@63: return false farmbuyer@99: end]] farmbuyer@63: end farmbuyer@133: return self:NewModule(modname,...) farmbuyer@63: end farmbuyer@58: end farmbuyer@58: farmbuyer@102: --[[ LCALLBACK farmbuyer@102: Standard ace3-style callback registration and dispatching. All player names farmbuyer@102: are simple (uncolored) strings. The "uniqueID" always refers to the unique farmbuyer@102: tag string stored as 'unique' in loot entries and used as keys in history. farmbuyer@102: Item IDs are always of numeric type. farmbuyer@102: farmbuyer@102: 'Activate', enabled_p, rebroadcast_p, threshold farmbuyer@102: The two boolean predicates are self-explanatory. The threshold is an farmbuyer@102: ITEM_QUALITY_* constant integer. farmbuyer@102: farmbuyer@125: 'Deactivate', raiderdata farmbuyer@125: After all system events have been unregistered. Argument is a holder of farmbuyer@125: the current g_loot.raiders table (in 'raidersnap'). farmbuyer@102: farmbuyer@102: 'Reset' farmbuyer@102: Clicking "Clear Loot", after all data manipulation is finished. farmbuyer@102: farmbuyer@102: 'NewBoss', boss farmbuyer@102: Boss event triggered by a local bossmod (DBM, etc) or a remote OL tracker. farmbuyer@102: Argument is a g_loot table entry of kind=='boss'. farmbuyer@102: farmbuyer@102: 'NewBossEntry', boss farmbuyer@102: New row in primary EOI table of kind=='boss'. Includes all 'NewBoss' farmbuyer@102: occasions, plus manual boss additions, testing, etc. Arg same as NewBoss. farmbuyer@102: farmbuyer@128: 'NewLootEntry', loot, row_index farmbuyer@102: 'DelLootEntry', loot farmbuyer@102: New or removed row in primary EOI table of kind=='loot'. Argument is a farmbuyer@128: g_loot table entry of kind=='loot', and the index into g_loot of where farmbuyer@128: the entry "is" (read: "will be" by the time the event fires). farmbuyer@128: farmbuyer@128: 'NewEOIEntry', entry, row_index farmbuyer@102: 'DelEOIEntry', entry farmbuyer@102: New or removed row in primary EOI table, of any kind. Argument is the farmbuyer@128: g_loot entry, already inserted into or removed from g_loot. Note that farmbuyer@128: boss entries may shift around after this event (if loot has happened and farmbuyer@128: needs to be re-sorted). farmbuyer@102: farmbuyer@102: 'NewHistory', player_name, uniqueID farmbuyer@102: 'DelHistory', player_name, uniqueID farmbuyer@102: New or removed entry in player history. Name argument self-explanatory. farmbuyer@102: ID is the corresponding loot, already inserted into or removed from the farmbuyer@102: history structures. farmbuyer@102: farmbuyer@102: 'Reassign', uniqueID, itemID, loot, from_player_name, to_player_name farmbuyer@102: Loot reassigned from one player to another. Loot represented by the farmbuyer@102: unique & item IDs, and the g_loot entry of kind=='loot'. The player farmbuyer@102: names are self-explanatory. farmbuyer@102: farmbuyer@102: 'MarkAs', uniqueID, itemID, loot, old_disposition, new_disposition farmbuyer@102: The "Mark as " action (as if from the item right-click menu, possibly farmbuyer@102: from a remote tracker) has finished. ID & loot arguments are as in farmbuyer@102: 'Reassign'. The old/new dispositions are those of the g_loot index farmbuyer@102: "disposition" (described at the top of core.lua), with the added possible farmbuyer@102: value of "normal" meaning exactly that. farmbuyer@102: ]] farmbuyer@100: do farmbuyer@102: -- We don't want to trigger plugins or other addons as soon as something farmbuyer@102: -- interesting happens, because a nontrivial amount of work happens "soon" farmbuyer@102: -- after the interesting event: cleanups/fixups, improvs from network, farmbuyer@102: -- etc. So firing a callback is delayed ever so briefly by human scales. farmbuyer@102: -- farmbuyer@102: -- For data safety, we replace any table arguments with read-only proxies farmbuyer@102: -- before passing them to the callbacks. The goal is to prevent accidents, farmbuyer@102: -- not fraud. farmbuyer@107: local unpack, setmetatable = unpack, setmetatable farmbuyer@102: local mtnewindex = function() --[[local]]error("This table is read-only", 3) end farmbuyer@101: local function make_readonly (t) farmbuyer@102: return setmetatable({}, { farmbuyer@101: __newindex = mtnewindex, farmbuyer@101: __index = t, farmbuyer@101: __metatable = false, farmbuyer@102: __tostring = getmetatable(t) and getmetatable(t).__tostring, farmbuyer@101: }) farmbuyer@101: end farmbuyer@101: farmbuyer@102: local queue = create_new_cache ('callbacks', 1.2, function (allcbs) farmbuyer@102: for _,c in ipairs(allcbs) do farmbuyer@102: addon.callbacks:Fire (unpack(c)) farmbuyer@102: flib.del(c) farmbuyer@102: end farmbuyer@102: end) farmbuyer@100: function addon:Fire (...) farmbuyer@100: self.dprint('callback', ...) farmbuyer@100: local capture = flib.new(...) farmbuyer@101: for k,v in ipairs(capture) do if type(v) == 'table' then farmbuyer@101: capture[k] = make_readonly(v) farmbuyer@101: end end farmbuyer@102: queue:add(capture) farmbuyer@100: end farmbuyer@100: end farmbuyer@100: 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@97: _G.OuroLootOptsDB = nil farmbuyer@73: _G.OuroLootSV_hist = nil farmbuyer@73: _G.OuroLootSV_log = nil farmbuyer@107: ReloadUI() farmbuyer@1: end farmbuyer@1: function addon:PLAYER_LOGOUT() farmbuyer@135: -- Can these still fire at the very last instant? farmbuyer@135: self:UnregisterEvent("GROUP_ROSTER_UPDATE") farmbuyer@16: self:UnregisterEvent("PLAYER_ENTERING_WORLD") farmbuyer@16: farmbuyer@107: local worth_saving = #g_loot > 0 or 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@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@102: local R_ACTIVE, R_OFFLINE, R_LEFT = 'online', 'offline', 'no_longer' farmbuyer@135: local was_in_raid farmbuyer@10: farmbuyer@10: local lastevent, now = 0, 0 farmbuyer@16: local redo_count = 0 farmbuyer@114: local redo, timer_handle, my_realm 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@100: -- XXX somewhere in here, we could fire a useful callback event farmbuyer@100: farmbuyer@16: if redo then farmbuyer@16: redo_count = redo_count + 1 farmbuyer@16: end farmbuyer@16: redo = false farmbuyer@135: for i = 1, GetNumGroupMembers() do farmbuyer@10: local unit = 'raid'..i farmbuyer@67: -- We grab a bunch of return values here, but only pay attention to farmbuyer@114: -- them under specific circumstances. Try to use as many of these farmbuyer@114: -- values as possible rather than multiple Unit* calls. farmbuyer@114: local fname, connected, subgroup, level, class, _ farmbuyer@114: fname, _, subgroup, level, _, class, connected = GetRaidRosterInfo(i) farmbuyer@10: -- No, that's not my typo, it really is "uknownbeing" in Blizzard's code. farmbuyer@114: if fname and fname ~= UNKNOWN and fname ~= UNKNOWNOBJECT and fname ~= UKNOWNBEING then farmbuyer@114: local name,realm = fname:match("(%S+)-(%S+)") farmbuyer@114: if realm and realm == my_realm then -- shouldn't happen farmbuyer@114: realm = nil farmbuyer@114: end farmbuyer@114: if not name then farmbuyer@114: assert(realm == nil) farmbuyer@114: name = fname farmbuyer@114: end 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@114: r.fname = fname 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@114: r.realm = realm 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@135: timer_handle = self:ScheduleRepeatingTimer("GROUP_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@135: function addon:GROUP_ROSTER_UPDATE (event) farmbuyer@135: if not IsInRaid() then farmbuyer@135: if was_in_raid then farmbuyer@135: -- Leaving a raid group. farmbuyer@135: self.dprint('flow', "no longer in raid group") farmbuyer@135: was_in_raid = false farmbuyer@135: if self.enabled and not self.debug.notraid then farmbuyer@135: self.dprint('flow', "enabled, leaving raid") farmbuyer@135: self.popped = nil farmbuyer@135: self:Deactivate() farmbuyer@135: self:CheckRoster(--[[leaving raid]]true) farmbuyer@135: end farmbuyer@135: _LFR_suppressing = nil farmbuyer@10: end farmbuyer@135: -- Flow for 5-player groups goes right to here. 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@135: if event == "GROUP_ROSTER_UPDATE" then farmbuyer@135: -- hot code path, be careful farmbuyer@135: farmbuyer@135: -- event registration from onload, joined a raid, maybe show popup farmbuyer@135: self.dprint('flow', "RRU check:", self.popped, opts.popup_on_join) farmbuyer@135: if (not self.popped) and opts.popup_on_join then farmbuyer@135: self.popped = StaticPopup_Show "OUROL_REMIND" farmbuyer@135: self.popped.data = self farmbuyer@135: return farmbuyer@135: end farmbuyer@135: elseif event == "Activate" then farmbuyer@114: -- dispatched from Activate farmbuyer@114: if opts.history_suppress_LFR farmbuyer@133: and GetLFGMode(LE_LFG_CATEGORY_RF) farmbuyer@114: then farmbuyer@114: _LFR_suppressing = true farmbuyer@114: end farmbuyer@114: my_realm = self.history.realm farmbuyer@114: _register_bossmod(self) farmbuyer@10: self:RegisterEvent("CHAT_MSG_LOOT") farmbuyer@135: was_in_raid = true farmbuyer@10: docheck = true farmbuyer@1: end farmbuyer@135: 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@80: -- This field is set on various boss interactions, so we've got a farmbuyer@80: -- kill/wipe entry already. farmbuyer@42: if addon.latest_instance then return end 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@107: local random = 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@87: -- Need to *find* an unused value. For now use a range of J*10^4 farmbuyer@87: -- where J is Jenny's Constant. Thank you, . 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: local origin = candidates[oldersig] and candidates[oldersig].bcast_from 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@109: if addon:_test_disposition (loot.disposition, 'affects_history') farmbuyer@114: and not _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@107: local strsplit, GetItemInfo, GetItemIcon, UnitClass = strsplit, GetItemInfo, GetItemIcon, 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@81: local err = unique_collision:format (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@73: sigmap[oldersig] = 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@73: seenit = recent_loot:test(signature) farmbuyer@73: end farmbuyer@73: farmbuyer@73: if seenit then farmbuyer@80: self.dprint('loot', "remote", prefix, "<", signature, farmbuyer@73: "> already in cache, skipping from", from) farmbuyer@73: break farmbuyer@73: end farmbuyer@73: farmbuyer@114: -- Most of the time this will already be in place and filled out, farmbuyer@114: -- but there are situations where not. Even if we can't get data farmbuyer@114: -- back, at least avoid errors (or the need for existence checks). farmbuyer@114: if not g_loot.raiders[recipient] then farmbuyer@114: g_loot.raiders[recipient] = { needinfo=true } farmbuyer@114: end farmbuyer@114: 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@114: person_class= g_loot.raiders[recipient].class, farmbuyer@114: person_realm= g_loot.raiders[recipient].realm, 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@109: if i.extratext then farmbuyer@109: for disp,text in self:_iter_dispositions('from_notes_text') do farmbuyer@109: if text == i.extratext then farmbuyer@109: i.disposition = disp farmbuyer@109: break farmbuyer@109: end farmbuyer@109: end farmbuyer@70: end farmbuyer@73: local looti = self._addLootEntry(i) farmbuyer@109: if self:_test_disposition (i.disposition, 'affects_history') farmbuyer@114: and not _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@80: self.dprint('loot', 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@129: local match = string.match 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@129: person, itemstring, count = match(msg,g_LOOT_ITEM_MULTIPLE_sss) farmbuyer@20: if not person then farmbuyer@129: person, itemstring = match(msg,g_LOOT_ITEM_ss) farmbuyer@20: end farmbuyer@20: if not person then farmbuyer@129: -- Could only do this text if in an LFR... but the restriction farmbuyer@129: -- might apply elsewhere soon enough. farmbuyer@129: person, itemstring = match(msg,g_LOOT_ITEM_WHILE_PLAYER_INELIGIBLE_ss) farmbuyer@129: end farmbuyer@129: if not person then farmbuyer@129: itemstring, count = match(msg,g_LOOT_ITEM_SELF_MULTIPLE_ss) farmbuyer@20: if not itemstring then farmbuyer@129: itemstring = match(msg,g_LOOT_ITEM_SELF_s) farmbuyer@20: end farmbuyer@20: end farmbuyer@20: farmbuyer@89: if not itemstring then return end -- "PlayerX selected Greed", etc, not looting farmbuyer@65: self.dprint('loot', "CHAT_MSG_LOOT, person is", person, farmbuyer@65: ", itemstring is", itemstring, ", count is", count) farmbuyer@1: farmbuyer@1: -- Name might be colorized, remove the highlighting farmbuyer@20: if person then farmbuyer@129: person = match(person,"|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@80: -- This only triggers on entering combat after a registered boss kill. farmbuyer@80: -- Clearing this field forces subsequent trash kills to generate an entry farmbuyer@80: -- via maybe_trash_kill_entry. farmbuyer@80: -- (Possibly what is wanted is to start a 3 or 5 minute timer, and *then* farmbuyer@80: -- look for the next combat?) farmbuyer@80: function addon:PLAYER_REGEN_DISABLED() farmbuyer@80: self:UnregisterEvent ("PLAYER_REGEN_DISABLED") farmbuyer@80: self.latest_instance = nil farmbuyer@80: end farmbuyer@80: 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@107: self.is_guilded = IsInGuild() farmbuyer@76: self.debug[arg] = not self.debug[arg] farmbuyer@107: 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@81: elseif cmd == "fix" then farmbuyer@81: if arg == "?" then farmbuyer@95: self:Print[['/ouroloot fix cache' updates loot that wasn't in the cache]] farmbuyer@95: self:Print[['/ouroloot fix history' repairs inconsistent data on the History tab]] farmbuyer@95: self:Print[['/ouroloot fix' changes no stored data, only allows the window to be displayed again (this is built into all fixes above)]] farmbuyer@81: return farmbuyer@81: elseif arg == "cache" then farmbuyer@114: self:do_item_cache_fixup(--[[force_silent=]]false) farmbuyer@81: elseif arg == "history" then farmbuyer@81: self:repair_history_integrity() farmbuyer@81: end farmbuyer@81: self.NOLOAD = nil farmbuyer@81: self:Print("Window unlocked, best of luck.") farmbuyer@76: farmbuyer@23: else farmbuyer@88: if self:OpenMainDisplayToTab(cmd,arg) 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@80: -- Both of these need to be (effectively) idempotent. farmbuyer@1: function addon:Activate (opt_threshold, opt_bcast_only) farmbuyer@16: self.dprint('flow', ":Activate is running") farmbuyer@135: self:RegisterEvent("GROUP_ROSTER_UPDATE") farmbuyer@16: self:RegisterEvent("PLAYER_ENTERING_WORLD", farmbuyer@135: function() self:ScheduleTimer("GROUP_ROSTER_UPDATE", 5, "PLAYER_ENTERING_WORLD") end) farmbuyer@1: self.popped = true farmbuyer@114: if self.DO_ITEMID_FIX then farmbuyer@114: self.DO_ITEMID_FIX = nil farmbuyer@114: self:do_item_cache_fixup(--[[force_silent=]]not self.author_debug) farmbuyer@115: self.loot_clean = nil farmbuyer@114: end farmbuyer@93: if IsInRaid() then farmbuyer@16: self.dprint('flow', ">:Activate calling RRU") farmbuyer@135: self:GROUP_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@106: if #g_loot > 400 then farmbuyer@106: self:Print("|cffff0505Crikey!|r You are carrying around a lot of", farmbuyer@106: "stored loot data. You should seriously consider clearing it", farmbuyer@106: "out, as some of the text generation routines can choke the", farmbuyer@106: "game client if they run for too long.") farmbuyer@106: end 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@100: self:Fire ('Activate', self.enabled, self.rebroadcast, self.threshold) farmbuyer@80: self:Print("Now %s; threshold currently %s.", farmbuyer@1: self.enabled and "tracking" or "only broadcasting", farmbuyer@1: self.thresholds[self.threshold]) farmbuyer@89: self:broadcast('revcheck',version_large) farmbuyer@1: end farmbuyer@1: farmbuyer@95: -- Note: running '/ouroloot 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@135: self:UnregisterEvent("GROUP_ROSTER_UPDATE") farmbuyer@10: self:UnregisterEvent("PLAYER_ENTERING_WORLD") farmbuyer@10: self:UnregisterEvent("CHAT_MSG_LOOT") farmbuyer@114: _LFR_suppressing = nil farmbuyer@125: -- Passing .raiders directly doesn't work with a proxy (again, WTB Lua farmbuyer@125: -- 5.2 and its __pairs iterators). Give it the same structure as a boss farmbuyer@125: -- entry instead. farmbuyer@125: self:Fire ('Deactivate', { raidersnap = g_loot.raiders }) farmbuyer@80: self:Print("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@96: st = self.display:GetUserData("GUI state").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@107: OuroLootSV = nil farmbuyer@1: self:_reset_timestamps() farmbuyer@1: if verbose_p then farmbuyer@107: if (OuroLootSV_saved and #OuroLootSV_saved>0) then farmbuyer@107: self:Print("Current loot data cleared, %d saved sets remaining.", #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@100: self:Fire ('Reset') 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@107: local date = 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@101: local change_chatframe farmbuyer@101: farmbuyer@101: function addon:_set_chatty_change_chatframe (arg, silent_p) farmbuyer@73: local frame farmbuyer@73: if type(arg) == 'number' then farmbuyer@107: arg = 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@101: 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@101: local function _notify (chatframe, source, index, olddisp, from_whom, from_class) farmbuyer@73: local e = g_loot[index] farmbuyer@80: if not e then farmbuyer@80: -- how did this happen? farmbuyer@80: return farmbuyer@80: end farmbuyer@89: if source == my_name then farmbuyer@89: source = _G.UNIT_YOU farmbuyer@89: end farmbuyer@86: local from_text, to_text farmbuyer@73: if from_whom then farmbuyer@89: if from_whom == my_name then farmbuyer@89: from_whom = _G.UNIT_YOU farmbuyer@89: end farmbuyer@86: from_text = addon:colorize (from_whom, from_class) farmbuyer@89: to_text = e.person == my_name and _G.UNIT_YOU or e.person farmbuyer@89: to_text = addon:colorize (to_text, e.person_class) 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@86: from_text = addon.disposition_colors[olddisp].hex farmbuyer@86: .. from_text .. "|r" farmbuyer@86: 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@109: to_text = addon.disposition_colors[e.disposition].hex farmbuyer@86: .. to_text .. "|r" farmbuyer@73: end farmbuyer@73: farmbuyer@81: addon.dprint ('loot', "notification:", source, index, farmbuyer@86: e.itemlink, from_text, to_text) farmbuyer@103: local jumpprefix = addon.chatprefix ("GoToLootLine", index) farmbuyer@103: addon:PCFPrint (chatframe, jumpprefix, remote_chatty, source, index, farmbuyer@86: e.itemlink, from_text, to_text) farmbuyer@73: end farmbuyer@81: farmbuyer@101: function _notify_about_change (sender, index, olddisp, from_whom, from_class) farmbuyer@101: _notify (change_chatframe, sender, index, olddisp, from_whom, from_class) farmbuyer@81: 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@107: 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@89: self:broadcast('revcheck',version_large) farmbuyer@27: end farmbuyer@27: farmbuyer@89: function addon:_check_version (otherrev) farmbuyer@93: self.dprint('comm', version_large, "revchecking against", otherrev) farmbuyer@27: otherrev = tonumber(otherrev) farmbuyer@89: if otherrev == version_large then farmbuyer@27: -- normal case farmbuyer@27: farmbuyer@89: elseif otherrev < version_large then farmbuyer@27: self.dprint('comm', "ours is newer, notifying") farmbuyer@89: self:broadcast('revcheck',version_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@92: -- Sadly, this is not generated by the packager, so hardcode it farmbuyer@92: -- for now. The 'data' field is handled differently for onshow farmbuyer@92: -- than for other callbacks. farmbuyer@92: StaticPopup_Show("OUROL_URL", farmbuyer@92: --[[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@107: fencepost = 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@118: -- Iterate through g_loot.raiders in sorted (alphabetical/collated) order. If farmbuyer@118: -- USE_FULLNAME then the realmname is appended. If ONLINE_FILTER is present, farmbuyer@118: -- then only raider entries with a matching 'online' key are included. Loop farmbuyer@118: -- variables are a running count, the raider name, and the corresponding entry farmbuyer@118: -- from g_loot.raiders. farmbuyer@118: do farmbuyer@118: local function nextr (list, index) farmbuyer@118: index = index + 1 farmbuyer@118: local name = list[index] farmbuyer@118: if not name then farmbuyer@118: flib.del(list) farmbuyer@118: return nil farmbuyer@118: end farmbuyer@118: return index, name, list.__safety[name] farmbuyer@118: end farmbuyer@118: farmbuyer@118: function addon:sorted_raiders_iter (use_fullname_p, opt_online_filter) farmbuyer@118: local t = flib.new() farmbuyer@118: for name,info in next, g_loot.raiders do farmbuyer@118: if (not opt_online_filter) or (info.online == opt_online_filter) then farmbuyer@118: -- this is not exactly "A?B:C" semantics, but it is exactly farmbuyer@118: -- the behavior we want when fname is not present farmbuyer@118: tinsert (t, use_fullname_p and info.fname or name) farmbuyer@118: end farmbuyer@118: end farmbuyer@118: table.sort(t) farmbuyer@118: t.__safety = g_loot.raiders farmbuyer@118: return nextr, t, 0 farmbuyer@118: end farmbuyer@118: end farmbuyer@118: 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@114: -- paranoia: make sure the GUI isn't stumbling over these later farmbuyer@114: local dofix, GetItemInfo = false, GetItemInfo farmbuyer@114: for i,e in self:filtered_loot_iter('loot') do farmbuyer@114: local missing_data = not GetItemInfo(e.id) farmbuyer@114: e.cache_miss = (e.cache_miss or missing_data) or nil farmbuyer@114: dofix = dofix or e.cache_miss farmbuyer@114: end farmbuyer@114: self.DO_ITEMID_FIX = dofix or nil farmbuyer@100: _do_loot_metas() 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@80: g_loot = {} farmbuyer@80: end farmbuyer@80: if type(g_loot.raiders) ~= 'table' then farmbuyer@80: g_loot.raiders = {} farmbuyer@80: end farmbuyer@80: if type(g_loot.printed) ~= 'table' then farmbuyer@80: g_loot.printed = {} farmbuyer@1: end farmbuyer@1: farmbuyer@16: self.threshold = opts.threshold or self.threshold -- in the case of restoring but not tracking farmbuyer@129: local g_loot_wrapper = self:gui_init (true, 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@116: possible_st:SetData(g_loot_wrapper) farmbuyer@1: end farmbuyer@118: -- Make sure we have a current .raiders array, since other dropdowns and farmbuyer@118: -- whatnot depend on that information. farmbuyer@118: self:CheckRoster() farmbuyer@1: farmbuyer@89: self.status_text = ("%s(r%s) communicating as ident %s commrev %s"): farmbuyer@89: format (self.version, self.revision, self.ident, self.commrev) farmbuyer@1: self:RegisterComm(self.ident) farmbuyer@1: self:RegisterComm(self.identTg, "OnCommReceivedNocache") 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@107: for name,info in 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@107: return ss, maxsize, instance, 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@100: addon:Fire ('NewBoss', boss) 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@80: addon:RegisterEvent ("PLAYER_REGEN_DISABLED") farmbuyer@1: addon:_mark_boss_kill (bossi) farmbuyer@1: if opts.chatty_on_kill then farmbuyer@103: local jumpprefix = addon.chatprefix ("GoToLootLine", bossi) farmbuyer@103: addon:PCFPrint(_G.DEFAULT_CHAT_FRAME, jumpprefix, farmbuyer@103: "Registered kill for '%s' in %s!", boss.bossname, boss.instance) farmbuyer@1: end farmbuyer@1: end farmbuyer@47: wipe(candidates) farmbuyer@1: end farmbuyer@102: -- Ten seconds is a long time, but occasionally DBM takes for-EVAH to farmbuyer@102: -- decide that a fight is over. 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@129: assert(g_gui.g_dloot:remove(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@107: local rawget, setmetatable = rawget, setmetatable farmbuyer@73: farmbuyer@100: --@debug@ farmbuyer@100: local tos = {} farmbuyer@100: tos.time = function (e) farmbuyer@100: return e.startday.text farmbuyer@100: end farmbuyer@100: tos.boss = function (e) farmbuyer@100: return e.bossname .. '/' .. e.reason farmbuyer@100: end farmbuyer@100: tos.loot = function (e) farmbuyer@100: return e.itemname .. '/' .. e.person .. '/' .. e.unique .. '/' farmbuyer@100: .. tostring(e.disposition) .. (e.extratext and ('/'..e.extratext) or '') farmbuyer@100: end farmbuyer@100: --@end-debug@ farmbuyer@1: local loot_entry_mt = { farmbuyer@1: __index = function (e,key) farmbuyer@100: -- This shouldn't be required, as the refresh should be picking farmbuyer@100: -- it up already. Sigh. farmbuyer@1: if key == 'cols' then farmbuyer@15: pprint('mt', e.kind, "key is", key) farmbuyer@1: addon:_fill_out_eoi_data(1) farmbuyer@1: end farmbuyer@1: return rawget(e,key) farmbuyer@100: end, farmbuyer@100: --@debug@ farmbuyer@100: __tostring = function (e) farmbuyer@100: local k = e.kind farmbuyer@100: if k then farmbuyer@100: return ("<%s/%s>"):format(k, tos[k] and tos[k](e) or "?") farmbuyer@100: end farmbuyer@100: return "" farmbuyer@100: end, farmbuyer@100: --@end-debug@ farmbuyer@100: } farmbuyer@100: function _do_loot_metas() farmbuyer@100: for i,e in ipairs(g_loot) do farmbuyer@100: setmetatable(e,loot_entry_mt) farmbuyer@1: end farmbuyer@100: _do_loot_metas = nil farmbuyer@100: end 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@84: -- FIXME this is horribabble 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@89: -- More precisely, anything new on the EOI tab hits this; it does not farmbuyer@89: -- necessarily need to be a looted item. 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@107: local h, m = GetGameTime() farmbuyer@10: --local localuptime = math.floor(GetTime()) farmbuyer@107: local time_t = time() farmbuyer@1: e.hour = h farmbuyer@1: e.minute = m farmbuyer@10: e.stamp = time_t --localuptime farmbuyer@128: local index = #g_loot + 1 farmbuyer@89: if e.kind == 'loot' then farmbuyer@89: if (not e.unique) or (#e.unique==0) then farmbuyer@107: e.unique = e.id .. (e.disposition or e.person) .. date("%Y/%m/%d %H:%M",e.stamp) farmbuyer@89: end farmbuyer@128: addon:Fire ('NewLootEntry', e, index) farmbuyer@89: end farmbuyer@1: g_loot[index] = e farmbuyer@129: g_gui.g_dloot[index] = nil farmbuyer@128: addon:Fire ('NewEOIEntry', e, index) farmbuyer@1: return index farmbuyer@1: end farmbuyer@16: farmbuyer@100: -- Safety/convenience wrapper only. 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@128: addon:Fire ('NewBossEntry', e, ret) 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@82: if is == should_be then 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: return farmbuyer@16: end farmbuyer@16: end farmbuyer@16: farmbuyer@16: assert(g_loot[is].kind == 'boss') farmbuyer@129: local boss = g_gui.g_dloot:remove(is) farmbuyer@129: g_gui.g_dloot:insert (should_be, boss) farmbuyer@16: return should_be farmbuyer@16: end farmbuyer@1: end farmbuyer@1: farmbuyer@109: -- Disposition control; more precisely, how various dispositions affect flow farmbuyer@109: -- and displays elsewhere. farmbuyer@109: do farmbuyer@109: local default = { farmbuyer@109: text = "", farmbuyer@109: hex = "|cffffffff", farmbuyer@109: r = 1.0, g = 1.0, b = 1.0, a = 1, farmbuyer@109: can_reassign = true, farmbuyer@109: affects_history = true, farmbuyer@109: from_notes_text = false, farmbuyer@109: } farmbuyer@109: local mt = { farmbuyer@109: __index = function (t,k) farmbuyer@109: -- don't point index directly at default, need to catch nils farmbuyer@109: return default farmbuyer@109: end, farmbuyer@109: } farmbuyer@109: local alldisps = setmetatable({}, mt) farmbuyer@109: addon.disposition_colors = setmetatable({}, mt) farmbuyer@109: farmbuyer@109: -- These two are clunky wrappers, probably rework this once gain some data. farmbuyer@109: function addon:_test_disposition (code, attrib) farmbuyer@109: return alldisps[code][attrib] farmbuyer@109: end farmbuyer@109: function addon:_iter_dispositions (attrib) farmbuyer@109: local r = {} farmbuyer@109: for code,disp in next, alldisps do farmbuyer@110: if disp[attrib] then farmbuyer@109: r[code] = disp.text farmbuyer@109: end farmbuyer@109: end farmbuyer@109: return next, r farmbuyer@109: end farmbuyer@109: farmbuyer@128: function _add_loot_disposition (self, code, rhex, ghex, bhex, text_notes, farmbuyer@110: text_menu, tooltip_txt, can_reassign_p, do_history_p, from_notes_text_p farmbuyer@109: ) farmbuyer@109: assert(type(code)=='string' and #code>0) farmbuyer@110: assert(type(text_notes)=='string') farmbuyer@109: assert(type(rhex)=='string') farmbuyer@109: assert(type(bhex)=='string') farmbuyer@109: assert(type(ghex)=='string') farmbuyer@110: farmbuyer@128: addon.disposition_colors[code] = { farmbuyer@110: text = text_notes, farmbuyer@109: -- for chat output by core code farmbuyer@109: hex = "|cff" .. rhex .. ghex .. bhex, farmbuyer@109: -- for lib-st farmbuyer@109: r = tonumber(rhex,16)/255, farmbuyer@109: g = tonumber(ghex,16)/255, farmbuyer@109: b = tonumber(bhex,16)/255, farmbuyer@109: a = 1, farmbuyer@109: } farmbuyer@109: -- not not = poor man's "toboolean", don't potentially hold arg farmbuyer@109: -- objects from garbage collection farmbuyer@109: alldisps[code] = { farmbuyer@110: text = text_notes, farmbuyer@109: can_reassign = not not can_reassign_p, farmbuyer@109: affects_history = not not do_history_p, farmbuyer@109: from_notes_text = not not from_notes_text_p, farmbuyer@109: } farmbuyer@110: farmbuyer@128: local dd = g_gui.add_dropdown_entry ('eoi_loot_mark', text_menu or text_notes, farmbuyer@110: --[[function_table=]]nil, 'df_DISPOSITION', code, tooltip_txt) farmbuyer@128: dd.colorCode = addon.disposition_colors[code].hex farmbuyer@128: addon.dprint('flow', ("Source '%s' adds loot disposition '%s', flags"): farmbuyer@128: format(self.name or tostring(self), code), farmbuyer@128: can_reassign_p, do_history_p, from_notes_text_p) farmbuyer@128: return dd farmbuyer@109: end farmbuyer@109: end farmbuyer@109: 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@114: function addon:do_item_cache_fixup (silent_p) farmbuyer@114: if not silent_p then farmbuyer@114: self:Print("Fixing up missing item cache data...") farmbuyer@114: end farmbuyer@19: farmbuyer@19: local numfound = 0 farmbuyer@114: local earliest_fixed 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@114: local borked_id = e.itemname:match(borkedpat) or e.id 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@114: earliest_fixed = earliest_fixed or i farmbuyer@114: if not silent_p then self:Print(msg, i, ilink) end farmbuyer@19: end farmbuyer@73: break farmbuyer@73: end end farmbuyer@19: farmbuyer@114: if earliest_fixed then farmbuyer@114: self.loot_clean = earliest_fixed-1 -- this works out even at i == 1 farmbuyer@114: end farmbuyer@114: if not silent_p then farmbuyer@114: self:Print("...finished. Found %d |4entry:entries; with weird data.", numfound) farmbuyer@114: end 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: 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@82: pprint('improv', "fixup for", exist, "starting with guid", farmbuyer@82: winner[1], "with tag", winner[2], "out of", #info, farmbuyer@82: "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@82: >winning_index == 1 farmbuyer@76: >g_loot and history already has the replacement value farmbuyer@76: B: winner was generated remotely farmbuyer@76: >need to scan and replace farmbuyer@82: Detecting A is strictly an optimization. We should be able to do farmbuyer@89: this code safely in all cases. Important to note: a local winner farmbuyer@92: will always be at index 1, but a winner at index 1 does not farmbuyer@92: necessarily mean it was locally generated (e.g., if the local farmbuyer@92: itemfilter drops it but a remote player does an improv). Just farmbuyer@92: do the general case until/unless this becomes a problem. farmbuyer@76: ]] farmbuyer@89: local cache = g_uniques:SEARCH(exist) farmbuyer@89: local looti,hi,ui = cache.loot, cache.history, cache.history_may farmbuyer@89: farmbuyer@89: -- Active loot farmbuyer@89: if looti and g_loot[looti].unique == exist then farmbuyer@89: pprint('improv', "found and replaced loot entry", looti) farmbuyer@89: g_loot[looti].unique = winner[2] farmbuyer@89: else farmbuyer@89: -- If sharded, filtered, or the improv was done by the local farmbuyer@89: -- player, then the "previous" unique would not have made it farmbuyer@89: -- into the tables to begin with. So don't flag an error. farmbuyer@89: pprint('improv', "No active loot found", looti, farmbuyer@89: looti and g_loot[looti].unique, winning_index) farmbuyer@89: end farmbuyer@89: farmbuyer@89: -- History farmbuyer@89: if hi ~= g_uniques.NOTFOUND then farmbuyer@89: hi = addon.history.byname[hi] farmbuyer@89: local hist = addon.history[hi] farmbuyer@89: if ui and hist.unique[ui] == exist then farmbuyer@89: -- ui is valid farmbuyer@82: else farmbuyer@89: ui = nil farmbuyer@89: for i,ui2 in ipairs(hist.unique) do farmbuyer@89: if ui2 == exist then farmbuyer@89: ui = i farmbuyer@89: break farmbuyer@76: end farmbuyer@76: end farmbuyer@89: end farmbuyer@89: if ui then farmbuyer@89: pprint('improv', "found and replacing history entry", hi, farmbuyer@89: ui, hist.name) farmbuyer@89: assert(exist ~= winner[2]) farmbuyer@89: hist.when[winner[2]] = hist.when[exist] farmbuyer@89: hist.id[winner[2]] = hist.id[exist] farmbuyer@89: hist.count[winner[2]] = hist.count[exist] farmbuyer@89: hist.unique[ui] = winner[2] farmbuyer@89: hist.when[exist] = nil farmbuyer@89: hist.id[exist] = nil farmbuyer@89: hist.count[exist] = nil farmbuyer@76: end farmbuyer@76: end farmbuyer@89: 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@107: gur.me = tonumber(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@81: function addon:horrible_horrible_error (err_msg) farmbuyer@81: if self.display then farmbuyer@81: local d = self.display farmbuyer@81: if d then farmbuyer@100: -- Take this down a piece at a time, on the assumption that farmbuyer@100: -- the main window won't be able to do so. farmbuyer@96: local gui = d:GetUserData("GUI state") farmbuyer@96: local eoist = gui.eoiST farmbuyer@81: if eoist then eoist:Hide() end farmbuyer@96: local histst = gui.histST farmbuyer@81: if histst then histst:Hide() end farmbuyer@81: d:Hide() farmbuyer@81: end farmbuyer@81: end farmbuyer@81: self.NOLOAD = err_msg farmbuyer@77: -- This should happen so rarely that it's not worth moving into gui.lua farmbuyer@81: if not StaticPopupDialogs["OUROL_ARGH"] then farmbuyer@81: StaticPopupDialogs["OUROL_ARGH"] = flib.StaticPopup{ farmbuyer@77: button1 = OKAY, farmbuyer@77: } farmbuyer@77: clicky = addon.format_hypertext( farmbuyer@77: [[ SYSTEM FAILURE -- RELEASE RINZLER ]], "|cffff0000", farmbuyer@81: function() StaticPopup_Show "OUROL_ARGH" end) farmbuyer@77: end farmbuyer@81: StaticPopupDialogs["OUROL_ARGH"].text = horrible_error_text:format(err_msg) farmbuyer@107: 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@81: farmbuyer@81: function _unavoidable_collision (err) farmbuyer@81: addon:horrible_horrible_error (err) farmbuyer@81: -- we don't actually need to kill the GUI in this case farmbuyer@81: addon.NOLOAD = nil farmbuyer@81: end farmbuyer@77: end farmbuyer@77: farmbuyer@1: farmbuyer@1: ------ Saved texts farmbuyer@1: function addon:check_saved_table(silent_p) farmbuyer@107: local s = OuroLootSV_saved farmbuyer@1: if s and (#s > 0) then return s end farmbuyer@107: 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@81: 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@107: OuroLootSV_saved = OuroLootSV_saved or {} farmbuyer@107: local SV = 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@81: 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@92: 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@81: 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@114: -- Up through 2.18.4 (specifically through rev 95), an individual player's farmbuyer@73: -- table looked like this: farmbuyer@73: -- ["name"] = "Farmbuyer", farmbuyer@114: -- -- most recent loot: farmbuyer@114: -- [1] = { id = nnnnn, when = "formatted timestamp for displaying" } farmbuyer@114: -- -- previous loot: farmbuyer@114: -- [2] = { ......., [count = "x3"] } farmbuyer@73: -- which was much easier to manipulate, but had a ton of memory overhead. farmbuyer@1: do farmbuyer@114: local new, del, date = flib.new, flib.del, date farmbuyer@114: 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@125: local new_uniques, uniques_bywhen, when_array = {}, new(), new() 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@107: 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@125: del(uniques_bywhen) farmbuyer@125: del(when_array) farmbuyer@73: end farmbuyer@73: farmbuyer@81: function addon:repair_history_integrity() farmbuyer@81: local rcount, pcount, hcount, errors = 0, 0, 0, 0 farmbuyer@81: local empties_to_delete = {} farmbuyer@81: farmbuyer@81: for rname,realm in pairs(self.history_all) do farmbuyer@81: for pk,player in ipairs(realm) do farmbuyer@81: local id, when, unique, count = {}, {}, {}, {} farmbuyer@81: for i,h in ipairs(player.unique) do farmbuyer@81: h = tostring(h) farmbuyer@81: if player.when[h] and player.id[h] then farmbuyer@81: unique[#unique+1] = h farmbuyer@81: id[h] = player.id[h] farmbuyer@81: when[h] = player.when[h] farmbuyer@81: count[h] = player.count[h] farmbuyer@81: else farmbuyer@108: self:Print("> Realm %s, player %s, entry %d: tag <%s>, id <%s>, time <%s>, count <%s>", farmbuyer@81: rname, player.name, i, h, tostring(player.id[h]), farmbuyer@81: tostring(player.when[h]), tostring(player.count[h])) farmbuyer@81: errors = errors + 1 farmbuyer@81: end farmbuyer@81: hcount = hcount + 1 farmbuyer@81: end farmbuyer@81: player.id, player.when, player.unique, player.count = farmbuyer@81: id, when, unique, count farmbuyer@114: player.person_class = player.person_class or farmbuyer@114: (g_loot.raiders[player.name] and g_loot.raiders[player.name].class) farmbuyer@81: if #player.unique > 1 then farmbuyer@81: sort_player(player) farmbuyer@81: elseif #player.unique == 0 then farmbuyer@81: tinsert (empties_to_delete, 1, pk) farmbuyer@81: end farmbuyer@81: pcount = pcount + 1 farmbuyer@81: end farmbuyer@81: if #empties_to_delete > 0 then farmbuyer@81: for _,pk in ipairs(empties_to_delete) do farmbuyer@81: local player = tremove (realm, pk) farmbuyer@108: self:Print("> Realm %s, player %s, is empty", rname, player.name) farmbuyer@108: errors = errors + 1 farmbuyer@81: end farmbuyer@81: wipe(empties_to_delete) farmbuyer@81: end farmbuyer@81: if #realm == 0 then farmbuyer@81: self.history_all[rname] = nil farmbuyer@108: self:Print("> Realm %s is empty", rname) farmbuyer@108: errors = errors + 1 farmbuyer@81: end farmbuyer@81: rcount = rcount + 1 farmbuyer@81: end farmbuyer@81: self:_build_history_names() farmbuyer@81: if errors > 0 then farmbuyer@81: self:Print("The listed entries have been erased from history.") farmbuyer@81: end farmbuyer@108: self:Print("Finished examining %d realms, %d players, %d history entries.", farmbuyer@108: rcount, pcount, hcount) farmbuyer@81: end farmbuyer@81: 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@87: g_uniques[u] = { history = player.name, 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@107: local hmmm = rawget(g_uniques,e.unique) farmbuyer@73: if hmmm then farmbuyer@82: hmmm.loot = i farmbuyer@109: elseif not self:_test_disposition (e.disposition, 'affects_history') then farmbuyer@73: g_uniques[e.unique] = { loot = i, history = g_uniques.NOTFOUND } farmbuyer@73: count = count + 1 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@73: end farmbuyer@73: else farmbuyer@89: -- The usual cause: when only source is from an older client farmbuyer@89: -- and the disposition did not trigger addhistory, then not farmbuyer@89: -- even a stub history entry happens. Code has now been added farmbuyer@89: -- to try harder to prevent this, but it's still best to not farmbuyer@89: -- simple ignore it. farmbuyer@73: trouble = true farmbuyer@89: pprint('loot', "ERROR precache loop found missing/outdated unique tag!", farmbuyer@89: 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@108: -- This displayed number is occasionally negative, if the GC happens farmbuyer@108: -- to run during the loops. How much should we care? 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@89: "realm's loot history. If you keep seeing this message, type", farmbuyer@89: "'/ouroloot fix ?' and try some of those actions.") 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: 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@114: -- Prepends data from the loot entry at LOOTINDEX to the history for that farmbuyer@114: -- player, making it the "most recent" entry regardless of actual data. farmbuyer@114: -- In the usual case, builds a formatted timestamp string from g_today and farmbuyer@114: -- the loot entry's recorded time (thus the formatted string really *will* farmbuyer@114: -- be the most recent entry). If g_today has not been set, then falls farmbuyer@114: -- back on formatting LOOTINDEX's time_t 'stamp' field. farmbuyer@114: -- farmbuyer@114: -- If RESORT_P is true-valued, then re-sorts the player's history based on farmbuyer@114: -- formatted timestmps, instead of leaving the new entry as the latest. farmbuyer@114: function addon:_addHistoryEntry (lootindex, resort_p) farmbuyer@1: local e = g_loot[lootindex] farmbuyer@6: if e.kind ~= 'loot' then return end farmbuyer@6: farmbuyer@114: if e.person_realm and opts.history_ignore_xrealm then farmbuyer@114: return farmbuyer@114: end farmbuyer@114: farmbuyer@6: local i,h = self:get_loot_history(e.person) farmbuyer@114: -- If we've added anything at all into g_loot this session, g_today farmbuyer@114: -- will be set. If we've logged on simply to manipulate history, then farmbuyer@114: -- try and fake a timestamp (it'll be "close enough"). farmbuyer@114: local when = g_today and self:format_timestamp (g_today,e) farmbuyer@114: or date("%Y/%m/%d %H:%M",e.stamp) farmbuyer@87: assert(h.name==e.person) farmbuyer@73: farmbuyer@114: -- Should rarely happen anymore: farmbuyer@71: if (not e.unique) or (#e.unique==0) then farmbuyer@84: e.unique = e.id .. e.person .. 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@114: if resort_p then farmbuyer@114: sort_player(h) farmbuyer@114: end farmbuyer@87: g_uniques[U] = { loot = lootindex, history = e.person } farmbuyer@100: self:Fire ('NewHistory', e.person, U) 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@87: player_i = addon.history.byname[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@114: errtxt = "ZOMG! %s was in history but now is gone. Possibly your history tables have been corrupted and should be recreated. This is likely a bug. Tell Farmbuyer what steps you took to cause this, with as many details as possible." farmbuyer@24: end farmbuyer@73: return nil, errtxt farmbuyer@25: end farmbuyer@24: farmbuyer@81: -- Handles reassigning loot between players. Arguments depend on who's farmbuyer@81: -- calling it: farmbuyer@81: -- "local", row_index, new_recipient farmbuyer@81: -- "remote", sender, unique_id, item_id, old_recipient, new_recipient farmbuyer@81: -- In the local case, must also broadcast a trigger. In the remote case, farmbuyer@81: -- must figure out the corresponding loot entry (if it exists). In both farmbuyer@81: -- cases, must update history appropriately. Returns nil if anything odd farmbuyer@81: -- happens; returns the affected loot index on success. farmbuyer@81: function addon:reassign_loot (how, ...) farmbuyer@81: -- This must all be filled out in all cases: farmbuyer@81: local e, index, from_name, to_name, unique, id farmbuyer@81: -- Only set in remote case: farmbuyer@81: local sender farmbuyer@81: farmbuyer@81: if how == "local" then farmbuyer@81: -- GUI doesn't allow reassignment unless the item is not-shard, farmbuyer@81: -- so we can assume the presence of a unique tag in this function. farmbuyer@81: index, to_name = ... farmbuyer@81: assert(type(to_name)=='string' and to_name:len()>0) farmbuyer@81: index = assert(tonumber(index)) farmbuyer@81: e = g_loot[index] farmbuyer@81: id = e.id farmbuyer@81: unique = assert(e.unique) farmbuyer@81: from_name = e.person farmbuyer@81: farmbuyer@81: elseif how == "remote" then farmbuyer@81: sender, unique, id, from_name, to_name = ... farmbuyer@81: id = tonumber(id) farmbuyer@81: local cache farmbuyer@81: local loop = 0 farmbuyer@81: repeat -- wtb continue statement pst farmbuyer@81: if loop > 1 then break end farmbuyer@81: e = nil farmbuyer@81: cache = cache and g_uniques:SEARCH(unique) or g_uniques[unique] farmbuyer@81: index = tonumber(cache.loot) farmbuyer@81: if index then farmbuyer@81: e = g_loot[index] farmbuyer@81: else farmbuyer@81: end farmbuyer@81: loop = loop + 1 farmbuyer@81: until e and (e.id == id) farmbuyer@81: farmbuyer@81: else farmbuyer@81: return -- silently ignore future cases from future clients farmbuyer@81: end farmbuyer@81: farmbuyer@81: if self.debug.loot then farmbuyer@81: local m = ("Reassign index %d (pre-unique %s) with id %d from '%s' to '%s'."): farmbuyer@81: format(index, unique, id, tostring(from_name), tostring(to_name)) farmbuyer@81: self.dprint('loot', m) farmbuyer@81: if sender == my_name then farmbuyer@81: self.dprint('loot',"(Returning early from debug mode's double self-reassign.)") farmbuyer@81: return index farmbuyer@81: end farmbuyer@81: end farmbuyer@81: farmbuyer@81: if not e then farmbuyer@81: -- say something? farmbuyer@81: return farmbuyer@81: end farmbuyer@81: farmbuyer@73: local from_i, from_h, hist_i = _history_by_loot_id (e, "reassign") farmbuyer@25: local to_i,to_h = self:get_loot_history(to_name) farmbuyer@25: farmbuyer@81: if not from_i then farmbuyer@81: if how == "local" then farmbuyer@81: -- from_h here is the formatted error text farmbuyer@81: self:Print(from_h .. " Loot will be reassigned, but history will NOT be updated.", e.itemlink) farmbuyer@81: end farmbuyer@25: else farmbuyer@81: -- XXX do some sanity checks here? from_name == from_h.name, etc? farmbuyer@87: -- If something were wrong at this point, what could we do? farmbuyer@81: farmbuyer@114: -- the Book of Job 1:21: "Naked I came from my faction capital farmbuyer@114: -- city, and naked I shall return thither." farmbuyer@114: if from_h ~= to_h then farmbuyer@114: local U = tremove (from_h.unique, hist_i) farmbuyer@114: -- "The loot master giveth..." farmbuyer@114: to_h.unique[#to_h.unique+1] = U farmbuyer@114: to_h.when[U] = from_h.when[U] farmbuyer@114: to_h.id[U] = from_h.id[U] farmbuyer@114: to_h.count[U] = from_h.count[U] farmbuyer@114: sort_player(to_h) farmbuyer@114: -- "...and the loot master taketh away." farmbuyer@114: from_h.when[U] = nil farmbuyer@114: from_h.id[U] = nil farmbuyer@114: from_h.count[U] = nil farmbuyer@114: end farmbuyer@114: -- "Blessed be the lookup cache of the loot master." farmbuyer@87: g_uniques[U] = { loot = index, history = to_name } farmbuyer@25: end farmbuyer@81: local from_person_class = e.person_class or from_h.person_class farmbuyer@81: or (g_loot.raiders[from_name] and g_loot.raiders[from_name].class) farmbuyer@107: or select(2,UnitClass(from_name)) farmbuyer@25: e.person = to_name farmbuyer@81: e.person_class = to_h.person_class farmbuyer@81: or (g_loot.raiders[to_name] and g_loot.raiders[to_name].class) farmbuyer@107: or select(2,UnitClass(to_name)) farmbuyer@25: self.hist_clean = nil farmbuyer@81: self.loot_clean = nil farmbuyer@81: farmbuyer@81: if how == "local" then farmbuyer@101: if opts.chatty_on_local_changes then farmbuyer@101: _notify_about_change (_G.UNIT_YOU, index, nil, from_name, from_person_class) farmbuyer@101: end farmbuyer@81: self:vbroadcast('reassign', unique, id, from_name, to_name) farmbuyer@81: elseif opts.chatty_on_remote_changes then farmbuyer@101: _notify_about_change (sender, index, nil, from_name, from_person_class) farmbuyer@81: end farmbuyer@81: if self.display then farmbuyer@96: self.display:GetUserData("GUI state").eoiST:OuroLoot_Refresh(index) farmbuyer@81: self:redisplay() farmbuyer@81: end farmbuyer@100: self:Fire ('Reassign', unique, id, e, from_name, to_name) farmbuyer@81: return index farmbuyer@25: end farmbuyer@25: farmbuyer@84: local function expunge (player, index_or_unique) farmbuyer@84: local i,u farmbuyer@84: if type(index_or_unique) == 'string' then farmbuyer@84: for u = 1, #player.unique do farmbuyer@84: if player.unique[u] == index_or_unique then farmbuyer@84: i = u farmbuyer@84: break farmbuyer@84: end farmbuyer@84: end farmbuyer@84: elseif type(index_or_unique) == 'number' then farmbuyer@84: i = index_or_unique farmbuyer@84: end farmbuyer@84: if not i then farmbuyer@84: return -- error here? farmbuyer@84: end farmbuyer@84: u = player.unique[i] farmbuyer@84: assert(#u>0) farmbuyer@84: tremove (player.unique, i) farmbuyer@84: player.when[u], player.id[u], player.count[u] = nil, nil, nil farmbuyer@84: g_uniques[u] = nil farmbuyer@84: addon.hist_clean = nil farmbuyer@100: addon:Fire ('DelHistory', player.name, u) farmbuyer@87: return #player.unique farmbuyer@84: end farmbuyer@84: farmbuyer@84: -- Mirror of _addHistoryEntry. Arguments are either: farmbuyer@84: -- E - loot entry farmbuyer@84: -- U,ITEM - unique tag, and a name/itemlink for errors farmbuyer@87: -- If this entry was the only one for that player, will also remove that farmbuyer@87: -- player's tables from the history array. farmbuyer@87: -- farmbuyer@87: -- On success, returns the number of remaining history entries for that farmbuyer@87: -- player (potentially zero). On failure, returns nil+error. farmbuyer@87: function addon:_delHistoryEntry (first, item) farmbuyer@84: if type(first) == 'table' then farmbuyer@87: item = first.itemlink or item farmbuyer@84: --elseif type(first) == 'string' then farmbuyer@84: end farmbuyer@84: farmbuyer@84: local from_i, from_h, hist_i = _history_by_loot_id (first, "delete") farmbuyer@84: farmbuyer@36: if not from_i then farmbuyer@36: -- from_h is the formatted error text farmbuyer@84: return nil, (from_h farmbuyer@84: .." Loot will be deleted, but history will NOT be updated." farmbuyer@87: ):format(item) farmbuyer@36: end farmbuyer@36: farmbuyer@87: local remaining = expunge (from_h, hist_i) farmbuyer@87: if not remaining then farmbuyer@87: return nil, "Something bizarre happening trying to delete "..item farmbuyer@87: elseif remaining > 0 then farmbuyer@87: return remaining farmbuyer@87: end farmbuyer@87: tremove (self.history, from_i) farmbuyer@87: self:_build_history_names() farmbuyer@87: return 0 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@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@109: if not self:_test_disposition (newdisp, 'affects_history') then farmbuyer@114: -- remove history entry if it exists farmbuyer@73: local name_i, name_h, hist_i = _history_by_loot_id (e, "mark") farmbuyer@25: if hist_i then farmbuyer@114: -- clears g_uniques and fires DelHistory farmbuyer@114: expunge (name_h, hist_i) farmbuyer@109: elseif not self:_test_disposition (olddisp, 'affects_history') 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@109: if (not self:_test_disposition (olddisp, 'affects_history')) farmbuyer@109: and self:_test_disposition (newdisp, 'affects_history') farmbuyer@25: then farmbuyer@114: -- Must create a new history entry. farmbuyer@25: local name_i, name_h = self:get_loot_history(name) farmbuyer@114: -- puts entry into g_uniques and fires NewHistory farmbuyer@114: self:_addHistoryEntry (index, --[[re-sort entries=]]true) farmbuyer@25: self.hist_clean = nil 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@128: -- 'local', row_index, new_disposition farmbuyer@128: -- '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@128: 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@128: elseif how == 'remote' then farmbuyer@73: sender, unique, id, olddisp, newdisp = ... farmbuyer@81: id = tonumber(id) farmbuyer@81: local cache farmbuyer@81: local loop = 0 farmbuyer@81: repeat -- wtb continue statement pst farmbuyer@81: if loop > 1 then break end farmbuyer@81: e = nil farmbuyer@81: cache = cache and g_uniques:SEARCH(unique) or g_uniques[unique] farmbuyer@73: index = tonumber(cache.loot) farmbuyer@81: if index then farmbuyer@81: e = g_loot[index] farmbuyer@81: else farmbuyer@81: end farmbuyer@81: loop = loop + 1 farmbuyer@81: until e and (e.id == id) 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@80: 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@81: self.hist_clean = nil farmbuyer@81: self.loot_clean = nil farmbuyer@73: -- A unique tag has been set by this point. farmbuyer@128: if how == 'local' then farmbuyer@73: unique = assert(e.unique) farmbuyer@101: if opts.chatty_on_local_changes then farmbuyer@101: _notify_about_change (_G.UNIT_YOU, index, olddisp) farmbuyer@101: end farmbuyer@73: self:vbroadcast('mark', unique, id, olddisp, newdisp) farmbuyer@73: end farmbuyer@100: self:Fire ('MarkAs', unique, id, e, olddisp or 'normal', newdisp or 'normal') 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: 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@89: -- can't use #msg since there might be nil 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@89: self:SendCommMessage(self.ident, msg, "RAID") farmbuyer@89: -- this is what lets us debug our own message traffic: farmbuyer@100: if self.debug.comm and self.is_guilded then farmbuyer@89: self:SendCommMessage(self.ident, msg, "GUILD") farmbuyer@89: end 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@89: local what = addon.enabled and "tracking" or farmbuyer@89: (addon.rebroadcast and "broadcasting" or "disabled") farmbuyer@89: addon:whispercast (sender, 'pong', addon.version, what, addon.revision) farmbuyer@1: end farmbuyer@89: OCR_funcs.pong = function (sender, _, ver, status, opt_rev) farmbuyer@89: local s = ("|cff00ff00%s|r %s(r%s) is |cff00ffff%s|r"): farmbuyer@89: format (sender, ver, opt_rev or "?", 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@89: addon:_check_version (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@101: _notify_about_change (sender, index, old) farmbuyer@73: end farmbuyer@73: end farmbuyer@73: farmbuyer@81: OCR_funcs['17reassign'] = function (sender, _, unique, item, from, to) farmbuyer@81: addon.dprint('comm', "DOTreassign/17, sender", sender, "unique", unique, farmbuyer@81: "item", item, "from", from, "to", to) farmbuyer@81: --[[local index =]] addon:reassign_loot ("remote", sender, unique, item, from, to) farmbuyer@81: -- Notification handled inside reassign_loot. farmbuyer@81: end farmbuyer@81: 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@122: 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@95: addon:Print("%s has requested additional broadcasters! Click %s to enable rebroadcasting, or %s to remain off and also ignore rebroadcast requests for as long as you're logged in.", 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@89: 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@128: addon:MODULE_PROTOTYPE_POINTERS() farmbuyer@109: addon.FILES_LOADED = 1 farmbuyer@1: -- vim:noet