farmbuyer@17: local nametag, addon = ... farmbuyer@1: farmbuyer@1: --[==[ farmbuyer@1: g_loot's numeric indices are loot entries (including titles, separators, farmbuyer@1: etc); its named indices are: farmbuyer@61: - forum saved text from forum markup window, default nil farmbuyer@61: - attend saved text from raid attendence window, default nil farmbuyer@61: - printed.FOO last loot index formatted into text window FOO, default 0 farmbuyer@61: - raiders accumulating raid roster data as we see raid members; indexed farmbuyer@57: by player name with subtable fields: farmbuyer@61: - class capitalized English codename ("WARRIOR", "DEATHKNIGHT", etc) farmbuyer@61: - subgroup 1-8 farmbuyer@61: - race English codename ("BloodElf", etc) farmbuyer@61: - sex 1 = unknown/error, 2 = male, 3 = female farmbuyer@61: - level can be 0 if player was offline at the time farmbuyer@61: - guild guild name, or missing if unguilded farmbuyer@61: - online 1 = online, 2 = offline, 3 = no longer in raid farmbuyer@57: [both of these next two fields use time_t values:] farmbuyer@61: - join time player joined the raid (or first time we've seen them) farmbuyer@61: - leave time player left the raid (or time we've left the raid, if farmbuyer@61: 'online' is not 3) farmbuyer@50: farmbuyer@54: Common g_loot entry indices: farmbuyer@61: - kind time/boss/loot farmbuyer@61: - hour 0-23, on the *physical instance server*, not the realm server farmbuyer@61: - minute 0-59, ditto farmbuyer@61: - stamp time_t on the local computer farmbuyer@61: - cols graphical display data; cleared when logging out farmbuyer@50: farmbuyer@50: Time specific g_loot indices: farmbuyer@61: - startday table with month/day/year/text fields from makedate() farmbuyer@57: text is always "dd Month yyyy" farmbuyer@50: farmbuyer@50: Boss specific g_loot indices: farmbuyer@61: - bossname name of boss/encounter; farmbuyer@57: may be changed if "snarky boss names" option is enabled farmbuyer@61: - reason wipe/kill ("pull" does not generate an entry) farmbuyer@61: - instance name of instance, including size and difficulty farmbuyer@61: - maxsize max raid size: 5/10/25, presumably also 15 and 40 could show farmbuyer@61: up; can be 0 if we're outside an instance and the player farmbuyer@61: inside has an older version farmbuyer@61: - duration in seconds; may be missing (only present if local) farmbuyer@61: - raidersnap copy of g_loot.raiders at the time of the boss event farmbuyer@50: farmbuyer@50: Loot specific g_loot indices: farmbuyer@61: - person recipient farmbuyer@61: - person_class class of recipient if available; may be missing; farmbuyer@57: will be classID-style (e.g., DEATHKNIGHT) farmbuyer@61: - itemname not including square brackets farmbuyer@61: - id itemID as number farmbuyer@61: - itemlink full clickable link farmbuyer@61: - itexture icon path (e.g., Interface\Icons\INV_Misc_Rune_01) farmbuyer@61: - quality ITEM_QUALITY_* number farmbuyer@61: - disposition offspec/gvault/shard; missing otherwise; can be set from farmbuyer@57: the extratext field farmbuyer@61: - count e.g., "x3"; missing otherwise; can be set/removed from farmbuyer@57: extratext; triggers only for a stack of items, not "the boss farmbuyer@57: dropped double axes today" farmbuyer@66: - variant 1 = heroic item, 2 = LFR item; missing otherwise farmbuyer@61: - cache_miss if GetItemInfo failed; SHOULD be missing (changes other fields) farmbuyer@65: - bcast_from player's name if received rebroadcast from that player; farmbuyer@65: missing otherwise; can be deleted as a result of in-game farmbuyer@65: fiddling of loot data farmbuyer@61: - extratext text in Note column, including disposition and rebroadcasting farmbuyer@61: - extratext_byhand true if text edited by player directly; missing otherwise farmbuyer@50: farmbuyer@1: farmbuyer@1: Functions arranged like this, with these lables (for jumping to). As a farmbuyer@1: rule, member functions with UpperCamelCase names are called directly by farmbuyer@1: user-facing code, ones with lowercase names are "one step removed", and farmbuyer@1: names with leading underscores are strictly internal helper functions. farmbuyer@1: ------ Saved variables farmbuyer@1: ------ Constants farmbuyer@1: ------ Addon member data farmbuyer@6: ------ Globals farmbuyer@1: ------ Expiring caches farmbuyer@1: ------ Ace3 framework stuff farmbuyer@1: ------ Event handlers farmbuyer@1: ------ Slash command handler farmbuyer@1: ------ On/off farmbuyer@1: ------ Behind the scenes routines farmbuyer@1: ------ Saved texts farmbuyer@1: ------ Loot histories farmbuyer@1: ------ Player communication farmbuyer@1: farmbuyer@1: This started off as part of a raid addon package written by somebody else. farmbuyer@1: After he retired, I began modifying the code. Eventually I set aside the farmbuyer@1: entire package and rewrote the loot tracker module from scratch. Many of the farmbuyer@1: variable/function naming conventions (sv_*, g_*, and family) stayed across the farmbuyer@16: rewrite. farmbuyer@16: farmbuyer@16: Some variables are needlessly initialized to nil just to look uniform. farmbuyer@1: farmbuyer@1: ]==] farmbuyer@1: farmbuyer@1: ------ Saved variables farmbuyer@16: OuroLootSV = nil -- possible copy of g_loot farmbuyer@16: OuroLootSV_saved = nil -- table of copies of saved texts, default nil; keys farmbuyer@16: -- are numeric indices of tables, subkeys of those farmbuyer@16: -- are name/forum/attend/date farmbuyer@16: OuroLootSV_opts = nil -- same as option_defaults until changed farmbuyer@16: -- autoshard: optional name of disenchanting player, default nil farmbuyer@16: -- threshold: optional loot threshold, default nil farmbuyer@16: OuroLootSV_hist = nil farmbuyer@19: OuroLootSV_log = {} farmbuyer@1: farmbuyer@1: farmbuyer@1: ------ Constants farmbuyer@1: local option_defaults = { farmbuyer@66: ['datarev'] = 18, -- cheating, this isn't actually an option farmbuyer@1: ['popup_on_join'] = true, farmbuyer@1: ['register_slashloot'] = true, farmbuyer@1: ['scroll_to_bottom'] = true, farmbuyer@1: ['chatty_on_kill'] = false, farmbuyer@1: ['no_tracking_wipes'] = false, farmbuyer@1: ['snarky_boss'] = true, farmbuyer@1: ['keybinding'] = false, farmbuyer@2: ['bossmod'] = "DBM", farmbuyer@1: ['keybinding_text'] = 'CTRL-SHIFT-O', farmbuyer@1: ['forum'] = { farmbuyer@25: ['[url] Wowhead'] = '[url=http://www.wowhead.com/?item=$I]$N[/url]$X - $T', farmbuyer@25: ['[url] MMO/Wowstead'] = '[http://db.mmo-champion.com/i/$I]$X - $T', farmbuyer@1: ['[item] by name'] = '[item]$N[/item]$X - $T', farmbuyer@1: ['[item] by ID'] = '[item]$I[/item]$X - $T', farmbuyer@1: ['Custom...'] = '', farmbuyer@1: }, farmbuyer@1: ['forum_current'] = '[item] by name', farmbuyer@57: ['display_disabled_LODs'] = false, farmbuyer@65: ['display_bcast_from'] = true, farmbuyer@1: } farmbuyer@1: local virgin = "First time loaded? Hi! Use the /ouroloot or /loot command" farmbuyer@1: .." to show the main display. You should probably browse the instructions" farmbuyer@1: .." if you've never used this before; %s to display the help window. This" farmbuyer@1: .." welcome message will not intrude again." farmbuyer@27: local newer_warning = "A newer version has been released. You can %s to display" farmbuyer@27: .." a download URL for copy-and-pasting. You can %s to ping other raiders" farmbuyer@27: .." for their installed versions (same as '/ouroloot ping' or clicking the" farmbuyer@27: .." 'Ping!' button on the options panel)." farmbuyer@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@40: local comm_cleanup_ttl = 4 -- seconds in the cache farmbuyer@27: local revision_large = nil -- defaults to 1, possibly changed by revision farmbuyer@20: local g_LOOT_ITEM_ss, g_LOOT_ITEM_MULTIPLE_sss, g_LOOT_ITEM_SELF_s, g_LOOT_ITEM_SELF_MULTIPLE_ss farmbuyer@1: farmbuyer@1: farmbuyer@1: ------ Addon member data farmbuyer@1: local flib = LibStub("LibFarmbuyer") farmbuyer@1: addon.author_debug = flib.author_debug farmbuyer@1: farmbuyer@6: -- Play cute games with namespaces here just to save typing. WTB Lua 5.2 PST. farmbuyer@1: do local _G = _G setfenv (1, addon) farmbuyer@1: farmbuyer@56: commrev = '16' farmbuyer@27: revision = _G.GetAddOnMetadata(nametag,"Version") or "?" -- "x.yy.z", etc farmbuyer@1: ident = "OuroLoot2" farmbuyer@1: identTg = "OuroLoot2Tg" farmbuyer@1: status_text = nil farmbuyer@1: farmbuyer@45: tekdebug = nil farmbuyer@45: if _G.tekDebug then farmbuyer@45: local tdframe = _G.tekDebug:GetFrame("Ouro Loot") farmbuyer@45: function tekdebug (txt) farmbuyer@45: -- tekDebug notices "|r:" farmbuyer@45: tdframe:AddMessage('|cff17ff0dOuro Loot|r:'..txt,1,1,1) farmbuyer@45: end farmbuyer@45: end farmbuyer@45: farmbuyer@1: DEBUG_PRINT = false farmbuyer@1: debug = { farmbuyer@1: comm = false, farmbuyer@1: loot = false, farmbuyer@1: flow = false, farmbuyer@1: notraid = false, farmbuyer@1: cache = false, farmbuyer@19: alsolog = false, farmbuyer@1: } farmbuyer@45: -- This looks ugly, but it factors out the load-time decisions from farmbuyer@45: -- the run-time ones. 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@1: enabled = false farmbuyer@1: rebroadcast = false farmbuyer@1: display = nil -- display frame, when visible farmbuyer@1: loot_clean = nil -- index of last GUI entry with known-current visual data farmbuyer@1: sender_list = {active={},names={}} -- this should be reworked 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@1: popped = nil -- non-nil when reminder has been shown, actual value unimportant farmbuyer@1: farmbuyer@2: bossmod_registered = nil farmbuyer@2: bossmods = {} farmbuyer@2: farmbuyer@1: requesting = nil -- for prompting for additional rebroadcasters farmbuyer@1: farmbuyer@38: -- don't use NUM_ITEM_QUALITIES as the upper bound unless we expect heirlooms to show up farmbuyer@11: 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@1: farmbuyer@6: ------ Globals farmbuyer@1: local g_loot = nil farmbuyer@1: local g_restore_p = nil farmbuyer@1: local g_wafer_thin = nil -- for prompting for additional rebroadcasters farmbuyer@1: local g_today = nil -- "today" entry in g_loot farmbuyer@16: local g_boss_signpost = nil farmbuyer@1: local opts = nil farmbuyer@1: farmbuyer@56: local pairs, ipairs, tinsert, tremove, tostring, tonumber, wipe = farmbuyer@56: pairs, ipairs, table.insert, table.remove, tostring, tonumber, table.wipe farmbuyer@1: local pprint, tabledump = addon.pprint, flib.tabledump farmbuyer@56: local CopyTable, GetNumRaidMembers = CopyTable, GetNumRaidMembers farmbuyer@1: -- En masse forward decls of symbols defined inside local blocks farmbuyer@41: local _register_bossmod, makedate, create_new_cache, _init, _log farmbuyer@1: farmbuyer@27: -- Try to extract numbers from the .toc "Version" and munge them into an farmbuyer@27: -- integral form for comparison. The result doesn't need to be meaningful as farmbuyer@38: -- long as we can reliably feed two of them to "<" and get useful answers. farmbuyer@29: -- farmbuyer@29: -- This makes/reinforces an assumption that revision_large of release packages farmbuyer@29: -- (e.g., 2016001) will always be higher than those of development packages farmbuyer@29: -- (e.g., 87), due to the tagging system versus subversion file revs. This farmbuyer@29: -- is good, as local dev code will never trigger a false positive update farmbuyer@62: -- warning for other users. farmbuyer@27: do farmbuyer@27: local r = 0 farmbuyer@27: for d in addon.revision:gmatch("%d+") do farmbuyer@27: r = 1000*r + d farmbuyer@27: end farmbuyer@62: -- If it's a big enough number to obviously be a release, then make farmbuyer@65: -- sure it's big enough to overcome many small previous point releases. farmbuyer@62: while r > 2000 and r < 2000000 do farmbuyer@62: r = 1000*r farmbuyer@62: end farmbuyer@27: revision_large = math.max(r,1) farmbuyer@27: end farmbuyer@27: farmbuyer@1: -- Hypertext support, inspired by DBM broadcast pizza timers farmbuyer@1: do farmbuyer@1: local hypertext_format_str = "|HOuroRaid:%s|h%s[%s]|r|h" farmbuyer@1: farmbuyer@38: -- TEXT will automatically be surrounded by brackets farmbuyer@38: -- COLOR can be item quality code or a hex string farmbuyer@1: function addon.format_hypertext (code, text, color) farmbuyer@1: return hypertext_format_str:format (code, farmbuyer@11: type(color)=='number' and ITEM_QUALITY_COLORS[color].hex or color, farmbuyer@1: text) farmbuyer@1: end farmbuyer@1: farmbuyer@1: DEFAULT_CHAT_FRAME:HookScript("OnHyperlinkClick", function(self, link, string, mousebutton) farmbuyer@1: local ltype, arg = strsplit(":",link) farmbuyer@1: if ltype ~= "OuroRaid" then return end farmbuyer@51: -- XXX this is crap, redo this as a dispatch table with code at the call site farmbuyer@1: if arg == 'openloot' then farmbuyer@1: addon:BuildMainDisplay() farmbuyer@27: elseif arg == 'popupurl' then farmbuyer@30: -- Sadly, this is not generated by the packager, so hardcode it for now. farmbuyer@30: -- The 'data' field is handled differently for onshow than for other callbacks. farmbuyer@30: StaticPopup_Show("OUROL_URL", --[[text_arg1=]]nil, --[[text_arg2=]]nil, farmbuyer@30: --[[data=]][[http://www.curse.com/addons/wow/ouroloot]]) farmbuyer@27: elseif arg == 'doping' then farmbuyer@27: addon:DoPing() farmbuyer@1: elseif arg == 'help' then farmbuyer@1: addon:BuildMainDisplay('help') farmbuyer@1: elseif arg == 'bcaston' then farmbuyer@1: if not addon.rebroadcast then farmbuyer@1: addon:Activate(nil,true) farmbuyer@1: end farmbuyer@1: addon:broadcast('bcast_responder') farmbuyer@1: elseif arg == 'waferthin' then -- mint? it's wafer thin! farmbuyer@1: g_wafer_thin = true -- fuck off, I'm full farmbuyer@1: addon:broadcast('bcast_denied') -- remove once tested farmbuyer@51: elseif arg == 'reload' then farmbuyer@51: addon:BuildMainDisplay('opt') farmbuyer@1: end farmbuyer@1: end) farmbuyer@1: farmbuyer@1: local old = ItemRefTooltip.SetHyperlink farmbuyer@1: function ItemRefTooltip:SetHyperlink (link, ...) farmbuyer@1: if link:match("^OuroRaid") then return end farmbuyer@1: return old (self, link, ...) farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@1: do farmbuyer@1: -- copied here because it's declared local to the calendar ui, thanks blizz >< farmbuyer@1: local CALENDAR_FULLDATE_MONTH_NAMES = { farmbuyer@1: FULLDATE_MONTH_JANUARY, FULLDATE_MONTH_FEBRUARY, FULLDATE_MONTH_MARCH, farmbuyer@1: FULLDATE_MONTH_APRIL, FULLDATE_MONTH_MAY, FULLDATE_MONTH_JUNE, farmbuyer@1: FULLDATE_MONTH_JULY, FULLDATE_MONTH_AUGUST, FULLDATE_MONTH_SEPTEMBER, farmbuyer@1: FULLDATE_MONTH_OCTOBER, FULLDATE_MONTH_NOVEMBER, FULLDATE_MONTH_DECEMBER, farmbuyer@1: } farmbuyer@1: -- returns "dd Month yyyy", mm, dd, yyyy farmbuyer@1: function makedate() farmbuyer@1: Calendar_LoadUI() farmbuyer@1: local _, M, D, Y = CalendarGetDate() farmbuyer@1: local text = ("%d %s %d"):format(D, CALENDAR_FULLDATE_MONTH_NAMES[M], Y) farmbuyer@1: return text, M, D, Y farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@56: -- Returns an instance name or abbreviation, followed by the raid size farmbuyer@1: local function instance_tag() farmbuyer@61: -- possibly redo this with the new GetRaidDifficulty function farmbuyer@1: local name, typeof, diffcode, diffstr, _, perbossheroic, isdynamic = GetInstanceInfo() farmbuyer@56: local t, r farmbuyer@1: name = addon.instance_abbrev[name] or name farmbuyer@56: if typeof == "none" then return name, MAX_RAID_MEMBERS end farmbuyer@1: -- diffstr is "5 Player", "10 Player (Heroic)", etc. ugh. farmbuyer@35: if (GetLFGMode()) and (GetLFGModeType() == 'raid') then farmbuyer@56: t,r = 'LFR', 25 farmbuyer@35: elseif diffcode == 1 then farmbuyer@56: t,r = (GetNumRaidMembers()>0) and "10",10 or "5",5 farmbuyer@1: elseif diffcode == 2 then farmbuyer@56: t,r = (GetNumRaidMembers()>0) and "25",25 or "5h",5 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: pprint("instance_tag final", t, r) 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@1: farmbuyer@1: ------ Expiring caches farmbuyer@1: --[[ farmbuyer@1: foo = create_new_cache("myfoo",15[,cleanup]) -- ttl farmbuyer@1: foo:add("blah") farmbuyer@1: foo:test("blah") -- returns true farmbuyer@1: ]] farmbuyer@1: do farmbuyer@1: local caches = {} farmbuyer@25: local cleanup_group = _G.AnimTimerFrame:CreateAnimationGroup() farmbuyer@10: local time = _G.time farmbuyer@1: cleanup_group:SetLooping("REPEAT") farmbuyer@1: cleanup_group:SetScript("OnLoop", function(cg) farmbuyer@1: addon.dprint('cache',"OnLoop firing") farmbuyer@10: local now = time() farmbuyer@1: local alldone = true farmbuyer@1: -- this is ass-ugly farmbuyer@25: for name,c in pairs(caches) do farmbuyer@25: local fifo = c.fifo farmbuyer@25: local active = #fifo > 0 farmbuyer@25: while (#fifo > 0) and (now - fifo[1].t > c.ttl) do farmbuyer@40: addon.dprint('cache', name, "cache removing", fifo[1].t, "<", fifo[1].m, ">") farmbuyer@25: tremove(fifo,1) farmbuyer@1: end farmbuyer@25: if active and #fifo == 0 and c.func then farmbuyer@25: addon.dprint('cache', name, "empty, firing cleanup") farmbuyer@25: c:func() farmbuyer@25: end farmbuyer@25: alldone = alldone and (#fifo == 0) farmbuyer@1: end farmbuyer@1: if alldone then farmbuyer@40: addon.dprint('cache',"OnLoop FINISHING animation group") farmbuyer@1: cleanup_group:Finish() farmbuyer@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@25: local datum = { t=time(), m=x } farmbuyer@25: cache.hash[x] = datum farmbuyer@25: tinsert (cache.fifo, datum) farmbuyer@1: if not cleanup_group:IsPlaying() then farmbuyer@41: addon.dprint('cache', cache.name, "with entry", datum.t, "<", datum.m, "> STARTING animation group") farmbuyer@40: cache.cleanup:SetDuration(1) -- hmmm farmbuyer@1: cleanup_group:Play() farmbuyer@1: end farmbuyer@1: end farmbuyer@1: local function _test (cache, x) farmbuyer@40: -- FIXME This can return false positives, if called after the onloop farmbuyer@40: -- fifo has been removed but before the GC has removed the weak entry. farmbuyer@40: -- What to do, what to do... 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@25: fifo = {}, farmbuyer@25: hash = setmetatable({}, {__mode='kv'}), farmbuyer@1: } farmbuyer@1: c.cleanup:SetOrder(1) farmbuyer@25: caches[name] = c farmbuyer@1: return c farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@1: farmbuyer@1: ------ Ace3 framework stuff farmbuyer@1: function addon:OnInitialize() farmbuyer@41: _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@1: g_restore_p = OuroLootSV ~= nil farmbuyer@1: self.dprint('flow', "oninit sets restore as", g_restore_p) farmbuyer@1: farmbuyer@1: if OuroLootSV_opts == nil then farmbuyer@1: OuroLootSV_opts = {} farmbuyer@1: self:ScheduleTimer(function(s) farmbuyer@1: s:Print(virgin, s.format_hypertext('help',"click here",ITEM_QUALITY_UNCOMMON)) farmbuyer@1: virgin = nil farmbuyer@1: end,10,self) farmbuyer@65: else farmbuyer@65: virgin = nil farmbuyer@1: end farmbuyer@1: opts = OuroLootSV_opts farmbuyer@66: local stored_datarev = opts.datarev or 14 farmbuyer@1: for opt,default in pairs(option_defaults) do farmbuyer@1: if opts[opt] == nil then farmbuyer@1: opts[opt] = default farmbuyer@1: end farmbuyer@1: end farmbuyer@56: opts.datarev = option_defaults.datarev farmbuyer@38: farmbuyer@25: -- transition&remove old options farmbuyer@25: opts['forum_use_itemid'] = nil farmbuyer@25: if opts['forum_format'] then farmbuyer@25: opts.forum['Custom...'] = opts['forum_format'] farmbuyer@25: opts['forum_format'] = nil farmbuyer@25: end farmbuyer@25: if opts.forum['[url]'] then farmbuyer@25: opts.forum['[url] Wowhead'] = opts.forum['[url]'] farmbuyer@25: opts.forum['[url]'] = nil farmbuyer@25: opts.forum['[url] MMO/Wowstead'] = option_defaults.forum['[url] MMO/Wowstead'] farmbuyer@25: if opts['forum_current'] == '[url]' then farmbuyer@25: opts['forum_current'] = '[url] Wowhead' farmbuyer@25: end farmbuyer@25: end farmbuyer@1: option_defaults = nil farmbuyer@16: if OuroLootSV then -- may not be the same as testing g_restore_p soon farmbuyer@16: if OuroLootSV.saved then farmbuyer@16: OuroLootSV_saved = OuroLootSV.saved; OuroLootSV.saved = nil farmbuyer@16: end farmbuyer@16: if OuroLootSV.threshold then farmbuyer@16: opts.threshold = OuroLootSV.threshold; OuroLootSV.threshold = nil farmbuyer@16: end farmbuyer@16: if OuroLootSV.autoshard then farmbuyer@16: opts.autoshard = OuroLootSV.autoshard; OuroLootSV.autoshard = nil farmbuyer@16: end farmbuyer@16: end farmbuyer@38: farmbuyer@1: -- get item filter table if needed farmbuyer@1: if opts.itemfilter == nil then farmbuyer@1: opts.itemfilter = addon.default_itemfilter farmbuyer@1: end farmbuyer@1: addon.default_itemfilter = nil farmbuyer@1: farmbuyer@1: self:RegisterChatCommand("ouroloot", "OnSlash") farmbuyer@1: if opts.register_slashloot then farmbuyer@50: -- NOTA BENE: do not use /loot in the LoadOn list, ChatTypeInfo gets confused farmbuyer@50: -- maybe try to detect if this command is already in use... farmbuyer@1: SLASH_ACECONSOLE_OUROLOOT2 = "/loot" farmbuyer@1: end farmbuyer@1: farmbuyer@1: self.history_all = self.history_all or OuroLootSV_hist or {} farmbuyer@4: local r = assert(GetRealmName()) 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@38: if (not InCombatLockdown()) and OuroLootSV_hist and farmbuyer@38: (OuroLootSV_hist.HISTFORMAT == nil) -- restored data but it's older farmbuyer@38: then farmbuyer@38: -- Big honkin' loop farmbuyer@38: for rname,realm in pairs(self.history_all) do farmbuyer@38: for pk,player in ipairs(realm) do farmbuyer@38: for lk,loot in ipairs(player) do farmbuyer@38: if loot.count == "" then farmbuyer@38: loot.count = nil farmbuyer@38: end farmbuyer@38: end farmbuyer@38: end farmbuyer@38: end farmbuyer@38: end farmbuyer@38: self.history_all.HISTFORMAT = nil -- don't keep this in live data farmbuyer@6: --OuroLootSV_hist = nil farmbuyer@1: farmbuyer@56: -- Handle changes to the stored data format in stages from oldest to newest. farmbuyer@56: if OuroLootSV then farmbuyer@56: local dirty = false farmbuyer@66: local bumpers = {} farmbuyer@66: bumpers[14] = function() farmbuyer@56: for i,e in ipairs(OuroLootSV) do farmbuyer@56: if e.bosskill then farmbuyer@56: e.bossname, e.bosskill = e.bosskill, nil farmbuyer@56: end farmbuyer@55: end farmbuyer@55: end farmbuyer@66: farmbuyer@66: bumpers[15] = function() farmbuyer@56: for i,e in ipairs(OuroLootSV) do farmbuyer@56: if e.kind == 'boss' then farmbuyer@56: e.maxsize, e.raiderlist, e.raidersnap = 0, nil, {} farmbuyer@56: end farmbuyer@56: end farmbuyer@56: OuroLootSV.raiders = OuroLootSV.raiders or {} farmbuyer@56: for name,r in pairs(OuroLootSV.raiders) do farmbuyer@56: r.subgroup = 0 farmbuyer@56: end farmbuyer@56: end farmbuyer@66: farmbuyer@66: bumpers[16] = function() farmbuyer@61: for i,e in ipairs(OuroLootSV) do farmbuyer@61: if e.kind == 'boss' then -- brown paper bag bugs farmbuyer@61: e.raidersnap = e.raidersnap or {} farmbuyer@61: e.maxsize = e.maxsize or 0 farmbuyer@61: end farmbuyer@61: end farmbuyer@66: end farmbuyer@66: farmbuyer@66: bumpers[17] = function() farmbuyer@66: for i,e in ipairs(OuroLootSV) do farmbuyer@66: if e.kind == 'loot' and e.is_heroic then farmbuyer@66: e.variant, e.is_heroic = 1, nil farmbuyer@66: -- Could try detecting any previous LFR loot here, but... gah farmbuyer@66: end farmbuyer@66: end farmbuyer@66: end farmbuyer@66: farmbuyer@66: --[===[ farmbuyer@66: local real = bumpers farmbuyer@66: bumpers = newproxy(true) farmbuyer@66: local mt = getmetatable(bumpers) farmbuyer@66: mt.__index = real farmbuyer@66: mt.__gc = function() print"whadda ya know, garbage collection works" end ]===] farmbuyer@66: farmbuyer@66: while stored_datarev < opts.datarev do farmbuyer@66: self:Printf("Transitioning saved data format to %d...", stored_datarev+1) farmbuyer@66: dirty = true farmbuyer@66: bumpers[stored_datarev]() farmbuyer@66: stored_datarev = stored_datarev + 1 farmbuyer@61: end farmbuyer@56: if dirty then self:Print("Saved data has been massaged into shape.") end farmbuyer@55: end farmbuyer@55: farmbuyer@1: _init(self) farmbuyer@27: self.dprint('flow', "version strings:", revision_large, self.status_text) farmbuyer@66: self.OnInitialize = nil -- free up ALL the things! farmbuyer@1: end farmbuyer@1: farmbuyer@1: function addon:OnEnable() farmbuyer@10: self:RegisterEvent("PLAYER_LOGOUT") farmbuyer@10: self:RegisterEvent("RAID_ROSTER_UPDATE") farmbuyer@1: farmbuyer@1: -- Cribbed from Talented. I like the way jerry thinks: the first argument farmbuyer@1: -- can be a format spec for the remainder of the arguments. (The new farmbuyer@1: -- AceConsole:Printf isn't used because we can't specify a prefix without farmbuyer@1: -- jumping through ridonkulous hoops.) The part about overriding :Print farmbuyer@1: -- with a version using prefix hyperlinks is my fault. farmbuyer@37: -- farmbuyer@37: -- There is no ITEM_QUALITY_LEGENDARY constant. Sigh. farmbuyer@1: do farmbuyer@1: local AC = LibStub("AceConsole-3.0") farmbuyer@1: local chat_prefix = self.format_hypertext('openloot',"Ouro Loot",--[[legendary]]5) farmbuyer@1: function addon:Print (str, ...) farmbuyer@1: if type(str) == 'string' and str:find("%", nil, --[[plainmatch=]]true) then farmbuyer@1: return AC:Print (chat_prefix, str:format(...)) farmbuyer@1: else farmbuyer@1: return AC:Print (chat_prefix, str, ...) farmbuyer@1: end farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@51: while opts.keybinding do farmbuyer@51: if InCombatLockdown() then farmbuyer@51: self:Print("Cannot create '%s' as a keybinding while in combat!", farmbuyer@51: opts.keybinding_text) farmbuyer@51: 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.", self.format_hypertext('reload',"the options tab",ITEM_QUALITY_UNCOMMON)) 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@15: -- a simple SaveBindings(GetCurrentBindingSet()) occasionally fails when GCBS farmbuyer@38: -- decides to return neither 1 nor 2 during load, for reasons nobody has ever learned farmbuyer@15: local c = GetCurrentBindingSet() farmbuyer@15: if c == ACCOUNT_BINDINGS or c == CHARACTER_BINDINGS then farmbuyer@15: SaveBindings(c) farmbuyer@15: end farmbuyer@1: else farmbuyer@51: self:Print("Error registering '%s' as a keybinding, check spelling!", farmbuyer@51: opts.keybinding_text) farmbuyer@1: end farmbuyer@51: break farmbuyer@1: end farmbuyer@1: farmbuyer@20: --[[ farmbuyer@20: The four loot format patterns of interest, changed into relatively tight farmbuyer@20: string match patterns. Done at enable-time rather than load-time against farmbuyer@20: the slim chance that one of the non-US "delocalizers" needs to mess with farmbuyer@20: the global patterns before we transform them. farmbuyer@20: farmbuyer@20: The SELF variants can be replaced with LOOT_ITEM_PUSHED_SELF[_MULTIPLE] to farmbuyer@20: trigger on 'receive item' instead, which would detect extracting stuff farmbuyer@20: from mail, or s/PUSHED/CREATED/ for things like healthstones and guild farmbuyer@20: cauldron flasks. farmbuyer@20: ]] farmbuyer@20: farmbuyer@20: -- LOOT_ITEM = "%s receives loot: %s." --> (.+) receives loot: (.+)%. farmbuyer@20: g_LOOT_ITEM_ss = _G.LOOT_ITEM:gsub('%.$','%%.'):gsub('%%s','(.+)') farmbuyer@20: farmbuyer@20: -- LOOT_ITEM_MULTIPLE = "%s receives loot: %sx%d." --> (.+) receives loot: (.+)(x%d+)%. farmbuyer@20: g_LOOT_ITEM_MULTIPLE_sss = _G.LOOT_ITEM_MULTIPLE:gsub('%.$','%%.'):gsub('%%s','(.+)'):gsub('x%%d','(x%%d+)') farmbuyer@20: farmbuyer@20: -- LOOT_ITEM_SELF = "You receive loot: %s." --> You receive loot: (.+)%. farmbuyer@20: g_LOOT_ITEM_SELF_s = _G.LOOT_ITEM_SELF:gsub('%.$','%%.'):gsub('%%s','(.+)') farmbuyer@20: farmbuyer@20: -- LOOT_ITEM_SELF_MULTIPLE = "You receive loot: %sx%d." --> You receive loot: (.+)(x%d+)%. farmbuyer@20: g_LOOT_ITEM_SELF_MULTIPLE_ss = _G.LOOT_ITEM_SELF_MULTIPLE:gsub('%.$','%%.'):gsub('%%s','(.+)'):gsub('x%%d','(x%%d+)') farmbuyer@20: farmbuyer@44: --[[ farmbuyer@44: Stick something in the Blizzard addons options list, where most users farmbuyer@44: will probably look these days. Try to be conservative about needless farmbuyer@44: frame creation. farmbuyer@44: ]] farmbuyer@44: local bliz = CreateFrame("Frame") farmbuyer@44: bliz.name = "Ouro Loot" farmbuyer@44: bliz:SetScript("OnShow", function(_b) farmbuyer@44: local button = CreateFrame("Button",nil,_b,"UIPanelButtonTemplate") farmbuyer@44: button:SetWidth(150) farmbuyer@44: button:SetHeight(22) farmbuyer@44: button:SetScript("OnClick", function() farmbuyer@44: _G.InterfaceOptionsFrameCancel:Click() farmbuyer@44: _G.HideUIPanel(GameMenuFrame) farmbuyer@44: addon:OpenMainDisplayToTab"Options" farmbuyer@44: end) farmbuyer@44: button:SetText('"/ouroloot opt"') farmbuyer@44: button:SetPoint("TOPLEFT",20,-20) farmbuyer@44: _b:SetScript("OnShow",nil) farmbuyer@44: end) farmbuyer@44: _G.InterfaceOptions_AddCategory(bliz) farmbuyer@44: farmbuyer@49: self:_scan_LOD_modules() farmbuyer@49: farmbuyer@1: if self.debug.flow then self:Print"is in control-flow debug mode." end farmbuyer@1: end farmbuyer@1: --function addon:OnDisable() end farmbuyer@1: farmbuyer@58: do farmbuyer@58: local prototype = {} farmbuyer@58: local function module_OnEnable (plugin) farmbuyer@58: if plugin.option_defaults then farmbuyer@58: local SVname = 'OuroLoot'..plugin:GetName()..'_opts' farmbuyer@58: if not _G[SVname] then farmbuyer@58: _G[SVname] = {} farmbuyer@58: if type(plugin.OnFirstTime) == 'function' then farmbuyer@58: plugin:OnFirstTime() farmbuyer@58: end farmbuyer@58: end farmbuyer@58: plugin.opts = _G[SVname] farmbuyer@58: for option,default in pairs(plugin.option_defaults) do farmbuyer@58: if plugin.opts[option] == nil then farmbuyer@58: plugin.opts[option] = default farmbuyer@58: end farmbuyer@58: end farmbuyer@58: plugin.option_defaults = nil farmbuyer@58: end farmbuyer@58: end farmbuyer@58: farmbuyer@58: -- By default, no plugins. First plugin to use the special registration farmbuyer@58: -- sets up code for any subsequent plugins. farmbuyer@58: addon.is_plugin = flib.nullfunc farmbuyer@58: local function module_rtg (plugin, text_type, ...) farmbuyer@58: local registry = { [text_type]=plugin } farmbuyer@58: addon.is_plugin = function(a,t) return registry[t] end farmbuyer@58: prototype.register_text_generator = function(p,t,...) farmbuyer@58: registry[t] = p farmbuyer@58: return addon:register_text_generator(t,...) farmbuyer@58: end farmbuyer@58: return addon:register_text_generator(text_type,...) farmbuyer@58: end farmbuyer@58: farmbuyer@58: prototype.OnEnable = module_OnEnable farmbuyer@58: prototype.default_OnEnable = module_OnEnable farmbuyer@58: prototype.register_text_generator = module_rtg farmbuyer@58: farmbuyer@58: addon:SetDefaultModuleLibraries("AceConsole-3.0") farmbuyer@58: addon:SetDefaultModulePrototype(prototype) farmbuyer@58: -- Fires before the plugin's own OnEnable (inherited or otherwise). farmbuyer@58: --function addon:OnModuleCreated (plugin) farmbuyer@58: -- print("created plugin", plugin:GetName()) farmbuyer@58: --end farmbuyer@63: farmbuyer@63: local olrev = tonumber("@project-revision@") or 0 farmbuyer@64: local err = [[Module '%s' cannot register itself because it failed a required condition: '%s']] farmbuyer@63: function addon:ConstrainedNewModule (modname, minrev, mincomm, mindata) farmbuyer@63: if not addon.author_debug then farmbuyer@64: if minrev and minrev > olrev then farmbuyer@63: self:Print(err,modname, farmbuyer@63: "revision "..olrev.." older than minimum "..minrev) farmbuyer@63: return false farmbuyer@63: end farmbuyer@64: if mincomm and mincomm > tonumber(self.commrev) then farmbuyer@63: self:Print(err,modname, farmbuyer@63: "commrev "..self.commrev.." older than minimum "..mincomm) farmbuyer@63: return false farmbuyer@63: end farmbuyer@64: if mindata and mindata > opts.datarev then farmbuyer@63: self:Print(err,modname, farmbuyer@63: "datarev "..opts.datarev.." older than minimum "..mindata) farmbuyer@63: return false farmbuyer@63: end farmbuyer@63: end farmbuyer@63: return self:NewModule(modname) farmbuyer@63: end farmbuyer@58: end farmbuyer@58: farmbuyer@1: farmbuyer@1: ------ Event handlers farmbuyer@1: function addon:_clear_SVs() farmbuyer@1: g_loot = {} -- not saved, just fooling PLAYER_LOGOUT tests farmbuyer@1: OuroLootSV = nil farmbuyer@16: OuroLootSV_saved = nil farmbuyer@1: OuroLootSV_opts = nil farmbuyer@1: OuroLootSV_hist = nil farmbuyer@22: OuroLootSV_log = nil farmbuyer@8: ReloadUI() farmbuyer@1: end farmbuyer@1: function addon:PLAYER_LOGOUT() farmbuyer@16: self:UnregisterEvent("RAID_ROSTER_UPDATE") farmbuyer@16: self:UnregisterEvent("PLAYER_ENTERING_WORLD") farmbuyer@16: farmbuyer@16: 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@1: for i,e in ipairs(g_loot) do farmbuyer@1: e.cols = nil farmbuyer@1: end farmbuyer@1: OuroLootSV = g_loot farmbuyer@16: else farmbuyer@16: 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@38: OuroLootSV_hist = self.history_all farmbuyer@38: OuroLootSV_hist.HISTFORMAT = 2 farmbuyer@38: else farmbuyer@38: OuroLootSV_hist = nil farmbuyer@38: end farmbuyer@19: OuroLootSV_log = #OuroLootSV_log > 0 and OuroLootSV_log or nil farmbuyer@1: end farmbuyer@1: farmbuyer@10: do farmbuyer@10: local IsInInstance, UnitName, UnitIsConnected, UnitClass, UnitRace, UnitSex, farmbuyer@56: UnitLevel, UnitInRaid, UnitIsVisible, GetGuildInfo, GetRaidRosterInfo = farmbuyer@10: IsInInstance, UnitName, UnitIsConnected, UnitClass, UnitRace, UnitSex, farmbuyer@56: UnitLevel, UnitInRaid, UnitIsVisible, GetGuildInfo, GetRaidRosterInfo farmbuyer@10: local time, difftime = time, difftime farmbuyer@10: local R_ACTIVE, R_OFFLINE, R_LEFT = 1, 2, 3 farmbuyer@10: farmbuyer@10: local lastevent, now = 0, 0 farmbuyer@16: local redo_count = 0 farmbuyer@16: local redo, timer_handle farmbuyer@10: farmbuyer@10: function addon:CheckRoster (leaving_p, now_a) farmbuyer@10: if not g_loot.raiders then return end -- bad transition farmbuyer@10: farmbuyer@10: now = now_a or time() farmbuyer@10: farmbuyer@10: if leaving_p then farmbuyer@16: if timer_handle then farmbuyer@16: self:CancelTimer(timer_handle) farmbuyer@16: timer_handle = nil farmbuyer@16: end farmbuyer@10: for name,r in pairs(g_loot.raiders) do farmbuyer@10: r.leave = r.leave or now farmbuyer@10: end farmbuyer@10: return farmbuyer@10: end farmbuyer@10: farmbuyer@10: for name,r in pairs(g_loot.raiders) do farmbuyer@10: if r.online ~= R_LEFT and not UnitInRaid(name) then farmbuyer@10: r.online = R_LEFT farmbuyer@10: r.leave = now farmbuyer@10: end farmbuyer@10: end farmbuyer@10: farmbuyer@16: if redo then farmbuyer@16: redo_count = redo_count + 1 farmbuyer@16: end farmbuyer@16: redo = false farmbuyer@10: for i = 1, GetNumRaidMembers() do farmbuyer@10: local unit = 'raid'..i farmbuyer@10: local name = UnitName(unit) farmbuyer@10: -- No, that's not my typo, it really is "uknownbeing" in Blizzard's code. farmbuyer@10: if name and name ~= UNKNOWN and name ~= UNKNOWNOBJECT and name ~= UKNOWNBEING then farmbuyer@10: if not g_loot.raiders[name] then farmbuyer@10: g_loot.raiders[name] = { needinfo=true } farmbuyer@10: end farmbuyer@10: local r = g_loot.raiders[name] farmbuyer@56: -- We grab a bunch of return values here, but only pay attention to farmbuyer@56: -- them under specific circumstances. farmbuyer@56: local grri_name, connected, subgroup, level, class, _ farmbuyer@56: grri_name, _, subgroup, level, _, class, connected = GetRaidRosterInfo(i) farmbuyer@61: if name ~= grri_name then farmbuyer@61: error("UnitName ("..tostring(name)..") =/= grri_name (".. farmbuyer@61: tostring(grri_name)..") of same raidindex ("..i..")") farmbuyer@61: end farmbuyer@56: r.subgroup = subgroup farmbuyer@10: if r.needinfo and UnitIsVisible(unit) then farmbuyer@10: r.needinfo = nil farmbuyer@56: r.class = class --select(2,UnitClass(unit)) farmbuyer@10: r.race = select(2,UnitRace(unit)) farmbuyer@10: r.sex = UnitSex(unit) farmbuyer@56: r.level = level --UnitLevel(unit) farmbuyer@10: r.guild = GetGuildInfo(unit) farmbuyer@10: end farmbuyer@56: --local connected = UnitIsConnected(unit) farmbuyer@10: if connected and r.online ~= R_ACTIVE then farmbuyer@10: r.join = r.join or now farmbuyer@10: r.online = R_ACTIVE farmbuyer@10: elseif (not connected) and r.online ~= R_OFFLINE then farmbuyer@10: r.leave = now farmbuyer@10: r.online = R_OFFLINE farmbuyer@10: end farmbuyer@10: redo = redo or r.needinfo farmbuyer@10: end farmbuyer@10: end farmbuyer@16: if redo then -- XXX test redo_count here and eventually give up? farmbuyer@16: if not timer_handle then farmbuyer@16: timer_handle = self:ScheduleRepeatingTimer("RAID_ROSTER_UPDATE", 60) farmbuyer@16: end farmbuyer@16: else farmbuyer@16: redo_count = 0 farmbuyer@16: if timer_handle then farmbuyer@16: self:CancelTimer(timer_handle) farmbuyer@16: timer_handle = nil farmbuyer@16: end farmbuyer@10: end farmbuyer@10: end farmbuyer@10: farmbuyer@10: function addon:RAID_ROSTER_UPDATE (event) farmbuyer@10: if GetNumRaidMembers() == 0 then farmbuyer@16: -- Leaving a raid group. farmbuyer@10: -- Because of PLAYER_ENTERING_WORLD, this code also executes on load farmbuyer@10: -- screens while soloing and in regular groups. Take care. farmbuyer@16: self.dprint('flow', "GetNumRaidMembers == 0") farmbuyer@16: if self.enabled and not self.debug.notraid then farmbuyer@16: self.dprint('flow', "enabled, leaving raid") farmbuyer@10: self.popped = nil farmbuyer@16: self:Deactivate() -- self:UnregisterEvent("CHAT_MSG_LOOT") farmbuyer@10: self:CheckRoster(--[[leaving raid]]true) farmbuyer@10: end farmbuyer@10: return farmbuyer@10: end farmbuyer@10: farmbuyer@1: local inside,whatkind = IsInInstance() farmbuyer@1: if inside and (whatkind == "pvp" or whatkind == "arena") then farmbuyer@41: self.dprint('flow', "got RRU event but in pvp zone, bailing") farmbuyer@41: return farmbuyer@1: end farmbuyer@10: farmbuyer@10: local docheck = self.enabled farmbuyer@1: if event == "Activate" then farmbuyer@1: -- dispatched manually from Activate farmbuyer@10: self:RegisterEvent("CHAT_MSG_LOOT") farmbuyer@2: _register_bossmod(self) farmbuyer@10: docheck = true farmbuyer@1: elseif event == "RAID_ROSTER_UPDATE" then farmbuyer@10: -- hot code path, be careful farmbuyer@10: farmbuyer@1: -- event registration from onload, joined a raid, maybe show popup farmbuyer@16: self.dprint('flow', "RRU check:", self.popped, opts.popup_on_join) farmbuyer@10: if (not self.popped) and opts.popup_on_join then farmbuyer@1: self.popped = StaticPopup_Show "OUROL_REMIND" farmbuyer@1: self.popped.data = self farmbuyer@10: return farmbuyer@1: end farmbuyer@1: end farmbuyer@11: -- Throttle the checks fired by common events. farmbuyer@10: if docheck and not InCombatLockdown() then farmbuyer@10: now = time() farmbuyer@10: if difftime(now,lastevent) > 45 then farmbuyer@10: lastevent = now farmbuyer@10: self:CheckRoster(false,now) farmbuyer@10: end farmbuyer@10: end farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@1: -- helper for CHAT_MSG_LOOT handler farmbuyer@1: do farmbuyer@42: local function maybe_trash_kill_entry() farmbuyer@42: -- this is set on various boss interactions, so we've got a kill/wipe farmbuyer@42: -- entry already farmbuyer@42: if addon.latest_instance then return end farmbuyer@42: addon.latest_instance = instance_tag() farmbuyer@61: local ss, max = addon:snapshot_raid() farmbuyer@42: addon:_mark_boss_kill (addon._addLootEntry{ 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@1: -- Recent loot cache farmbuyer@25: local candidates = {} 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@25: addon.dprint('loot', i, "was found") 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@25: if (loot.disposition ~= 'shard') farmbuyer@25: and (loot.disposition ~= 'gvault') farmbuyer@25: and (not addon.history_suppress) farmbuyer@25: then farmbuyer@25: addon:_addHistoryEntry(looti) farmbuyer@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@25: end farmbuyer@40: addon.recent_loot = create_new_cache ('loot', comm_cleanup_ttl+3, prefer_local_loots) farmbuyer@1: farmbuyer@6: local GetItemInfo, GetItemIcon = GetItemInfo, GetItemIcon farmbuyer@1: farmbuyer@1: -- 'from' and onwards only present if this is triggered by a broadcast farmbuyer@1: function addon:_do_loot (local_override, recipient, itemid, count, from, extratext) farmbuyer@6: local itexture = GetItemIcon(itemid) farmbuyer@6: local iname, ilink, iquality = GetItemInfo(itemid) farmbuyer@19: local i farmbuyer@6: if (not iname) or (not itexture) then farmbuyer@19: i = true farmbuyer@6: iname, ilink, iquality, itexture = farmbuyer@6: UNKNOWN..': '..itemid, 'item:6948', ITEM_QUALITY_COMMON, [[ICONS\INV_Misc_QuestionMark]] farmbuyer@6: end farmbuyer@19: self.dprint('loot',">>_do_loot, R:", recipient, "I:", itemid, "C:", count, "frm:", from, "ex:", extratext, "q:", iquality) farmbuyer@1: farmbuyer@19: itemid = tonumber(ilink:match("item:(%d+)") or 0) farmbuyer@39: -- This is only a 'while' to make jumping out of it easy and still do cleanup below. farmbuyer@25: while local_override or ((iquality >= self.threshold) and not opts.itemfilter[itemid]) do farmbuyer@1: if (self.rebroadcast and (not from)) and not local_override then farmbuyer@56: self:vbroadcast('loot', recipient, itemid, count) farmbuyer@1: end farmbuyer@25: if (not self.enabled) and (not local_override) then break end farmbuyer@25: local signature = recipient .. iname .. (count or "") farmbuyer@25: if from and self.recent_loot:test(signature) then farmbuyer@39: self.dprint('cache', "remote loot <",signature,"> already in cache, skipping") farmbuyer@25: else farmbuyer@25: -- There is some redundancy in all this, in the interests of ease-of-coding farmbuyer@25: i = { farmbuyer@25: kind = 'loot', farmbuyer@25: person = recipient, farmbuyer@25: person_class= select(2,UnitClass(recipient)), farmbuyer@25: cache_miss = i and true or nil, farmbuyer@25: quality = iquality, farmbuyer@25: itemname = iname, farmbuyer@25: id = itemid, farmbuyer@25: itemlink = ilink, farmbuyer@25: itexture = itexture, farmbuyer@25: disposition = (recipient == self.sharder) and 'shard' or nil, farmbuyer@38: count = (count and count ~= "") and count or nil, farmbuyer@25: bcast_from = from, farmbuyer@25: extratext = extratext, farmbuyer@66: variant = self:is_variant_item(ilink), farmbuyer@25: } farmbuyer@34: if local_override then farmbuyer@39: -- player is adding loot by hand, don't wait for network cache timeouts farmbuyer@39: -- keep this sync'd with prefer_local_loots above farmbuyer@34: if i.extratext == 'shard' farmbuyer@34: or i.extratext == 'gvault' farmbuyer@34: or i.extratext == 'offspec' farmbuyer@34: then farmbuyer@34: i.disposition = i.extratext farmbuyer@34: end farmbuyer@34: local looti = self._addLootEntry(i) farmbuyer@34: if (i.disposition ~= 'shard') farmbuyer@34: and (i.disposition ~= 'gvault') farmbuyer@34: and (not self.history_suppress) farmbuyer@34: then farmbuyer@34: self:_addHistoryEntry(looti) farmbuyer@34: end farmbuyer@39: i = looti -- return value mostly for gui's manual entry farmbuyer@34: else farmbuyer@34: self.recent_loot:add(signature) farmbuyer@34: candidates[signature] = i farmbuyer@34: tinsert (candidates, signature) farmbuyer@39: self.dprint('cache', "loot <",signature,"> added to cache as candidate", #candidates) farmbuyer@34: end farmbuyer@1: end farmbuyer@25: break farmbuyer@1: end farmbuyer@1: self.dprint('loot',"<<_do_loot out") farmbuyer@1: return i farmbuyer@1: end farmbuyer@1: farmbuyer@1: function addon:CHAT_MSG_LOOT (event, ...) farmbuyer@1: if (not self.rebroadcast) and (not self.enabled) and (event ~= "manual") then return end farmbuyer@1: farmbuyer@1: --[[ farmbuyer@1: iname: Hearthstone farmbuyer@1: iquality: integer farmbuyer@1: ilink: clickable formatted link farmbuyer@1: itemstring: item:6948:.... farmbuyer@1: itexture: inventory icon texture farmbuyer@1: ]] farmbuyer@1: farmbuyer@1: if event == "CHAT_MSG_LOOT" then farmbuyer@1: local msg = ... farmbuyer@20: local person, itemstring, count farmbuyer@21: --ChatFrame2:AddMessage("original string: >"..(msg:gsub("\124","\124\124")).."<") farmbuyer@20: farmbuyer@20: -- test in most likely order: other people get more loot than "you" do farmbuyer@20: person, itemstring, count = msg:match(g_LOOT_ITEM_MULTIPLE_sss) farmbuyer@20: if not person then farmbuyer@20: person, itemstring = msg:match(g_LOOT_ITEM_ss) farmbuyer@20: end farmbuyer@20: if not person then farmbuyer@20: itemstring, count = msg:match(g_LOOT_ITEM_SELF_MULTIPLE_ss) farmbuyer@20: if not itemstring then farmbuyer@20: itemstring = msg:match(g_LOOT_ITEM_SELF_s) farmbuyer@20: end farmbuyer@20: end farmbuyer@20: farmbuyer@65: self.dprint('loot', "CHAT_MSG_LOOT, person is", person, farmbuyer@65: ", itemstring is", itemstring, ", count is", count) farmbuyer@20: if not itemstring then return end -- "So-and-So selected Greed", etc, not actual looting farmbuyer@1: farmbuyer@1: -- Name might be colorized, remove the highlighting farmbuyer@20: if person then farmbuyer@20: person = person:match("|c%x%x%x%x%x%x%x%x(%S+)") or person farmbuyer@20: else farmbuyer@38: person = my_name -- UNIT_YOU / You farmbuyer@20: end farmbuyer@1: farmbuyer@20: --local id = tonumber((select(2, strsplit(":", itemstring)))) farmbuyer@20: local id = tonumber(itemstring:match('|Hitem:(%d+):')) farmbuyer@1: farmbuyer@1: return self:_do_loot (false, person, id, count) farmbuyer@1: farmbuyer@1: elseif event == "broadcast" then farmbuyer@1: return self:_do_loot(false, ...) farmbuyer@1: farmbuyer@1: elseif event == "manual" then farmbuyer@1: local r,i,n = ... farmbuyer@1: return self:_do_loot(true, r,i,nil,nil,n) farmbuyer@1: end farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@1: farmbuyer@1: ------ Slash command handler farmbuyer@1: -- Thought about breaking this up into a table-driven dispatcher. But farmbuyer@1: -- that would result in a pile of teensy functions, most of which would farmbuyer@1: -- never be called. Too much overhead. (2.0: Most of these removed now farmbuyer@1: -- that GUI is in place.) farmbuyer@1: function addon:OnSlash (txt) --, editbox) farmbuyer@1: txt = strtrim(txt:lower()) farmbuyer@1: local cmd, arg = "" farmbuyer@1: do farmbuyer@1: local s,e = txt:find("^%a+") farmbuyer@1: if s then farmbuyer@1: cmd = txt:sub(s,e) farmbuyer@1: s = txt:find("%S", e+2) farmbuyer@1: if s then arg = txt:sub(s,-1) end farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@1: if cmd == "" then farmbuyer@1: if InCombatLockdown() then farmbuyer@65: return self:Print("Shouldn't display window in combat.") farmbuyer@1: else farmbuyer@1: return self:BuildMainDisplay() farmbuyer@1: end farmbuyer@1: farmbuyer@1: elseif cmd:find("^thre") then farmbuyer@1: self:SetThreshold(arg) farmbuyer@1: farmbuyer@1: elseif cmd == "on" then self:Activate(arg) farmbuyer@1: elseif cmd == "off" then self:Deactivate() farmbuyer@1: elseif cmd == "broadcast" or cmd == "bcast" then self:Activate(nil,true) farmbuyer@1: farmbuyer@23: elseif cmd == "toggle" then farmbuyer@23: if self.display then farmbuyer@23: self.display:Hide() farmbuyer@23: else farmbuyer@23: return self:BuildMainDisplay() farmbuyer@23: end farmbuyer@23: farmbuyer@1: elseif cmd == "fake" then -- maybe comment this out for real users farmbuyer@1: self:_mark_boss_kill (self._addLootEntry{ farmbuyer@55: kind='boss',reason='kill',bossname="Baron Steamroller",instance=instance_tag(),duration=0 farmbuyer@1: }) farmbuyer@1: self:CHAT_MSG_LOOT ('manual', my_name, 54797) farmbuyer@1: if self.display then farmbuyer@1: self:redisplay() farmbuyer@1: end farmbuyer@1: self:Print "Baron Steamroller has been slain. Congratulations on your rug." farmbuyer@1: farmbuyer@1: elseif cmd == "debug" then farmbuyer@1: if arg then farmbuyer@1: self.debug[arg] = not self.debug[arg] farmbuyer@1: _G.print(arg,self.debug[arg]) farmbuyer@1: if self.debug[arg] then self.DEBUG_PRINT = true end farmbuyer@1: else farmbuyer@1: self.DEBUG_PRINT = not self.DEBUG_PRINT farmbuyer@1: end farmbuyer@1: farmbuyer@1: elseif cmd == "save" and arg and arg:len() > 0 then farmbuyer@1: self:save_saveas(arg) farmbuyer@1: elseif cmd == "list" then farmbuyer@1: self:save_list() farmbuyer@1: elseif cmd == "restore" and arg and arg:len() > 0 then farmbuyer@1: self:save_restore(tonumber(arg)) farmbuyer@1: elseif cmd == "delete" and arg and arg:len() > 0 then farmbuyer@1: self:save_delete(tonumber(arg)) farmbuyer@1: farmbuyer@1: elseif cmd == "help" then farmbuyer@1: self:BuildMainDisplay('help') farmbuyer@23: elseif cmd == "ping" then farmbuyer@23: self:DoPing() farmbuyer@1: farmbuyer@19: elseif cmd == "fixcache" then farmbuyer@19: self:do_item_cache_fixup() farmbuyer@19: farmbuyer@1: else farmbuyer@1: if self:OpenMainDisplayToTab(cmd) then farmbuyer@1: return farmbuyer@1: end farmbuyer@1: self:Print("Unknown command '%s'. %s to see the help window.", farmbuyer@1: cmd, self.format_hypertext('help',"Click here",ITEM_QUALITY_UNCOMMON)) 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@1: q = math.floor(q+0.001) farmbuyer@1: if q<0 or q>6 then farmbuyer@1: return self:Print("Threshold must be 0-6.") farmbuyer@1: end farmbuyer@1: else farmbuyer@1: q = qualnames[arg] farmbuyer@1: if not q then farmbuyer@1: return self:Print("Unrecognized item quality argument.") farmbuyer@1: end farmbuyer@1: end farmbuyer@1: self.threshold = q farmbuyer@1: if not quiet_p then self:Print("Threshold now set to %s.", self.thresholds[q]) end farmbuyer@1: end farmbuyer@1: farmbuyer@1: farmbuyer@1: ------ On/off farmbuyer@1: function addon:Activate (opt_threshold, opt_bcast_only) farmbuyer@16: self.dprint('flow', ":Activate is running") farmbuyer@10: self:RegisterEvent("RAID_ROSTER_UPDATE") farmbuyer@16: self:RegisterEvent("PLAYER_ENTERING_WORLD", farmbuyer@16: function() self:ScheduleTimer("RAID_ROSTER_UPDATE", 5, "PLAYER_ENTERING_WORLD") end) farmbuyer@1: self.popped = true farmbuyer@1: if GetNumRaidMembers() > 0 then farmbuyer@16: self.dprint('flow', ">:Activate calling RRU") farmbuyer@1: self:RAID_ROSTER_UPDATE("Activate") farmbuyer@1: elseif self.debug.notraid then farmbuyer@16: self.dprint('flow', ">: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@1: self:Print("Ouro Raid Loot restored previous data, but not in a raid", farmbuyer@48: "and 5-player mode not active. |cffff0505NOT tracking loot|r;", farmbuyer@1: "use 'enable' to activate loot tracking, or 'clear' to erase", farmbuyer@1: "previous data, or 'help' to read about saved-texts commands.") farmbuyer@1: return farmbuyer@1: end farmbuyer@1: self.rebroadcast = true -- hardcode to true; this used to be more complicated farmbuyer@1: self.enabled = not opt_bcast_only farmbuyer@1: if opt_threshold then farmbuyer@1: self:SetThreshold (opt_threshold, --[[quiet_p=]]true) farmbuyer@1: end farmbuyer@1: self:Print("Ouro Raid Loot is %s. Threshold currently %s.", farmbuyer@1: self.enabled and "tracking" or "only broadcasting", farmbuyer@1: self.thresholds[self.threshold]) farmbuyer@27: self:broadcast('revcheck',revision_large) farmbuyer@1: end farmbuyer@1: farmbuyer@1: -- Note: running '/loot off' will also avoid the popup reminder when farmbuyer@1: -- joining a raid, but will not change the saved option setting. farmbuyer@1: function addon:Deactivate() farmbuyer@1: self.enabled = false farmbuyer@1: self.rebroadcast = false farmbuyer@10: self:UnregisterEvent("RAID_ROSTER_UPDATE") farmbuyer@10: self:UnregisterEvent("PLAYER_ENTERING_WORLD") farmbuyer@10: self:UnregisterEvent("CHAT_MSG_LOOT") farmbuyer@1: self:Print("Ouro Raid Loot deactivated.") farmbuyer@1: end farmbuyer@1: farmbuyer@1: function addon:Clear(verbose_p) farmbuyer@1: local repopup, st farmbuyer@1: if self.display then farmbuyer@1: -- in the new version, this is likely to always be the case farmbuyer@1: repopup = true farmbuyer@1: st = self.display:GetUserData("eoiST") farmbuyer@1: if not st then farmbuyer@1: self.dprint('flow', "Clear: display visible but eoiST not set??") farmbuyer@1: end farmbuyer@1: self.display:Hide() farmbuyer@1: end farmbuyer@1: g_restore_p = nil farmbuyer@1: OuroLootSV = nil farmbuyer@1: self:_reset_timestamps() farmbuyer@1: if verbose_p then farmbuyer@16: if (OuroLootSV_saved and #OuroLootSV_saved>0) then farmbuyer@16: 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@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@41: -- Putting _log local to here can result in this sequence: farmbuyer@41: -- 1) logging happens, followed by reload or logout/login farmbuyer@41: -- 2) _log points to SV_log farmbuyer@41: -- 3) VARIABLES_LOADED replaces SV_log pointer with restored version farmbuyer@41: -- 4) logging happens to _log table (now with no other references) farmbuyer@41: -- 5) at logout, nothing new has been entered in the table being saved farmbuyer@19: local date = _G.date farmbuyer@19: function addon:log_with_timestamp (msg) farmbuyer@41: 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@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@1: table.sort(temp) farmbuyer@47: wipe(dest) farmbuyer@1: for i = 1, #temp do farmbuyer@1: dest[i] = src[temp[i]] farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@1: function addon.sender_list.sort() farmbuyer@1: sort (addon.sender_list.active, byindex) farmbuyer@47: wipe(temp) farmbuyer@1: addon.sender_list.activeI = #byindex farmbuyer@1: sort (addon.sender_list.names, byindex) farmbuyer@47: wipe(temp) farmbuyer@1: end farmbuyer@1: addon.sender_list.namesI = byindex farmbuyer@1: end farmbuyer@1: farmbuyer@23: function addon:DoPing() farmbuyer@23: self:Print("Give me a ping, Vasili. One ping only, please.") farmbuyer@23: self.sender_list.active = {} farmbuyer@23: self.sender_list.names = {} farmbuyer@23: self:broadcast('ping') farmbuyer@27: self:broadcast('revcheck',revision_large) farmbuyer@27: end farmbuyer@27: farmbuyer@27: function addon:_check_revision (otherrev) farmbuyer@27: self.dprint('comm', "revchecking against", otherrev) farmbuyer@27: otherrev = tonumber(otherrev) farmbuyer@27: if otherrev == revision_large then farmbuyer@27: -- normal case farmbuyer@27: farmbuyer@27: elseif otherrev < revision_large then farmbuyer@27: self.dprint('comm', "ours is newer, notifying") farmbuyer@27: self:broadcast('revcheck',revision_large) farmbuyer@27: farmbuyer@27: else farmbuyer@27: self.dprint('comm', "ours is older, yammering") farmbuyer@27: if newer_warning then farmbuyer@27: self:Print(newer_warning, farmbuyer@29: self.format_hypertext('popupurl',"click here",ITEM_QUALITY_UNCOMMON), farmbuyer@27: self.format_hypertext('doping',"click here",ITEM_QUALITY_UNCOMMON)) 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@28: 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@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@1: -- Called when first loading up, 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@1: g_loot = OuroLootSV farmbuyer@16: self.popped = #g_loot > 0 farmbuyer@1: self.dprint('flow', "restoring", #g_loot, "entries") farmbuyer@16: self:ScheduleTimer("Activate", 12, opts.threshold) farmbuyer@1: -- FIXME printed could be too large if entries were deleted, how much do we care? farmbuyer@16: self.sharder = opts.autoshard farmbuyer@1: else farmbuyer@10: g_loot = { printed = {}, raiders = {} } farmbuyer@1: end farmbuyer@1: farmbuyer@16: self.threshold = opts.threshold or self.threshold -- in the case of restoring but not tracking farmbuyer@1: self:gui_init(g_loot) farmbuyer@16: opts.autoshard = nil farmbuyer@16: opts.threshold = nil farmbuyer@1: farmbuyer@1: if g_restore_p then farmbuyer@1: self:zero_printed_fenceposts() -- g_loot.printed.* = previous/safe values farmbuyer@1: else farmbuyer@1: self:zero_printed_fenceposts(0) -- g_loot.printed.* = 0 farmbuyer@1: end farmbuyer@1: if possible_st then farmbuyer@1: possible_st:SetData(g_loot) farmbuyer@1: end farmbuyer@1: farmbuyer@56: self.status_text = ("%s communicating as ident %s commrev %s"):format(self.revision,self.ident,self.commrev) farmbuyer@1: self:RegisterComm(self.ident) farmbuyer@1: self:RegisterComm(self.identTg, "OnCommReceivedNocache") farmbuyer@1: farmbuyer@1: if self.author_debug then farmbuyer@1: _G.OL = self farmbuyer@1: _G.Oloot = g_loot farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@61: -- Raid roster snapshots farmbuyer@61: do farmbuyer@61: function addon:snapshot_raid (only_inraid_p) farmbuyer@61: local ss = CopyTable(g_loot.raiders) farmbuyer@61: local instance,maxsize = instance_tag() farmbuyer@61: if only_inraid_p then farmbuyer@61: 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@61: 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@1: bossi = addon._addLootEntry(boss) farmbuyer@19: -- addon. farmbuyer@19: bossi = addon._adjustBossOrder (bossi, g_boss_signpost) or bossi farmbuyer@16: g_boss_signpost = nil farmbuyer@42: addon.latest_instance = boss.instance farmbuyer@19: addon.dprint('loot', "added boss entry", bossi) farmbuyer@1: if boss.reason == 'kill' then farmbuyer@1: addon:_mark_boss_kill (bossi) farmbuyer@1: if opts.chatty_on_kill then farmbuyer@55: addon:Print("Registered kill for '%s' in %s!", boss.bossname, boss.instance) farmbuyer@1: end farmbuyer@1: end farmbuyer@47: wipe(candidates) farmbuyer@1: end farmbuyer@1: addon.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@1: if not_from_local and self.recent_boss:test(signature) then farmbuyer@39: self.dprint('cache', "remote boss <",signature,"> already in cache, skipping") farmbuyer@1: else farmbuyer@1: self.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@1: local i,d = 1,g_loot[1] farmbuyer@1: while d ~= e do farmbuyer@55: if d.bossname and farmbuyer@55: d.bossname == e.bossname and farmbuyer@53: d.instance == e.instance and farmbuyer@1: d.reason == 'wipe' farmbuyer@1: then farmbuyer@1: first = first or i farmbuyer@1: attempts = attempts + 1 farmbuyer@1: assert(tremove(g_loot,i)==d,"_mark_boss_kill screwed up data badly") farmbuyer@1: else farmbuyer@1: i = i + 1 farmbuyer@1: end farmbuyer@1: d = g_loot[i] farmbuyer@1: end farmbuyer@1: e.reason = 'kill' farmbuyer@1: e.attempts = attempts farmbuyer@1: self.loot_clean = first or index-1 farmbuyer@1: end farmbuyer@1: farmbuyer@2: function addon:register_boss_mod (name, registration_func, deregistration_func) farmbuyer@2: assert(type(name)=='string') farmbuyer@2: assert(type(registration_func)=='function') farmbuyer@2: if deregistration_func ~= nil then assert(type(deregistration_func)=='function') 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@1: -- This shouldn't be required. /sadface farmbuyer@1: local loot_entry_mt = { farmbuyer@1: __index = function (e,key) farmbuyer@1: if key == 'cols' then farmbuyer@15: pprint('mt', e.kind, "key is", key) farmbuyer@1: --tabledump(e) -- not actually that useful farmbuyer@1: addon:_fill_out_eoi_data(1) farmbuyer@1: end farmbuyer@1: return rawget(e,key) farmbuyer@1: end farmbuyer@1: } farmbuyer@1: farmbuyer@1: -- Given a loot index, searches backwards for a timestamp. Returns that farmbuyer@1: -- index and the time entry, or nil if it falls off the beginning. Pass an farmbuyer@65: -- optional second index to search no earlier than that. farmbuyer@1: -- May also be able to make good use of this in forum-generation routine. farmbuyer@1: function addon:find_previous_time_entry(i,stop) farmbuyer@17: stop = stop or 0 farmbuyer@1: while i > stop do farmbuyer@1: if g_loot[i].kind == 'time' then farmbuyer@1: return i, g_loot[i] farmbuyer@1: end farmbuyer@1: i = i - 1 farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@1: -- format_timestamp (["format_string"], Day, [Loot]) farmbuyer@1: -- DAY is a loot entry with kind=='time', and controls the date printed. farmbuyer@1: -- LOOT may be any kind of entry in the g_loot table. If present, it farmbuyer@1: -- overrides the hour and minute printed; if absent, those values are farmbuyer@1: -- taken from the DAY entry. farmbuyer@1: -- FORMAT_STRING may contain $x (x in Y/M/D/h/m) tokens. farmbuyer@1: local format_timestamp_values, point2dee = {}, "%.2d" farmbuyer@1: function addon:format_timestamp (fmt_opt, day_entry, time_entry_opt) farmbuyer@1: if not time_entry_opt then farmbuyer@1: if type(fmt_opt) == 'table' then -- Two entries, default format farmbuyer@1: time_entry_opt, day_entry = day_entry, fmt_opt farmbuyer@1: fmt_opt = "$Y/$M/$D $h:$m" farmbuyer@41: --elseif type(fmt_opt) == "string" then -- Day entry only, caller-specified format farmbuyer@1: end farmbuyer@1: end farmbuyer@1: --format_timestamp_values.Y = point2dee:format (day_entry.startday.year % 100) farmbuyer@1: format_timestamp_values.Y = ("%.4d"):format (day_entry.startday.year) farmbuyer@1: format_timestamp_values.M = point2dee:format (day_entry.startday.month) farmbuyer@1: format_timestamp_values.D = point2dee:format (day_entry.startday.day) farmbuyer@1: format_timestamp_values.h = point2dee:format ((time_entry_opt or day_entry).hour) farmbuyer@1: format_timestamp_values.m = point2dee:format ((time_entry_opt or day_entry).minute) farmbuyer@1: return fmt_opt:gsub ('%$([YMDhm])', format_timestamp_values) farmbuyer@1: end farmbuyer@1: farmbuyer@1: local done_todays_date farmbuyer@1: function addon:_reset_timestamps() farmbuyer@1: done_todays_date = nil farmbuyer@1: end farmbuyer@1: local function do_todays_date() farmbuyer@1: local text, M, D, Y = makedate() farmbuyer@1: local found,ts = #g_loot+1 farmbuyer@1: repeat farmbuyer@1: found,ts = addon:find_previous_time_entry(found-1) farmbuyer@1: if found and ts.startday.text == text then farmbuyer@1: done_todays_date = true farmbuyer@1: end farmbuyer@1: until done_todays_date or (not found) farmbuyer@1: if done_todays_date then farmbuyer@1: g_today = ts farmbuyer@1: else farmbuyer@1: done_todays_date = true farmbuyer@1: g_today = g_loot[addon._addLootEntry{ farmbuyer@1: kind = 'time', farmbuyer@1: startday = { farmbuyer@1: text = text, month = M, day = D, year = Y farmbuyer@1: } farmbuyer@1: }] farmbuyer@1: end farmbuyer@1: addon:_fill_out_eoi_data(1) farmbuyer@1: end farmbuyer@1: farmbuyer@1: -- Adding anything original to g_loot goes through this routine. farmbuyer@1: function addon._addLootEntry (e) farmbuyer@1: setmetatable(e,loot_entry_mt) farmbuyer@1: farmbuyer@1: if not done_todays_date then do_todays_date() end farmbuyer@1: farmbuyer@1: local h, m = GetGameTime() farmbuyer@10: --local localuptime = math.floor(GetTime()) farmbuyer@10: local time_t = time() farmbuyer@1: e.hour = h farmbuyer@1: e.minute = m farmbuyer@10: e.stamp = time_t --localuptime farmbuyer@1: local index = #g_loot + 1 farmbuyer@1: g_loot[index] = e farmbuyer@1: return index farmbuyer@1: end farmbuyer@16: farmbuyer@16: -- Problem: (1) boss kill happens, (2) fast looting happens, (3) boss farmbuyer@16: -- cache cleanup fires. Result: loot shows up before boss kill entry. farmbuyer@16: -- Solution: We need to shuffle the boss entry above any of the loot farmbuyer@16: -- from that boss. farmbuyer@16: function addon._adjustBossOrder (is, should_be) farmbuyer@16: --pprint('loot', is, should_be) farmbuyer@16: if is == should_be then --pprint('loot', "equal, yay") farmbuyer@16: return farmbuyer@16: end farmbuyer@17: if (type(is)~='number') or (type(should_be)~='number') or (is < should_be) then farmbuyer@19: pprint('loot', is, should_be, "...the hell? bailing") farmbuyer@16: return farmbuyer@16: end farmbuyer@16: if g_loot[should_be].kind == 'time' then farmbuyer@16: should_be = should_be + 1 farmbuyer@16: if is == should_be then farmbuyer@16: --pprint('loot', "needed to mark day entry, otherwise equal, yay") farmbuyer@16: return farmbuyer@16: end farmbuyer@16: end farmbuyer@16: farmbuyer@16: assert(g_loot[is].kind == 'boss') farmbuyer@16: local boss = tremove (g_loot, is) farmbuyer@55: --pprint('loot', "MOVING", boss.bossname) farmbuyer@16: tinsert (g_loot, should_be, boss) farmbuyer@16: return should_be farmbuyer@16: end farmbuyer@1: end farmbuyer@1: farmbuyer@19: -- In the rare case of items getting put into the loot table without current farmbuyer@19: -- item cache data (which will have arrived by now). farmbuyer@19: function addon:do_item_cache_fixup() farmbuyer@19: self:Print("Fixing up missing item cache data...") farmbuyer@19: farmbuyer@19: local numfound = 0 farmbuyer@19: local borkedpat = '^'..UNKNOWN..': (%S+)' farmbuyer@19: farmbuyer@19: for i,e in self:filtered_loot_iter('loot') do farmbuyer@19: if e.cache_miss then farmbuyer@19: local borked_id = e.itemname:match(borkedpat) farmbuyer@19: if borked_id then farmbuyer@19: numfound = numfound + 1 farmbuyer@19: -- Best to use the safest and most flexible API here, which is GII and farmbuyer@19: -- its assload of return values. farmbuyer@19: local iname, ilink, iquality, _,_,_,_,_,_, itexture = GetItemInfo(borked_id) farmbuyer@19: if iname then farmbuyer@19: self:Print(" Entry %d patched up with %s.", i, ilink) farmbuyer@19: e.quality = iquality farmbuyer@19: e.itemname = iname farmbuyer@19: e.id = tonumber(ilink:match("item:(%d+)")) farmbuyer@19: e.itemlink = ilink farmbuyer@19: e.itexture = itexture farmbuyer@19: e.cache_miss = nil farmbuyer@19: end farmbuyer@19: end farmbuyer@19: end farmbuyer@19: end farmbuyer@19: farmbuyer@19: self:Print("...finished. Found %d |4entry:entries; with weird data.", numfound) farmbuyer@19: end farmbuyer@19: farmbuyer@1: farmbuyer@1: ------ Saved texts farmbuyer@1: function addon:check_saved_table(silent_p) farmbuyer@16: local s = OuroLootSV_saved farmbuyer@1: if s and (#s > 0) then return s end farmbuyer@16: OuroLootSV_saved = nil farmbuyer@1: if not silent_p then self:Print("There are no saved loot texts.") end farmbuyer@1: end farmbuyer@1: farmbuyer@1: function addon:save_list() farmbuyer@1: local s = self:check_saved_table(); if not s then return end; farmbuyer@1: for i,t in ipairs(s) do farmbuyer@1: self:Print("#%d %s %d entries %s", i, t.date, t.count, t.name) farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@1: function addon:save_saveas(name) farmbuyer@16: OuroLootSV_saved = OuroLootSV_saved or {} farmbuyer@16: 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@1: local s = self:check_saved_table(); if not s then return end; farmbuyer@1: if (not num) or (num > #s) then farmbuyer@1: return self:Print("Saved text number must be 1 - "..#s) farmbuyer@1: end farmbuyer@1: local save = s[num] farmbuyer@1: self:Print("Overwriting current loot data with saved text #%d '%s'", num, save.name) farmbuyer@1: self:Clear(--[[verbose_p=]]false) farmbuyer@1: -- Clear will already have displayed the window, and re-selected the first farmbuyer@1: -- tab. Set these up for when the text tabs are clicked. farmbuyer@10: for text in self:registered_textgen_iter() do farmbuyer@10: g_loot[text] = save[text] farmbuyer@10: end farmbuyer@1: end farmbuyer@1: farmbuyer@1: function addon:save_delete(num) farmbuyer@1: local s = self:check_saved_table(); if not s then return end; farmbuyer@1: if (not num) or (num > #s) then farmbuyer@1: return self:Print("Saved text number must be 1 - "..#s) farmbuyer@1: end farmbuyer@1: self:Print("Deleting saved text #"..num) farmbuyer@1: tremove(s,num) farmbuyer@1: return self:save_list() farmbuyer@1: end farmbuyer@1: farmbuyer@1: farmbuyer@1: ------ Loot histories farmbuyer@1: -- history_all = { farmbuyer@1: -- ["Kilrogg"] = { farmbuyer@1: -- ["realm"] = "Kilrogg", -- not saved farmbuyer@1: -- ["st"] = { lib-st display table }, -- not saved farmbuyer@1: -- ["byname"] = { -- not saved farmbuyer@1: -- ["OtherPlayer"] = 2, farmbuyer@1: -- ["Farmbuyer"] = 1, farmbuyer@1: -- } farmbuyer@1: -- [1] = { farmbuyer@1: -- ["name"] = "Farmbuyer", farmbuyer@1: -- [1] = { id = nnnnn, when = "formatted timestamp for displaying" } -- most recent loot farmbuyer@1: -- [2] = { ......., [count = "x3"] } -- previous loot farmbuyer@1: -- }, farmbuyer@1: -- [2] = { farmbuyer@1: -- ["name"] = "OtherPlayer", farmbuyer@1: -- ...... farmbuyer@1: -- }, ...... farmbuyer@1: -- }, farmbuyer@1: -- ["OtherRealm"] = ...... farmbuyer@1: -- } farmbuyer@1: do farmbuyer@6: local tsort = table.sort farmbuyer@6: local comp = function(L,R) return L.when > R.when 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@1: hist.byname = m farmbuyer@1: end farmbuyer@1: farmbuyer@1: -- Prepares and returns table to be used as self.history. farmbuyer@1: function addon:_prep_new_history_category (prev_table, realmname) farmbuyer@1: local t = prev_table or { farmbuyer@1: --kind = 'realm', farmbuyer@6: --realm = realmname, farmbuyer@1: } farmbuyer@6: t.realm = realmname farmbuyer@1: farmbuyer@1: --[[ farmbuyer@1: t.cols = setmetatable({ farmbuyer@1: { value = realmname }, farmbuyer@1: }, self.time_column1_used_mt) farmbuyer@1: ]] farmbuyer@1: farmbuyer@1: if not t.byname then farmbuyer@1: self:_build_history_names (t) farmbuyer@1: end farmbuyer@1: farmbuyer@1: return t farmbuyer@1: end farmbuyer@1: farmbuyer@4: -- Maps a name to an array index, creating new tables if needed. Returns farmbuyer@6: -- the index and the table at that index. farmbuyer@4: function addon:get_loot_history (name) farmbuyer@4: local i farmbuyer@4: i = self.history.byname[name] farmbuyer@4: if not i then farmbuyer@4: i = #self.history + 1 farmbuyer@4: self.history[i] = { name=name } farmbuyer@4: self.history.byname[name] = i farmbuyer@4: end farmbuyer@6: return i, self.history[i] farmbuyer@4: end farmbuyer@4: farmbuyer@1: function addon:_addHistoryEntry (lootindex) farmbuyer@1: local e = g_loot[lootindex] farmbuyer@6: if e.kind ~= 'loot' then return end farmbuyer@6: farmbuyer@6: local i,h = self:get_loot_history(e.person) farmbuyer@25: -- If any of these change, update the end of history_handle_disposition. farmbuyer@1: local n = { farmbuyer@1: id = e.id, farmbuyer@1: when = self:format_timestamp (g_today, e), farmbuyer@1: count = e.count, farmbuyer@1: } farmbuyer@6: tinsert (h, 1, n) farmbuyer@24: e.history_unique = n.id .. ' ' .. n.when 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@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@6: tsort (h, comp) farmbuyer@6: end farmbuyer@6: end farmbuyer@6: farmbuyer@24: -- Clears all but latest entry for each player. farmbuyer@6: function addon:preen_history (realmname) farmbuyer@6: local r = assert(realmname) farmbuyer@6: for i,h in ipairs(self.history) do farmbuyer@6: tsort (h, comp) farmbuyer@6: while #h > 1 do farmbuyer@6: tremove (h) farmbuyer@6: end farmbuyer@6: end farmbuyer@1: end farmbuyer@24: farmbuyer@25: -- Given an entry in a g_loot table, looks up the corresponding history farmbuyer@25: -- entry. Returns the player's index and history table (as in get_loot_history) farmbuyer@25: -- and the index into that table of the loot entry. On failure, returns nil farmbuyer@25: -- and an error message ready to be formatted with the loot's name/itemlink. farmbuyer@25: function addon:_history_by_loot_id (loot, operation_text) farmbuyer@25: -- Using assert() here would be concatenating error strings that probably farmbuyer@25: -- wouldn't be used. Do more verbose testing instead. farmbuyer@25: if type(loot) ~= 'table' then farmbuyer@25: error("trying to "..operation_text.." nonexistant entry") farmbuyer@25: end farmbuyer@25: if loot.kind ~= 'loot' then farmbuyer@25: error("trying to "..operation_text.." something that isn't loot") farmbuyer@25: end farmbuyer@24: farmbuyer@25: local player = loot.person farmbuyer@25: local tag = loot.history_unique farmbuyer@24: local errtxt farmbuyer@25: local player_i, player_h, hist_i farmbuyer@24: farmbuyer@24: if not tag then farmbuyer@24: errtxt = "Entry for %s is missing a history tag!" farmbuyer@24: else farmbuyer@25: player_i,player_h = self:get_loot_history(player) farmbuyer@25: for i,h in ipairs(player_h) do farmbuyer@24: local unique = h.id .. ' ' .. h.when farmbuyer@24: if unique == tag then farmbuyer@25: hist_i = i farmbuyer@24: break farmbuyer@24: end farmbuyer@24: end farmbuyer@25: if not hist_i then farmbuyer@24: -- 1) loot an item, 2) clear old history, 3) reassign from current loot farmbuyer@24: -- Bah. Anybody that tricky is already recoding the tables directly anyhow. farmbuyer@24: errtxt = "There is no record of %s ever having been assigned!" farmbuyer@24: end farmbuyer@24: end farmbuyer@24: farmbuyer@24: if errtxt then farmbuyer@25: return nil, errtxt farmbuyer@24: end farmbuyer@25: return player_i, player_h, hist_i farmbuyer@25: end farmbuyer@24: farmbuyer@25: function addon:reassign_loot (index, to_name) farmbuyer@25: assert(type(to_name)=='string' and to_name:len()>0) farmbuyer@25: local e = g_loot[index] farmbuyer@25: local from_i, from_h, hist_i = self:_history_by_loot_id (e, "reassign") farmbuyer@25: local from_name = e.person farmbuyer@25: local to_i,to_h = self:get_loot_history(to_name) farmbuyer@25: farmbuyer@25: if not from_i then farmbuyer@25: -- from_h is the formatted error text farmbuyer@25: self:Print(from_h .. " Loot will be reassigned, but history will NOT be updated.", e.itemlink) farmbuyer@25: else farmbuyer@25: local hist_h = tremove (from_h, hist_i) farmbuyer@25: tinsert (to_h, 1, hist_h) farmbuyer@25: tsort (from_h, comp) farmbuyer@25: tsort (to_h, comp) farmbuyer@25: end farmbuyer@25: e.person = to_name farmbuyer@25: e.person_class = select(2,UnitClass(to_name)) farmbuyer@25: self.hist_clean = nil farmbuyer@25: farmbuyer@25: self:Print("Reassigned entry %d/%s from '%s' to '%s'.", index, e.itemlink, from_name, to_name) farmbuyer@25: end farmbuyer@25: farmbuyer@36: -- Similar to _addHistoryEntry. The second arg may be a loot entry farmbuyer@36: -- (which used to be at LOOTINDEX), or nil (and the loot entry will farmbuyer@36: -- be pulled from LOOTINDEX instead). farmbuyer@36: function addon:_delHistoryEntry (lootindex, opt_e) farmbuyer@36: local e = opt_e or g_loot[lootindex] farmbuyer@36: if e.kind ~= 'loot' then return end farmbuyer@36: farmbuyer@36: local from_i, from_h, hist_i = self:_history_by_loot_id (e, "delete") farmbuyer@36: if not from_i then farmbuyer@36: -- from_h is the formatted error text farmbuyer@36: self:Print(from_h .. " Loot will be deleted, but history will NOT be updated.", e.itemlink) farmbuyer@36: return farmbuyer@36: end farmbuyer@36: farmbuyer@36: --[[local hist_h = ]]tremove (from_h, hist_i) farmbuyer@36: tsort (from_h, comp) farmbuyer@36: self.hist_clean = nil farmbuyer@36: farmbuyer@36: self:Print("Removed history entry %d/%s from '%s'.", lootindex, e.itemlink, e.person) farmbuyer@36: end farmbuyer@36: farmbuyer@25: -- Any extra work for the "Mark as " dropdown actions. The farmbuyer@25: -- corresponding will already have been assigned in the loot entry. farmbuyer@25: local deleted_cache = {} --setmetatable({}, {__mode='k'}) farmbuyer@25: function addon:history_handle_disposition (index, olddisp) farmbuyer@25: local e = g_loot[index] farmbuyer@25: -- Standard disposition has a nil entry, but that's tedious in debug farmbuyer@25: -- output, so force to a string instead. farmbuyer@25: olddisp = olddisp or 'normal' farmbuyer@25: local newdisp = e.disposition or 'normal' farmbuyer@25: -- Ignore misclicks and the like farmbuyer@25: if olddisp == newdisp then return end farmbuyer@25: farmbuyer@25: local name = e.person farmbuyer@25: farmbuyer@25: if (newdisp == 'shard' or newdisp == 'gvault') then farmbuyer@25: local name_i, name_h, hist_i = self:_history_by_loot_id (e, "mark") farmbuyer@25: -- remove history entry farmbuyer@25: if hist_i then farmbuyer@25: local hist_h = tremove (name_h, hist_i) farmbuyer@25: deleted_cache[e.history_unique] = hist_h farmbuyer@25: self.hist_clean = nil farmbuyer@25: elseif (olddisp == 'shard' or olddisp == 'gvault') then farmbuyer@25: -- Sharding a vault item, or giving the auto-sharder something to bank, farmbuyer@25: -- etc, wouldn't necessarily have had a history entry to begin with. farmbuyer@25: else farmbuyer@25: self:Print(name_h .. " Loot has been marked, but history will NOT be updated.", e.itemlink) farmbuyer@25: end farmbuyer@25: return farmbuyer@25: end farmbuyer@25: farmbuyer@25: if (olddisp == 'shard' or olddisp == 'gvault') farmbuyer@25: and (newdisp == 'normal' or newdisp == 'offspec') farmbuyer@25: then farmbuyer@25: local name_i, name_h = self:get_loot_history(name) farmbuyer@25: farmbuyer@25: -- Must create a new history entry. Could call '_addHistoryEntry(index)' farmbuyer@25: -- but that would duplicate a lot of effort. To start with, check the farmbuyer@25: -- cache of stuff we've already deleted; if it's not there then just do farmbuyer@25: -- the same steps as _addHistoryEntry. farmbuyer@25: local entry farmbuyer@25: if e.history_unique and deleted_cache[e.history_unique] then farmbuyer@25: entry = deleted_cache[e.history_unique] farmbuyer@25: deleted_cache[e.history_unique] = nil farmbuyer@25: end farmbuyer@25: local when = g_today and self:format_timestamp (g_today, e) or tostring(e.stamp) farmbuyer@25: entry = entry or { farmbuyer@25: id = e.id, farmbuyer@25: when = when, farmbuyer@25: count = e.count, farmbuyer@25: } farmbuyer@25: tinsert (name_h, 1, entry) farmbuyer@25: e.history_unique = e.history_unique or (entry.id .. ' ' .. entry.when) farmbuyer@25: self.hist_clean = nil farmbuyer@25: return farmbuyer@25: end farmbuyer@24: end farmbuyer@1: end farmbuyer@1: farmbuyer@1: farmbuyer@1: ------ Player communication farmbuyer@1: do farmbuyer@56: local select, tconcat, strsplit = select, table.concat, strsplit farmbuyer@56: --[[ old way: repeated string concatenations, BAD farmbuyer@56: new way: new table on every call, BAD farmbuyer@56: local msg = ... farmbuyer@56: for i = 2, select('#',...) do farmbuyer@56: msg = msg .. '\a' .. (select(i,...) or "") farmbuyer@56: end farmbuyer@56: return msg farmbuyer@56: ]] farmbuyer@56: local function assemble(t,...) farmbuyer@56: if select('#',...) > 0 then farmbuyer@56: local msg = {t,...} farmbuyer@56: -- tconcat requires strings, but T is known to be one already farmbuyer@56: for i = 2, #msg do farmbuyer@56: msg[i] = tostring(msg[i]) or "" farmbuyer@56: end farmbuyer@56: return tconcat (msg, '\a') farmbuyer@56: end farmbuyer@56: return t farmbuyer@56: end farmbuyer@56: farmbuyer@56: -- broadcast('tag', ) farmbuyer@56: -- vbroadcast('tag', ) farmbuyer@56: function addon:vbroadcast(tag,...) farmbuyer@56: return self:broadcast(self.commrev..tag,...) farmbuyer@56: end farmbuyer@56: function addon:broadcast(tag,...) farmbuyer@56: local msg = assemble(tag,...) farmbuyer@56: self.dprint('comm', ":", msg) farmbuyer@56: -- the "GUILD" here is just so that we can also pick up on it farmbuyer@56: self:SendCommMessage(self.ident, msg, self.debug.comm and "GUILD" or "RAID") farmbuyer@56: end farmbuyer@56: -- whispercast(, 'tag', ) farmbuyer@56: function addon:whispercast(to,...) farmbuyer@56: local msg = assemble(...) farmbuyer@56: self.dprint('comm', "@", to, ":", msg) farmbuyer@56: self:SendCommMessage(self.identTg, msg, "WHISPER", to) farmbuyer@56: end farmbuyer@56: farmbuyer@1: local function adduser (name, status, active) farmbuyer@1: if status then addon.sender_list.names[name] = status end farmbuyer@1: if active then addon.sender_list.active[name] = active end farmbuyer@1: end farmbuyer@1: farmbuyer@1: -- Incoming handler functions. All take the sender name and the incoming farmbuyer@1: -- tag as the first two arguments. All of these are active even when the farmbuyer@1: -- player is not tracking loot, so test for that when appropriate. farmbuyer@1: local OCR_funcs = {} farmbuyer@1: farmbuyer@1: OCR_funcs.ping = function (sender) farmbuyer@1: pprint('comm', "incoming ping from", sender) farmbuyer@1: addon:whispercast (sender, 'pong', addon.revision, farmbuyer@1: addon.enabled and "tracking" or (addon.rebroadcast and "broadcasting" or "disabled")) farmbuyer@1: end farmbuyer@1: OCR_funcs.pong = function (sender, _, rev, status) farmbuyer@17: local s = ("|cff00ff00%s|r %s is |cff00ffff%s|r"):format(sender,rev,status) farmbuyer@1: addon:Print("Echo: ", s) farmbuyer@1: adduser (sender, s, status=="tracking" or status=="broadcasting" or nil) farmbuyer@1: end farmbuyer@27: OCR_funcs.revcheck = function (sender, _, revlarge) farmbuyer@27: addon.dprint('comm', "revcheck, sender", sender) farmbuyer@27: addon:_check_revision (revlarge) farmbuyer@27: end farmbuyer@1: farmbuyer@1: OCR_funcs.loot = function (sender, _, recip, item, count, extratext) farmbuyer@1: addon.dprint('comm', "DOTloot, sender", sender, "recip", recip, "item", item, "count", count) farmbuyer@1: if not addon.enabled then return end farmbuyer@1: adduser (sender, nil, true) farmbuyer@1: addon:CHAT_MSG_LOOT ("broadcast", recip, item, count, sender, extratext) farmbuyer@1: end farmbuyer@56: OCR_funcs['16loot'] = OCR_funcs.loot 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@56: addon.dprint('comm', "DOTboss16, 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@1: farmbuyer@1: OCR_funcs.bcast_req = function (sender) farmbuyer@1: if addon.debug.comm or ((not g_wafer_thin) and (not addon.rebroadcast)) farmbuyer@1: then farmbuyer@38: addon:Print("%s has requested additional broadcasters! Choose %s to enable rebroadcasting, or %s to remain off and also ignore rebroadcast requests for as long as you're logged in.", farmbuyer@1: sender, farmbuyer@1: addon.format_hypertext('bcaston',"the red pill",'|cffff4040'), farmbuyer@1: addon.format_hypertext('waferthin',"the blue pill",'|cff0070dd')) 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@1: -- Incoming message dispatcher farmbuyer@1: local function dotdotdot (sender, tag, ...) farmbuyer@1: local f = OCR_funcs[tag] farmbuyer@1: addon.dprint('comm', ":... processing",tag,"from",sender) farmbuyer@1: if f then return f(sender,tag,...) end farmbuyer@1: addon.dprint('comm', "unknown comm message",tag",from", sender) farmbuyer@1: end farmbuyer@1: -- Recent message cache farmbuyer@1: addon.recent_messages = create_new_cache ('comm', comm_cleanup_ttl) farmbuyer@1: farmbuyer@1: function addon:OnCommReceived (prefix, msg, distribution, sender) farmbuyer@1: if prefix ~= self.ident then return end farmbuyer@1: if not self.debug.comm then farmbuyer@1: if distribution ~= "RAID" and distribution ~= "WHISPER" then return end farmbuyer@1: if sender == my_name then return end farmbuyer@1: end farmbuyer@1: self.dprint('comm', ":OCR from", sender, "message is", msg) farmbuyer@1: farmbuyer@1: if self.recent_messages:test(msg) then farmbuyer@40: self.dprint('cache', "OCR message <",msg,"> already in cache, skipping") farmbuyer@40: return farmbuyer@1: end farmbuyer@1: self.recent_messages:add(msg) farmbuyer@1: farmbuyer@1: -- Nothing is actually returned, just (ab)using tail calls. farmbuyer@1: return dotdotdot(sender,strsplit('\a',msg)) farmbuyer@1: end farmbuyer@1: farmbuyer@1: function addon:OnCommReceivedNocache (prefix, msg, distribution, sender) farmbuyer@1: if prefix ~= self.identTg then return end farmbuyer@1: if not self.debug.comm then farmbuyer@1: if distribution ~= "WHISPER" then return end farmbuyer@1: if sender == my_name then return end farmbuyer@1: end farmbuyer@1: self.dprint('comm', ":OCRN from", sender, "message is", msg) farmbuyer@1: return dotdotdot(sender,strsplit('\a',msg)) farmbuyer@1: end farmbuyer@1: end farmbuyer@1: farmbuyer@1: -- vim:noet