annotate core.lua @ 101:f7162a1cadc7

Unify remote/local change notifications, and allow option toggles for each. When passing entry tables to the registered callbacks, make proxies for them first (the goal is to prevent accidents, not fraud).
author Farmbuyer of US-Kilrogg <farmbuyer@gmail.com>
date Wed, 01 Aug 2012 06:51:52 +0000
parents a57133ee3c9b
children fe04f5c4114a
rev   line source
farmbuyer@17 1 local nametag, addon = ...
farmbuyer@1 2
farmbuyer@1 3 --[==[
farmbuyer@1 4 g_loot's numeric indices are loot entries (including titles, separators,
farmbuyer@1 5 etc); its named indices are:
farmbuyer@61 6 - forum saved text from forum markup window, default nil
farmbuyer@61 7 - attend saved text from raid attendence window, default nil
farmbuyer@61 8 - printed.FOO last loot index formatted into text window FOO, default 0
farmbuyer@61 9 - raiders accumulating raid roster data as we see raid members; indexed
farmbuyer@57 10 by player name with subtable fields:
farmbuyer@61 11 - class capitalized English codename ("WARRIOR", "DEATHKNIGHT", etc)
farmbuyer@68 12 - subgroup 1-8 (NUM_RAID_GROUPS), NRG+1 if something's wrong
farmbuyer@61 13 - race English codename ("BloodElf", etc)
farmbuyer@61 14 - sex 1 = unknown/error, 2 = male, 3 = female
farmbuyer@61 15 - level can be 0 if player was offline at the time
farmbuyer@61 16 - guild guild name, or missing if unguilded
farmbuyer@61 17 - online 1 = online, 2 = offline, 3 = no longer in raid
farmbuyer@57 18 [both of these next two fields use time_t values:]
farmbuyer@61 19 - join time player joined the raid (or first time we've seen them)
farmbuyer@61 20 - leave time player left the raid (or time we've left the raid, if
farmbuyer@61 21 'online' is not 3)
farmbuyer@50 22
farmbuyer@54 23 Common g_loot entry indices:
farmbuyer@61 24 - kind time/boss/loot
farmbuyer@61 25 - hour 0-23, on the *physical instance server*, not the realm server
farmbuyer@61 26 - minute 0-59, ditto
farmbuyer@61 27 - stamp time_t on the local computer
farmbuyer@61 28 - cols graphical display data; cleared when logging out
farmbuyer@50 29
farmbuyer@50 30 Time specific g_loot indices:
farmbuyer@61 31 - startday table with month/day/year/text fields from makedate()
farmbuyer@57 32 text is always "dd Month yyyy"
farmbuyer@50 33
farmbuyer@50 34 Boss specific g_loot indices:
farmbuyer@61 35 - bossname name of boss/encounter;
farmbuyer@57 36 may be changed if "snarky boss names" option is enabled
farmbuyer@61 37 - reason wipe/kill ("pull" does not generate an entry)
farmbuyer@61 38 - instance name of instance, including size and difficulty
farmbuyer@61 39 - maxsize max raid size: 5/10/25, presumably also 15 and 40 could show
farmbuyer@61 40 up; can be 0 if we're outside an instance and the player
farmbuyer@61 41 inside has an older version
farmbuyer@61 42 - duration in seconds; may be missing (only present if local)
farmbuyer@69 43 - raidersnap copy of g_loot.raiders at the time of the boss event; may be
farmbuyer@69 44 empty for manual snapshots the player didn't want included
farmbuyer@69 45 (not necessarily an "error" if this is missing entirely)
farmbuyer@50 46
farmbuyer@50 47 Loot specific g_loot indices:
farmbuyer@61 48 - person recipient
farmbuyer@61 49 - person_class class of recipient if available; may be missing;
farmbuyer@57 50 will be classID-style (e.g., DEATHKNIGHT)
farmbuyer@61 51 - itemname not including square brackets
farmbuyer@61 52 - id itemID as number
farmbuyer@61 53 - itemlink full clickable link
farmbuyer@61 54 - itexture icon path (e.g., Interface\Icons\INV_Misc_Rune_01)
farmbuyer@61 55 - quality ITEM_QUALITY_* number
farmbuyer@71 56 - unique an almost-certainly-unique string, content meaningless
farmbuyer@61 57 - disposition offspec/gvault/shard; missing otherwise; can be set from
farmbuyer@57 58 the extratext field
farmbuyer@61 59 - count e.g., "x3"; missing otherwise; can be set/removed from
farmbuyer@57 60 extratext; triggers only for a stack of items, not "the boss
farmbuyer@57 61 dropped double axes today"
farmbuyer@66 62 - variant 1 = heroic item, 2 = LFR item; missing otherwise
farmbuyer@61 63 - cache_miss if GetItemInfo failed; SHOULD be missing (changes other fields)
farmbuyer@65 64 - bcast_from player's name if received rebroadcast from that player;
farmbuyer@65 65 missing otherwise; can be deleted as a result of in-game
farmbuyer@65 66 fiddling of loot data
farmbuyer@61 67 - extratext text in Note column, including disposition and rebroadcasting
farmbuyer@61 68 - extratext_byhand true if text edited by player directly; missing otherwise
farmbuyer@50 69
farmbuyer@1 70
farmbuyer@1 71 Functions arranged like this, with these lables (for jumping to). As a
farmbuyer@1 72 rule, member functions with UpperCamelCase names are called directly by
farmbuyer@1 73 user-facing code, ones with lowercase names are "one step removed", and
farmbuyer@1 74 names with leading underscores are strictly internal helper functions.
farmbuyer@1 75 ------ Saved variables
farmbuyer@1 76 ------ Constants
farmbuyer@1 77 ------ Addon member data
farmbuyer@6 78 ------ Globals
farmbuyer@1 79 ------ Expiring caches
farmbuyer@1 80 ------ Ace3 framework stuff
farmbuyer@1 81 ------ Event handlers
farmbuyer@1 82 ------ Slash command handler
farmbuyer@1 83 ------ On/off
farmbuyer@1 84 ------ Behind the scenes routines
farmbuyer@1 85 ------ Saved texts
farmbuyer@1 86 ------ Loot histories
farmbuyer@1 87 ------ Player communication
farmbuyer@1 88
farmbuyer@1 89 This started off as part of a raid addon package written by somebody else.
farmbuyer@1 90 After he retired, I began modifying the code. Eventually I set aside the
farmbuyer@1 91 entire package and rewrote the loot tracker module from scratch. Many of the
farmbuyer@1 92 variable/function naming conventions (sv_*, g_*, and family) stayed across the
farmbuyer@16 93 rewrite.
farmbuyer@16 94
farmbuyer@73 95 Some variables are needlessly initialized to nil just to look uniform and
farmbuyer@76 96 serve as a spelling reminder.
farmbuyer@1 97
farmbuyer@1 98 ]==]
farmbuyer@1 99
farmbuyer@1 100 ------ Saved variables
farmbuyer@16 101 OuroLootSV = nil -- possible copy of g_loot
farmbuyer@16 102 OuroLootSV_saved = nil -- table of copies of saved texts, default nil; keys
farmbuyer@16 103 -- are numeric indices of tables, subkeys of those
farmbuyer@16 104 -- are name/forum/attend/date
farmbuyer@16 105 OuroLootSV_hist = nil
farmbuyer@19 106 OuroLootSV_log = {}
farmbuyer@1 107
farmbuyer@1 108
farmbuyer@1 109 ------ Constants
farmbuyer@90 110 local RAID_ROSTER_UPDATE_EVENT =
farmbuyer@90 111 (select(4,GetBuildInfo()) >= 50000) and 'GROUP_ROSTER_UPDATE' or 'RAID_ROSTER_UPDATE'
farmbuyer@99 112 local option_defaults = { profile = {
farmbuyer@99 113 --['datarev'] = 20, -- cheating, this isn't actually an option
farmbuyer@1 114 ['popup_on_join'] = true,
farmbuyer@94 115 ['register_slash_synonyms'] = false,
farmbuyer@94 116 ['slash_synonyms'] = '/ol,/oloot',
farmbuyer@1 117 ['scroll_to_bottom'] = true,
farmbuyer@83 118 ['gui_noob'] = true,
farmbuyer@1 119 ['chatty_on_kill'] = false,
farmbuyer@1 120 ['no_tracking_wipes'] = false,
farmbuyer@1 121 ['snarky_boss'] = true,
farmbuyer@1 122 ['keybinding'] = false,
farmbuyer@2 123 ['bossmod'] = "DBM",
farmbuyer@1 124 ['keybinding_text'] = 'CTRL-SHIFT-O',
farmbuyer@1 125 ['forum'] = {
farmbuyer@25 126 ['[url] Wowhead'] = '[url=http://www.wowhead.com/?item=$I]$N[/url]$X - $T',
farmbuyer@25 127 ['[url] MMO/Wowstead'] = '[http://db.mmo-champion.com/i/$I]$X - $T',
farmbuyer@1 128 ['[item] by name'] = '[item]$N[/item]$X - $T',
farmbuyer@1 129 ['[item] by ID'] = '[item]$I[/item]$X - $T',
farmbuyer@1 130 ['Custom...'] = '',
farmbuyer@1 131 },
farmbuyer@1 132 ['forum_current'] = '[item] by name',
farmbuyer@57 133 ['display_disabled_LODs'] = false,
farmbuyer@65 134 ['display_bcast_from'] = true,
farmbuyer@73 135 ['precache_history_uniques'] = false,
farmbuyer@73 136 ['chatty_on_remote_changes'] = false,
farmbuyer@101 137 ['chatty_on_local_changes'] = false,
farmbuyer@101 138 ['chatty_on_changes_frame'] = 1,
farmbuyer@97 139 ['itemfilter'] = {},
farmbuyer@97 140 ['itemvault'] = {},
farmbuyer@99 141 } }
farmbuyer@95 142 local virgin = "First time loaded? Hi! Use the /ouroloot command"
farmbuyer@1 143 .." to show the main display. You should probably browse the instructions"
farmbuyer@1 144 .." if you've never used this before; %s to display the help window. This"
farmbuyer@1 145 .." welcome message will not intrude again."
farmbuyer@27 146 local newer_warning = "A newer version has been released. You can %s to display"
farmbuyer@27 147 .." a download URL for copy-and-pasting. You can %s to ping other raiders"
farmbuyer@27 148 .." for their installed versions (same as '/ouroloot ping' or clicking the"
farmbuyer@27 149 .." 'Ping!' button on the options panel)."
farmbuyer@81 150 local horrible_error_text = [[|cffff1010]] .. ERROR_CAPS
farmbuyer@81 151 ..[[:|n|cffffff00Something unrecoverable has happened. The error message]]
farmbuyer@81 152 ..[[ which was provided follows in white:|r|n|n%s|n|n|cffffff00Ouro Loot]]
farmbuyer@81 153 ..[[ will not display a window until this situation is corrected. ]]
farmbuyer@81 154 ..[[ You can try typing|n|cff00ff40/ouroloot fix ?|n]]
farmbuyer@81 155 ..[[|cffffff00to see what can be done by software alone. You may still]]
farmbuyer@81 156 ..[[ need to do a "/reload" afterwards, or even restart the game client.]]
farmbuyer@81 157 local unique_collision = "Item '%s' was carrying unique tag <%s>, but that was already in use; tried to generate a new tag and failed!|n|nRemote sender was '%s', previous cache entry was <%s/%s>.|n|nThis may require a live human to figure out; the loot in question has not been stored."
farmbuyer@99 158 local new_profile_warning = [[Be aware that profiles only store addon & plugin settings from the <Options> tab; loot and generated text is account-wide data, unrelated to your current profile.]]
farmbuyer@86 159 local remote_chatty = "|cff00ff00%s|r changed %d/%s from %s to %s"
farmbuyer@1 160 local qualnames = {
farmbuyer@1 161 ['gray'] = 0, ['grey'] = 0, ['poor'] = 0, ['trash'] = 0,
farmbuyer@1 162 ['white'] = 1, ['common'] = 1,
farmbuyer@1 163 ['green'] = 2, ['uncommon'] = 2,
farmbuyer@1 164 ['blue'] = 3, ['rare'] = 3,
farmbuyer@1 165 ['epic'] = 4, ['purple'] = 4,
farmbuyer@1 166 ['legendary'] = 5, ['orange'] = 5,
farmbuyer@1 167 ['artifact'] = 6,
farmbuyer@1 168 --['heirloom'] = 7,
farmbuyer@1 169 }
farmbuyer@1 170 local my_name = UnitName('player')
farmbuyer@76 171 local comm_cleanup_ttl = 4 -- seconds in the communications cache
farmbuyer@89 172 local version_large = nil -- defaults to 1, possibly changed by version
farmbuyer@20 173 local g_LOOT_ITEM_ss, g_LOOT_ITEM_MULTIPLE_sss, g_LOOT_ITEM_SELF_s, g_LOOT_ITEM_SELF_MULTIPLE_ss
farmbuyer@1 174
farmbuyer@1 175
farmbuyer@1 176 ------ Addon member data
farmbuyer@1 177 local flib = LibStub("LibFarmbuyer")
farmbuyer@1 178 addon.author_debug = flib.author_debug
farmbuyer@1 179
farmbuyer@6 180 -- Play cute games with namespaces here just to save typing. WTB Lua 5.2 PST.
farmbuyer@1 181 do local _G = _G setfenv (1, addon)
farmbuyer@1 182
farmbuyer@71 183 commrev = '17'
farmbuyer@89 184 version = _G.GetAddOnMetadata(nametag,"Version") or "?" -- "x.yy.z", etc
farmbuyer@1 185 ident = "OuroLoot2"
farmbuyer@1 186 identTg = "OuroLoot2Tg"
farmbuyer@1 187 status_text = nil
farmbuyer@89 188 revision = "@project-revision@"
farmbuyer@89 189 --@debug@
farmbuyer@89 190 revision = "DEVEL"
farmbuyer@89 191 --@end-debug@
farmbuyer@1 192
farmbuyer@45 193 tekdebug = nil
farmbuyer@45 194 if _G.tekDebug then
farmbuyer@45 195 local tdframe = _G.tekDebug:GetFrame("Ouro Loot")
farmbuyer@45 196 function tekdebug (txt)
farmbuyer@45 197 -- tekDebug notices "<name passed to getframe>|r:"
farmbuyer@45 198 tdframe:AddMessage('|cff17ff0dOuro Loot|r:'..txt,1,1,1)
farmbuyer@45 199 end
farmbuyer@45 200 end
farmbuyer@45 201
farmbuyer@1 202 DEBUG_PRINT = false
farmbuyer@1 203 debug = {
farmbuyer@76 204 comm = false,
farmbuyer@76 205 loot = false,
farmbuyer@76 206 flow = false,
farmbuyer@76 207 notraid = false,
farmbuyer@76 208 cache = false,
farmbuyer@100 209 callback = false,
farmbuyer@76 210 alsolog = false,
farmbuyer@1 211 }
farmbuyer@77 212 --@debug@
farmbuyer@77 213 DEBUG_PRINT = true
farmbuyer@77 214 debug.loot = true
farmbuyer@77 215 debug.comm = true
farmbuyer@100 216 is_guilded = _G.IsInGuild()
farmbuyer@77 217 --@end-debug@
farmbuyer@77 218
farmbuyer@45 219 -- This looks ugly, but it factors out the load-time decisions from
farmbuyer@73 220 -- the run-time ones. Args to [dp]print are concatenated with spaces.
farmbuyer@45 221 if tekdebug then
farmbuyer@45 222 function dprint (t,...)
farmbuyer@45 223 if DEBUG_PRINT and debug[t] then
farmbuyer@45 224 local text = flib.safefprint(tekdebug,"<"..t.."> ",...)
farmbuyer@45 225 if debug.alsolog then
farmbuyer@45 226 addon:log_with_timestamp(text)
farmbuyer@45 227 end
farmbuyer@45 228 end
farmbuyer@45 229 end
farmbuyer@45 230 else
farmbuyer@45 231 function dprint (t,...)
farmbuyer@45 232 if DEBUG_PRINT and debug[t] then
farmbuyer@45 233 local text = flib.safeprint("<"..t.."> ",...)
farmbuyer@45 234 if debug.alsolog then
farmbuyer@45 235 addon:log_with_timestamp(text)
farmbuyer@45 236 end
farmbuyer@19 237 end
farmbuyer@19 238 end
farmbuyer@1 239 end
farmbuyer@1 240
farmbuyer@45 241 if author_debug and tekdebug then
farmbuyer@45 242 function pprint (t,...)
farmbuyer@45 243 local text = flib.safefprint(tekdebug,"<<"..t..">> ",...)
farmbuyer@19 244 if debug.alsolog then
farmbuyer@19 245 addon:log_with_timestamp(text)
farmbuyer@19 246 end
farmbuyer@1 247 end
farmbuyer@1 248 else
farmbuyer@1 249 pprint = flib.nullfunc
farmbuyer@1 250 end
farmbuyer@1 251
farmbuyer@76 252 -- The same observable behavior as the Lua builtins, but with slightly
farmbuyer@76 253 -- different hardcoded strings and, more importantly, implicit logging.
farmbuyer@76 254 function error(txt,lvl)
farmbuyer@76 255 pprint('ERROR()', txt)
farmbuyer@76 256 pprint('DEBUGSTACK()', _G.debugstack())
farmbuyer@76 257 _G.error(txt,lvl)
farmbuyer@76 258 end
farmbuyer@76 259 function assert(cond,msg,...)
farmbuyer@76 260 if cond then
farmbuyer@76 261 return cond,msg,...
farmbuyer@76 262 else
farmbuyer@76 263 error('ASSERT() FAILED: '..tostring(msg or 'nil'))
farmbuyer@76 264 end
farmbuyer@76 265 end
farmbuyer@76 266
farmbuyer@1 267 enabled = false
farmbuyer@1 268 rebroadcast = false
farmbuyer@76 269 display = nil -- reference to display frame iff visible
farmbuyer@1 270 loot_clean = nil -- index of last GUI entry with known-current visual data
farmbuyer@1 271 threshold = debug.loot and 0 or 3 -- rare by default
farmbuyer@1 272 sharder = nil -- name of person whose loot is marked as shards
farmbuyer@1 273
farmbuyer@1 274 -- The rest is also used in the GUI:
farmbuyer@1 275
farmbuyer@76 276 sender_list = {active={},names={}} -- this should be reworked
farmbuyer@1 277 popped = nil -- non-nil when reminder has been shown, actual value unimportant
farmbuyer@1 278
farmbuyer@2 279 bossmod_registered = nil
farmbuyer@76 280 bossmods = {}
farmbuyer@2 281
farmbuyer@76 282 requesting = nil -- prompting for additional rebroadcasters
farmbuyer@1 283
farmbuyer@76 284 -- don't use NUM_ITEM_QUALITIES as the upper loop bound unless we expect
farmbuyer@76 285 -- heirlooms to show up
farmbuyer@76 286 thresholds = {}
farmbuyer@1 287 for i = 0,6 do
farmbuyer@11 288 thresholds[i] = _G.ITEM_QUALITY_COLORS[i].hex .. _G["ITEM_QUALITY"..i.."_DESC"] .. "|r"
farmbuyer@1 289 end
farmbuyer@1 290
farmbuyer@1 291 _G.setfenv (1, _G)
farmbuyer@1 292 end
farmbuyer@1 293
farmbuyer@1 294 addon = LibStub("AceAddon-3.0"):NewAddon(addon, "Ouro Loot",
farmbuyer@1 295 "AceTimer-3.0", "AceComm-3.0", "AceConsole-3.0", "AceEvent-3.0")
farmbuyer@1 296
farmbuyer@67 297 -- if given, MSG should be a complete-ish sentence
farmbuyer@67 298 function addon:load_assert (cond, msg, ...)
farmbuyer@67 299 if cond then
farmbuyer@67 300 return cond, msg, ...
farmbuyer@67 301 end
farmbuyer@67 302 msg = msg or "load-time assertion failed!"
farmbuyer@67 303 self.NOLOAD = msg
farmbuyer@67 304 self:Printf([[|cffff1010ERROR:|r <|cff00ff00%s|r> Ouro Loot cannot finish loading. You will need to type |cff30adff%s|r once these problems are resolved, and try again.]], msg, _G.SLASH_RELOAD1)
farmbuyer@67 305 SLASH_ACECONSOLE_OUROLOOT1 = nil
farmbuyer@67 306 SLASH_ACECONSOLE_OUROLOOT2 = nil
farmbuyer@76 307 self.error (msg, --[[level=]]2)
farmbuyer@67 308 end
farmbuyer@67 309
farmbuyer@67 310 -- Seriously? ORLY?
farmbuyer@67 311 -- YARLY. Go ahead and guess what was involved in tracking this down. If
farmbuyer@67 312 -- more such effects are added in the future, the "id==xxxxx" will need to
farmbuyer@67 313 -- change into a probe of a table of known-problematic IDs.
farmbuyer@67 314 for i = 1, 40 do -- BUFF_MAX_DISPLAY==32, enh
farmbuyer@67 315 local id = select(11,UnitAura('player', i, 'HELPFUL'))
farmbuyer@67 316 if id == 88715 then
farmbuyer@67 317 -- What I really want to do is pause until the thing is clicked off,
farmbuyer@67 318 -- then continue with the rest of the file. No can do. Could also
farmbuyer@67 319 -- just set some hooks and then re-OnInit/OnEnable after the aura
farmbuyer@67 320 -- expires, but that's a hassle. GAH. Punt.
farmbuyer@67 321 local text = UnitAura('player', i, 'HELPFUL')
farmbuyer@67 322 text = ([[Cannot initialize while |cff71d5ff|Hspell:88715|h[%s]|h|cff00ff00 is active!]]):
farmbuyer@67 323 format(text)
farmbuyer@67 324 addon:load_assert(nil,text)
farmbuyer@67 325 return -- were this C code running through lint, I'd put NOTREACHED
farmbuyer@67 326 end
farmbuyer@67 327 end
farmbuyer@67 328
farmbuyer@86 329 -- Class color support. Do the expensive string.format calls up front, and
farmbuyer@86 330 -- the cheap all-string-all-at-once single-op concatenation as needed.
farmbuyer@86 331 do
farmbuyer@86 332 local cc = {}
farmbuyer@86 333 local function extract (color_info)
farmbuyer@93 334 local hex
farmbuyer@93 335 if color_info.colorStr then -- MoP
farmbuyer@93 336 hex = "|c" .. color_info.colorStr
farmbuyer@93 337 else -- pre-MoP
farmbuyer@93 338 hex = ("|cff%.2x%.2x%.2x"):format(255*color_info.r,
farmbuyer@93 339 255*color_info.g, 255*color_info.b)
farmbuyer@93 340 end
farmbuyer@92 341 return { r=color_info.r, g=color_info.g, b=color_info.b, a=1, hex=hex }
farmbuyer@86 342 end
farmbuyer@86 343 local function fill_out_class_colors()
farmbuyer@86 344 for class,color in pairs(CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS) do
farmbuyer@86 345 cc[class] = extract(color)
farmbuyer@86 346 end
farmbuyer@86 347 cc.DEFAULT = extract(_G.NORMAL_FONT_COLOR)
farmbuyer@86 348 end
farmbuyer@86 349 if CUSTOM_CLASS_COLORS and CUSTOM_CLASS_COLORS.RegisterCallback then
farmbuyer@86 350 CUSTOM_CLASS_COLORS:RegisterCallback(fill_out_class_colors)
farmbuyer@86 351 end
farmbuyer@93 352 addon.class_colors = cc -- this stays around
farmbuyer@93 353 addon.fill_out_class_colors = fill_out_class_colors -- this doesn't
farmbuyer@93 354
farmbuyer@86 355 -- What I really want is to have the hooked :Print understand a special
farmbuyer@86 356 -- format specifier like "%Cs" and do the colorizing automatically.
farmbuyer@86 357 function addon:colorize (text, class)
farmbuyer@86 358 return ((class and cc[class]) or cc.DEFAULT).hex .. text .. "|r"
farmbuyer@86 359 end
farmbuyer@86 360 end
farmbuyer@86 361
farmbuyer@1 362
farmbuyer@6 363 ------ Globals
farmbuyer@1 364 local g_loot = nil
farmbuyer@1 365 local g_restore_p = nil
farmbuyer@76 366 local g_wafer_thin = nil -- prompting for additional rebroadcasters
farmbuyer@1 367 local g_today = nil -- "today" entry in g_loot
farmbuyer@16 368 local g_boss_signpost = nil
farmbuyer@73 369 local g_seeing_oldsigs = nil
farmbuyer@73 370 local g_uniques = nil -- memoization of unique loot events
farmbuyer@76 371 local g_unique_replace = nil
farmbuyer@1 372 local opts = nil
farmbuyer@1 373
farmbuyer@76 374 local error = addon.error
farmbuyer@76 375 local assert = addon.assert
farmbuyer@76 376
farmbuyer@73 377 -- for speeding up local loads, not because I think _G will change
farmbuyer@73 378 local _G = _G
farmbuyer@73 379 local type = _G.type
farmbuyer@73 380 local select = _G.select
farmbuyer@73 381 local pairs = _G.pairs
farmbuyer@73 382 local ipairs = _G.ipairs
farmbuyer@73 383 local tinsert = _G.table.insert
farmbuyer@73 384 local tremove = _G.table.remove
farmbuyer@73 385 local tostring = _G.tostring
farmbuyer@73 386 local tonumber = _G.tonumber
farmbuyer@73 387 local wipe = _G.table.wipe
farmbuyer@73 388
farmbuyer@1 389 local pprint, tabledump = addon.pprint, flib.tabledump
farmbuyer@90 390 local CopyTable = _G.CopyTable
farmbuyer@90 391 local GetNumRaidMembers = _G.GetNumGroupMembers or _G.GetNumRaidMembers
farmbuyer@93 392 local IsInRaid = _G.IsInRaid or (function() return GetNumRaidMembers() > 0 end)
farmbuyer@1 393 -- En masse forward decls of symbols defined inside local blocks
farmbuyer@100 394 local _register_bossmod, makedate, create_new_cache, _init, _log, _do_loot_metas
farmbuyer@81 395 local _history_by_loot_id, _setup_unique_replace, _unavoidable_collision
farmbuyer@101 396 local _notify_about_change
farmbuyer@1 397
farmbuyer@27 398 -- Try to extract numbers from the .toc "Version" and munge them into an
farmbuyer@27 399 -- integral form for comparison. The result doesn't need to be meaningful as
farmbuyer@38 400 -- long as we can reliably feed two of them to "<" and get useful answers.
farmbuyer@29 401 --
farmbuyer@89 402 -- This makes/reinforces an assumption that version_large of release packages
farmbuyer@29 403 -- (e.g., 2016001) will always be higher than those of development packages
farmbuyer@29 404 -- (e.g., 87), due to the tagging system versus subversion file revs. This
farmbuyer@29 405 -- is good, as local dev code will never trigger a false positive update
farmbuyer@62 406 -- warning for other users.
farmbuyer@27 407 do
farmbuyer@27 408 local r = 0
farmbuyer@89 409 for d in addon.version:gmatch("%d+") do
farmbuyer@27 410 r = 1000*r + d
farmbuyer@27 411 end
farmbuyer@62 412 -- If it's a big enough number to obviously be a release, then make
farmbuyer@65 413 -- sure it's big enough to overcome many small previous point releases.
farmbuyer@62 414 while r > 2000 and r < 2000000 do
farmbuyer@62 415 r = 1000*r
farmbuyer@62 416 end
farmbuyer@89 417 version_large = math.max(r,1)
farmbuyer@27 418 end
farmbuyer@27 419
farmbuyer@1 420 -- Hypertext support, inspired by DBM broadcast pizza timers
farmbuyer@1 421 do
farmbuyer@76 422 local hypertext_format_str = "|HOuroLoot:%d|h%s[%s]|r|h"
farmbuyer@76 423 local func_map = {} --_G.setmetatable({}, {__mode = 'k'})
farmbuyer@76 424 local text_map = {} --_G.setmetatable({}, {__mode = 'kv'})
farmbuyer@76 425 local base = _G.newproxy(true)
farmbuyer@76 426 _G.getmetatable(base).__tostring = function(ud) return text_map[ud] end
farmbuyer@76 427 --@debug@
farmbuyer@76 428 -- collecting these tokens is an interesting micro-optimization but not yet
farmbuyer@76 429 _G.getmetatable(base).__gc = function(ud)
farmbuyer@76 430 print("Collecting hyperlink object <",tostring(ud),">")
farmbuyer@76 431 end
farmbuyer@76 432 --@end-debug@
farmbuyer@1 433
farmbuyer@38 434 -- TEXT will automatically be surrounded by brackets
farmbuyer@76 435 -- COLOR can be ITEM_QUALITY_* or a formatting string ("|cff...")
farmbuyer@76 436 -- FUNC can be "MethodName", "tab_title", or a function
farmbuyer@76 437 --
farmbuyer@78 438 -- Returns an opaque token and a matching number. Calling tostring() on
farmbuyer@78 439 -- the token will yield a formatted clickable string that can be displayed
farmbuyer@78 440 -- in chat. The MethodName and raw function callbacks will both be
farmbuyer@78 441 -- passed the addon table and the same matching number.
farmbuyer@78 442 --
farmbuyer@78 443 -- This is largely an excuse to fool around with Lua data constructs.
farmbuyer@76 444 function addon.format_hypertext (text, color, func)
farmbuyer@76 445 local ret = _G.newproxy(base)
farmbuyer@76 446 local num = #text_map + 1
farmbuyer@76 447 text_map[ret] = hypertext_format_str:format (num,
farmbuyer@76 448 type(color)=='number' and ITEM_QUALITY_COLORS[color].hex or color,
farmbuyer@76 449 text)
farmbuyer@76 450 text_map[num] = ret
farmbuyer@76 451 func_map[ret] = func
farmbuyer@78 452 return ret, num
farmbuyer@1 453 end
farmbuyer@1 454
farmbuyer@76 455 --[[
farmbuyer@76 456 link: OuroLoot:n
farmbuyer@76 457 fullstring: |HOuroLoot:n|h|cff.....[foo]|r|h
farmbuyer@76 458 mousebutton: "LeftButton", "MiddleButton", "RightButton"
farmbuyer@76 459
farmbuyer@76 460 amusingly, print()'ing the fullstring below as a debugging aid yields
farmbuyer@76 461 another clickable link, yay data reproducability
farmbuyer@76 462 ]]
farmbuyer@76 463 local strsplit = _G.strsplit
farmbuyer@76 464 DEFAULT_CHAT_FRAME:HookScript("OnHyperlinkClick", function(self, link, fullstring, mousebutton)
farmbuyer@1 465 local ltype, arg = strsplit(":",link)
farmbuyer@76 466 if ltype ~= "OuroLoot" then return end
farmbuyer@78 467 arg = tonumber(arg)
farmbuyer@78 468 local f = func_map[text_map[arg]]
farmbuyer@76 469 if type(f) == 'function' then
farmbuyer@78 470 f (addon, arg)
farmbuyer@76 471 elseif type(f) == 'string' then
farmbuyer@76 472 if type(addon[f]) == 'function' then
farmbuyer@78 473 addon[f](addon,arg) -- method name
farmbuyer@76 474 else
farmbuyer@76 475 addon:BuildMainDisplay(f) -- tab title fragment
farmbuyer@1 476 end
farmbuyer@1 477 end
farmbuyer@1 478 end)
farmbuyer@1 479
farmbuyer@1 480 local old = ItemRefTooltip.SetHyperlink
farmbuyer@1 481 function ItemRefTooltip:SetHyperlink (link, ...)
farmbuyer@76 482 if link:match("^OuroLoot") then return end
farmbuyer@1 483 return old (self, link, ...)
farmbuyer@1 484 end
farmbuyer@1 485 end
farmbuyer@1 486
farmbuyer@1 487 do
farmbuyer@1 488 -- copied here because it's declared local to the calendar ui, thanks blizz ><
farmbuyer@1 489 local CALENDAR_FULLDATE_MONTH_NAMES = {
farmbuyer@1 490 FULLDATE_MONTH_JANUARY, FULLDATE_MONTH_FEBRUARY, FULLDATE_MONTH_MARCH,
farmbuyer@1 491 FULLDATE_MONTH_APRIL, FULLDATE_MONTH_MAY, FULLDATE_MONTH_JUNE,
farmbuyer@1 492 FULLDATE_MONTH_JULY, FULLDATE_MONTH_AUGUST, FULLDATE_MONTH_SEPTEMBER,
farmbuyer@1 493 FULLDATE_MONTH_OCTOBER, FULLDATE_MONTH_NOVEMBER, FULLDATE_MONTH_DECEMBER,
farmbuyer@1 494 }
farmbuyer@1 495 -- returns "dd Month yyyy", mm, dd, yyyy
farmbuyer@1 496 function makedate()
farmbuyer@1 497 Calendar_LoadUI()
farmbuyer@1 498 local _, M, D, Y = CalendarGetDate()
farmbuyer@1 499 local text = ("%d %s %d"):format(D, CALENDAR_FULLDATE_MONTH_NAMES[M], Y)
farmbuyer@1 500 return text, M, D, Y
farmbuyer@1 501 end
farmbuyer@1 502 end
farmbuyer@1 503
farmbuyer@56 504 -- Returns an instance name or abbreviation, followed by the raid size
farmbuyer@1 505 local function instance_tag()
farmbuyer@61 506 -- possibly redo this with the new GetRaidDifficulty function
farmbuyer@1 507 local name, typeof, diffcode, diffstr, _, perbossheroic, isdynamic = GetInstanceInfo()
farmbuyer@56 508 local t, r
farmbuyer@1 509 name = addon.instance_abbrev[name] or name
farmbuyer@56 510 if typeof == "none" then return name, MAX_RAID_MEMBERS end
farmbuyer@1 511 -- diffstr is "5 Player", "10 Player (Heroic)", etc. ugh.
farmbuyer@35 512 if (GetLFGMode()) and (GetLFGModeType() == 'raid') then
farmbuyer@56 513 t,r = 'LFR', 25
farmbuyer@35 514 elseif diffcode == 1 then
farmbuyer@93 515 if IsInRaid() then
farmbuyer@76 516 t,r = "10",10
farmbuyer@76 517 else
farmbuyer@76 518 t,r = "5",5
farmbuyer@76 519 end
farmbuyer@1 520 elseif diffcode == 2 then
farmbuyer@93 521 if IsInRaid() then
farmbuyer@76 522 t,r = "25",25
farmbuyer@76 523 else
farmbuyer@76 524 t,r = "5h",5
farmbuyer@76 525 end
farmbuyer@1 526 elseif diffcode == 3 then
farmbuyer@56 527 t,r = "10h", 10
farmbuyer@1 528 elseif diffcode == 4 then
farmbuyer@56 529 t,r = "25h", 25
farmbuyer@1 530 end
farmbuyer@1 531 -- dynamic difficulties always return normal "codes"
farmbuyer@1 532 if isdynamic and perbossheroic == 1 then
farmbuyer@1 533 t = t .. "h"
farmbuyer@1 534 end
farmbuyer@56 535 return name .. "(" .. t .. ")", r
farmbuyer@1 536 end
farmbuyer@1 537 addon.instance_tag = instance_tag -- grumble
farmbuyer@42 538 addon.latest_instance = nil -- spelling reminder, assigned elsewhere
farmbuyer@1 539
farmbuyer@73 540 -- Memoizing cache of unique IDs as we generate or search for them. Keys are
farmbuyer@73 541 -- the uniques, values are the following:
farmbuyer@87 542 -- 'history' active player name in self.history
farmbuyer@73 543 -- 'history_may' index into player's uniques list, CAN QUICKLY BE OUTDATED
farmbuyer@73 544 -- and will instantly be wrong after manual insertion
farmbuyer@73 545 -- 'loot' active index into g_loot
farmbuyer@73 546 -- with all but the history entry optional. Values of g_uniqes.NOTFOUND
farmbuyer@73 547 -- indicate a known missing status. Use g_uniques:RESET() to wipe the cache
farmbuyer@73 548 -- and return to searching mode.
farmbuyer@73 549 do
farmbuyer@73 550 local notfound = -1
farmbuyer@73 551 local notfound_ret = { history = notfound }
farmbuyer@73 552 local mt
farmbuyer@73 553
farmbuyer@73 554 -- This can either be its own function or a slightly redundant __index.
farmbuyer@73 555 local function m_probe_only (t, k)
farmbuyer@73 556 return rawget(t,k) or notfound_ret
farmbuyer@73 557 end
farmbuyer@73 558
farmbuyer@73 559 -- Expensive search.
farmbuyer@73 560 local function m_full_search (t, k)
farmbuyer@73 561 local L, H, HU, loot
farmbuyer@73 562 -- Try active loot entries first
farmbuyer@73 563 for i,e in addon:filtered_loot_iter('loot') do
farmbuyer@73 564 if k == e.unique then
farmbuyer@73 565 L,loot = i,e
farmbuyer@73 566 break
farmbuyer@73 567 end
farmbuyer@73 568 end
farmbuyer@73 569 -- If it's active, try looking through that player's history first.
farmbuyer@73 570 if L then
farmbuyer@73 571 local hi,h = addon:get_loot_history (loot.person)
farmbuyer@73 572 for ui,u in ipairs(h.unique) do
farmbuyer@73 573 if k == u then
farmbuyer@87 574 H, HU = h.name, ui
farmbuyer@73 575 break
farmbuyer@73 576 end
farmbuyer@73 577 end
farmbuyer@73 578 else
farmbuyer@73 579 -- No luck? Ugh, may have been reassigned and we're probing from
farmbuyer@73 580 -- older data. Search the rest of current realm's history.
farmbuyer@73 581 for hi,h in ipairs(addon.history) do
farmbuyer@73 582 for ui,u in ipairs(h.unique) do
farmbuyer@73 583 if k == u then
farmbuyer@87 584 H, HU = h.name, ui
farmbuyer@73 585 break
farmbuyer@73 586 end
farmbuyer@73 587 end
farmbuyer@73 588 end
farmbuyer@73 589 end
farmbuyer@73 590 local ret = { loot = L, history = H or notfound, history_may = HU }
farmbuyer@73 591 t[k] = ret
farmbuyer@73 592 return ret
farmbuyer@73 593 end
farmbuyer@73 594
farmbuyer@73 595 local function m_setmode (self, mode)
farmbuyer@73 596 mt.__index = (mode == 'probe') and m_probe_only or
farmbuyer@73 597 (mode == 'search') and m_full_search or
farmbuyer@73 598 nil -- maybe error() here?
farmbuyer@73 599 end
farmbuyer@73 600
farmbuyer@73 601 local function m_reset (self)
farmbuyer@73 602 wipe(self)
farmbuyer@73 603 self[''] = notfound_ret -- special case for receiving older broadcast
farmbuyer@73 604 self.NOTFOUND = notfound
farmbuyer@73 605 self.RESET = m_reset
farmbuyer@73 606 self.SEARCH = m_full_search
farmbuyer@73 607 self.TEST = m_probe_only
farmbuyer@73 608 self.SETMODE = m_setmode
farmbuyer@73 609 mt.__index = m_full_search
farmbuyer@73 610 return self
farmbuyer@73 611 end
farmbuyer@73 612
farmbuyer@73 613 -- If unique keys ever change into objects instead of strings, change
farmbuyer@73 614 -- this into a weakly-keyed table.
farmbuyer@73 615 mt = { __metatable = 'Should be using setmode.' }
farmbuyer@73 616
farmbuyer@100 617 g_uniques = _G.setmetatable (m_reset{}, mt)
farmbuyer@73 618 end
farmbuyer@73 619
farmbuyer@1 620
farmbuyer@1 621 ------ Expiring caches
farmbuyer@1 622 --[[
farmbuyer@73 623 cache = create_new_cache ("mycache", 15 [,cleanup])
farmbuyer@73 624 cache:add(foo)
farmbuyer@73 625 cache:test(foo) -- returns true
farmbuyer@73 626 ....5 seconds pass
farmbuyer@73 627 cache:add(bar)
farmbuyer@73 628 ....10 seconds pass
farmbuyer@73 629 cache:test(foo) -- returns false
farmbuyer@73 630 cache:test(bar) -- returns true
farmbuyer@73 631 ....5 seconds pass
farmbuyer@73 632 ....bar also gone, cleanup() called
farmbuyer@1 633 ]]
farmbuyer@1 634 do
farmbuyer@1 635 local caches = {}
farmbuyer@25 636 local cleanup_group = _G.AnimTimerFrame:CreateAnimationGroup()
farmbuyer@10 637 local time = _G.time
farmbuyer@1 638 cleanup_group:SetLooping("REPEAT")
farmbuyer@1 639 cleanup_group:SetScript("OnLoop", function(cg)
farmbuyer@1 640 addon.dprint('cache',"OnLoop firing")
farmbuyer@10 641 local now = time()
farmbuyer@1 642 local alldone = true
farmbuyer@1 643 -- this is ass-ugly
farmbuyer@25 644 for name,c in pairs(caches) do
farmbuyer@25 645 local fifo = c.fifo
farmbuyer@25 646 local active = #fifo > 0
farmbuyer@73 647 while (#fifo > 0) and (now > fifo[1].t) do
farmbuyer@40 648 addon.dprint('cache', name, "cache removing", fifo[1].t, "<", fifo[1].m, ">")
farmbuyer@25 649 tremove(fifo,1)
farmbuyer@1 650 end
farmbuyer@25 651 if active and #fifo == 0 and c.func then
farmbuyer@25 652 addon.dprint('cache', name, "empty, firing cleanup")
farmbuyer@25 653 c:func()
farmbuyer@25 654 end
farmbuyer@25 655 alldone = alldone and (#fifo == 0)
farmbuyer@1 656 end
farmbuyer@1 657 if alldone then
farmbuyer@40 658 addon.dprint('cache',"OnLoop FINISHING animation group")
farmbuyer@1 659 cleanup_group:Finish()
farmbuyer@73 660 _G.collectgarbage()
farmbuyer@40 661 else
farmbuyer@40 662 addon.dprint('cache',"OnLoop done, not yet finished")
farmbuyer@1 663 end
farmbuyer@1 664 end)
farmbuyer@1 665
farmbuyer@1 666 local function _add (cache, x)
farmbuyer@73 667 local datum = { t=time()+cache.ttl, m=x }
farmbuyer@25 668 cache.hash[x] = datum
farmbuyer@25 669 tinsert (cache.fifo, datum)
farmbuyer@1 670 if not cleanup_group:IsPlaying() then
farmbuyer@41 671 addon.dprint('cache', cache.name, "with entry", datum.t, "<", datum.m, "> STARTING animation group")
farmbuyer@40 672 cache.cleanup:SetDuration(1) -- hmmm
farmbuyer@1 673 cleanup_group:Play()
farmbuyer@1 674 end
farmbuyer@1 675 end
farmbuyer@1 676 local function _test (cache, x)
farmbuyer@40 677 -- FIXME This can return false positives, if called after the onloop
farmbuyer@40 678 -- fifo has been removed but before the GC has removed the weak entry.
farmbuyer@76 679 -- What to do, what to do... try forcing a GC during alldone.
farmbuyer@25 680 return cache.hash[x] ~= nil
farmbuyer@1 681 end
farmbuyer@25 682
farmbuyer@1 683 function create_new_cache (name, ttl, on_alldone)
farmbuyer@25 684 -- setting OnFinished for cleanup fires at the end of each inner loop,
farmbuyer@25 685 -- with no 'requested' argument to distinguish cases. thus, on_alldone.
farmbuyer@76 686 -- FWIW, on_alldone is passed this table as its sole argument:
farmbuyer@1 687 local c = {
farmbuyer@1 688 ttl = ttl,
farmbuyer@1 689 name = name,
farmbuyer@1 690 add = _add,
farmbuyer@1 691 test = _test,
farmbuyer@1 692 cleanup = cleanup_group:CreateAnimation("Animation"),
farmbuyer@1 693 func = on_alldone,
farmbuyer@25 694 fifo = {},
farmbuyer@100 695 hash = _G.setmetatable({}, {__mode='kv'}),
farmbuyer@1 696 }
farmbuyer@1 697 c.cleanup:SetOrder(1)
farmbuyer@25 698 caches[name] = c
farmbuyer@1 699 return c
farmbuyer@1 700 end
farmbuyer@1 701 end
farmbuyer@1 702
farmbuyer@1 703
farmbuyer@1 704 ------ Ace3 framework stuff
farmbuyer@98 705 function addon:DBProfileRefresh()
farmbuyer@98 706 opts = self.db.profile
farmbuyer@98 707 end
farmbuyer@98 708
farmbuyer@1 709 function addon:OnInitialize()
farmbuyer@67 710 if self.author_debug then
farmbuyer@67 711 _G.OL = self
farmbuyer@73 712 _G.g_uniques = g_uniques
farmbuyer@67 713 end
farmbuyer@73 714 _log = _G.OuroLootSV_log
farmbuyer@41 715
farmbuyer@1 716 -- VARIABLES_LOADED has fired by this point; test if we're doing something like
farmbuyer@1 717 -- relogging during a raid and already have collected loot data
farmbuyer@73 718 local OuroLootSV = _G.OuroLootSV
farmbuyer@1 719 g_restore_p = OuroLootSV ~= nil
farmbuyer@1 720 self.dprint('flow', "oninit sets restore as", g_restore_p)
farmbuyer@1 721
farmbuyer@100 722 -- Primarily for plugins, but can be of use to me also...
farmbuyer@100 723 self.callbacks = _G.LibStub("CallbackHandler-1.0"):New(self)
farmbuyer@100 724 --function self.callbacks:OnUsed (target_aka_self, eventname) end
farmbuyer@100 725 --function self.callbacks:OnUnused (target_aka_self, eventname) end
farmbuyer@100 726
farmbuyer@97 727 if _G.OuroLootOptsDB == nil then
farmbuyer@76 728 local vclick = self.format_hypertext ([[click here]], ITEM_QUALITY_UNCOMMON, 'help')
farmbuyer@1 729 self:ScheduleTimer(function(s)
farmbuyer@97 730 for id in pairs(self.default_itemfilter) do
farmbuyer@97 731 opts.itemfilter[id] = true
farmbuyer@97 732 end
farmbuyer@97 733 for id in pairs(self.default_itemvault) do
farmbuyer@97 734 opts.itemvault[id] = true
farmbuyer@97 735 end
farmbuyer@76 736 s:Print(virgin, tostring(vclick))
farmbuyer@1 737 virgin = nil
farmbuyer@1 738 end,10,self)
farmbuyer@65 739 else
farmbuyer@65 740 virgin = nil
farmbuyer@1 741 end
farmbuyer@99 742 self.db = _G.LibStub("AceDB-3.0"):New("OuroLootOptsDB", option_defaults , --[[Default=]]true)
farmbuyer@99 743 self.db.RegisterCallback (self, "OnNewProfile", function()
farmbuyer@99 744 self:Print(new_profile_warning)
farmbuyer@99 745 end)
farmbuyer@98 746 self.db.RegisterCallback (self, "OnProfileChanged", "DBProfileRefresh")
farmbuyer@98 747 self.db.RegisterCallback (self, "OnProfileCopied", "DBProfileRefresh")
farmbuyer@98 748 self.db.RegisterCallback (self, "OnProfileReset", "DBProfileRefresh")
farmbuyer@98 749 self:DBProfileRefresh()
farmbuyer@97 750
farmbuyer@97 751 --[[
farmbuyer@66 752 local stored_datarev = opts.datarev or 14
farmbuyer@1 753 for opt,default in pairs(option_defaults) do
farmbuyer@1 754 if opts[opt] == nil then
farmbuyer@1 755 opts[opt] = default
farmbuyer@1 756 end
farmbuyer@1 757 end
farmbuyer@97 758 opts.datarev = option_defaults.datarev]]
farmbuyer@1 759
farmbuyer@1 760 self:RegisterChatCommand("ouroloot", "OnSlash")
farmbuyer@94 761 if opts.register_slash_synonyms then
farmbuyer@94 762 -- Maybe use %w here for non-English locales?
farmbuyer@94 763 local n = 2
farmbuyer@94 764 for s in opts.slash_synonyms:gmatch("/%a+") do
farmbuyer@94 765 _G['SLASH_ACECONSOLE_OUROLOOT'..n] = s
farmbuyer@94 766 n = n + 1
farmbuyer@94 767 end
farmbuyer@1 768 end
farmbuyer@1 769
farmbuyer@73 770 self.history_all = self.history_all or _G.OuroLootSV_hist or {}
farmbuyer@73 771 local r = self:load_assert (_G.GetRealmName(), "how the freak does GetRealmName() fail?")
farmbuyer@1 772 self.history_all[r] = self:_prep_new_history_category (self.history_all[r], r)
farmbuyer@1 773 self.history = self.history_all[r]
farmbuyer@71 774
farmbuyer@71 775 local histformat = self.history_all.HISTFORMAT
farmbuyer@71 776 self.history_all.HISTFORMAT = nil -- don't keep this in live data
farmbuyer@73 777 if _G.OuroLootSV_hist
farmbuyer@73 778 and (histformat == nil or histformat < 4)
farmbuyer@73 779 then -- some big honkin' loops
farmbuyer@38 780 for rname,realm in pairs(self.history_all) do
farmbuyer@38 781 for pk,player in ipairs(realm) do
farmbuyer@73 782 if histformat == nil or histformat < 3 then
farmbuyer@73 783 for lk,loot in ipairs(player) do
farmbuyer@73 784 if loot.count == "" then
farmbuyer@73 785 loot.count = nil
farmbuyer@73 786 end
farmbuyer@73 787 if not loot.unique then
farmbuyer@73 788 loot.unique = loot.id .. ' ' .. loot.when
farmbuyer@73 789 end
farmbuyer@71 790 end
farmbuyer@38 791 end
farmbuyer@73 792 -- format 3 to format 4 was a major revamp of per-player data
farmbuyer@76 793 self:_uplift_history_format(player)
farmbuyer@38 794 end
farmbuyer@38 795 end
farmbuyer@38 796 end
farmbuyer@73 797 self._uplift_history_format = nil
farmbuyer@6 798 --OuroLootSV_hist = nil
farmbuyer@1 799
farmbuyer@56 800 -- Handle changes to the stored data format in stages from oldest to newest.
farmbuyer@73 801 -- bumpers[X] is responsible for updating from X to X+1.
farmbuyer@73 802 -- (This is turning into a lot of loops over the same table. Consolidate?)
farmbuyer@97 803 if false and OuroLootSV then
farmbuyer@56 804 local dirty = false
farmbuyer@66 805 local bumpers = {}
farmbuyer@97 806 --bumpers[14] = function() start
farmbuyer@97 807 --bumpers[19] = function() latest
farmbuyer@71 808
farmbuyer@66 809 --[===[
farmbuyer@66 810 local real = bumpers
farmbuyer@66 811 bumpers = newproxy(true)
farmbuyer@66 812 local mt = getmetatable(bumpers)
farmbuyer@66 813 mt.__index = real
farmbuyer@66 814 mt.__gc = function() print"whadda ya know, garbage collection works" end ]===]
farmbuyer@66 815
farmbuyer@66 816 while stored_datarev < opts.datarev do
farmbuyer@66 817 self:Printf("Transitioning saved data format to %d...", stored_datarev+1)
farmbuyer@66 818 dirty = true
farmbuyer@66 819 bumpers[stored_datarev]()
farmbuyer@66 820 stored_datarev = stored_datarev + 1
farmbuyer@61 821 end
farmbuyer@56 822 if dirty then self:Print("Saved data has been massaged into shape.") end
farmbuyer@55 823 end
farmbuyer@55 824
farmbuyer@95 825 self:FINISH_SPECIAL_TABS()
farmbuyer@1 826 _init(self)
farmbuyer@89 827 self.dprint('flow', "version strings:", version_large, self.revision, self.status_text)
farmbuyer@95 828 self.gui_state_pointer = nil
farmbuyer@67 829 self.load_assert = nil
farmbuyer@66 830 self.OnInitialize = nil -- free up ALL the things!
farmbuyer@1 831 end
farmbuyer@1 832
farmbuyer@1 833 function addon:OnEnable()
farmbuyer@10 834 self:RegisterEvent("PLAYER_LOGOUT")
farmbuyer@90 835 self:RegisterEvent(RAID_ROSTER_UPDATE_EVENT,"RAID_ROSTER_UPDATE")
farmbuyer@1 836
farmbuyer@73 837 -- Cribbed from Talented. I like the way jerry thinks: the first string
farmbuyer@73 838 -- argument can be a format spec for the remainder of the arguments.
farmbuyer@1 839 -- AceConsole:Printf isn't used because we can't specify a prefix without
farmbuyer@73 840 -- jumping through ridonkulous hoops. The part about overriding :Print
farmbuyer@1 841 -- with a version using prefix hyperlinks is my fault.
farmbuyer@37 842 --
farmbuyer@73 843 -- CFPrint added instead of the usual Print testing of the first arg for
farmbuyer@73 844 -- frame-ness, which would slow down all printing and only rarely be useful.
farmbuyer@73 845 --
farmbuyer@37 846 -- There is no ITEM_QUALITY_LEGENDARY constant. Sigh.
farmbuyer@1 847 do
farmbuyer@1 848 local AC = LibStub("AceConsole-3.0")
farmbuyer@76 849 local chat_prefix = self.format_hypertext ("Ouro Loot", --[[legendary]]5,
farmbuyer@76 850 --[[empty -> nil -> main tab]]'')
farmbuyer@76 851 local chat_prefix_s = tostring(chat_prefix)
farmbuyer@1 852 function addon:Print (str, ...)
farmbuyer@1 853 if type(str) == 'string' and str:find("%", nil, --[[plainmatch=]]true) then
farmbuyer@76 854 return AC:Print (chat_prefix_s, str:format(...))
farmbuyer@1 855 else
farmbuyer@76 856 return AC:Print (chat_prefix_s, str, ...)
farmbuyer@1 857 end
farmbuyer@1 858 end
farmbuyer@73 859 function addon:CFPrint (frame, str, ...)
farmbuyer@73 860 assert(type(frame)=='table' and frame.AddMessage)
farmbuyer@73 861 if type(str) == 'string' and str:find("%", nil, --[[plainmatch=]]true) then
farmbuyer@76 862 return AC:Print (frame, chat_prefix_s, str:format(...))
farmbuyer@73 863 else
farmbuyer@76 864 return AC:Print (frame, chat_prefix_s, str, ...)
farmbuyer@73 865 end
farmbuyer@73 866 end
farmbuyer@1 867 end
farmbuyer@1 868
farmbuyer@93 869 -- Copy these over once, now that other addons have mostly loaded. Any
farmbuyer@93 870 -- future tweaks via CUSTOM_CLASS_COLORS will trigger the same callback.
farmbuyer@93 871 if addon.fill_out_class_colors then
farmbuyer@93 872 addon.fill_out_class_colors()
farmbuyer@93 873 addon.fill_out_class_colors = nil
farmbuyer@93 874 end
farmbuyer@93 875
farmbuyer@51 876 while opts.keybinding do
farmbuyer@51 877 if InCombatLockdown() then
farmbuyer@76 878 local reload = self.format_hypertext ([[the options tab]],
farmbuyer@76 879 ITEM_QUALITY_UNCOMMON, 'opt')
farmbuyer@51 880 self:Print("Cannot create '%s' as a keybinding while in combat!",
farmbuyer@51 881 opts.keybinding_text)
farmbuyer@76 882 self:Print("The rest of the addon will continue to work, but you will need to reload out of combat to get the keybinding. Either type /reload or use the button on %s in the lower right.",
farmbuyer@76 883 tostring(reload))
farmbuyer@51 884 break
farmbuyer@51 885 end
farmbuyer@51 886
farmbuyer@15 887 KeyBindingFrame_LoadUI()
farmbuyer@1 888 local btn = CreateFrame("Button", "OuroLootBindingOpen", nil, "SecureActionButtonTemplate")
farmbuyer@1 889 btn:SetAttribute("type", "macro")
farmbuyer@1 890 btn:SetAttribute("macrotext", "/ouroloot toggle")
farmbuyer@1 891 if SetBindingClick(opts.keybinding_text, "OuroLootBindingOpen") then
farmbuyer@73 892 -- a simple SaveBindings(GetCurrentBindingSet()) occasionally fails when
farmbuyer@73 893 -- GCBS() decides to return neither 1 nor 2 during load, for reasons nobody
farmbuyer@73 894 -- has ever learned
farmbuyer@15 895 local c = GetCurrentBindingSet()
farmbuyer@15 896 if c == ACCOUNT_BINDINGS or c == CHARACTER_BINDINGS then
farmbuyer@15 897 SaveBindings(c)
farmbuyer@15 898 end
farmbuyer@1 899 else
farmbuyer@51 900 self:Print("Error registering '%s' as a keybinding, check spelling!",
farmbuyer@51 901 opts.keybinding_text)
farmbuyer@1 902 end
farmbuyer@51 903 break
farmbuyer@1 904 end
farmbuyer@1 905
farmbuyer@20 906 --[[
farmbuyer@20 907 The four loot format patterns of interest, changed into relatively tight
farmbuyer@20 908 string match patterns. Done at enable-time rather than load-time against
farmbuyer@20 909 the slim chance that one of the non-US "delocalizers" needs to mess with
farmbuyer@20 910 the global patterns before we transform them.
farmbuyer@20 911
farmbuyer@20 912 The SELF variants can be replaced with LOOT_ITEM_PUSHED_SELF[_MULTIPLE] to
farmbuyer@20 913 trigger on 'receive item' instead, which would detect extracting stuff
farmbuyer@20 914 from mail, or s/PUSHED/CREATED/ for things like healthstones and guild
farmbuyer@20 915 cauldron flasks.
farmbuyer@76 916
farmbuyer@76 917 ??? do something with LOOT_ITEM_WHILE_PLAYER_INELIGIBLE for locked LFRs?
farmbuyer@20 918 ]]
farmbuyer@20 919
farmbuyer@20 920 -- LOOT_ITEM = "%s receives loot: %s." --> (.+) receives loot: (.+)%.
farmbuyer@20 921 g_LOOT_ITEM_ss = _G.LOOT_ITEM:gsub('%.$','%%.'):gsub('%%s','(.+)')
farmbuyer@20 922
farmbuyer@20 923 -- LOOT_ITEM_MULTIPLE = "%s receives loot: %sx%d." --> (.+) receives loot: (.+)(x%d+)%.
farmbuyer@20 924 g_LOOT_ITEM_MULTIPLE_sss = _G.LOOT_ITEM_MULTIPLE:gsub('%.$','%%.'):gsub('%%s','(.+)'):gsub('x%%d','(x%%d+)')
farmbuyer@20 925
farmbuyer@20 926 -- LOOT_ITEM_SELF = "You receive loot: %s." --> You receive loot: (.+)%.
farmbuyer@20 927 g_LOOT_ITEM_SELF_s = _G.LOOT_ITEM_SELF:gsub('%.$','%%.'):gsub('%%s','(.+)')
farmbuyer@20 928
farmbuyer@20 929 -- LOOT_ITEM_SELF_MULTIPLE = "You receive loot: %sx%d." --> You receive loot: (.+)(x%d+)%.
farmbuyer@20 930 g_LOOT_ITEM_SELF_MULTIPLE_ss = _G.LOOT_ITEM_SELF_MULTIPLE:gsub('%.$','%%.'):gsub('%%s','(.+)'):gsub('x%%d','(x%%d+)')
farmbuyer@20 931
farmbuyer@44 932 --[[
farmbuyer@44 933 Stick something in the Blizzard addons options list, where most users
farmbuyer@44 934 will probably look these days. Try to be conservative about needless
farmbuyer@44 935 frame creation.
farmbuyer@44 936 ]]
farmbuyer@44 937 local bliz = CreateFrame("Frame")
farmbuyer@44 938 bliz.name = "Ouro Loot"
farmbuyer@44 939 bliz:SetScript("OnShow", function(_b)
farmbuyer@44 940 local button = CreateFrame("Button",nil,_b,"UIPanelButtonTemplate")
farmbuyer@44 941 button:SetWidth(150)
farmbuyer@44 942 button:SetHeight(22)
farmbuyer@44 943 button:SetScript("OnClick", function()
farmbuyer@44 944 _G.InterfaceOptionsFrameCancel:Click()
farmbuyer@44 945 _G.HideUIPanel(GameMenuFrame)
farmbuyer@44 946 addon:OpenMainDisplayToTab"Options"
farmbuyer@44 947 end)
farmbuyer@44 948 button:SetText('"/ouroloot opt"')
farmbuyer@44 949 button:SetPoint("TOPLEFT",20,-20)
farmbuyer@44 950 _b:SetScript("OnShow",nil)
farmbuyer@44 951 end)
farmbuyer@44 952 _G.InterfaceOptions_AddCategory(bliz)
farmbuyer@44 953
farmbuyer@73 954 -- Maybe load up g_uniques now?
farmbuyer@73 955 if opts.precache_history_uniques then
farmbuyer@73 956 self:_cache_history_uniques()
farmbuyer@73 957 end
farmbuyer@73 958
farmbuyer@49 959 self:_scan_LOD_modules()
farmbuyer@49 960
farmbuyer@101 961 self:_set_chatty_change_chatframe (opts.chatty_on_changes_frame, --[[silent_p=]]true)
farmbuyer@73 962
farmbuyer@1 963 if self.debug.flow then self:Print"is in control-flow debug mode." end
farmbuyer@1 964 end
farmbuyer@1 965 --function addon:OnDisable() end
farmbuyer@1 966
farmbuyer@58 967 do
farmbuyer@99 968 --[[
farmbuyer@99 969 Module support (aka plugins). Field names with special meanings:
farmbuyer@99 970 - option_defaults: (IN) Standard AceDB-style table. Use a profiles key!
farmbuyer@99 971 - db: (OUT) AceDB object, set during init.
farmbuyer@99 972 - opts: (OUT) Pointer to plugin's "db.profile" subtable.
farmbuyer@99 973
farmbuyer@99 974 OnInitialize, [default_]OnEnable, register_text_generator, register_tab_control
farmbuyer@99 975 are all inherited.
farmbuyer@99 976 ]]
farmbuyer@58 977 local prototype = {}
farmbuyer@95 978 local registry
farmbuyer@95 979
farmbuyer@95 980 -- By default, no plugins. First one in sets up code for any after.
farmbuyer@95 981 addon.get_plugin = flib.nullfunc
farmbuyer@95 982
farmbuyer@95 983 -- Fires before the plugin's own OnEnable (inherited or otherwise).
farmbuyer@95 984 function addon:OnModuleCreated (plugin)
farmbuyer@95 985 if not registry then
farmbuyer@95 986 registry = {}
farmbuyer@95 987 addon.get_plugin = function(a,t) return registry[t] end
farmbuyer@95 988 prototype.register_text_generator = function(p,t,...)
farmbuyer@95 989 registry[t] = p
farmbuyer@95 990 return addon:register_text_generator(t,...)
farmbuyer@95 991 end
farmbuyer@95 992 prototype.register_tab_control = function(p,t,...)
farmbuyer@95 993 registry[t] = p
farmbuyer@95 994 return addon:register_tab_control(t,...)
farmbuyer@95 995 end
farmbuyer@95 996 end
farmbuyer@95 997 end
farmbuyer@95 998
farmbuyer@99 999 local function module_OnInit (plugin)
farmbuyer@58 1000 if plugin.option_defaults then
farmbuyer@99 1001 plugin.db = addon.db:RegisterNamespace (plugin.moduleName, plugin.option_defaults)
farmbuyer@99 1002 plugin.opts = plugin.db.profile
farmbuyer@99 1003 --plugin:SetEnabledState(plugin.db.profile.enabled) if that flag is needed later
farmbuyer@58 1004 end
farmbuyer@58 1005 end
farmbuyer@58 1006
farmbuyer@99 1007 local function module_OnEnable (plugin)
farmbuyer@99 1008 end
farmbuyer@99 1009
farmbuyer@99 1010 local function module_GetOption (plugin, info)
farmbuyer@99 1011 local name = info[#info]
farmbuyer@99 1012 return plugin.db.profile[name]
farmbuyer@99 1013 end
farmbuyer@99 1014 local function module_SetOption (plugin, info, value)
farmbuyer@99 1015 local name = info[#info]
farmbuyer@99 1016 plugin.db.profile[name] = value
farmbuyer@99 1017 local arg = info.arg
farmbuyer@99 1018 if type(arg) == 'function' then
farmbuyer@99 1019 plugin[arg](plugin,info)
farmbuyer@99 1020 end
farmbuyer@99 1021 end
farmbuyer@99 1022
farmbuyer@99 1023 prototype.OnInitialize = module_OnInit
farmbuyer@58 1024 prototype.OnEnable = module_OnEnable
farmbuyer@58 1025 prototype.default_OnEnable = module_OnEnable
farmbuyer@99 1026 prototype.GetOption = module_GetOption
farmbuyer@99 1027 prototype.SetOption = module_SetOption
farmbuyer@58 1028
farmbuyer@58 1029 addon:SetDefaultModuleLibraries("AceConsole-3.0")
farmbuyer@58 1030 addon:SetDefaultModulePrototype(prototype)
farmbuyer@63 1031
farmbuyer@64 1032 local err = [[Module '%s' cannot register itself because it failed a required condition: '%s']]
farmbuyer@63 1033 function addon:ConstrainedNewModule (modname, minrev, mincomm, mindata)
farmbuyer@63 1034 if not addon.author_debug then
farmbuyer@89 1035 if minrev and tonumber(minrev) > (tonumber(self.revision) or math.huge) then
farmbuyer@63 1036 self:Print(err,modname,
farmbuyer@89 1037 "revision "..self.revision.." older than minimum "..minrev)
farmbuyer@63 1038 return false
farmbuyer@63 1039 end
farmbuyer@89 1040 if mincomm and tonumber(mincomm) > tonumber(self.commrev) then
farmbuyer@63 1041 self:Print(err,modname,
farmbuyer@63 1042 "commrev "..self.commrev.." older than minimum "..mincomm)
farmbuyer@63 1043 return false
farmbuyer@63 1044 end
farmbuyer@99 1045 --[[if mindata and tonumber(mindata) > opts.datarev then
farmbuyer@63 1046 self:Print(err,modname,
farmbuyer@63 1047 "datarev "..opts.datarev.." older than minimum "..mindata)
farmbuyer@63 1048 return false
farmbuyer@99 1049 end]]
farmbuyer@63 1050 end
farmbuyer@63 1051 return self:NewModule(modname)
farmbuyer@63 1052 end
farmbuyer@58 1053 end
farmbuyer@58 1054
farmbuyer@100 1055 -- We don't want to trigger plugins and another addons as soon as something
farmbuyer@100 1056 -- interesting happens, because a nontrivial amount of work happens "quickly"
farmbuyer@100 1057 -- after the interesting event: cleanups/fixups, improvs from network, etc.
farmbuyer@100 1058 -- So firing a callback is delayed ever so briefly by human standards.
farmbuyer@100 1059 --
farmbuyer@100 1060 -- Can't *quite* use the expiring caches for this, but that's okay.
farmbuyer@100 1061 do
farmbuyer@101 1062 local mtnewindex = function() error("This table is read-only", 3) end
farmbuyer@101 1063 local function make_readonly (t)
farmbuyer@101 1064 return _G.setmetatable({}, {
farmbuyer@101 1065 __newindex = mtnewindex,
farmbuyer@101 1066 __index = t,
farmbuyer@101 1067 __metatable = false,
farmbuyer@101 1068 })
farmbuyer@101 1069 end
farmbuyer@101 1070
farmbuyer@100 1071 local unpack = _G.unpack
farmbuyer@100 1072 local function F (t)
farmbuyer@100 1073 addon.callbacks:Fire (unpack(t))
farmbuyer@100 1074 flib.del(t)
farmbuyer@100 1075 end
farmbuyer@100 1076 function addon:Fire (...)
farmbuyer@100 1077 self.dprint('callback', ...)
farmbuyer@100 1078 local capture = flib.new(...)
farmbuyer@101 1079 for k,v in ipairs(capture) do if type(v) == 'table' then
farmbuyer@101 1080 capture[k] = make_readonly(v)
farmbuyer@101 1081 end end
farmbuyer@100 1082 self:ScheduleTimer (F, 1.2, capture)
farmbuyer@100 1083 end
farmbuyer@100 1084 end
farmbuyer@100 1085
farmbuyer@1 1086
farmbuyer@1 1087 ------ Event handlers
farmbuyer@1 1088 function addon:_clear_SVs()
farmbuyer@1 1089 g_loot = {} -- not saved, just fooling PLAYER_LOGOUT tests
farmbuyer@73 1090 _G.OuroLootSV = nil
farmbuyer@73 1091 _G.OuroLootSV_saved = nil
farmbuyer@97 1092 _G.OuroLootOptsDB = nil
farmbuyer@73 1093 _G.OuroLootSV_hist = nil
farmbuyer@73 1094 _G.OuroLootSV_log = nil
farmbuyer@73 1095 _G.ReloadUI()
farmbuyer@1 1096 end
farmbuyer@1 1097 function addon:PLAYER_LOGOUT()
farmbuyer@90 1098 self:UnregisterEvent(RAID_ROSTER_UPDATE_EVENT)
farmbuyer@16 1099 self:UnregisterEvent("PLAYER_ENTERING_WORLD")
farmbuyer@16 1100
farmbuyer@73 1101 local worth_saving = #g_loot > 0 or _G.next(g_loot.raiders)
farmbuyer@16 1102 if not worth_saving then for text in self:registered_textgen_iter() do
farmbuyer@16 1103 worth_saving = worth_saving or g_loot.printed[text] > 0
farmbuyer@16 1104 end end
farmbuyer@16 1105 if worth_saving then
farmbuyer@16 1106 opts.autoshard = self.sharder
farmbuyer@16 1107 opts.threshold = self.threshold
farmbuyer@1 1108 for i,e in ipairs(g_loot) do
farmbuyer@1 1109 e.cols = nil
farmbuyer@1 1110 end
farmbuyer@73 1111 _G.OuroLootSV = g_loot
farmbuyer@16 1112 else
farmbuyer@73 1113 _G.OuroLootSV = nil
farmbuyer@1 1114 end
farmbuyer@16 1115
farmbuyer@38 1116 worth_saving = false
farmbuyer@6 1117 for r,t in pairs(self.history_all) do if type(t) == 'table' then
farmbuyer@8 1118 if #t == 0 then
farmbuyer@8 1119 self.history_all[r] = nil
farmbuyer@8 1120 else
farmbuyer@38 1121 worth_saving = true
farmbuyer@8 1122 t.realm = nil
farmbuyer@8 1123 t.st = nil
farmbuyer@8 1124 t.byname = nil
farmbuyer@8 1125 end
farmbuyer@6 1126 end end
farmbuyer@38 1127 if worth_saving then
farmbuyer@73 1128 _G.OuroLootSV_hist = self.history_all
farmbuyer@73 1129 _G.OuroLootSV_hist.HISTFORMAT = 4
farmbuyer@38 1130 else
farmbuyer@73 1131 _G.OuroLootSV_hist = nil
farmbuyer@38 1132 end
farmbuyer@73 1133 _G.OuroLootSV_log = #_G.OuroLootSV_log > 0 and _G.OuroLootSV_log or nil
farmbuyer@1 1134 end
farmbuyer@1 1135
farmbuyer@10 1136 do
farmbuyer@67 1137 local IsInInstance, UnitIsConnected, UnitClass, UnitRace, UnitSex,
farmbuyer@56 1138 UnitLevel, UnitInRaid, UnitIsVisible, GetGuildInfo, GetRaidRosterInfo =
farmbuyer@67 1139 IsInInstance, UnitIsConnected, UnitClass, UnitRace, UnitSex,
farmbuyer@56 1140 UnitLevel, UnitInRaid, UnitIsVisible, GetGuildInfo, GetRaidRosterInfo
farmbuyer@10 1141 local time, difftime = time, difftime
farmbuyer@10 1142 local R_ACTIVE, R_OFFLINE, R_LEFT = 1, 2, 3
farmbuyer@10 1143
farmbuyer@10 1144 local lastevent, now = 0, 0
farmbuyer@16 1145 local redo_count = 0
farmbuyer@16 1146 local redo, timer_handle
farmbuyer@10 1147
farmbuyer@10 1148 function addon:CheckRoster (leaving_p, now_a)
farmbuyer@10 1149 if not g_loot.raiders then return end -- bad transition
farmbuyer@10 1150
farmbuyer@10 1151 now = now_a or time()
farmbuyer@10 1152
farmbuyer@10 1153 if leaving_p then
farmbuyer@16 1154 if timer_handle then
farmbuyer@16 1155 self:CancelTimer(timer_handle)
farmbuyer@16 1156 timer_handle = nil
farmbuyer@16 1157 end
farmbuyer@10 1158 for name,r in pairs(g_loot.raiders) do
farmbuyer@10 1159 r.leave = r.leave or now
farmbuyer@10 1160 end
farmbuyer@10 1161 return
farmbuyer@10 1162 end
farmbuyer@10 1163
farmbuyer@10 1164 for name,r in pairs(g_loot.raiders) do
farmbuyer@10 1165 if r.online ~= R_LEFT and not UnitInRaid(name) then
farmbuyer@10 1166 r.online = R_LEFT
farmbuyer@10 1167 r.leave = now
farmbuyer@10 1168 end
farmbuyer@10 1169 end
farmbuyer@10 1170
farmbuyer@100 1171 -- XXX somewhere in here, we could fire a useful callback event
farmbuyer@100 1172
farmbuyer@16 1173 if redo then
farmbuyer@16 1174 redo_count = redo_count + 1
farmbuyer@16 1175 end
farmbuyer@16 1176 redo = false
farmbuyer@10 1177 for i = 1, GetNumRaidMembers() do
farmbuyer@10 1178 local unit = 'raid'..i
farmbuyer@67 1179 -- We grab a bunch of return values here, but only pay attention to
farmbuyer@67 1180 -- them under specific circumstances.
farmbuyer@67 1181 local name, connected, subgroup, level, class, _
farmbuyer@67 1182 name, _, subgroup, level, _, class, connected = GetRaidRosterInfo(i)
farmbuyer@10 1183 -- No, that's not my typo, it really is "uknownbeing" in Blizzard's code.
farmbuyer@10 1184 if name and name ~= UNKNOWN and name ~= UNKNOWNOBJECT and name ~= UKNOWNBEING then
farmbuyer@10 1185 if not g_loot.raiders[name] then
farmbuyer@10 1186 g_loot.raiders[name] = { needinfo=true }
farmbuyer@10 1187 end
farmbuyer@10 1188 local r = g_loot.raiders[name]
farmbuyer@68 1189 r.subgroup = subgroup or (NUM_RAID_GROUPS+1)
farmbuyer@10 1190 if r.needinfo and UnitIsVisible(unit) then
farmbuyer@10 1191 r.needinfo = nil
farmbuyer@56 1192 r.class = class --select(2,UnitClass(unit))
farmbuyer@10 1193 r.race = select(2,UnitRace(unit))
farmbuyer@10 1194 r.sex = UnitSex(unit)
farmbuyer@56 1195 r.level = level --UnitLevel(unit)
farmbuyer@10 1196 r.guild = GetGuildInfo(unit)
farmbuyer@10 1197 end
farmbuyer@56 1198 --local connected = UnitIsConnected(unit)
farmbuyer@10 1199 if connected and r.online ~= R_ACTIVE then
farmbuyer@10 1200 r.join = r.join or now
farmbuyer@10 1201 r.online = R_ACTIVE
farmbuyer@10 1202 elseif (not connected) and r.online ~= R_OFFLINE then
farmbuyer@10 1203 r.leave = now
farmbuyer@10 1204 r.online = R_OFFLINE
farmbuyer@10 1205 end
farmbuyer@10 1206 redo = redo or r.needinfo
farmbuyer@10 1207 end
farmbuyer@10 1208 end
farmbuyer@16 1209 if redo then -- XXX test redo_count here and eventually give up?
farmbuyer@16 1210 if not timer_handle then
farmbuyer@16 1211 timer_handle = self:ScheduleRepeatingTimer("RAID_ROSTER_UPDATE", 60)
farmbuyer@16 1212 end
farmbuyer@16 1213 else
farmbuyer@16 1214 redo_count = 0
farmbuyer@16 1215 if timer_handle then
farmbuyer@16 1216 self:CancelTimer(timer_handle)
farmbuyer@16 1217 timer_handle = nil
farmbuyer@16 1218 end
farmbuyer@10 1219 end
farmbuyer@10 1220 end
farmbuyer@10 1221
farmbuyer@10 1222 function addon:RAID_ROSTER_UPDATE (event)
farmbuyer@10 1223 if GetNumRaidMembers() == 0 then
farmbuyer@16 1224 -- Leaving a raid group.
farmbuyer@10 1225 -- Because of PLAYER_ENTERING_WORLD, this code also executes on load
farmbuyer@10 1226 -- screens while soloing and in regular groups. Take care.
farmbuyer@16 1227 self.dprint('flow', "GetNumRaidMembers == 0")
farmbuyer@16 1228 if self.enabled and not self.debug.notraid then
farmbuyer@16 1229 self.dprint('flow', "enabled, leaving raid")
farmbuyer@10 1230 self.popped = nil
farmbuyer@80 1231 self:Deactivate()
farmbuyer@10 1232 self:CheckRoster(--[[leaving raid]]true)
farmbuyer@10 1233 end
farmbuyer@10 1234 return
farmbuyer@10 1235 end
farmbuyer@10 1236
farmbuyer@1 1237 local inside,whatkind = IsInInstance()
farmbuyer@1 1238 if inside and (whatkind == "pvp" or whatkind == "arena") then
farmbuyer@41 1239 self.dprint('flow', "got RRU event but in pvp zone, bailing")
farmbuyer@41 1240 return
farmbuyer@1 1241 end
farmbuyer@10 1242
farmbuyer@10 1243 local docheck = self.enabled
farmbuyer@1 1244 if event == "Activate" then
farmbuyer@1 1245 -- dispatched manually from Activate
farmbuyer@10 1246 self:RegisterEvent("CHAT_MSG_LOOT")
farmbuyer@2 1247 _register_bossmod(self)
farmbuyer@10 1248 docheck = true
farmbuyer@90 1249 elseif event == RAID_ROSTER_UPDATE_EVENT then
farmbuyer@10 1250 -- hot code path, be careful
farmbuyer@10 1251
farmbuyer@1 1252 -- event registration from onload, joined a raid, maybe show popup
farmbuyer@16 1253 self.dprint('flow', "RRU check:", self.popped, opts.popup_on_join)
farmbuyer@10 1254 if (not self.popped) and opts.popup_on_join then
farmbuyer@1 1255 self.popped = StaticPopup_Show "OUROL_REMIND"
farmbuyer@1 1256 self.popped.data = self
farmbuyer@10 1257 return
farmbuyer@1 1258 end
farmbuyer@1 1259 end
farmbuyer@11 1260 -- Throttle the checks fired by common events.
farmbuyer@10 1261 if docheck and not InCombatLockdown() then
farmbuyer@10 1262 now = time()
farmbuyer@10 1263 if difftime(now,lastevent) > 45 then
farmbuyer@10 1264 lastevent = now
farmbuyer@10 1265 self:CheckRoster(false,now)
farmbuyer@10 1266 end
farmbuyer@10 1267 end
farmbuyer@1 1268 end
farmbuyer@1 1269 end
farmbuyer@1 1270
farmbuyer@73 1271 --[=[ CHAT_MSG_LOOT handler and its helpers.
farmbuyer@73 1272 Situations for "unique tag" generation, given N people seeing local loot
farmbuyer@73 1273 events, M people seeing remote rebroadcasts, and player Z adding manually:
farmbuyer@73 1274
farmbuyer@73 1275 + Local tracking: All LOCALs should see the same itemstring, thus the same
farmbuyer@73 1276 unique ID stripped out of field #9. LOCALn includes this in the broadcast
farmbuyer@73 1277 to REMOTEm. Tag is a large number, meaningless for clients and players.
farmbuyer@73 1278
farmbuyer@73 1279 + Local broadcasting, remote tracking: same as local tracking. Possibly
farmbuyer@73 1280 some weirdness if all local versions are significantly older than the remote
farmbuyer@73 1281 versions; in this case each REMOTEn will generate their own tags of the form
farmbuyer@73 1282 itemID+formatted_date, which will not be "unique" for the next 60 seconds.
farmbuyer@73 1283 As long as at least one LOCALn is recent enough to strip and broadcast a
farmbuyer@73 1284 proper ID, multiple items of the same visible name will not be "lost".
farmbuyer@73 1285
farmbuyer@73 1286 + Z manually inserts a loot entry: Z generates a tag, preserved locally.
farmbuyer@73 1287 If Z rebrodcasts that entry, all REMOTEs will see it. Tag is of the form
farmbuyer@73 1288 "n" followed by a random number.
farmbuyer@73 1289 ]=]
farmbuyer@1 1290 do
farmbuyer@73 1291 local counter, _do_loot
farmbuyer@73 1292 do
farmbuyer@73 1293 local count = 0
farmbuyer@73 1294 function counter() count = count + 1; return count; end
farmbuyer@73 1295 end
farmbuyer@73 1296
farmbuyer@42 1297 local function maybe_trash_kill_entry()
farmbuyer@80 1298 -- This field is set on various boss interactions, so we've got a
farmbuyer@80 1299 -- kill/wipe entry already.
farmbuyer@42 1300 if addon.latest_instance then return end
farmbuyer@69 1301 local ss, max, inst = addon:snapshot_raid()
farmbuyer@69 1302 addon.latest_instance = inst
farmbuyer@69 1303 addon:_mark_boss_kill (addon._addBossEntry{
farmbuyer@61 1304 kind='boss', reason='kill', bossname=[[trash]],
farmbuyer@61 1305 instance=addon.latest_instance, duration=0,
farmbuyer@61 1306 raidersnap=ss, maxsize=max
farmbuyer@42 1307 })
farmbuyer@42 1308 end
farmbuyer@42 1309
farmbuyer@76 1310 -- Alert other trackers that unique tag EXISTING in subsequent 'casts
farmbuyer@76 1311 -- should be replaced by REPLACE instead. If multiple players all saw
farmbuyer@76 1312 -- the same loot event, this will cause a flurry of cross-improvs.
farmbuyer@76 1313 local function _announce_unique_improvisation (existing, replace)
farmbuyer@76 1314 if not g_unique_replace then _setup_unique_replace() end
farmbuyer@76 1315 g_unique_replace.new_entry (g_unique_replace.me, existing, replace, 'improv')
farmbuyer@76 1316 addon:vbroadcast('improv', g_unique_replace.me, existing, replace)
farmbuyer@76 1317 end
farmbuyer@76 1318
farmbuyer@73 1319 local random = _G.math.random
farmbuyer@76 1320 local function _many_uniques_handle_it (u, prefix)
farmbuyer@76 1321 if u then
farmbuyer@73 1322 -- Check and alert for an existing value.
farmbuyer@73 1323 u = tostring(u)
farmbuyer@73 1324 if g_uniques[u].history ~= g_uniques.NOTFOUND then
farmbuyer@76 1325 if not g_unique_replace then _setup_unique_replace() end
farmbuyer@76 1326 local maybe = g_unique_replace.get_previous_replacement (u)
farmbuyer@76 1327 if maybe then
farmbuyer@76 1328 addon.dprint('loot',"previous replaced tag ("..u
farmbuyer@76 1329 ..") with ("..maybe.."), using that instead")
farmbuyer@76 1330 return false, u, maybe
farmbuyer@76 1331 end
farmbuyer@76 1332 local can_replace_p,improv = _many_uniques_handle_it (nil, 'c')
farmbuyer@76 1333 if can_replace_p then
farmbuyer@76 1334 _announce_unique_improvisation (u, improv)
farmbuyer@76 1335 return false, u, improv
farmbuyer@76 1336 end
farmbuyer@76 1337 return false, u
farmbuyer@73 1338 end
farmbuyer@73 1339 addon.dprint('loot',"verified unique tag ("..u..")")
farmbuyer@73 1340 else
farmbuyer@87 1341 -- Need to *find* an unused value. For now use a range of J*10^4
farmbuyer@87 1342 -- where J is Jenny's Constant. Thank you, <xkcd.com/1047>.
farmbuyer@76 1343 prefix = prefix or 'n'
farmbuyer@73 1344 repeat
farmbuyer@76 1345 u = prefix .. random(8675309)
farmbuyer@76 1346 until g_uniques:TEST(u).history == g_uniques.NOTFOUND
farmbuyer@76 1347 addon.dprint('loot',"created unique tag ("..u..")")
farmbuyer@73 1348 end
farmbuyer@73 1349 return true, u
farmbuyer@73 1350 end
farmbuyer@73 1351
farmbuyer@1 1352 -- Recent loot cache
farmbuyer@25 1353 local candidates = {}
farmbuyer@73 1354 local sigmap = {}
farmbuyer@73 1355 local function preempt_older_signature (oldersig, newersig)
farmbuyer@76 1356 local origin = candidates[oldersig] and candidates[oldersig].bcast_from
farmbuyer@73 1357 if origin and g_seeing_oldsigs[origin] then
farmbuyer@73 1358 -- replace entry from older client with this newer one
farmbuyer@73 1359 candidates[oldersig] = nil
farmbuyer@76 1360 addon.dprint('loot', "preempting signature <", oldersig, "> from", origin)
farmbuyer@73 1361 end
farmbuyer@73 1362 return false
farmbuyer@73 1363 end
farmbuyer@73 1364
farmbuyer@25 1365 local function prefer_local_loots (cache)
farmbuyer@25 1366 -- The function name is a bit of a misnomer, as local entries overwrite
farmbuyer@25 1367 -- remote entries as the candidate table is populated. This routine is
farmbuyer@39 1368 -- here to extract the final results once the cache timers have expired.
farmbuyer@38 1369 --
farmbuyer@34 1370 -- Keep this sync'd with the local_override branch below.
farmbuyer@25 1371 for i,sig in ipairs(candidates) do
farmbuyer@25 1372 addon.dprint('loot', "processing candidate entry", i, sig)
farmbuyer@25 1373 local loot = candidates[sig]
farmbuyer@25 1374 if loot then
farmbuyer@42 1375 maybe_trash_kill_entry() -- Generate *some* kind of boss/location entry
farmbuyer@25 1376 candidates[sig] = nil
farmbuyer@25 1377 local looti = addon._addLootEntry(loot)
farmbuyer@73 1378 addon.dprint('loot', "entry", i, "was found, added at index", looti)
farmbuyer@25 1379 if (loot.disposition ~= 'shard')
farmbuyer@25 1380 and (loot.disposition ~= 'gvault')
farmbuyer@25 1381 and (not addon.history_suppress)
farmbuyer@25 1382 then
farmbuyer@25 1383 addon:_addHistoryEntry(looti)
farmbuyer@76 1384 elseif #loot.unique > 0 then
farmbuyer@76 1385 g_uniques[loot.unique] = -- stub entry
farmbuyer@76 1386 { loot = looti, history = g_uniques.NOTFOUND }
farmbuyer@25 1387 end
farmbuyer@25 1388 end
farmbuyer@25 1389 end
farmbuyer@25 1390
farmbuyer@25 1391 if addon.display then
farmbuyer@25 1392 addon:redisplay()
farmbuyer@25 1393 end
farmbuyer@47 1394 wipe(candidates)
farmbuyer@73 1395 wipe(sigmap)
farmbuyer@25 1396 end
farmbuyer@73 1397 local recent_loot = create_new_cache ('loot', comm_cleanup_ttl+3, prefer_local_loots)
farmbuyer@1 1398
farmbuyer@73 1399 local strsplit, GetItemInfo, GetItemIcon, UnitClass =
farmbuyer@73 1400 _G.strsplit, _G.GetItemInfo, _G.GetItemIcon, _G.UnitClass
farmbuyer@1 1401
farmbuyer@71 1402 -- 'from' only present if this is triggered by a broadcast
farmbuyer@73 1403 function _do_loot (self, local_override, recipient, unique, itemid, count, from, extratext)
farmbuyer@73 1404 local prefix = "_do_loot[" .. counter() .. "]"
farmbuyer@6 1405 local itexture = GetItemIcon(itemid)
farmbuyer@6 1406 local iname, ilink, iquality = GetItemInfo(itemid)
farmbuyer@73 1407 local cache_miss
farmbuyer@6 1408 if (not iname) or (not itexture) then
farmbuyer@73 1409 cache_miss = true
farmbuyer@6 1410 iname, ilink, iquality, itexture =
farmbuyer@6 1411 UNKNOWN..': '..itemid, 'item:6948', ITEM_QUALITY_COMMON, [[ICONS\INV_Misc_QuestionMark]]
farmbuyer@6 1412 end
farmbuyer@73 1413 self.dprint('loot',">>"..prefix, "R:", recipient, "U:", unique, "I:",
farmbuyer@71 1414 itemid, "C:", count, "frm:", from, "ex:", extratext, "q:", iquality)
farmbuyer@1 1415
farmbuyer@19 1416 itemid = tonumber(ilink:match("item:(%d+)") or 0)
farmbuyer@73 1417
farmbuyer@73 1418 -- This is only a 'while' to make jumping out of it easy.
farmbuyer@76 1419 local i, unique_okay, replacement, ret1, ret2
farmbuyer@73 1420 while local_override
farmbuyer@73 1421 or ((iquality >= self.threshold) and not opts.itemfilter[itemid])
farmbuyer@73 1422 do
farmbuyer@76 1423 unique_okay, unique, replacement =
farmbuyer@76 1424 _many_uniques_handle_it ((not local_override) and unique)
farmbuyer@73 1425 if not unique_okay then
farmbuyer@76 1426 if replacement then
farmbuyer@76 1427 -- collision, but we've generated a placeholder for now
farmbuyer@76 1428 -- and broadcast the fact
farmbuyer@76 1429 self.dprint('loot', "substituting", unique, "with", replacement)
farmbuyer@76 1430 else
farmbuyer@76 1431 i = g_uniques[unique]
farmbuyer@81 1432 local err = unique_collision:format (iname, unique,
farmbuyer@76 1433 tostring(from), tostring(i.loot), tostring(i.history))
farmbuyer@77 1434 _unavoidable_collision (err)
farmbuyer@76 1435 -- Make sure this is logged one way or another
farmbuyer@76 1436 ;(self.debug.loot and self.dprint or pprint)('loot', "COLLISION", prefix, err);
farmbuyer@76 1437 ret1, ret2 = nil, err
farmbuyer@76 1438 break
farmbuyer@76 1439 end
farmbuyer@73 1440 end
farmbuyer@73 1441
farmbuyer@1 1442 if (self.rebroadcast and (not from)) and not local_override then
farmbuyer@71 1443 self:vbroadcast('loot', recipient, unique, itemid, count)
farmbuyer@1 1444 end
farmbuyer@25 1445 if (not self.enabled) and (not local_override) then break end
farmbuyer@73 1446
farmbuyer@73 1447 local oldersig = recipient .. iname .. (count or "")
farmbuyer@73 1448 local signature, seenit
farmbuyer@73 1449 if #unique > 0 then
farmbuyer@73 1450 -- newer case
farmbuyer@73 1451 signature = unique .. oldersig
farmbuyer@73 1452 sigmap[oldersig] = signature
farmbuyer@76 1453 seenit = (from and recent_loot:test(signature))
farmbuyer@73 1454 -- The following clause is what handles older 'casts arriving
farmbuyer@73 1455 -- earlier. All this is tested inside-out to maximize short
farmbuyer@76 1456 -- circuit evaluation; the preempt function always returns
farmbuyer@76 1457 -- false to force seenit off.
farmbuyer@76 1458 or (g_seeing_oldsigs and preempt_older_signature(oldersig,signature))
farmbuyer@25 1459 else
farmbuyer@73 1460 -- older case, only remote
farmbuyer@73 1461 assert(from)
farmbuyer@73 1462 signature = sigmap[oldersig] or oldersig
farmbuyer@73 1463 seenit = recent_loot:test(signature)
farmbuyer@73 1464 end
farmbuyer@73 1465
farmbuyer@73 1466 if seenit then
farmbuyer@80 1467 self.dprint('loot', "remote", prefix, "<", signature,
farmbuyer@73 1468 "> already in cache, skipping from", from)
farmbuyer@73 1469 break
farmbuyer@73 1470 end
farmbuyer@73 1471
farmbuyer@73 1472 -- There is some redundancy in all this, in the interests of ease-of-coding
farmbuyer@73 1473 i = {
farmbuyer@73 1474 kind = 'loot',
farmbuyer@73 1475 person = recipient,
farmbuyer@73 1476 person_class= select(2,UnitClass(recipient)),
farmbuyer@73 1477 cache_miss = cache_miss,
farmbuyer@73 1478 quality = iquality,
farmbuyer@73 1479 itemname = iname,
farmbuyer@73 1480 id = itemid,
farmbuyer@73 1481 itemlink = ilink,
farmbuyer@73 1482 itexture = itexture,
farmbuyer@76 1483 unique = replacement or unique,
farmbuyer@73 1484 count = (count and count ~= "") and count or nil,
farmbuyer@73 1485 bcast_from = from,
farmbuyer@73 1486 extratext = extratext,
farmbuyer@73 1487 variant = self:is_variant_item(ilink),
farmbuyer@73 1488 }
farmbuyer@73 1489 if opts.itemvault[itemid] then
farmbuyer@73 1490 i.disposition = 'gvault'
farmbuyer@73 1491 elseif recipient == self.sharder then
farmbuyer@73 1492 i.disposition = 'shard'
farmbuyer@73 1493 end
farmbuyer@73 1494 if local_override then
farmbuyer@73 1495 -- player is adding loot by hand, don't wait for network cache timeouts
farmbuyer@73 1496 -- keep this sync'd with prefer_local_loots above
farmbuyer@73 1497 if i.extratext == 'shard'
farmbuyer@73 1498 or i.extratext == 'gvault'
farmbuyer@73 1499 or i.extratext == 'offspec'
farmbuyer@73 1500 then
farmbuyer@73 1501 i.disposition = i.extratext
farmbuyer@70 1502 end
farmbuyer@73 1503 local looti = self._addLootEntry(i)
farmbuyer@73 1504 if (i.disposition ~= 'shard')
farmbuyer@73 1505 and (i.disposition ~= 'gvault')
farmbuyer@73 1506 and (not self.history_suppress)
farmbuyer@73 1507 then
farmbuyer@73 1508 self:_addHistoryEntry(looti)
farmbuyer@76 1509 else
farmbuyer@76 1510 g_uniques[i.unique] = -- stub entry
farmbuyer@76 1511 { loot = looti, history = g_uniques.NOTFOUND }
farmbuyer@34 1512 end
farmbuyer@73 1513 ret1 = looti -- return value mostly for gui's manual entry
farmbuyer@76 1514 self.dprint('loot', "manual", looti)
farmbuyer@73 1515 else
farmbuyer@73 1516 recent_loot:add(signature)
farmbuyer@73 1517 candidates[signature] = i
farmbuyer@73 1518 tinsert (candidates, signature)
farmbuyer@80 1519 self.dprint('loot', prefix, "<", signature,
farmbuyer@73 1520 "> added to cache as candidate", #candidates)
farmbuyer@1 1521 end
farmbuyer@25 1522 break
farmbuyer@1 1523 end
farmbuyer@73 1524 self.dprint('loot',"<<"..prefix, "out")
farmbuyer@73 1525 return ret1, ret2
farmbuyer@1 1526 end
farmbuyer@1 1527
farmbuyer@73 1528 -- Returns the index of the resulting new loot entry, or nil after
farmbuyer@73 1529 -- displaying any errors.
farmbuyer@1 1530 function addon:CHAT_MSG_LOOT (event, ...)
farmbuyer@1 1531 if (not self.rebroadcast) and (not self.enabled) and (event ~= "manual") then return end
farmbuyer@1 1532
farmbuyer@1 1533 --[[
farmbuyer@1 1534 iname: Hearthstone
farmbuyer@1 1535 iquality: integer
farmbuyer@1 1536 ilink: clickable formatted link
farmbuyer@1 1537 itemstring: item:6948:....
farmbuyer@1 1538 itexture: inventory icon texture
farmbuyer@1 1539 ]]
farmbuyer@1 1540
farmbuyer@1 1541 if event == "CHAT_MSG_LOOT" then
farmbuyer@1 1542 local msg = ...
farmbuyer@20 1543 local person, itemstring, count
farmbuyer@21 1544 --ChatFrame2:AddMessage("original string: >"..(msg:gsub("\124","\124\124")).."<")
farmbuyer@20 1545
farmbuyer@20 1546 -- test in most likely order: other people get more loot than "you" do
farmbuyer@20 1547 person, itemstring, count = msg:match(g_LOOT_ITEM_MULTIPLE_sss)
farmbuyer@20 1548 if not person then
farmbuyer@20 1549 person, itemstring = msg:match(g_LOOT_ITEM_ss)
farmbuyer@20 1550 end
farmbuyer@20 1551 if not person then
farmbuyer@20 1552 itemstring, count = msg:match(g_LOOT_ITEM_SELF_MULTIPLE_ss)
farmbuyer@20 1553 if not itemstring then
farmbuyer@20 1554 itemstring = msg:match(g_LOOT_ITEM_SELF_s)
farmbuyer@20 1555 end
farmbuyer@20 1556 end
farmbuyer@20 1557
farmbuyer@89 1558 if not itemstring then return end -- "PlayerX selected Greed", etc, not looting
farmbuyer@65 1559 self.dprint('loot', "CHAT_MSG_LOOT, person is", person,
farmbuyer@65 1560 ", itemstring is", itemstring, ", count is", count)
farmbuyer@1 1561
farmbuyer@1 1562 -- Name might be colorized, remove the highlighting
farmbuyer@20 1563 if person then
farmbuyer@20 1564 person = person:match("|c%x%x%x%x%x%x%x%x(%S+)") or person
farmbuyer@20 1565 else
farmbuyer@38 1566 person = my_name -- UNIT_YOU / You
farmbuyer@20 1567 end
farmbuyer@1 1568
farmbuyer@71 1569 --local id = tonumber(itemstring:match('|Hitem:(%d+):'))
farmbuyer@71 1570 local id,unique,_
farmbuyer@71 1571 _,id,_,_,_,_,_,_,unique = strsplit (":", itemstring)
farmbuyer@71 1572 if unique == 0 then unique = nil end
farmbuyer@1 1573
farmbuyer@73 1574 return _do_loot (self, false, person, unique, id, count)
farmbuyer@1 1575
farmbuyer@1 1576 elseif event == "broadcast" then
farmbuyer@73 1577 return _do_loot(self, false, ...)
farmbuyer@1 1578
farmbuyer@1 1579 elseif event == "manual" then
farmbuyer@1 1580 local r,i,n = ...
farmbuyer@73 1581 return _do_loot(self, true, r, --[[unique=]]nil, i,
farmbuyer@71 1582 --[[count=]]nil, --[[from=]]nil, n)
farmbuyer@1 1583 end
farmbuyer@1 1584 end
farmbuyer@1 1585 end
farmbuyer@1 1586
farmbuyer@80 1587 -- This only triggers on entering combat after a registered boss kill.
farmbuyer@80 1588 -- Clearing this field forces subsequent trash kills to generate an entry
farmbuyer@80 1589 -- via maybe_trash_kill_entry.
farmbuyer@80 1590 -- (Possibly what is wanted is to start a 3 or 5 minute timer, and *then*
farmbuyer@80 1591 -- look for the next combat?)
farmbuyer@80 1592 function addon:PLAYER_REGEN_DISABLED()
farmbuyer@80 1593 self:UnregisterEvent ("PLAYER_REGEN_DISABLED")
farmbuyer@80 1594 self.latest_instance = nil
farmbuyer@80 1595 end
farmbuyer@80 1596
farmbuyer@1 1597
farmbuyer@1 1598 ------ Slash command handler
farmbuyer@1 1599 -- Thought about breaking this up into a table-driven dispatcher. But
farmbuyer@1 1600 -- that would result in a pile of teensy functions, most of which would
farmbuyer@1 1601 -- never be called. Too much overhead. (2.0: Most of these removed now
farmbuyer@1 1602 -- that GUI is in place.)
farmbuyer@76 1603 do
farmbuyer@76 1604 local green_help_link = addon.format_hypertext ([[Click here]],
farmbuyer@76 1605 ITEM_QUALITY_UNCOMMON, 'help')
farmbuyer@76 1606 function addon:OnSlash (txt) --, editbox)
farmbuyer@76 1607 txt = strtrim(txt:lower())
farmbuyer@76 1608 local cmd, arg = ""
farmbuyer@76 1609 do
farmbuyer@76 1610 local s,e = txt:find("^%a+")
farmbuyer@76 1611 if s then
farmbuyer@76 1612 cmd = txt:sub(s,e)
farmbuyer@76 1613 s = txt:find("%S", e+2)
farmbuyer@76 1614 if s then arg = txt:sub(s,-1) end
farmbuyer@76 1615 end
farmbuyer@1 1616 end
farmbuyer@1 1617
farmbuyer@76 1618 if cmd == "" then
farmbuyer@76 1619 if InCombatLockdown() then
farmbuyer@76 1620 return self:Print("Shouldn't display window in combat.")
farmbuyer@76 1621 else
farmbuyer@76 1622 return self:BuildMainDisplay()
farmbuyer@76 1623 end
farmbuyer@1 1624
farmbuyer@76 1625 elseif cmd:find("^thre") then
farmbuyer@76 1626 self:SetThreshold(arg)
farmbuyer@1 1627
farmbuyer@76 1628 elseif cmd == "on" then self:Activate(arg)
farmbuyer@76 1629 elseif cmd == "off" then self:Deactivate()
farmbuyer@76 1630 elseif cmd == "broadcast" or cmd == "bcast" then self:Activate(nil,true)
farmbuyer@76 1631
farmbuyer@76 1632 elseif cmd == "toggle" then
farmbuyer@76 1633 if self.display then
farmbuyer@76 1634 self.display:Hide()
farmbuyer@76 1635 else
farmbuyer@76 1636 return self:BuildMainDisplay()
farmbuyer@76 1637 end
farmbuyer@76 1638
farmbuyer@76 1639 elseif cmd == "fake" then -- maybe comment this out for real users
farmbuyer@76 1640 self:_mark_boss_kill (self._addBossEntry{
farmbuyer@76 1641 kind='boss',reason='kill',bossname="Baron Steamroller",duration=0
farmbuyer@76 1642 })
farmbuyer@76 1643 self:CHAT_MSG_LOOT ('manual', my_name, 54797)
farmbuyer@76 1644 if self.display then
farmbuyer@76 1645 self:redisplay()
farmbuyer@76 1646 end
farmbuyer@76 1647 self:Print "Baron Steamroller has been slain. Congratulations on your rug."
farmbuyer@76 1648
farmbuyer@76 1649 elseif cmd == "debug" then
farmbuyer@76 1650 if arg then
farmbuyer@100 1651 self.is_guilded = _G.IsInGuild()
farmbuyer@76 1652 self.debug[arg] = not self.debug[arg]
farmbuyer@76 1653 _G.print(arg,self.debug[arg])
farmbuyer@76 1654 if self.debug[arg] then self.DEBUG_PRINT = true end
farmbuyer@76 1655 else
farmbuyer@76 1656 self.DEBUG_PRINT = not self.DEBUG_PRINT
farmbuyer@76 1657 end
farmbuyer@76 1658
farmbuyer@76 1659 elseif cmd == "save" and arg and arg:len() > 0 then
farmbuyer@76 1660 self:save_saveas(arg)
farmbuyer@76 1661 elseif cmd == "list" then
farmbuyer@76 1662 self:save_list()
farmbuyer@76 1663 elseif cmd == "restore" and arg and arg:len() > 0 then
farmbuyer@76 1664 self:save_restore(tonumber(arg))
farmbuyer@76 1665 elseif cmd == "delete" and arg and arg:len() > 0 then
farmbuyer@76 1666 self:save_delete(tonumber(arg))
farmbuyer@76 1667
farmbuyer@76 1668 elseif cmd == "help" then
farmbuyer@76 1669 self:BuildMainDisplay('help')
farmbuyer@76 1670 elseif cmd == "ping" then
farmbuyer@76 1671 self:DoPing()
farmbuyer@76 1672
farmbuyer@81 1673 elseif cmd == "fix" then
farmbuyer@81 1674 if arg == "?" then
farmbuyer@95 1675 self:Print[['/ouroloot fix cache' updates loot that wasn't in the cache]]
farmbuyer@95 1676 self:Print[['/ouroloot fix history' repairs inconsistent data on the History tab]]
farmbuyer@95 1677 self:Print[['/ouroloot fix' changes no stored data, only allows the window to be displayed again (this is built into all fixes above)]]
farmbuyer@81 1678 return
farmbuyer@81 1679 elseif arg == "cache" then
farmbuyer@81 1680 self:do_item_cache_fixup()
farmbuyer@81 1681 elseif arg == "history" then
farmbuyer@81 1682 self:repair_history_integrity()
farmbuyer@81 1683 end
farmbuyer@81 1684 self.NOLOAD = nil
farmbuyer@81 1685 self:Print("Window unlocked, best of luck.")
farmbuyer@76 1686
farmbuyer@23 1687 else
farmbuyer@88 1688 if self:OpenMainDisplayToTab(cmd,arg) then
farmbuyer@76 1689 return
farmbuyer@76 1690 end
farmbuyer@76 1691 self:Print("Unknown command '%s'. %s to see the help window.",
farmbuyer@76 1692 cmd, tostring(green_help_link))
farmbuyer@23 1693 end
farmbuyer@1 1694 end
farmbuyer@1 1695 end
farmbuyer@1 1696
farmbuyer@1 1697 function addon:SetThreshold (arg, quiet_p)
farmbuyer@1 1698 local q = tonumber(arg)
farmbuyer@1 1699 if q then
farmbuyer@76 1700 q = math.floor(q+0.1)
farmbuyer@1 1701 if q<0 or q>6 then
farmbuyer@1 1702 return self:Print("Threshold must be 0-6.")
farmbuyer@1 1703 end
farmbuyer@1 1704 else
farmbuyer@1 1705 q = qualnames[arg]
farmbuyer@1 1706 if not q then
farmbuyer@1 1707 return self:Print("Unrecognized item quality argument.")
farmbuyer@1 1708 end
farmbuyer@1 1709 end
farmbuyer@1 1710 self.threshold = q
farmbuyer@1 1711 if not quiet_p then self:Print("Threshold now set to %s.", self.thresholds[q]) end
farmbuyer@1 1712 end
farmbuyer@1 1713
farmbuyer@1 1714
farmbuyer@1 1715 ------ On/off
farmbuyer@80 1716 -- Both of these need to be (effectively) idempotent.
farmbuyer@1 1717 function addon:Activate (opt_threshold, opt_bcast_only)
farmbuyer@16 1718 self.dprint('flow', ":Activate is running")
farmbuyer@90 1719 self:RegisterEvent(RAID_ROSTER_UPDATE_EVENT,"RAID_ROSTER_UPDATE")
farmbuyer@16 1720 self:RegisterEvent("PLAYER_ENTERING_WORLD",
farmbuyer@16 1721 function() self:ScheduleTimer("RAID_ROSTER_UPDATE", 5, "PLAYER_ENTERING_WORLD") end)
farmbuyer@1 1722 self.popped = true
farmbuyer@93 1723 if IsInRaid() then
farmbuyer@16 1724 self.dprint('flow', ">:Activate calling RRU")
farmbuyer@1 1725 self:RAID_ROSTER_UPDATE("Activate")
farmbuyer@1 1726 elseif self.debug.notraid then
farmbuyer@76 1727 self.dprint('flow', ">:(notraid) Activate registering loot and bossmods")
farmbuyer@10 1728 self:RegisterEvent("CHAT_MSG_LOOT")
farmbuyer@2 1729 _register_bossmod(self)
farmbuyer@1 1730 elseif g_restore_p then
farmbuyer@1 1731 g_restore_p = nil
farmbuyer@16 1732 self.popped = nil -- get the reminder if later joining a raid
farmbuyer@16 1733 if #g_loot == 0 then
farmbuyer@16 1734 -- only generated text and raider join/leave data, not worth verbage
farmbuyer@16 1735 self.dprint('flow', ">:Activate restored generated texts, un-popping")
farmbuyer@16 1736 return
farmbuyer@16 1737 end
farmbuyer@78 1738 self:Print("Restored previous data, but not in a raid",
farmbuyer@48 1739 "and 5-player mode not active. |cffff0505NOT tracking loot|r;",
farmbuyer@1 1740 "use 'enable' to activate loot tracking, or 'clear' to erase",
farmbuyer@1 1741 "previous data, or 'help' to read about saved-texts commands.")
farmbuyer@1 1742 return
farmbuyer@1 1743 end
farmbuyer@1 1744 self.rebroadcast = true -- hardcode to true; this used to be more complicated
farmbuyer@1 1745 self.enabled = not opt_bcast_only
farmbuyer@73 1746 g_seeing_oldsigs = nil
farmbuyer@1 1747 if opt_threshold then
farmbuyer@1 1748 self:SetThreshold (opt_threshold, --[[quiet_p=]]true)
farmbuyer@1 1749 end
farmbuyer@100 1750 self:Fire ('Activate', self.enabled, self.rebroadcast, self.threshold)
farmbuyer@80 1751 self:Print("Now %s; threshold currently %s.",
farmbuyer@1 1752 self.enabled and "tracking" or "only broadcasting",
farmbuyer@1 1753 self.thresholds[self.threshold])
farmbuyer@89 1754 self:broadcast('revcheck',version_large)
farmbuyer@1 1755 end
farmbuyer@1 1756
farmbuyer@95 1757 -- Note: running '/ouroloot off' will also avoid the popup reminder when
farmbuyer@1 1758 -- joining a raid, but will not change the saved option setting.
farmbuyer@1 1759 function addon:Deactivate()
farmbuyer@1 1760 self.enabled = false
farmbuyer@1 1761 self.rebroadcast = false
farmbuyer@90 1762 self:UnregisterEvent(RAID_ROSTER_UPDATE_EVENT)
farmbuyer@10 1763 self:UnregisterEvent("PLAYER_ENTERING_WORLD")
farmbuyer@10 1764 self:UnregisterEvent("CHAT_MSG_LOOT")
farmbuyer@100 1765 self:Fire ('Deactivate')
farmbuyer@80 1766 self:Print("Deactivated.")
farmbuyer@1 1767 end
farmbuyer@1 1768
farmbuyer@1 1769 function addon:Clear(verbose_p)
farmbuyer@1 1770 local repopup, st
farmbuyer@1 1771 if self.display then
farmbuyer@1 1772 -- in the new version, this is likely to always be the case
farmbuyer@1 1773 repopup = true
farmbuyer@96 1774 st = self.display:GetUserData("GUI state").eoiST
farmbuyer@1 1775 if not st then
farmbuyer@1 1776 self.dprint('flow', "Clear: display visible but eoiST not set??")
farmbuyer@1 1777 end
farmbuyer@1 1778 self.display:Hide()
farmbuyer@1 1779 end
farmbuyer@1 1780 g_restore_p = nil
farmbuyer@73 1781 _G.OuroLootSV = nil
farmbuyer@1 1782 self:_reset_timestamps()
farmbuyer@1 1783 if verbose_p then
farmbuyer@73 1784 if (_G.OuroLootSV_saved and #_G.OuroLootSV_saved>0) then
farmbuyer@73 1785 self:Print("Current loot data cleared, %d saved sets remaining.", #_G.OuroLootSV_saved)
farmbuyer@1 1786 else
farmbuyer@1 1787 self:Print("Current loot data cleared.")
farmbuyer@1 1788 end
farmbuyer@1 1789 end
farmbuyer@1 1790 _init(self,st)
farmbuyer@100 1791 self:Fire ('Reset')
farmbuyer@1 1792 if repopup then
farmbuyer@1 1793 addon:BuildMainDisplay()
farmbuyer@1 1794 end
farmbuyer@1 1795 end
farmbuyer@1 1796
farmbuyer@1 1797
farmbuyer@1 1798 ------ Behind the scenes routines
farmbuyer@19 1799 -- Semi-experimental debugging aid.
farmbuyer@19 1800 do
farmbuyer@76 1801 -- Declaring _log as local to here can result in this sequence:
farmbuyer@41 1802 -- 1) logging happens, followed by reload or logout/login
farmbuyer@41 1803 -- 2) _log points to SV_log
farmbuyer@41 1804 -- 3) VARIABLES_LOADED replaces SV_log pointer with restored version
farmbuyer@41 1805 -- 4) logging happens to _log table (now with no other references)
farmbuyer@41 1806 -- 5) at logout, nothing new has been entered in the table being saved
farmbuyer@19 1807 local date = _G.date
farmbuyer@19 1808 function addon:log_with_timestamp (msg)
farmbuyer@73 1809 tinsert (_log, date('%m/%d %H:%M:%S ')..msg)
farmbuyer@19 1810 end
farmbuyer@19 1811 end
farmbuyer@19 1812
farmbuyer@49 1813 -- Check for plugins which haven't already been loaded, and add hooks for
farmbuyer@49 1814 -- them. Credit to DBM for the approach here.
farmbuyer@49 1815 function addon:_scan_LOD_modules()
farmbuyer@49 1816 for i = 1, GetNumAddOns() do
farmbuyer@49 1817 if GetAddOnMetadata (i, "X-OuroLoot-Plugin")
farmbuyer@49 1818 and IsAddOnLoadOnDemand(i)
farmbuyer@49 1819 and not IsAddOnLoaded(i)
farmbuyer@49 1820 then
farmbuyer@49 1821 local folder, _, _, enabled, _, reason = GetAddOnInfo(i)
farmbuyer@57 1822 if enabled or opts.display_disabled_LODs then
farmbuyer@57 1823 local tabtitle = GetAddOnMetadata (i, "X-OuroLoot-Plugin")
farmbuyer@57 1824 self:_gui_add_LOD_tab (tabtitle, folder, i, enabled, reason)
farmbuyer@57 1825 end
farmbuyer@49 1826 end
farmbuyer@49 1827 end
farmbuyer@49 1828 end
farmbuyer@49 1829
farmbuyer@73 1830 -- Routines for printing changes made by remote users.
farmbuyer@73 1831 do
farmbuyer@101 1832 local change_chatframe
farmbuyer@101 1833
farmbuyer@101 1834 function addon:_set_chatty_change_chatframe (arg, silent_p)
farmbuyer@73 1835 local frame
farmbuyer@73 1836 if type(arg) == 'number' then
farmbuyer@73 1837 arg = _G.math.min (arg, _G.NUM_CHAT_WINDOWS)
farmbuyer@73 1838 frame = _G['ChatFrame'..arg]
farmbuyer@73 1839 elseif type(arg) == 'string' then
farmbuyer@73 1840 frame = _G[arg]
farmbuyer@73 1841 end
farmbuyer@73 1842 if type(frame) == 'table' and type(frame.AddMessage) == 'function' then
farmbuyer@101 1843 change_chatframe = frame
farmbuyer@73 1844 if not silent_p then
farmbuyer@73 1845 local msg = "Now printing to chat frame " .. arg
farmbuyer@73 1846 if frame.GetName then
farmbuyer@73 1847 msg = msg .. " (" .. tostring(frame:GetName()) .. ")"
farmbuyer@73 1848 end
farmbuyer@73 1849 self:Print(msg)
farmbuyer@73 1850 if frame ~= _G.DEFAULT_CHAT_FRAME then
farmbuyer@73 1851 self:CFPrint (frame, msg)
farmbuyer@73 1852 end
farmbuyer@73 1853 end
farmbuyer@73 1854 return frame
farmbuyer@73 1855 else
farmbuyer@73 1856 self:Print("'%s' was not a valid chat frame number/name, no change has been made.", arg)
farmbuyer@73 1857 end
farmbuyer@73 1858 end
farmbuyer@73 1859
farmbuyer@101 1860 local function _notify (chatframe, source, index, olddisp, from_whom, from_class)
farmbuyer@73 1861 local e = g_loot[index]
farmbuyer@80 1862 if not e then
farmbuyer@80 1863 -- how did this happen?
farmbuyer@80 1864 return
farmbuyer@80 1865 end
farmbuyer@89 1866 if source == my_name then
farmbuyer@89 1867 source = _G.UNIT_YOU
farmbuyer@89 1868 end
farmbuyer@86 1869 local from_text, to_text
farmbuyer@73 1870 if from_whom then
farmbuyer@89 1871 if from_whom == my_name then
farmbuyer@89 1872 from_whom = _G.UNIT_YOU
farmbuyer@89 1873 end
farmbuyer@86 1874 from_text = addon:colorize (from_whom, from_class)
farmbuyer@89 1875 to_text = e.person == my_name and _G.UNIT_YOU or e.person
farmbuyer@89 1876 to_text = addon:colorize (to_text, e.person_class)
farmbuyer@73 1877 else
farmbuyer@73 1878 if olddisp then
farmbuyer@73 1879 from_text = addon.disposition_colors[olddisp].text
farmbuyer@73 1880 else
farmbuyer@73 1881 olddisp = "normal"
farmbuyer@73 1882 from_text = "normal"
farmbuyer@73 1883 end
farmbuyer@86 1884 from_text = addon.disposition_colors[olddisp].hex
farmbuyer@86 1885 .. from_text .. "|r"
farmbuyer@86 1886
farmbuyer@73 1887 if e.disposition then
farmbuyer@73 1888 to_text = addon.disposition_colors[e.disposition].text
farmbuyer@73 1889 else
farmbuyer@73 1890 to_text = "normal"
farmbuyer@73 1891 end
farmbuyer@86 1892 to_text = addon.disposition_colors[e.disposition or "normal"].hex
farmbuyer@86 1893 .. to_text .. "|r"
farmbuyer@73 1894 end
farmbuyer@73 1895
farmbuyer@81 1896 addon.dprint ('loot', "notification:", source, index,
farmbuyer@86 1897 e.itemlink, from_text, to_text)
farmbuyer@81 1898 addon:CFPrint (chatframe, remote_chatty, source, index,
farmbuyer@86 1899 e.itemlink, from_text, to_text)
farmbuyer@73 1900 end
farmbuyer@81 1901
farmbuyer@101 1902 function _notify_about_change (sender, index, olddisp, from_whom, from_class)
farmbuyer@101 1903 _notify (change_chatframe, sender, index, olddisp, from_whom, from_class)
farmbuyer@81 1904 end
farmbuyer@73 1905 end
farmbuyer@73 1906
farmbuyer@1 1907 -- Adds indices to traverse the tables in a nice sorted order.
farmbuyer@1 1908 do
farmbuyer@1 1909 local byindex, temp = {}, {}
farmbuyer@1 1910 local function sort (src, dest)
farmbuyer@1 1911 for k in pairs(src) do
farmbuyer@1 1912 temp[#temp+1] = k
farmbuyer@1 1913 end
farmbuyer@73 1914 _G.table.sort(temp)
farmbuyer@47 1915 wipe(dest)
farmbuyer@1 1916 for i = 1, #temp do
farmbuyer@1 1917 dest[i] = src[temp[i]]
farmbuyer@1 1918 end
farmbuyer@1 1919 end
farmbuyer@1 1920
farmbuyer@1 1921 function addon.sender_list.sort()
farmbuyer@1 1922 sort (addon.sender_list.active, byindex)
farmbuyer@47 1923 wipe(temp)
farmbuyer@1 1924 addon.sender_list.activeI = #byindex
farmbuyer@1 1925 sort (addon.sender_list.names, byindex)
farmbuyer@47 1926 wipe(temp)
farmbuyer@1 1927 end
farmbuyer@1 1928 addon.sender_list.namesI = byindex
farmbuyer@1 1929 end
farmbuyer@1 1930
farmbuyer@23 1931 function addon:DoPing()
farmbuyer@23 1932 self:Print("Give me a ping, Vasili. One ping only, please.")
farmbuyer@23 1933 self.sender_list.active = {}
farmbuyer@23 1934 self.sender_list.names = {}
farmbuyer@23 1935 self:broadcast('ping')
farmbuyer@89 1936 self:broadcast('revcheck',version_large)
farmbuyer@27 1937 end
farmbuyer@27 1938
farmbuyer@89 1939 function addon:_check_version (otherrev)
farmbuyer@93 1940 self.dprint('comm', version_large, "revchecking against", otherrev)
farmbuyer@27 1941 otherrev = tonumber(otherrev)
farmbuyer@89 1942 if otherrev == version_large then
farmbuyer@27 1943 -- normal case
farmbuyer@27 1944
farmbuyer@89 1945 elseif otherrev < version_large then
farmbuyer@27 1946 self.dprint('comm', "ours is newer, notifying")
farmbuyer@89 1947 self:broadcast('revcheck',version_large)
farmbuyer@27 1948
farmbuyer@27 1949 else
farmbuyer@76 1950 self.dprint('comm', "ours is older, (possibly) yammering")
farmbuyer@27 1951 if newer_warning then
farmbuyer@76 1952 local pop = addon.format_hypertext ([[click here]], ITEM_QUALITY_UNCOMMON,
farmbuyer@76 1953 function()
farmbuyer@92 1954 -- Sadly, this is not generated by the packager, so hardcode it
farmbuyer@92 1955 -- for now. The 'data' field is handled differently for onshow
farmbuyer@92 1956 -- than for other callbacks.
farmbuyer@92 1957 StaticPopup_Show("OUROL_URL",
farmbuyer@92 1958 --[[text_arg1=]]nil, --[[text_arg2=]]nil,
farmbuyer@76 1959 --[[data=]][[http://www.curse.com/addons/wow/ouroloot]])
farmbuyer@76 1960 end)
farmbuyer@76 1961 local ping = addon.format_hypertext ([[click here]], ITEM_QUALITY_UNCOMMON, 'DoPing')
farmbuyer@76 1962 self:Print(newer_warning, tostring(pop), tostring(ping))
farmbuyer@27 1963 newer_warning = nil
farmbuyer@27 1964 end
farmbuyer@27 1965 end
farmbuyer@23 1966 end
farmbuyer@23 1967
farmbuyer@1 1968 -- Generic helpers
farmbuyer@28 1969 -- Returns index and entry at that index, or nil if not found.
farmbuyer@1 1970 function addon._find_next_after (kind, index)
farmbuyer@1 1971 index = index + 1
farmbuyer@1 1972 while index <= #g_loot do
farmbuyer@1 1973 if g_loot[index].kind == kind then
farmbuyer@1 1974 return index, g_loot[index]
farmbuyer@1 1975 end
farmbuyer@1 1976 index = index + 1
farmbuyer@1 1977 end
farmbuyer@1 1978 end
farmbuyer@28 1979 -- Essentially a _find_next_after('time'-or-'boss'), but if KIND is
farmbuyer@28 1980 -- 'boss', will also stop upon finding a timestamp. Returns nil if
farmbuyer@28 1981 -- appropriate fencepost is not found.
farmbuyer@28 1982 function addon._find_timeboss_fencepost (kind, index)
farmbuyer@28 1983 local fencepost
farmbuyer@28 1984 local closest_time = addon._find_next_after('time',index)
farmbuyer@28 1985 if kind == 'time' then
farmbuyer@28 1986 fencepost = closest_time
farmbuyer@28 1987 elseif kind == 'boss' then
farmbuyer@28 1988 local closest_boss = addon._find_next_after('boss',index)
farmbuyer@28 1989 if not closest_boss then
farmbuyer@28 1990 fencepost = closest_time
farmbuyer@28 1991 elseif not closest_time then
farmbuyer@28 1992 fencepost = closest_boss
farmbuyer@28 1993 else
farmbuyer@73 1994 fencepost = _G.math.min(closest_time,closest_boss)
farmbuyer@28 1995 end
farmbuyer@28 1996 end
farmbuyer@28 1997 return fencepost
farmbuyer@28 1998 end
farmbuyer@1 1999
farmbuyer@1 2000 -- Iterate through g_loot entries according to the KIND field. Loop variables
farmbuyer@1 2001 -- are g_loot indices and the corresponding entries (essentially ipairs + some
farmbuyer@1 2002 -- conditionals).
farmbuyer@1 2003 function addon:filtered_loot_iter (filter_kind)
farmbuyer@1 2004 return self._find_next_after, filter_kind, 0
farmbuyer@1 2005 end
farmbuyer@1 2006
farmbuyer@1 2007 do
farmbuyer@1 2008 local itt
farmbuyer@1 2009 local function create()
farmbuyer@1 2010 local tip, lefts = CreateFrame("GameTooltip"), {}
farmbuyer@1 2011 for i = 1, 2 do -- scanning idea here also snagged from Talented
farmbuyer@1 2012 local L,R = tip:CreateFontString(), tip:CreateFontString()
farmbuyer@1 2013 L:SetFontObject(GameFontNormal)
farmbuyer@1 2014 R:SetFontObject(GameFontNormal)
farmbuyer@1 2015 tip:AddFontStrings(L,R)
farmbuyer@1 2016 lefts[i] = L
farmbuyer@1 2017 end
farmbuyer@1 2018 tip.lefts = lefts
farmbuyer@1 2019 return tip
farmbuyer@1 2020 end
farmbuyer@66 2021 function addon:is_variant_item(item) -- returns number or *nil*
farmbuyer@1 2022 itt = itt or create()
farmbuyer@1 2023 itt:SetOwner(UIParent,"ANCHOR_NONE")
farmbuyer@1 2024 itt:ClearLines()
farmbuyer@1 2025 itt:SetHyperlink(item)
farmbuyer@1 2026 local t = itt.lefts[2]:GetText()
farmbuyer@1 2027 itt:Hide()
farmbuyer@66 2028 return (t == ITEM_HEROIC and 1)
farmbuyer@66 2029 or (t == RAID_FINDER and 2) -- no ITEM_ for this, apparently
farmbuyer@66 2030 or nil
farmbuyer@1 2031 end
farmbuyer@1 2032 end
farmbuyer@1 2033
farmbuyer@79 2034 -- Called at the end of OnInit, and then also when a 'clear' is being
farmbuyer@16 2035 -- performed. If SV's are present then g_restore_p will be true.
farmbuyer@1 2036 function _init (self, possible_st)
farmbuyer@1 2037 self.dprint('flow',"_init running")
farmbuyer@1 2038 self.loot_clean = nil
farmbuyer@1 2039 self.hist_clean = nil
farmbuyer@1 2040 if g_restore_p then
farmbuyer@73 2041 g_loot = _G.OuroLootSV
farmbuyer@16 2042 self.popped = #g_loot > 0
farmbuyer@1 2043 self.dprint('flow', "restoring", #g_loot, "entries")
farmbuyer@100 2044 _do_loot_metas()
farmbuyer@16 2045 self:ScheduleTimer("Activate", 12, opts.threshold)
farmbuyer@1 2046 -- FIXME printed could be too large if entries were deleted, how much do we care?
farmbuyer@16 2047 self.sharder = opts.autoshard
farmbuyer@1 2048 else
farmbuyer@80 2049 g_loot = {}
farmbuyer@80 2050 end
farmbuyer@80 2051 if type(g_loot.raiders) ~= 'table' then
farmbuyer@80 2052 g_loot.raiders = {}
farmbuyer@80 2053 end
farmbuyer@80 2054 if type(g_loot.printed) ~= 'table' then
farmbuyer@80 2055 g_loot.printed = {}
farmbuyer@1 2056 end
farmbuyer@1 2057
farmbuyer@16 2058 self.threshold = opts.threshold or self.threshold -- in the case of restoring but not tracking
farmbuyer@79 2059 self:gui_init (g_loot, g_uniques)
farmbuyer@16 2060 opts.autoshard = nil
farmbuyer@16 2061 opts.threshold = nil
farmbuyer@1 2062
farmbuyer@1 2063 if g_restore_p then
farmbuyer@73 2064 self:zero_printed_fenceposts() -- g_loot.printed.* = previous/safe values
farmbuyer@1 2065 else
farmbuyer@73 2066 self:zero_printed_fenceposts(0) -- g_loot.printed.* = 0
farmbuyer@1 2067 end
farmbuyer@1 2068 if possible_st then
farmbuyer@1 2069 possible_st:SetData(g_loot)
farmbuyer@1 2070 end
farmbuyer@1 2071
farmbuyer@89 2072 self.status_text = ("%s(r%s) communicating as ident %s commrev %s"):
farmbuyer@89 2073 format (self.version, self.revision, self.ident, self.commrev)
farmbuyer@1 2074 self:RegisterComm(self.ident)
farmbuyer@1 2075 self:RegisterComm(self.identTg, "OnCommReceivedNocache")
farmbuyer@1 2076
farmbuyer@1 2077 if self.author_debug then
farmbuyer@1 2078 _G.Oloot = g_loot
farmbuyer@1 2079 end
farmbuyer@1 2080 end
farmbuyer@1 2081
farmbuyer@61 2082 -- Raid roster snapshots
farmbuyer@61 2083 do
farmbuyer@61 2084 function addon:snapshot_raid (only_inraid_p)
farmbuyer@61 2085 local ss = CopyTable(g_loot.raiders)
farmbuyer@61 2086 local instance,maxsize = instance_tag()
farmbuyer@61 2087 if only_inraid_p then
farmbuyer@73 2088 for name,info in _G.next, ss do
farmbuyer@61 2089 if info.online == 3 then
farmbuyer@61 2090 ss[name] = nil
farmbuyer@61 2091 end
farmbuyer@61 2092 end
farmbuyer@61 2093 end
farmbuyer@73 2094 return ss, maxsize, instance, _G.time()
farmbuyer@61 2095 end
farmbuyer@61 2096 end
farmbuyer@61 2097
farmbuyer@25 2098 -- Tie-in with Deadly Boss Mods (or other such addons)
farmbuyer@1 2099 do
farmbuyer@25 2100 local candidates = {}
farmbuyer@25 2101 local location
farmbuyer@1 2102 local function fixup_durations (cache)
farmbuyer@1 2103 local boss, bossi
farmbuyer@1 2104 boss = candidates[1]
farmbuyer@1 2105 if #candidates == 1 then
farmbuyer@1 2106 -- (1) or (2)
farmbuyer@1 2107 boss.duration = boss.duration or 0
farmbuyer@19 2108 addon.dprint('loot', "only one boss candidate")
farmbuyer@1 2109 else
farmbuyer@1 2110 -- (3), should only be one 'cast entry and our local entry
farmbuyer@1 2111 if #candidates ~= 2 then
farmbuyer@1 2112 -- could get a bunch of 'cast entries on the heels of one another
farmbuyer@1 2113 -- before the local one ever fires, apparently... sigh
farmbuyer@1 2114 --addon:Print("<warning> s3 cache has %d entries, does that seem right to you?", #candidates)
farmbuyer@1 2115 end
farmbuyer@1 2116 if candidates[2].duration == nil then
farmbuyer@1 2117 --addon:Print("<warning> s3's second entry is not the local trigger, does that seem right to you?")
farmbuyer@1 2118 end
farmbuyer@1 2119 -- try and be generic anyhow
farmbuyer@1 2120 for i,c in ipairs(candidates) do
farmbuyer@1 2121 if c.duration then
farmbuyer@1 2122 boss = c
farmbuyer@19 2123 addon.dprint('loot', "fixup found boss candidate", i, "duration", c.duration)
farmbuyer@1 2124 break
farmbuyer@1 2125 end
farmbuyer@1 2126 end
farmbuyer@1 2127 end
farmbuyer@69 2128 bossi = addon._addBossEntry(boss)
farmbuyer@100 2129 addon:Fire ('NewBoss', boss)
farmbuyer@19 2130 bossi = addon._adjustBossOrder (bossi, g_boss_signpost) or bossi
farmbuyer@16 2131 g_boss_signpost = nil
farmbuyer@42 2132 addon.latest_instance = boss.instance
farmbuyer@19 2133 addon.dprint('loot', "added boss entry", bossi)
farmbuyer@1 2134 if boss.reason == 'kill' then
farmbuyer@80 2135 addon:RegisterEvent ("PLAYER_REGEN_DISABLED")
farmbuyer@1 2136 addon:_mark_boss_kill (bossi)
farmbuyer@1 2137 if opts.chatty_on_kill then
farmbuyer@55 2138 addon:Print("Registered kill for '%s' in %s!", boss.bossname, boss.instance)
farmbuyer@1 2139 end
farmbuyer@1 2140 end
farmbuyer@47 2141 wipe(candidates)
farmbuyer@1 2142 end
farmbuyer@73 2143 local recent_boss = create_new_cache ('boss', 10, fixup_durations)
farmbuyer@1 2144
farmbuyer@1 2145 -- Similar to _do_loot, but duration+ parms only present when locally generated.
farmbuyer@56 2146 local function _do_boss (self, reason, bossname, intag, maxsize, duration)
farmbuyer@56 2147 self.dprint('loot',">>_do_boss, R:", reason, "B:", bossname,
farmbuyer@56 2148 "T:", intag, "MS:", maxsize, "D:", duration)
farmbuyer@1 2149 if self.rebroadcast and duration then
farmbuyer@56 2150 self:vbroadcast('boss', reason, bossname, intag, maxsize)
farmbuyer@1 2151 end
farmbuyer@1 2152 -- This is only a loop to make jumping out of it easy, and still do cleanup below.
farmbuyer@1 2153 while self.enabled do
farmbuyer@1 2154 if reason == 'wipe' and opts.no_tracking_wipes then break end
farmbuyer@1 2155 bossname = (opts.snarky_boss and self.boss_abbrev[bossname] or bossname) or bossname
farmbuyer@1 2156 local not_from_local = duration == nil
farmbuyer@1 2157 local signature = bossname .. reason
farmbuyer@73 2158 if not_from_local and recent_boss:test(signature) then
farmbuyer@39 2159 self.dprint('cache', "remote boss <",signature,"> already in cache, skipping")
farmbuyer@1 2160 else
farmbuyer@73 2161 recent_boss:add(signature)
farmbuyer@16 2162 g_boss_signpost = #g_loot + 1
farmbuyer@19 2163 self.dprint('loot', "added boss signpost", g_boss_signpost)
farmbuyer@1 2164 -- Possible scenarios: (1) we don't see a boss event at all (e.g., we're
farmbuyer@1 2165 -- outside the instance) and so this only happens once as a non-local event,
farmbuyer@1 2166 -- (2) we see a local event first and all non-local events are filtered
farmbuyer@1 2167 -- by the cache, (3) we happen to get some non-local events before doing
farmbuyer@1 2168 -- our local event (not because of network weirdness but because our local
farmbuyer@1 2169 -- DBM might not trigger for a while).
farmbuyer@1 2170 local c = {
farmbuyer@1 2171 kind = 'boss',
farmbuyer@55 2172 bossname = bossname,
farmbuyer@1 2173 reason = reason,
farmbuyer@1 2174 instance = intag,
farmbuyer@56 2175 duration = duration, -- deliberately may be nil
farmbuyer@61 2176 raidersnap = self:snapshot_raid(),
farmbuyer@56 2177 maxsize = maxsize,
farmbuyer@1 2178 }
farmbuyer@1 2179 tinsert(candidates,c)
farmbuyer@1 2180 end
farmbuyer@17 2181 break
farmbuyer@1 2182 end
farmbuyer@1 2183 self.dprint('loot',"<<_do_boss out")
farmbuyer@1 2184 end
farmbuyer@56 2185 -- This exposes the function to OCR, and can be a wrapper layer later.
farmbuyer@1 2186 addon.on_boss_broadcast = _do_boss
farmbuyer@1 2187
farmbuyer@1 2188 function addon:_mark_boss_kill (index)
farmbuyer@1 2189 local e = g_loot[index]
farmbuyer@17 2190 if not e then
farmbuyer@17 2191 self:Print("Something horribly wrong;", index, "is not a valid entry!")
farmbuyer@17 2192 return
farmbuyer@17 2193 end
farmbuyer@55 2194 if not e.bossname then
farmbuyer@16 2195 self:Print("Something horribly wrong;", index, "is not a boss entry!")
farmbuyer@16 2196 return
farmbuyer@1 2197 end
farmbuyer@1 2198 if e.reason ~= 'wipe' then
farmbuyer@1 2199 -- enh, bail
farmbuyer@1 2200 self.loot_clean = index-1
farmbuyer@1 2201 end
farmbuyer@1 2202 local attempts = 1
farmbuyer@1 2203 local first
farmbuyer@1 2204
farmbuyer@69 2205 -- Maybe redo this to only collapse *contiguous* wipes...?
farmbuyer@1 2206 local i,d = 1,g_loot[1]
farmbuyer@1 2207 while d ~= e do
farmbuyer@55 2208 if d.bossname and
farmbuyer@55 2209 d.bossname == e.bossname and
farmbuyer@53 2210 d.instance == e.instance and
farmbuyer@1 2211 d.reason == 'wipe'
farmbuyer@1 2212 then
farmbuyer@1 2213 first = first or i
farmbuyer@1 2214 attempts = attempts + 1
farmbuyer@1 2215 assert(tremove(g_loot,i)==d,"_mark_boss_kill screwed up data badly")
farmbuyer@1 2216 else
farmbuyer@1 2217 i = i + 1
farmbuyer@1 2218 end
farmbuyer@1 2219 d = g_loot[i]
farmbuyer@1 2220 end
farmbuyer@1 2221 e.reason = 'kill'
farmbuyer@1 2222 e.attempts = attempts
farmbuyer@1 2223 self.loot_clean = first or index-1
farmbuyer@1 2224 end
farmbuyer@1 2225
farmbuyer@2 2226 function addon:register_boss_mod (name, registration_func, deregistration_func)
farmbuyer@2 2227 assert(type(name)=='string')
farmbuyer@2 2228 assert(type(registration_func)=='function')
farmbuyer@76 2229 if deregistration_func ~= nil then
farmbuyer@76 2230 assert(type(deregistration_func)=='function')
farmbuyer@76 2231 end
farmbuyer@2 2232 self.bossmods[#self.bossmods+1] = {
farmbuyer@2 2233 n = name,
farmbuyer@2 2234 r = registration_func,
farmbuyer@2 2235 d = deregistration_func,
farmbuyer@2 2236 }
farmbuyer@5 2237 self.bossmods[name] = self.bossmods[#self.bossmods]
farmbuyer@5 2238 assert(self.bossmods[name].n == self.bossmods[#self.bossmods].n)
farmbuyer@2 2239 end
farmbuyer@1 2240
farmbuyer@2 2241 function _register_bossmod (self, force_p)
farmbuyer@2 2242 local x = self.bossmod_registered and self.bossmods[self.bossmod_registered]
farmbuyer@2 2243 if x then
farmbuyer@2 2244 if x.n == opts.bossmod and not force_p then
farmbuyer@2 2245 -- trying to register with already-registered boss mod
farmbuyer@2 2246 return
farmbuyer@2 2247 else
farmbuyer@2 2248 -- deregister
farmbuyer@2 2249 if x.d then x.d(self) end
farmbuyer@2 2250 end
farmbuyer@1 2251 end
farmbuyer@1 2252
farmbuyer@2 2253 x = nil
farmbuyer@2 2254 for k,v in ipairs(self.bossmods) do
farmbuyer@2 2255 if v.n == opts.bossmod then
farmbuyer@2 2256 x = k
farmbuyer@2 2257 break
farmbuyer@2 2258 end
farmbuyer@1 2259 end
farmbuyer@1 2260
farmbuyer@2 2261 if not x then
farmbuyer@2 2262 self.status_text = "|cffff1010No boss-mod found!|r"
farmbuyer@2 2263 self:Print(self.status_text)
farmbuyer@2 2264 return
farmbuyer@1 2265 end
farmbuyer@1 2266
farmbuyer@2 2267 if self.bossmods[x].r (self, _do_boss) then
farmbuyer@5 2268 self.bossmod_registered = self.bossmods[x].n
farmbuyer@1 2269 else
farmbuyer@2 2270 self:Print("|cffff1010Boss mod registration failed|r")
farmbuyer@1 2271 end
farmbuyer@1 2272 end
farmbuyer@2 2273 end
farmbuyer@1 2274
farmbuyer@1 2275 -- Adding entries to the loot record, and tracking the corresponding timestamp.
farmbuyer@1 2276 do
farmbuyer@73 2277 local rawget, setmetatable = _G.rawget, _G.setmetatable
farmbuyer@73 2278
farmbuyer@100 2279 --@debug@
farmbuyer@100 2280 local tos = {}
farmbuyer@100 2281 tos.time = function (e)
farmbuyer@100 2282 return e.startday.text
farmbuyer@100 2283 end
farmbuyer@100 2284 tos.boss = function (e)
farmbuyer@100 2285 return e.bossname .. '/' .. e.reason
farmbuyer@100 2286 end
farmbuyer@100 2287 tos.loot = function (e)
farmbuyer@100 2288 return e.itemname .. '/' .. e.person .. '/' .. e.unique .. '/'
farmbuyer@100 2289 .. tostring(e.disposition) .. (e.extratext and ('/'..e.extratext) or '')
farmbuyer@100 2290 end
farmbuyer@100 2291 --@end-debug@
farmbuyer@1 2292 local loot_entry_mt = {
farmbuyer@1 2293 __index = function (e,key)
farmbuyer@100 2294 -- This shouldn't be required, as the refresh should be picking
farmbuyer@100 2295 -- it up already. Sigh.
farmbuyer@1 2296 if key == 'cols' then
farmbuyer@15 2297 pprint('mt', e.kind, "key is", key)
farmbuyer@1 2298 addon:_fill_out_eoi_data(1)
farmbuyer@1 2299 end
farmbuyer@1 2300 return rawget(e,key)
farmbuyer@100 2301 end,
farmbuyer@100 2302 --@debug@
farmbuyer@100 2303 __tostring = function (e)
farmbuyer@100 2304 local k = e.kind
farmbuyer@100 2305 if k then
farmbuyer@100 2306 return ("<%s/%s>"):format(k, tos[k] and tos[k](e) or "?")
farmbuyer@100 2307 end
farmbuyer@100 2308 return "<unknown loot entry type>"
farmbuyer@100 2309 end,
farmbuyer@100 2310 --@end-debug@
farmbuyer@100 2311 }
farmbuyer@100 2312 function _do_loot_metas()
farmbuyer@100 2313 for i,e in ipairs(g_loot) do
farmbuyer@100 2314 setmetatable(e,loot_entry_mt)
farmbuyer@1 2315 end
farmbuyer@100 2316 _do_loot_metas = nil
farmbuyer@100 2317 end
farmbuyer@1 2318
farmbuyer@1 2319 -- Given a loot index, searches backwards for a timestamp. Returns that
farmbuyer@1 2320 -- index and the time entry, or nil if it falls off the beginning. Pass an
farmbuyer@65 2321 -- optional second index to search no earlier than that.
farmbuyer@1 2322 -- May also be able to make good use of this in forum-generation routine.
farmbuyer@1 2323 function addon:find_previous_time_entry(i,stop)
farmbuyer@17 2324 stop = stop or 0
farmbuyer@1 2325 while i > stop do
farmbuyer@1 2326 if g_loot[i].kind == 'time' then
farmbuyer@1 2327 return i, g_loot[i]
farmbuyer@1 2328 end
farmbuyer@1 2329 i = i - 1
farmbuyer@1 2330 end
farmbuyer@1 2331 end
farmbuyer@1 2332
farmbuyer@1 2333 -- format_timestamp (["format_string"], Day, [Loot])
farmbuyer@1 2334 -- DAY is a loot entry with kind=='time', and controls the date printed.
farmbuyer@1 2335 -- LOOT may be any kind of entry in the g_loot table. If present, it
farmbuyer@1 2336 -- overrides the hour and minute printed; if absent, those values are
farmbuyer@1 2337 -- taken from the DAY entry.
farmbuyer@1 2338 -- FORMAT_STRING may contain $x (x in Y/M/D/h/m) tokens.
farmbuyer@84 2339 -- FIXME this is horribabble
farmbuyer@1 2340 local format_timestamp_values, point2dee = {}, "%.2d"
farmbuyer@1 2341 function addon:format_timestamp (fmt_opt, day_entry, time_entry_opt)
farmbuyer@1 2342 if not time_entry_opt then
farmbuyer@1 2343 if type(fmt_opt) == 'table' then -- Two entries, default format
farmbuyer@1 2344 time_entry_opt, day_entry = day_entry, fmt_opt
farmbuyer@1 2345 fmt_opt = "$Y/$M/$D $h:$m"
farmbuyer@41 2346 --elseif type(fmt_opt) == "string" then -- Day entry only, caller-specified format
farmbuyer@1 2347 end
farmbuyer@1 2348 end
farmbuyer@1 2349 --format_timestamp_values.Y = point2dee:format (day_entry.startday.year % 100)
farmbuyer@1 2350 format_timestamp_values.Y = ("%.4d"):format (day_entry.startday.year)
farmbuyer@1 2351 format_timestamp_values.M = point2dee:format (day_entry.startday.month)
farmbuyer@1 2352 format_timestamp_values.D = point2dee:format (day_entry.startday.day)
farmbuyer@1 2353 format_timestamp_values.h = point2dee:format ((time_entry_opt or day_entry).hour)
farmbuyer@1 2354 format_timestamp_values.m = point2dee:format ((time_entry_opt or day_entry).minute)
farmbuyer@1 2355 return fmt_opt:gsub ('%$([YMDhm])', format_timestamp_values)
farmbuyer@1 2356 end
farmbuyer@1 2357
farmbuyer@1 2358 local done_todays_date
farmbuyer@1 2359 function addon:_reset_timestamps()
farmbuyer@1 2360 done_todays_date = nil
farmbuyer@1 2361 end
farmbuyer@1 2362 local function do_todays_date()
farmbuyer@1 2363 local text, M, D, Y = makedate()
farmbuyer@1 2364 local found,ts = #g_loot+1
farmbuyer@1 2365 repeat
farmbuyer@1 2366 found,ts = addon:find_previous_time_entry(found-1)
farmbuyer@1 2367 if found and ts.startday.text == text then
farmbuyer@1 2368 done_todays_date = true
farmbuyer@1 2369 end
farmbuyer@1 2370 until done_todays_date or (not found)
farmbuyer@1 2371 if done_todays_date then
farmbuyer@1 2372 g_today = ts
farmbuyer@1 2373 else
farmbuyer@1 2374 done_todays_date = true
farmbuyer@1 2375 g_today = g_loot[addon._addLootEntry{
farmbuyer@1 2376 kind = 'time',
farmbuyer@1 2377 startday = {
farmbuyer@1 2378 text = text, month = M, day = D, year = Y
farmbuyer@1 2379 }
farmbuyer@1 2380 }]
farmbuyer@1 2381 end
farmbuyer@1 2382 addon:_fill_out_eoi_data(1)
farmbuyer@1 2383 end
farmbuyer@1 2384
farmbuyer@1 2385 -- Adding anything original to g_loot goes through this routine.
farmbuyer@89 2386 -- More precisely, anything new on the EOI tab hits this; it does not
farmbuyer@89 2387 -- necessarily need to be a looted item.
farmbuyer@1 2388 function addon._addLootEntry (e)
farmbuyer@1 2389 setmetatable(e,loot_entry_mt)
farmbuyer@1 2390
farmbuyer@1 2391 if not done_todays_date then do_todays_date() end
farmbuyer@1 2392
farmbuyer@73 2393 local h, m = _G.GetGameTime()
farmbuyer@10 2394 --local localuptime = math.floor(GetTime())
farmbuyer@73 2395 local time_t = _G.time()
farmbuyer@1 2396 e.hour = h
farmbuyer@1 2397 e.minute = m
farmbuyer@10 2398 e.stamp = time_t --localuptime
farmbuyer@89 2399 if e.kind == 'loot' then
farmbuyer@89 2400 if (not e.unique) or (#e.unique==0) then
farmbuyer@89 2401 e.unique = e.id .. (e.disposition or e.person) .. _G.date("%Y/%m/%d %H:%M",e.stamp)
farmbuyer@89 2402 end
farmbuyer@100 2403 addon:Fire ('NewLootEntry', e)
farmbuyer@89 2404 end
farmbuyer@1 2405 local index = #g_loot + 1
farmbuyer@1 2406 g_loot[index] = e
farmbuyer@100 2407 addon:Fire ('NewEOIEntry', e)
farmbuyer@1 2408 return index
farmbuyer@1 2409 end
farmbuyer@16 2410
farmbuyer@100 2411 -- Safety/convenience wrapper only.
farmbuyer@69 2412 function addon._addBossEntry (e)
farmbuyer@69 2413 local ret = addon._addLootEntry(e)
farmbuyer@69 2414 assert(e.kind=='boss')
farmbuyer@69 2415 local needSize = e.maxsize == nil
farmbuyer@69 2416 local needSnap = e.raidersnap == nil
farmbuyer@69 2417 local needInst = e.instance == nil
farmbuyer@69 2418 if needSize or needSnap then
farmbuyer@69 2419 local ss, max, inst = addon:snapshot_raid()
farmbuyer@69 2420 if needSize then e.maxsize = max end
farmbuyer@69 2421 if needSnap then e.raidersnap = ss end
farmbuyer@69 2422 if needInst then e.instance = inst end
farmbuyer@69 2423 end
farmbuyer@100 2424 addon:Fire ('NewBossEntry', e)
farmbuyer@69 2425 return ret
farmbuyer@69 2426 end
farmbuyer@69 2427
farmbuyer@16 2428 -- Problem: (1) boss kill happens, (2) fast looting happens, (3) boss
farmbuyer@16 2429 -- cache cleanup fires. Result: loot shows up before boss kill entry.
farmbuyer@16 2430 -- Solution: We need to shuffle the boss entry above any of the loot
farmbuyer@16 2431 -- from that boss.
farmbuyer@16 2432 function addon._adjustBossOrder (is, should_be)
farmbuyer@82 2433 if is == should_be then
farmbuyer@16 2434 return
farmbuyer@16 2435 end
farmbuyer@17 2436 if (type(is)~='number') or (type(should_be)~='number') or (is < should_be) then
farmbuyer@19 2437 pprint('loot', is, should_be, "...the hell? bailing")
farmbuyer@16 2438 return
farmbuyer@16 2439 end
farmbuyer@16 2440 if g_loot[should_be].kind == 'time' then
farmbuyer@16 2441 should_be = should_be + 1
farmbuyer@16 2442 if is == should_be then
farmbuyer@16 2443 return
farmbuyer@16 2444 end
farmbuyer@16 2445 end
farmbuyer@16 2446
farmbuyer@16 2447 assert(g_loot[is].kind == 'boss')
farmbuyer@16 2448 local boss = tremove (g_loot, is)
farmbuyer@16 2449 tinsert (g_loot, should_be, boss)
farmbuyer@16 2450 return should_be
farmbuyer@16 2451 end
farmbuyer@1 2452 end
farmbuyer@1 2453
farmbuyer@19 2454 -- In the rare case of items getting put into the loot table without current
farmbuyer@19 2455 -- item cache data (which will have arrived by now).
farmbuyer@19 2456 function addon:do_item_cache_fixup()
farmbuyer@19 2457 self:Print("Fixing up missing item cache data...")
farmbuyer@19 2458
farmbuyer@19 2459 local numfound = 0
farmbuyer@19 2460 local borkedpat = '^'..UNKNOWN..': (%S+)'
farmbuyer@19 2461
farmbuyer@73 2462 -- 'while true' so that we can use (inner) break as (outer) continue
farmbuyer@73 2463 for i,e in self:filtered_loot_iter('loot') do while true do
farmbuyer@73 2464 if not e.cache_miss then break end
farmbuyer@73 2465 local borked_id = e.itemname:match(borkedpat)
farmbuyer@73 2466 if not borked_id then break end
farmbuyer@73 2467 numfound = numfound + 1
farmbuyer@73 2468 -- Best to use the safest and most flexible API here, which is GII and
farmbuyer@73 2469 -- its assload of return values.
farmbuyer@73 2470 local iname, ilink, iquality, _,_,_,_,_,_, itexture = GetItemInfo(borked_id)
farmbuyer@73 2471 if iname then
farmbuyer@73 2472 local msg = [[ Entry %d patched up with %s.]]
farmbuyer@73 2473 e.quality = iquality
farmbuyer@73 2474 e.itemname = iname
farmbuyer@73 2475 e.id = tonumber(ilink:match("item:(%d+)"))
farmbuyer@73 2476 e.itemlink = ilink
farmbuyer@73 2477 e.itexture = itexture
farmbuyer@73 2478 e.cache_miss = nil
farmbuyer@73 2479 if e.unique then
farmbuyer@73 2480 local gu = g_uniques[e.unique]
farmbuyer@73 2481 local player_i, player_h, hist_i = _history_by_loot_id (e.unique, "fixcache")
farmbuyer@73 2482 if gu.loot ~= i then -- is this an actual problem?
farmbuyer@76 2483 pprint ('loot',
farmbuyer@76 2484 ("Unique value '%s' had iterator value %d but g_uniques index %s."):
farmbuyer@76 2485 format(e.unique,i,tostring(gu.loot)))
farmbuyer@73 2486 end
farmbuyer@73 2487 if player_i then
farmbuyer@73 2488 player_h.id[e.unique] = e.id
farmbuyer@73 2489 msg = [[ Entry %d (and history) patched up with %s.]]
farmbuyer@19 2490 end
farmbuyer@19 2491 end
farmbuyer@73 2492 self:Print(msg, i, ilink)
farmbuyer@19 2493 end
farmbuyer@73 2494 break
farmbuyer@73 2495 end end
farmbuyer@19 2496
farmbuyer@19 2497 self:Print("...finished. Found %d |4entry:entries; with weird data.", numfound)
farmbuyer@19 2498 end
farmbuyer@19 2499
farmbuyer@76 2500 do
farmbuyer@76 2501 local gur
farmbuyer@76 2502
farmbuyer@76 2503 -- Strictly speaking, we'd want to handle individual 'exist' entries
farmbuyer@76 2504 -- as they expire, rather then waiting for all of them to expire and then
farmbuyer@76 2505 -- doing them as a group. But, if there're more than one of these per
farmbuyer@76 2506 -- boss, something else is funky anyhow and certainly not a hurry.
farmbuyer@76 2507 local function fixup_unique_replacements()
farmbuyer@76 2508 for exist,info in pairs(gur.replacements) do
farmbuyer@76 2509 local winning_index = 1
farmbuyer@76 2510 local winner = info[1]
farmbuyer@76 2511 info[1] = nil
farmbuyer@82 2512 pprint('improv', "fixup for", exist, "starting with guid",
farmbuyer@82 2513 winner[1], "with tag", winner[2], "out of", #info,
farmbuyer@82 2514 "total entries")
farmbuyer@76 2515 -- Lowest player GUID wins. Seniority gotta count for something.
farmbuyer@76 2516 for i = 2, #info do
farmbuyer@76 2517 if winner[1] <= info[i][1] then
farmbuyer@76 2518 pprint('improv', "champ wins against", i, info[i][1])
farmbuyer@76 2519 flib.del(info[i])
farmbuyer@76 2520 else
farmbuyer@76 2521 pprint('improv', "challenger wins with", i, info[i][1])
farmbuyer@76 2522 flib.del(winner)
farmbuyer@76 2523 winner = info[i]
farmbuyer@76 2524 winning_index = i
farmbuyer@76 2525 end
farmbuyer@76 2526 info[i] = nil
farmbuyer@76 2527 end
farmbuyer@76 2528 pprint('improv', "final:", winner[1], winner[2])
farmbuyer@76 2529 --[[
farmbuyer@76 2530 A: winner was generated locally
farmbuyer@82 2531 >winning_index == 1
farmbuyer@76 2532 >g_loot and history already has the replacement value
farmbuyer@76 2533 B: winner was generated remotely
farmbuyer@76 2534 >need to scan and replace
farmbuyer@82 2535 Detecting A is strictly an optimization. We should be able to do
farmbuyer@89 2536 this code safely in all cases. Important to note: a local winner
farmbuyer@92 2537 will always be at index 1, but a winner at index 1 does not
farmbuyer@92 2538 necessarily mean it was locally generated (e.g., if the local
farmbuyer@92 2539 itemfilter drops it but a remote player does an improv). Just
farmbuyer@92 2540 do the general case until/unless this becomes a problem.
farmbuyer@76 2541 ]]
farmbuyer@89 2542 local cache = g_uniques:SEARCH(exist)
farmbuyer@89 2543 local looti,hi,ui = cache.loot, cache.history, cache.history_may
farmbuyer@89 2544
farmbuyer@89 2545 -- Active loot
farmbuyer@89 2546 if looti and g_loot[looti].unique == exist then
farmbuyer@89 2547 pprint('improv', "found and replaced loot entry", looti)
farmbuyer@89 2548 g_loot[looti].unique = winner[2]
farmbuyer@89 2549 else
farmbuyer@89 2550 -- If sharded, filtered, or the improv was done by the local
farmbuyer@89 2551 -- player, then the "previous" unique would not have made it
farmbuyer@89 2552 -- into the tables to begin with. So don't flag an error.
farmbuyer@89 2553 pprint('improv', "No active loot found", looti,
farmbuyer@89 2554 looti and g_loot[looti].unique, winning_index)
farmbuyer@89 2555 end
farmbuyer@89 2556
farmbuyer@89 2557 -- History
farmbuyer@89 2558 if hi ~= g_uniques.NOTFOUND then
farmbuyer@89 2559 hi = addon.history.byname[hi]
farmbuyer@89 2560 local hist = addon.history[hi]
farmbuyer@89 2561 if ui and hist.unique[ui] == exist then
farmbuyer@89 2562 -- ui is valid
farmbuyer@82 2563 else
farmbuyer@89 2564 ui = nil
farmbuyer@89 2565 for i,ui2 in ipairs(hist.unique) do
farmbuyer@89 2566 if ui2 == exist then
farmbuyer@89 2567 ui = i
farmbuyer@89 2568 break
farmbuyer@76 2569 end
farmbuyer@76 2570 end
farmbuyer@89 2571 end
farmbuyer@89 2572 if ui then
farmbuyer@89 2573 pprint('improv', "found and replacing history entry", hi,
farmbuyer@89 2574 ui, hist.name)
farmbuyer@89 2575 assert(exist ~= winner[2])
farmbuyer@89 2576 hist.when[winner[2]] = hist.when[exist]
farmbuyer@89 2577 hist.id[winner[2]] = hist.id[exist]
farmbuyer@89 2578 hist.count[winner[2]] = hist.count[exist]
farmbuyer@89 2579 hist.unique[ui] = winner[2]
farmbuyer@89 2580 hist.when[exist] = nil
farmbuyer@89 2581 hist.id[exist] = nil
farmbuyer@89 2582 hist.count[exist] = nil
farmbuyer@76 2583 end
farmbuyer@76 2584 end
farmbuyer@89 2585
farmbuyer@76 2586 pprint('improv', "finished with", exist, "into", winner[2])
farmbuyer@76 2587 flib.del(winner)
farmbuyer@76 2588 flib.del(info)
farmbuyer@76 2589 gur.replacements[exist] = nil
farmbuyer@76 2590 end
farmbuyer@76 2591 end
farmbuyer@76 2592
farmbuyer@76 2593 local function new_entry (id, exist, repl, is_local)
farmbuyer@76 2594 pprint('improv', "new_entry", id, exist, repl, is_local)
farmbuyer@76 2595 gur.replacements[exist] = gur.replacements[exist] or flib.new()
farmbuyer@76 2596 tinsert (gur.replacements[exist], flib.new (tonumber(id), repl))
farmbuyer@76 2597 if is_local then
farmbuyer@76 2598 gur.replacements[exist].LOCAL = repl
farmbuyer@76 2599 end
farmbuyer@76 2600 gur.cache:add (exist)
farmbuyer@76 2601 end
farmbuyer@76 2602
farmbuyer@76 2603 local function get_previous_replacement (exist)
farmbuyer@76 2604 local l = gur.replacements[exist]
farmbuyer@76 2605 if l and l.LOCAL then
farmbuyer@76 2606 pprint('improv', "check for previous", exist, "returns valid",
farmbuyer@76 2607 l.LOCAL)
farmbuyer@76 2608 return l.LOCAL
farmbuyer@76 2609 end
farmbuyer@76 2610 pprint('improv', "check for previous", exist, "returns nil")
farmbuyer@76 2611 end
farmbuyer@76 2612
farmbuyer@76 2613 function _setup_unique_replace ()
farmbuyer@76 2614 gur = {}
farmbuyer@76 2615 gur.cache = create_new_cache ('improv', 10, fixup_unique_replacements)
farmbuyer@76 2616 gur.me = tonumber(_G.UnitGUID('player'):sub(-7),16)
farmbuyer@76 2617 gur.replacements = {}
farmbuyer@76 2618 gur.new_entry = new_entry
farmbuyer@76 2619 gur.get_previous_replacement = get_previous_replacement
farmbuyer@76 2620 g_unique_replace = gur
farmbuyer@76 2621 _setup_unique_replace = nil
farmbuyer@76 2622 end
farmbuyer@76 2623 end
farmbuyer@76 2624
farmbuyer@77 2625 do
farmbuyer@77 2626 local clicky
farmbuyer@81 2627 function addon:horrible_horrible_error (err_msg)
farmbuyer@81 2628 if self.display then
farmbuyer@81 2629 local d = self.display
farmbuyer@81 2630 if d then
farmbuyer@100 2631 -- Take this down a piece at a time, on the assumption that
farmbuyer@100 2632 -- the main window won't be able to do so.
farmbuyer@96 2633 local gui = d:GetUserData("GUI state")
farmbuyer@96 2634 local eoist = gui.eoiST
farmbuyer@81 2635 if eoist then eoist:Hide() end
farmbuyer@96 2636 local histst = gui.histST
farmbuyer@81 2637 if histst then histst:Hide() end
farmbuyer@81 2638 d:Hide()
farmbuyer@81 2639 end
farmbuyer@81 2640 end
farmbuyer@81 2641 self.NOLOAD = err_msg
farmbuyer@77 2642 -- This should happen so rarely that it's not worth moving into gui.lua
farmbuyer@81 2643 if not StaticPopupDialogs["OUROL_ARGH"] then
farmbuyer@81 2644 StaticPopupDialogs["OUROL_ARGH"] = flib.StaticPopup{
farmbuyer@77 2645 button1 = OKAY,
farmbuyer@77 2646 }
farmbuyer@77 2647 clicky = addon.format_hypertext(
farmbuyer@77 2648 [[ SYSTEM FAILURE -- RELEASE RINZLER ]], "|cffff0000",
farmbuyer@81 2649 function() StaticPopup_Show "OUROL_ARGH" end)
farmbuyer@77 2650 end
farmbuyer@81 2651 StaticPopupDialogs["OUROL_ARGH"].text = horrible_error_text:format(err_msg)
farmbuyer@77 2652 _G.PlaySoundFile ([[Interface\AddOns\Ouro_Loot\sfrr.ogg]], "Master")
farmbuyer@77 2653 addon:Print (" ")
farmbuyer@77 2654 addon:Print (" ", clicky)
farmbuyer@77 2655 addon:Print (" ")
farmbuyer@77 2656 end
farmbuyer@81 2657
farmbuyer@81 2658 function _unavoidable_collision (err)
farmbuyer@81 2659 addon:horrible_horrible_error (err)
farmbuyer@81 2660 -- we don't actually need to kill the GUI in this case
farmbuyer@81 2661 addon.NOLOAD = nil
farmbuyer@81 2662 end
farmbuyer@77 2663 end
farmbuyer@77 2664
farmbuyer@1 2665
farmbuyer@1 2666 ------ Saved texts
farmbuyer@1 2667 function addon:check_saved_table(silent_p)
farmbuyer@73 2668 local s = _G.OuroLootSV_saved
farmbuyer@1 2669 if s and (#s > 0) then return s end
farmbuyer@73 2670 _G.OuroLootSV_saved = nil
farmbuyer@1 2671 if not silent_p then self:Print("There are no saved loot texts.") end
farmbuyer@1 2672 end
farmbuyer@1 2673
farmbuyer@1 2674 function addon:save_list()
farmbuyer@81 2675 local s = self:check_saved_table(); if not s then return end
farmbuyer@1 2676 for i,t in ipairs(s) do
farmbuyer@1 2677 self:Print("#%d %s %d entries %s", i, t.date, t.count, t.name)
farmbuyer@1 2678 end
farmbuyer@1 2679 end
farmbuyer@1 2680
farmbuyer@1 2681 function addon:save_saveas(name)
farmbuyer@73 2682 _G.OuroLootSV_saved = _G.OuroLootSV_saved or {}
farmbuyer@73 2683 local SV = _G.OuroLootSV_saved
farmbuyer@16 2684 local n = #SV + 1
farmbuyer@1 2685 local save = {
farmbuyer@1 2686 name = name,
farmbuyer@1 2687 date = makedate(),
farmbuyer@1 2688 count = #g_loot,
farmbuyer@1 2689 }
farmbuyer@10 2690 for text in self:registered_textgen_iter() do
farmbuyer@10 2691 save[text] = g_loot[text]
farmbuyer@10 2692 end
farmbuyer@1 2693 self:Print("Saving current loot texts to #%d '%s'", n, name)
farmbuyer@16 2694 SV[n] = save
farmbuyer@1 2695 return self:save_list()
farmbuyer@1 2696 end
farmbuyer@1 2697
farmbuyer@1 2698 function addon:save_restore(num)
farmbuyer@81 2699 local s = self:check_saved_table(); if not s then return end
farmbuyer@1 2700 if (not num) or (num > #s) then
farmbuyer@1 2701 return self:Print("Saved text number must be 1 - "..#s)
farmbuyer@1 2702 end
farmbuyer@1 2703 local save = s[num]
farmbuyer@92 2704 self:Print("Overwriting current loot data with saved text #%d '%s'",num,save.name)
farmbuyer@1 2705 self:Clear(--[[verbose_p=]]false)
farmbuyer@1 2706 -- Clear will already have displayed the window, and re-selected the first
farmbuyer@1 2707 -- tab. Set these up for when the text tabs are clicked.
farmbuyer@10 2708 for text in self:registered_textgen_iter() do
farmbuyer@10 2709 g_loot[text] = save[text]
farmbuyer@10 2710 end
farmbuyer@1 2711 end
farmbuyer@1 2712
farmbuyer@1 2713 function addon:save_delete(num)
farmbuyer@81 2714 local s = self:check_saved_table(); if not s then return end
farmbuyer@1 2715 if (not num) or (num > #s) then
farmbuyer@1 2716 return self:Print("Saved text number must be 1 - "..#s)
farmbuyer@1 2717 end
farmbuyer@1 2718 self:Print("Deleting saved text #"..num)
farmbuyer@1 2719 tremove(s,num)
farmbuyer@1 2720 return self:save_list()
farmbuyer@1 2721 end
farmbuyer@1 2722
farmbuyer@1 2723
farmbuyer@1 2724 ------ Loot histories
farmbuyer@1 2725 -- history_all = {
farmbuyer@1 2726 -- ["Kilrogg"] = {
farmbuyer@1 2727 -- ["realm"] = "Kilrogg", -- not saved
farmbuyer@1 2728 -- ["st"] = { lib-st display table }, -- not saved
farmbuyer@1 2729 -- ["byname"] = { -- not saved
farmbuyer@1 2730 -- ["OtherPlayer"] = 2,
farmbuyer@1 2731 -- ["Farmbuyer"] = 1,
farmbuyer@1 2732 -- }
farmbuyer@1 2733 -- [1] = {
farmbuyer@1 2734 -- ["name"] = "Farmbuyer",
farmbuyer@73 2735 -- ["person_class"] = "PRIEST", -- may be missing, used in display only
farmbuyer@73 2736 -- -- sorted array:
farmbuyer@73 2737 -- ["unique"] = { most_recent_tag, previous_tag, .... },
farmbuyer@73 2738 -- -- these are indexed by unique tags, and 'count' may be missing:
farmbuyer@73 2739 -- ["when"] = { ["tag"] = "formatted timestamp for displaying loot" },
farmbuyer@73 2740 -- ["id"] = { ["tag"] = 11111 },
farmbuyer@73 2741 -- ["count"] = { ["tag"] = "x3", .... },
farmbuyer@1 2742 -- },
farmbuyer@1 2743 -- [2] = {
farmbuyer@1 2744 -- ["name"] = "OtherPlayer",
farmbuyer@1 2745 -- ......
farmbuyer@1 2746 -- }, ......
farmbuyer@1 2747 -- },
farmbuyer@1 2748 -- ["OtherRealm"] = ......
farmbuyer@1 2749 -- }
farmbuyer@73 2750 --
farmbuyer@73 2751 -- Up through 2.81.4 (specifically through rev 95), an individual player's
farmbuyer@73 2752 -- table looked like this:
farmbuyer@73 2753 -- ["name"] = "Farmbuyer",
farmbuyer@73 2754 -- [1] = { id = nnnnn, when = "formatted timestamp for displaying" } -- most recent loot
farmbuyer@73 2755 -- [2] = { ......., [count = "x3"] } -- previous loot
farmbuyer@73 2756 -- which was much easier to manipulate, but had a ton of memory overhead.
farmbuyer@1 2757 do
farmbuyer@73 2758 -- Sorts a player's history from newest to oldest, according to the
farmbuyer@73 2759 -- formatted timestamp. This is expensive, and destructive for P.unique.
farmbuyer@76 2760 local function compare_timestamps (L, R)
farmbuyer@76 2761 return L > R -- reverse of normal order, newest first
farmbuyer@76 2762 end
farmbuyer@73 2763 local function sort_player (p)
farmbuyer@73 2764 local new_uniques, uniques_bywhen, when_array = {}, {}, {}
farmbuyer@73 2765 for u,tstamp in pairs(p.when) do
farmbuyer@73 2766 uniques_bywhen[tstamp] = u
farmbuyer@73 2767 when_array[#when_array+1] = tstamp
farmbuyer@73 2768 end
farmbuyer@76 2769 _G.table.sort (when_array, compare_timestamps)
farmbuyer@73 2770 for i,tstamp in ipairs(when_array) do
farmbuyer@73 2771 new_uniques[i] = uniques_bywhen[tstamp]
farmbuyer@73 2772 end
farmbuyer@73 2773 p.unique = new_uniques
farmbuyer@73 2774 end
farmbuyer@73 2775
farmbuyer@81 2776 function addon:repair_history_integrity()
farmbuyer@81 2777 local rcount, pcount, hcount, errors = 0, 0, 0, 0
farmbuyer@81 2778 local empties_to_delete = {}
farmbuyer@81 2779
farmbuyer@81 2780 for rname,realm in pairs(self.history_all) do
farmbuyer@81 2781 for pk,player in ipairs(realm) do
farmbuyer@81 2782 local id, when, unique, count = {}, {}, {}, {}
farmbuyer@81 2783 for i,h in ipairs(player.unique) do
farmbuyer@81 2784 h = tostring(h)
farmbuyer@81 2785 if player.when[h] and player.id[h] then
farmbuyer@81 2786 unique[#unique+1] = h
farmbuyer@81 2787 id[h] = player.id[h]
farmbuyer@81 2788 when[h] = player.when[h]
farmbuyer@81 2789 count[h] = player.count[h]
farmbuyer@81 2790 else
farmbuyer@81 2791 self:Print("Realm %s, player %s, entry %d: tag <%s>, id <%s>, time <%s>, count <%s>",
farmbuyer@81 2792 rname, player.name, i, h, tostring(player.id[h]),
farmbuyer@81 2793 tostring(player.when[h]), tostring(player.count[h]))
farmbuyer@81 2794 errors = errors + 1
farmbuyer@81 2795 end
farmbuyer@81 2796 hcount = hcount + 1
farmbuyer@81 2797 end
farmbuyer@81 2798 player.id, player.when, player.unique, player.count =
farmbuyer@81 2799 id, when, unique, count
farmbuyer@81 2800 player.person_class = g_loot.raiders[player.name]
farmbuyer@81 2801 and g_loot.raiders[player.name].class
farmbuyer@81 2802 if #player.unique > 1 then
farmbuyer@81 2803 sort_player(player)
farmbuyer@81 2804 elseif #player.unique == 0 then
farmbuyer@81 2805 tinsert (empties_to_delete, 1, pk)
farmbuyer@81 2806 end
farmbuyer@81 2807 pcount = pcount + 1
farmbuyer@81 2808 end
farmbuyer@81 2809 if #empties_to_delete > 0 then
farmbuyer@81 2810 for _,pk in ipairs(empties_to_delete) do
farmbuyer@81 2811 local player = tremove (realm, pk)
farmbuyer@81 2812 self:Print("Realm %s, player %s, is empty", rname, player.name)
farmbuyer@81 2813 end
farmbuyer@81 2814 wipe(empties_to_delete)
farmbuyer@81 2815 end
farmbuyer@81 2816 if #realm == 0 then
farmbuyer@81 2817 self.history_all[rname] = nil
farmbuyer@81 2818 self:Print("Realm %s is empty", rname)
farmbuyer@81 2819 end
farmbuyer@81 2820 rcount = rcount + 1
farmbuyer@81 2821 end
farmbuyer@81 2822 self:_build_history_names()
farmbuyer@81 2823 if errors > 0 then
farmbuyer@81 2824 self:Print("The listed entries have been erased from history.")
farmbuyer@81 2825 end
farmbuyer@81 2826 end
farmbuyer@81 2827
farmbuyer@73 2828 -- Possibly called during login. Cleared when no longer needed.
farmbuyer@73 2829 -- Rewrites a PLAYER table from format 3 to format 4.
farmbuyer@76 2830 function addon:_uplift_history_format (player)
farmbuyer@73 2831 local unique, when, id, count = {}, {}, {}, {}
farmbuyer@73 2832 local name = player.name
farmbuyer@73 2833
farmbuyer@73 2834 for i,h in ipairs(player) do
farmbuyer@73 2835 local U = h.unique
farmbuyer@73 2836 unique[i] = U
farmbuyer@73 2837 when[U] = h.when
farmbuyer@73 2838 id[U] = h.id
farmbuyer@73 2839 count[U] = h.count
farmbuyer@73 2840 end
farmbuyer@73 2841
farmbuyer@73 2842 wipe(player)
farmbuyer@73 2843 player.name = name
farmbuyer@73 2844 player.id, player.when, player.unique, player.count =
farmbuyer@73 2845 id, when, unique, count
farmbuyer@73 2846 end
farmbuyer@76 2847
farmbuyer@73 2848 function addon:_cache_history_uniques()
farmbuyer@73 2849 UpdateAddOnMemoryUsage()
farmbuyer@73 2850 local before = GetAddOnMemoryUsage(nametag)
farmbuyer@73 2851 local trouble
farmbuyer@73 2852 local count = 0
farmbuyer@73 2853 for hi,player in ipairs(self.history) do
farmbuyer@73 2854 for ui,u in ipairs(player.unique) do
farmbuyer@87 2855 g_uniques[u] = { history = player.name, history_may = ui }
farmbuyer@73 2856 count = count + 1
farmbuyer@73 2857 end
farmbuyer@73 2858 end
farmbuyer@73 2859 for i,e in self:filtered_loot_iter('loot') do
farmbuyer@73 2860 if e.unique and e.unique ~= "" then
farmbuyer@73 2861 local hmmm = _G.rawget(g_uniques,e.unique)
farmbuyer@73 2862 if hmmm then
farmbuyer@82 2863 hmmm.loot = i
farmbuyer@73 2864 elseif e.disposition == 'shard' or e.disposition == 'gvault' then
farmbuyer@73 2865 g_uniques[e.unique] = { loot = i, history = g_uniques.NOTFOUND }
farmbuyer@73 2866 count = count + 1
farmbuyer@73 2867 else
farmbuyer@76 2868 hmmm = "active data not found in history ("..i.."/"..tostring(e.unique)
farmbuyer@76 2869 ..") in precache loop! trying to fixup for this session"
farmbuyer@76 2870 pprint(hmmm) -- more?
farmbuyer@73 2871 -- try to simply fix up errors as we go
farmbuyer@73 2872 g_uniques[e.unique] = { loot = i, history = g_uniques.NOTFOUND }
farmbuyer@73 2873 end
farmbuyer@73 2874 else
farmbuyer@89 2875 -- The usual cause: when only source is from an older client
farmbuyer@89 2876 -- and the disposition did not trigger addhistory, then not
farmbuyer@89 2877 -- even a stub history entry happens. Code has now been added
farmbuyer@89 2878 -- to try harder to prevent this, but it's still best to not
farmbuyer@89 2879 -- simple ignore it.
farmbuyer@73 2880 trouble = true
farmbuyer@89 2881 pprint('loot', "ERROR precache loop found missing/outdated unique tag!",
farmbuyer@89 2882 i, "tag <"..tostring(e.unique).."> from?", tostring(e.bcast_from))
farmbuyer@73 2883 end
farmbuyer@73 2884 end
farmbuyer@73 2885 UpdateAddOnMemoryUsage()
farmbuyer@73 2886 local after = GetAddOnMemoryUsage(nametag)
farmbuyer@73 2887 self:Print("Pre-scanning history for faster loot handling on %s used %.2f MB of memory across %d entries.",
farmbuyer@73 2888 self.history.realm, (after-before)/1024, count)
farmbuyer@73 2889 if trouble then
farmbuyer@73 2890 self:Print("Note that there were inconsistencies in the data;",
farmbuyer@73 2891 "you should consider submitting a bug report (including your",
farmbuyer@73 2892 "SavedVariables file), and regenerating or preening this",
farmbuyer@89 2893 "realm's loot history. If you keep seeing this message, type",
farmbuyer@89 2894 "'/ouroloot fix ?' and try some of those actions.")
farmbuyer@73 2895 end
farmbuyer@73 2896 g_uniques:SETMODE('probe')
farmbuyer@73 2897 self._cache_history_uniques = nil
farmbuyer@73 2898 end
farmbuyer@6 2899
farmbuyer@4 2900 -- Builds the map of names to array indices, using passed table or
farmbuyer@4 2901 -- self.history, and stores the result into its 'byname' field. Also
farmbuyer@4 2902 -- called from the GUI code at least once.
farmbuyer@1 2903 function addon:_build_history_names (opt_hist)
farmbuyer@1 2904 local hist = opt_hist or self.history
farmbuyer@1 2905 local m = {}
farmbuyer@1 2906 for i = 1, #hist do
farmbuyer@1 2907 m[hist[i].name] = i
farmbuyer@1 2908 end
farmbuyer@73 2909 -- why yes, I *did* spend many years as a YP/NIS admin, how did you know?
farmbuyer@1 2910 hist.byname = m
farmbuyer@1 2911 end
farmbuyer@1 2912
farmbuyer@1 2913 -- Prepares and returns table to be used as self.history.
farmbuyer@1 2914 function addon:_prep_new_history_category (prev_table, realmname)
farmbuyer@1 2915 local t = prev_table or {
farmbuyer@1 2916 --kind = 'realm',
farmbuyer@6 2917 --realm = realmname,
farmbuyer@1 2918 }
farmbuyer@6 2919 t.realm = realmname
farmbuyer@1 2920
farmbuyer@1 2921 --[[
farmbuyer@1 2922 t.cols = setmetatable({
farmbuyer@1 2923 { value = realmname },
farmbuyer@1 2924 }, self.time_column1_used_mt)
farmbuyer@1 2925 ]]
farmbuyer@1 2926
farmbuyer@1 2927 if not t.byname then
farmbuyer@1 2928 self:_build_history_names (t)
farmbuyer@1 2929 end
farmbuyer@1 2930
farmbuyer@1 2931 return t
farmbuyer@1 2932 end
farmbuyer@1 2933
farmbuyer@4 2934 -- Maps a name to an array index, creating new tables if needed. Returns
farmbuyer@6 2935 -- the index and the table at that index.
farmbuyer@4 2936 function addon:get_loot_history (name)
farmbuyer@4 2937 local i
farmbuyer@4 2938 i = self.history.byname[name]
farmbuyer@4 2939 if not i then
farmbuyer@4 2940 i = #self.history + 1
farmbuyer@73 2941 self.history[i] = { name=name, id={}, when={}, unique={}, count={} }
farmbuyer@4 2942 self.history.byname[name] = i
farmbuyer@4 2943 end
farmbuyer@6 2944 return i, self.history[i]
farmbuyer@4 2945 end
farmbuyer@4 2946
farmbuyer@73 2947 -- Prepends data from the loot entry at LOOTINDEX to be the new most
farmbuyer@73 2948 -- recent history entry for that player.
farmbuyer@1 2949 function addon:_addHistoryEntry (lootindex)
farmbuyer@1 2950 local e = g_loot[lootindex]
farmbuyer@6 2951 if e.kind ~= 'loot' then return end
farmbuyer@6 2952
farmbuyer@6 2953 local i,h = self:get_loot_history(e.person)
farmbuyer@73 2954 local when = self:format_timestamp (g_today, e)
farmbuyer@87 2955 assert(h.name==e.person)
farmbuyer@73 2956
farmbuyer@25 2957 -- If any of these change, update the end of history_handle_disposition.
farmbuyer@71 2958 if (not e.unique) or (#e.unique==0) then
farmbuyer@84 2959 e.unique = e.id .. e.person .. when
farmbuyer@71 2960 end
farmbuyer@73 2961 local U = e.unique
farmbuyer@73 2962 tinsert (h.unique, 1, U)
farmbuyer@73 2963 h.when[U] = when
farmbuyer@73 2964 h.id[U] = e.id
farmbuyer@73 2965 h.count[U] = e.count
farmbuyer@73 2966
farmbuyer@87 2967 g_uniques[U] = { loot = lootindex, history = e.person }
farmbuyer@100 2968 self:Fire ('NewHistory', e.person, U)
farmbuyer@6 2969 end
farmbuyer@6 2970
farmbuyer@24 2971 -- Create new history table based on current loot.
farmbuyer@6 2972 function addon:rewrite_history (realmname)
farmbuyer@6 2973 local r = assert(realmname)
farmbuyer@6 2974 self.history_all[r] = self:_prep_new_history_category (nil, r)
farmbuyer@6 2975 self.history = self.history_all[r]
farmbuyer@73 2976 g_uniques:RESET()
farmbuyer@6 2977
farmbuyer@6 2978 local g_today_real = g_today
farmbuyer@6 2979 for i,e in ipairs(g_loot) do
farmbuyer@6 2980 if e.kind == 'time' then
farmbuyer@6 2981 g_today = e
farmbuyer@6 2982 elseif e.kind == 'loot' then
farmbuyer@6 2983 self:_addHistoryEntry(i)
farmbuyer@6 2984 end
farmbuyer@6 2985 end
farmbuyer@6 2986 g_today = g_today_real
farmbuyer@6 2987 self.hist_clean = nil
farmbuyer@6 2988
farmbuyer@6 2989 -- safety measure: resort players' tables based on formatted timestamp
farmbuyer@6 2990 for i,h in ipairs(self.history) do
farmbuyer@73 2991 sort_player(h)
farmbuyer@6 2992 end
farmbuyer@6 2993 end
farmbuyer@6 2994
farmbuyer@76 2995 -- Clears all but the most recent HOWMANY (optional, default 1) entries
farmbuyer@76 2996 -- for each player on REALMNAME.
farmbuyer@76 2997 -- This function's name is the legacy of the orignal fsck(8) "-p" option,
farmbuyer@76 2998 -- which has a similar feel.
farmbuyer@76 2999 function addon:preen_history (realmname, howmany)
farmbuyer@6 3000 local r = assert(realmname)
farmbuyer@76 3001 howmany = tonumber(howmany) or 1
farmbuyer@76 3002 if type(self.history_all[r]) ~= 'table' then
farmbuyer@76 3003 return
farmbuyer@76 3004 end
farmbuyer@73 3005 g_uniques:RESET()
farmbuyer@76 3006 for i,h in ipairs(self.history_all[r]) do
farmbuyer@73 3007 -- This is going to do horrible things to memory. The subtables
farmbuyer@73 3008 -- after this step would be large and sparse, with no good way
farmbuyer@73 3009 -- of shrinking the allocation...
farmbuyer@73 3010 sort_player(h)
farmbuyer@73 3011 -- ...so it's better in the long run to discard them.
farmbuyer@76 3012 local new_unique, new_id, new_when, new_count = {}, {}, {}, {}
farmbuyer@76 3013 for ui = 1, howmany do
farmbuyer@76 3014 local U = h.unique[ui]
farmbuyer@76 3015 if not U then break end
farmbuyer@76 3016 new_unique[ui] = U
farmbuyer@76 3017 new_id[U] = h.id[U]
farmbuyer@76 3018 new_when[U] = h.when[U]
farmbuyer@76 3019 new_count[U] = h.count[U]
farmbuyer@76 3020 end
farmbuyer@76 3021 h.unique, h.id, h.when, h.count =
farmbuyer@76 3022 new_unique, new_id, new_when, new_count
farmbuyer@6 3023 end
farmbuyer@1 3024 end
farmbuyer@24 3025
farmbuyer@73 3026 -- Given a unique tag OR an entry in a g_loot table, looks up the
farmbuyer@73 3027 -- corresponding history entry. Returns the player's index and history
farmbuyer@73 3028 -- table (as in get_loot_history) and the index into that table of the
farmbuyer@73 3029 -- loot entry. On failure, returns nil and an error message ready to be
farmbuyer@73 3030 -- formatted with the loot's name/itemlink.
farmbuyer@73 3031 function _history_by_loot_id (needle, operation_text)
farmbuyer@73 3032 local errtxt
farmbuyer@73 3033 if type(needle) == 'string' then
farmbuyer@73 3034 -- unique tag
farmbuyer@73 3035 elseif type(needle) == 'table' then
farmbuyer@73 3036 if needle.kind ~= 'loot' then
farmbuyer@73 3037 error("trying to "..operation_text.." something that isn't loot")
farmbuyer@73 3038 end
farmbuyer@73 3039 needle = needle.unique
farmbuyer@73 3040 if not needle then
farmbuyer@73 3041 return nil, --[[errtxt=]]"Entry for %s is missing a history tag!"
farmbuyer@73 3042 end
farmbuyer@73 3043 else
farmbuyer@73 3044 error("'"..tostring(needle).."' is neither unique string nor loot entry!")
farmbuyer@25 3045 end
farmbuyer@24 3046
farmbuyer@73 3047 local player_i, player_h
farmbuyer@73 3048 local cache = g_uniques[needle]
farmbuyer@24 3049
farmbuyer@73 3050 if cache.history == g_uniques.NOTFOUND then
farmbuyer@73 3051 -- 1) loot an item, 2) clear old history, 3) reassign from current loot
farmbuyer@73 3052 -- Bah. Anybody that tricky is already recoding the tables directly anyhow.
farmbuyer@73 3053 errtxt = "There is no record of %s ever having been assigned!"
farmbuyer@24 3054 else
farmbuyer@87 3055 player_i = addon.history.byname[cache.history]
farmbuyer@73 3056 player_h = addon.history[player_i]
farmbuyer@73 3057 if cache.history_may
farmbuyer@73 3058 and needle == player_h.unique[cache.history_may]
farmbuyer@73 3059 then
farmbuyer@73 3060 return player_i, player_h, cache.history_may
farmbuyer@73 3061 end
farmbuyer@73 3062 for i,u in ipairs(player_h.unique) do
farmbuyer@73 3063 if needle == u then
farmbuyer@73 3064 cache.history_may = i -- might help, might not
farmbuyer@73 3065 return player_i, player_h, i
farmbuyer@24 3066 end
farmbuyer@24 3067 end
farmbuyer@24 3068 end
farmbuyer@24 3069
farmbuyer@73 3070 if not errtxt then
farmbuyer@73 3071 -- The cache finder got a hit, but now it's gone? WTF?
farmbuyer@73 3072 errtxt = "ZOMG! %s was in history but now is gone. Possibly your history tables have been corrupted and should be recreated. This is likely a bug. Tell Farmbuyer what steps you took to cause this."
farmbuyer@24 3073 end
farmbuyer@73 3074 return nil, errtxt
farmbuyer@25 3075 end
farmbuyer@24 3076
farmbuyer@81 3077 -- Handles reassigning loot between players. Arguments depend on who's
farmbuyer@81 3078 -- calling it:
farmbuyer@81 3079 -- "local", row_index, new_recipient
farmbuyer@81 3080 -- "remote", sender, unique_id, item_id, old_recipient, new_recipient
farmbuyer@81 3081 -- In the local case, must also broadcast a trigger. In the remote case,
farmbuyer@81 3082 -- must figure out the corresponding loot entry (if it exists). In both
farmbuyer@81 3083 -- cases, must update history appropriately. Returns nil if anything odd
farmbuyer@81 3084 -- happens; returns the affected loot index on success.
farmbuyer@81 3085 --function addon:reassign_loot (index, to_name)
farmbuyer@81 3086 function addon:reassign_loot (how, ...)
farmbuyer@81 3087 -- This must all be filled out in all cases:
farmbuyer@81 3088 local e, index, from_name, to_name, unique, id
farmbuyer@81 3089 -- Only set in remote case:
farmbuyer@81 3090 local sender
farmbuyer@81 3091
farmbuyer@81 3092 if how == "local" then
farmbuyer@81 3093 -- GUI doesn't allow reassignment unless the item is not-shard,
farmbuyer@81 3094 -- so we can assume the presence of a unique tag in this function.
farmbuyer@81 3095 index, to_name = ...
farmbuyer@81 3096 assert(type(to_name)=='string' and to_name:len()>0)
farmbuyer@81 3097 index = assert(tonumber(index))
farmbuyer@81 3098 e = g_loot[index]
farmbuyer@81 3099 id = e.id
farmbuyer@81 3100 unique = assert(e.unique)
farmbuyer@81 3101 from_name = e.person
farmbuyer@81 3102
farmbuyer@81 3103 elseif how == "remote" then
farmbuyer@81 3104 sender, unique, id, from_name, to_name = ...
farmbuyer@81 3105 id = tonumber(id)
farmbuyer@81 3106 local cache
farmbuyer@81 3107 local loop = 0
farmbuyer@81 3108 repeat -- wtb continue statement pst
farmbuyer@81 3109 if loop > 1 then break end
farmbuyer@81 3110 e = nil
farmbuyer@81 3111 cache = cache and g_uniques:SEARCH(unique) or g_uniques[unique]
farmbuyer@81 3112 index = tonumber(cache.loot)
farmbuyer@81 3113 if index then
farmbuyer@81 3114 e = g_loot[index]
farmbuyer@81 3115 else
farmbuyer@81 3116 end
farmbuyer@81 3117 loop = loop + 1
farmbuyer@81 3118 until e and (e.id == id)
farmbuyer@81 3119
farmbuyer@81 3120 else
farmbuyer@81 3121 return -- silently ignore future cases from future clients
farmbuyer@81 3122 end
farmbuyer@81 3123
farmbuyer@81 3124 if self.debug.loot then
farmbuyer@81 3125 local m = ("Reassign index %d (pre-unique %s) with id %d from '%s' to '%s'."):
farmbuyer@81 3126 format(index, unique, id, tostring(from_name), tostring(to_name))
farmbuyer@81 3127 self.dprint('loot', m)
farmbuyer@81 3128 if sender == my_name then
farmbuyer@81 3129 self.dprint('loot',"(Returning early from debug mode's double self-reassign.)")
farmbuyer@81 3130 return index
farmbuyer@81 3131 end
farmbuyer@81 3132 end
farmbuyer@81 3133
farmbuyer@81 3134 if not e then
farmbuyer@81 3135 -- say something?
farmbuyer@81 3136 return
farmbuyer@81 3137 end
farmbuyer@81 3138
farmbuyer@73 3139 local from_i, from_h, hist_i = _history_by_loot_id (e, "reassign")
farmbuyer@25 3140 local to_i,to_h = self:get_loot_history(to_name)
farmbuyer@25 3141
farmbuyer@81 3142 if not from_i then
farmbuyer@81 3143 if how == "local" then
farmbuyer@81 3144 -- from_h here is the formatted error text
farmbuyer@81 3145 self:Print(from_h .. " Loot will be reassigned, but history will NOT be updated.", e.itemlink)
farmbuyer@81 3146 end
farmbuyer@25 3147 else
farmbuyer@81 3148 -- XXX do some sanity checks here? from_name == from_h.name, etc?
farmbuyer@87 3149 -- If something were wrong at this point, what could we do?
farmbuyer@81 3150
farmbuyer@73 3151 local U = tremove (from_h.unique, hist_i)
farmbuyer@73 3152 -- The loot master giveth...
farmbuyer@73 3153 to_h.unique[#to_h.unique+1] = U
farmbuyer@73 3154 to_h.when[U] = from_h.when[U]
farmbuyer@73 3155 to_h.id[U] = from_h.id[U]
farmbuyer@73 3156 to_h.count[U] = from_h.count[U]
farmbuyer@73 3157 sort_player(to_h)
farmbuyer@73 3158 -- ...and the loot master taketh away.
farmbuyer@73 3159 from_h.when[U] = nil
farmbuyer@73 3160 from_h.id[U] = nil
farmbuyer@73 3161 from_h.count[U] = nil
farmbuyer@73 3162 -- Blessed be the lookup cache of the loot master.
farmbuyer@87 3163 g_uniques[U] = { loot = index, history = to_name }
farmbuyer@25 3164 end
farmbuyer@81 3165 local from_person_class = e.person_class or from_h.person_class
farmbuyer@81 3166 or (g_loot.raiders[from_name] and g_loot.raiders[from_name].class)
farmbuyer@81 3167 or select(2,_G.UnitClass(from_name))
farmbuyer@25 3168 e.person = to_name
farmbuyer@81 3169 e.person_class = to_h.person_class
farmbuyer@81 3170 or (g_loot.raiders[to_name] and g_loot.raiders[to_name].class)
farmbuyer@81 3171 or select(2,_G.UnitClass(to_name))
farmbuyer@25 3172 self.hist_clean = nil
farmbuyer@81 3173 self.loot_clean = nil
farmbuyer@81 3174
farmbuyer@81 3175 if how == "local" then
farmbuyer@101 3176 if opts.chatty_on_local_changes then
farmbuyer@101 3177 _notify_about_change (_G.UNIT_YOU, index, nil, from_name, from_person_class)
farmbuyer@101 3178 end
farmbuyer@81 3179 self:vbroadcast('reassign', unique, id, from_name, to_name)
farmbuyer@81 3180 elseif opts.chatty_on_remote_changes then
farmbuyer@101 3181 _notify_about_change (sender, index, nil, from_name, from_person_class)
farmbuyer@81 3182 end
farmbuyer@81 3183 if self.display then
farmbuyer@96 3184 self.display:GetUserData("GUI state").eoiST:OuroLoot_Refresh(index)
farmbuyer@81 3185 self:redisplay()
farmbuyer@81 3186 end
farmbuyer@100 3187 self:Fire ('Reassign', unique, id, e, from_name, to_name)
farmbuyer@81 3188 return index
farmbuyer@25 3189 end
farmbuyer@25 3190
farmbuyer@84 3191 local function expunge (player, index_or_unique)
farmbuyer@84 3192 local i,u
farmbuyer@84 3193 if type(index_or_unique) == 'string' then
farmbuyer@84 3194 for u = 1, #player.unique do
farmbuyer@84 3195 if player.unique[u] == index_or_unique then
farmbuyer@84 3196 i = u
farmbuyer@84 3197 break
farmbuyer@84 3198 end
farmbuyer@84 3199 end
farmbuyer@84 3200 elseif type(index_or_unique) == 'number' then
farmbuyer@84 3201 i = index_or_unique
farmbuyer@84 3202 end
farmbuyer@84 3203 if not i then
farmbuyer@84 3204 return -- error here?
farmbuyer@84 3205 end
farmbuyer@84 3206 u = player.unique[i]
farmbuyer@84 3207 assert(#u>0)
farmbuyer@84 3208 tremove (player.unique, i)
farmbuyer@84 3209 player.when[u], player.id[u], player.count[u] = nil, nil, nil
farmbuyer@84 3210 g_uniques[u] = nil
farmbuyer@84 3211 addon.hist_clean = nil
farmbuyer@100 3212 addon:Fire ('DelHistory', player.name, u)
farmbuyer@87 3213 return #player.unique
farmbuyer@84 3214 end
farmbuyer@84 3215
farmbuyer@84 3216 -- Mirror of _addHistoryEntry. Arguments are either:
farmbuyer@84 3217 -- E - loot entry
farmbuyer@84 3218 -- U,ITEM - unique tag, and a name/itemlink for errors
farmbuyer@87 3219 -- If this entry was the only one for that player, will also remove that
farmbuyer@87 3220 -- player's tables from the history array.
farmbuyer@87 3221 --
farmbuyer@87 3222 -- On success, returns the number of remaining history entries for that
farmbuyer@87 3223 -- player (potentially zero). On failure, returns nil+error.
farmbuyer@87 3224 function addon:_delHistoryEntry (first, item)
farmbuyer@84 3225 if type(first) == 'table' then
farmbuyer@87 3226 item = first.itemlink or item
farmbuyer@84 3227 --elseif type(first) == 'string' then
farmbuyer@84 3228 end
farmbuyer@84 3229
farmbuyer@84 3230 local from_i, from_h, hist_i = _history_by_loot_id (first, "delete")
farmbuyer@84 3231
farmbuyer@36 3232 if not from_i then
farmbuyer@36 3233 -- from_h is the formatted error text
farmbuyer@84 3234 return nil, (from_h
farmbuyer@84 3235 .." Loot will be deleted, but history will NOT be updated."
farmbuyer@87 3236 ):format(item)
farmbuyer@36 3237 end
farmbuyer@36 3238
farmbuyer@87 3239 local remaining = expunge (from_h, hist_i)
farmbuyer@87 3240 if not remaining then
farmbuyer@87 3241 return nil, "Something bizarre happening trying to delete "..item
farmbuyer@87 3242 elseif remaining > 0 then
farmbuyer@87 3243 return remaining
farmbuyer@87 3244 end
farmbuyer@87 3245 tremove (self.history, from_i)
farmbuyer@87 3246 self:_build_history_names()
farmbuyer@87 3247 return 0
farmbuyer@36 3248 end
farmbuyer@36 3249
farmbuyer@25 3250 -- Any extra work for the "Mark as <x>" dropdown actions. The
farmbuyer@25 3251 -- corresponding <x> will already have been assigned in the loot entry.
farmbuyer@73 3252 local deleted_cache = {}
farmbuyer@25 3253 function addon:history_handle_disposition (index, olddisp)
farmbuyer@25 3254 local e = g_loot[index]
farmbuyer@25 3255 -- Standard disposition has a nil entry, but that's tedious in debug
farmbuyer@25 3256 -- output, so force to a string instead.
farmbuyer@25 3257 olddisp = olddisp or 'normal'
farmbuyer@25 3258 local newdisp = e.disposition or 'normal'
farmbuyer@25 3259 -- Ignore misclicks and the like
farmbuyer@25 3260 if olddisp == newdisp then return end
farmbuyer@25 3261
farmbuyer@25 3262 local name = e.person
farmbuyer@25 3263
farmbuyer@25 3264 if (newdisp == 'shard' or newdisp == 'gvault') then
farmbuyer@73 3265 local name_i, name_h, hist_i = _history_by_loot_id (e, "mark")
farmbuyer@73 3266 -- remove history entry if it exists
farmbuyer@84 3267 -- FIXME revist this and use expunge
farmbuyer@25 3268 if hist_i then
farmbuyer@73 3269 local c = flib.new()
farmbuyer@73 3270 local hist_u = tremove (name_h.unique, hist_i)
farmbuyer@73 3271 c.when = name_h.when[hist_u]
farmbuyer@73 3272 c.id = name_h.id[hist_u]
farmbuyer@73 3273 c.count = name_h.count[hist_u]
farmbuyer@73 3274 deleted_cache[hist_u] = c
farmbuyer@73 3275 name_h.when[hist_u] = nil
farmbuyer@73 3276 name_h.id[hist_u] = nil
farmbuyer@73 3277 name_h.count[hist_u] = nil
farmbuyer@25 3278 self.hist_clean = nil
farmbuyer@25 3279 elseif (olddisp == 'shard' or olddisp == 'gvault') then
farmbuyer@25 3280 -- Sharding a vault item, or giving the auto-sharder something to bank,
farmbuyer@25 3281 -- etc, wouldn't necessarily have had a history entry to begin with.
farmbuyer@73 3282 -- So this isn't treated as an error.
farmbuyer@25 3283 else
farmbuyer@25 3284 self:Print(name_h .. " Loot has been marked, but history will NOT be updated.", e.itemlink)
farmbuyer@25 3285 end
farmbuyer@25 3286 return
farmbuyer@25 3287 end
farmbuyer@25 3288
farmbuyer@25 3289 if (olddisp == 'shard' or olddisp == 'gvault')
farmbuyer@25 3290 and (newdisp == 'normal' or newdisp == 'offspec')
farmbuyer@25 3291 then
farmbuyer@25 3292 local name_i, name_h = self:get_loot_history(name)
farmbuyer@25 3293
farmbuyer@25 3294 -- Must create a new history entry. Could call '_addHistoryEntry(index)'
farmbuyer@25 3295 -- but that would duplicate a lot of effort. To start with, check the
farmbuyer@25 3296 -- cache of stuff we've already deleted; if it's not there then just do
farmbuyer@25 3297 -- the same steps as _addHistoryEntry.
farmbuyer@73 3298 -- FIXME The deleted cache isn't nearly as useful now with the new data structures.
farmbuyer@73 3299 local when
farmbuyer@73 3300 if (not e.unique) or (#e.unique==0) then
farmbuyer@85 3301 when = g_today and self:format_timestamp (g_today, e) or date("%Y/%m/%d %H:%M",e.stamp)
farmbuyer@84 3302 e.unique = e.id .. name .. when
farmbuyer@25 3303 end
farmbuyer@73 3304 local U = e.unique
farmbuyer@73 3305 local c = deleted_cache[U]
farmbuyer@73 3306 deleted_cache[U] = nil
farmbuyer@73 3307 name_h.unique[#name_h.unique+1] = U
farmbuyer@85 3308 name_h.when[U] = c and c.when or when or date("%Y/%m/%d %H:%M",e.stamp)
farmbuyer@73 3309 name_h.id[U] = e.id -- c.id
farmbuyer@73 3310 name_h.count[U] = c and c.count or e.count
farmbuyer@73 3311 sort_player(name_h)
farmbuyer@87 3312 g_uniques[U] = { loot = index, history = name }
farmbuyer@25 3313 self.hist_clean = nil
farmbuyer@73 3314
farmbuyer@73 3315 if c then flib.del(c) end
farmbuyer@73 3316
farmbuyer@25 3317 return
farmbuyer@25 3318 end
farmbuyer@24 3319 end
farmbuyer@73 3320
farmbuyer@73 3321 -- This is not entirely "history" but not completely anything else either.
farmbuyer@73 3322 -- Handles the primary "Mark as <x>" action. Arguments depend on who's
farmbuyer@73 3323 -- calling it:
farmbuyer@73 3324 -- "local", row_index, new_disposition
farmbuyer@73 3325 -- "remote", sender, unique_id, item_id, old_disposition, new_disposition
farmbuyer@73 3326 -- In the local case, must also broadcast a trigger. In the remote case,
farmbuyer@73 3327 -- must figure out the corresponding loot entry (if it exists). In both
farmbuyer@73 3328 -- cases, must update history appropriately. Returns nil if anything odd
farmbuyer@73 3329 -- happens (not necessarily an error!); returns the affected loot index
farmbuyer@73 3330 -- on success.
farmbuyer@73 3331 function addon:loot_mark_disposition (how, ...)
farmbuyer@73 3332 -- This must all be filled out in all cases:
farmbuyer@73 3333 local e, index, olddisp, newdisp, unique, id
farmbuyer@73 3334 -- Only set in remote case:
farmbuyer@73 3335 local sender
farmbuyer@73 3336
farmbuyer@73 3337 if how == "local" then
farmbuyer@73 3338 index, newdisp = ...
farmbuyer@73 3339 index = assert(tonumber(index))
farmbuyer@73 3340 e = g_loot[index]
farmbuyer@73 3341 id = e.id
farmbuyer@73 3342 unique = e.unique -- can potentially still be nil at this step
farmbuyer@73 3343 olddisp = e.disposition
farmbuyer@73 3344
farmbuyer@73 3345 elseif how == "remote" then
farmbuyer@73 3346 sender, unique, id, olddisp, newdisp = ...
farmbuyer@81 3347 id = tonumber(id)
farmbuyer@81 3348 local cache
farmbuyer@81 3349 local loop = 0
farmbuyer@81 3350 repeat -- wtb continue statement pst
farmbuyer@81 3351 if loop > 1 then break end
farmbuyer@81 3352 e = nil
farmbuyer@81 3353 cache = cache and g_uniques:SEARCH(unique) or g_uniques[unique]
farmbuyer@73 3354 index = tonumber(cache.loot)
farmbuyer@81 3355 if index then
farmbuyer@81 3356 e = g_loot[index]
farmbuyer@81 3357 else
farmbuyer@81 3358 end
farmbuyer@81 3359 loop = loop + 1
farmbuyer@81 3360 until e and (e.id == id)
farmbuyer@73 3361
farmbuyer@73 3362 else
farmbuyer@76 3363 return -- silently ignore future cases from future clients
farmbuyer@73 3364 end
farmbuyer@73 3365
farmbuyer@73 3366 if self.debug.loot then
farmbuyer@80 3367 local m = ("Re-mark index %d (pre-unique %s) with id %d from '%s' to '%s'."):
farmbuyer@73 3368 format(index, unique, id, tostring(olddisp), tostring(newdisp))
farmbuyer@73 3369 self.dprint('loot', m)
farmbuyer@73 3370 if sender == my_name then
farmbuyer@76 3371 self.dprint('loot',"(Returning early from debug mode's double self-mark.)")
farmbuyer@73 3372 return index
farmbuyer@73 3373 end
farmbuyer@73 3374 end
farmbuyer@73 3375
farmbuyer@73 3376 if not e then
farmbuyer@73 3377 -- say something?
farmbuyer@73 3378 return
farmbuyer@73 3379 end
farmbuyer@73 3380
farmbuyer@73 3381 e.disposition = newdisp
farmbuyer@73 3382 e.bcast_from = nil -- I actually don't remember now why this gets cleared...
farmbuyer@73 3383 e.extratext = nil
farmbuyer@73 3384 self:history_handle_disposition (index, olddisp)
farmbuyer@81 3385 self.hist_clean = nil
farmbuyer@81 3386 self.loot_clean = nil
farmbuyer@73 3387 -- A unique tag has been set by this point.
farmbuyer@73 3388 if how == "local" then
farmbuyer@73 3389 unique = assert(e.unique)
farmbuyer@101 3390 if opts.chatty_on_local_changes then
farmbuyer@101 3391 _notify_about_change (_G.UNIT_YOU, index, olddisp)
farmbuyer@101 3392 end
farmbuyer@73 3393 self:vbroadcast('mark', unique, id, olddisp, newdisp)
farmbuyer@73 3394 end
farmbuyer@100 3395 self:Fire ('MarkAs', unique, id, e, olddisp or 'normal', newdisp or 'normal')
farmbuyer@73 3396 return index
farmbuyer@73 3397 end
farmbuyer@1 3398 end
farmbuyer@1 3399
farmbuyer@1 3400
farmbuyer@1 3401 ------ Player communication
farmbuyer@1 3402 do
farmbuyer@73 3403 local select, tconcat, strsplit, unpack = select, table.concat, strsplit, unpack
farmbuyer@56 3404 --[[ old way: repeated string concatenations, BAD
farmbuyer@76 3405 new way: new table on every broadcast, BAD
farmbuyer@56 3406 local msg = ...
farmbuyer@56 3407 for i = 2, select('#',...) do
farmbuyer@56 3408 msg = msg .. '\a' .. (select(i,...) or "")
farmbuyer@56 3409 end
farmbuyer@56 3410 return msg
farmbuyer@56 3411 ]]
farmbuyer@56 3412 local function assemble(t,...)
farmbuyer@73 3413 local n = select('#',...)
farmbuyer@73 3414 if n > 0 then
farmbuyer@56 3415 local msg = {t,...}
farmbuyer@56 3416 -- tconcat requires strings, but T is known to be one already
farmbuyer@89 3417 -- can't use #msg since there might be nil holes
farmbuyer@73 3418 for i = 2, n+1 do
farmbuyer@71 3419 msg[i] = tostring(msg[i] or "")
farmbuyer@56 3420 end
farmbuyer@56 3421 return tconcat (msg, '\a')
farmbuyer@56 3422 end
farmbuyer@56 3423 return t
farmbuyer@56 3424 end
farmbuyer@56 3425
farmbuyer@56 3426 -- broadcast('tag', <stuff>)
farmbuyer@56 3427 -- vbroadcast('tag', <stuff>)
farmbuyer@56 3428 function addon:vbroadcast(tag,...)
farmbuyer@56 3429 return self:broadcast(self.commrev..tag,...)
farmbuyer@56 3430 end
farmbuyer@56 3431 function addon:broadcast(tag,...)
farmbuyer@56 3432 local msg = assemble(tag,...)
farmbuyer@56 3433 self.dprint('comm', "<broadcast>:", msg)
farmbuyer@89 3434 self:SendCommMessage(self.ident, msg, "RAID")
farmbuyer@89 3435 -- this is what lets us debug our own message traffic:
farmbuyer@100 3436 if self.debug.comm and self.is_guilded then
farmbuyer@89 3437 self:SendCommMessage(self.ident, msg, "GUILD")
farmbuyer@89 3438 end
farmbuyer@56 3439 end
farmbuyer@56 3440 -- whispercast(<to>, 'tag', <stuff>)
farmbuyer@56 3441 function addon:whispercast(to,...)
farmbuyer@56 3442 local msg = assemble(...)
farmbuyer@56 3443 self.dprint('comm', "<whispercast>@", to, ":", msg)
farmbuyer@56 3444 self:SendCommMessage(self.identTg, msg, "WHISPER", to)
farmbuyer@56 3445 end
farmbuyer@56 3446
farmbuyer@1 3447 local function adduser (name, status, active)
farmbuyer@1 3448 if status then addon.sender_list.names[name] = status end
farmbuyer@1 3449 if active then addon.sender_list.active[name] = active end
farmbuyer@1 3450 end
farmbuyer@1 3451
farmbuyer@1 3452 -- Incoming handler functions. All take the sender name and the incoming
farmbuyer@1 3453 -- tag as the first two arguments. All of these are active even when the
farmbuyer@1 3454 -- player is not tracking loot, so test for that when appropriate.
farmbuyer@1 3455 local OCR_funcs = {}
farmbuyer@1 3456
farmbuyer@1 3457 OCR_funcs.ping = function (sender)
farmbuyer@1 3458 pprint('comm', "incoming ping from", sender)
farmbuyer@89 3459 local what = addon.enabled and "tracking" or
farmbuyer@89 3460 (addon.rebroadcast and "broadcasting" or "disabled")
farmbuyer@89 3461 addon:whispercast (sender, 'pong', addon.version, what, addon.revision)
farmbuyer@1 3462 end
farmbuyer@89 3463 OCR_funcs.pong = function (sender, _, ver, status, opt_rev)
farmbuyer@89 3464 local s = ("|cff00ff00%s|r %s(r%s) is |cff00ffff%s|r"):
farmbuyer@89 3465 format (sender, ver, opt_rev or "?", status)
farmbuyer@1 3466 addon:Print("Echo: ", s)
farmbuyer@1 3467 adduser (sender, s, status=="tracking" or status=="broadcasting" or nil)
farmbuyer@1 3468 end
farmbuyer@27 3469 OCR_funcs.revcheck = function (sender, _, revlarge)
farmbuyer@27 3470 addon.dprint('comm', "revcheck, sender", sender)
farmbuyer@89 3471 addon:_check_version (revlarge)
farmbuyer@27 3472 end
farmbuyer@1 3473
farmbuyer@76 3474 OCR_funcs['17improv'] = function (sender, _, senderid, existing, replace)
farmbuyer@76 3475 addon.dprint('comm', "DOTimprov/17, sender", sender, "id", senderid,
farmbuyer@76 3476 "existing", existing, "replace", replace)
farmbuyer@76 3477 if not g_unique_replace then _setup_unique_replace() end
farmbuyer@76 3478 g_unique_replace.new_entry (senderid, existing, replace)
farmbuyer@76 3479 end
farmbuyer@76 3480
farmbuyer@73 3481 OCR_funcs['17mark'] = function (sender, _, unique, item, old, new)
farmbuyer@73 3482 addon.dprint('comm', "DOTmark/17, sender", sender, "unique", unique,
farmbuyer@73 3483 "item", item, "from old", old, "to new", new)
farmbuyer@73 3484 local index = addon:loot_mark_disposition ("remote", sender, unique, item, old, new)
farmbuyer@73 3485 --if not addon.enabled then return end -- hmm
farmbuyer@73 3486 if index and opts.chatty_on_remote_changes then
farmbuyer@101 3487 _notify_about_change (sender, index, old)
farmbuyer@73 3488 end
farmbuyer@73 3489 end
farmbuyer@73 3490
farmbuyer@81 3491 OCR_funcs['17reassign'] = function (sender, _, unique, item, from, to)
farmbuyer@81 3492 addon.dprint('comm', "DOTreassign/17, sender", sender, "unique", unique,
farmbuyer@81 3493 "item", item, "from", from, "to", to)
farmbuyer@81 3494 --[[local index =]] addon:reassign_loot ("remote", sender, unique, item, from, to)
farmbuyer@81 3495 -- Notification handled inside reassign_loot.
farmbuyer@81 3496 end
farmbuyer@81 3497
farmbuyer@71 3498 OCR_funcs['16loot'] = function (sender, _, recip, item, count, extratext)
farmbuyer@71 3499 addon.dprint('comm', "DOTloot/16, sender", sender, "recip", recip, "item", item, "count", count)
farmbuyer@1 3500 if not addon.enabled then return end
farmbuyer@1 3501 adduser (sender, nil, true)
farmbuyer@73 3502 -- Empty unique string will pass through all of the loot handling,
farmbuyer@73 3503 -- and then be rewritten by the history routine (into older string
farmbuyer@73 3504 -- of ID+date).
farmbuyer@73 3505 g_seeing_oldsigs = g_seeing_oldsigs or {}
farmbuyer@73 3506 g_seeing_oldsigs[sender] = true
farmbuyer@71 3507 addon:CHAT_MSG_LOOT ("broadcast", recip, --[[unique=]]"", item, count, sender, extratext)
farmbuyer@1 3508 end
farmbuyer@71 3509 OCR_funcs.loot = OCR_funcs['16loot'] -- old unversioned stuff goes to 16
farmbuyer@71 3510 OCR_funcs['17loot'] = function (sender, _, recip, unique, item, count, extratext)
farmbuyer@73 3511 addon.dprint('comm', "DOTloot/17, sender", sender, "recip", recip,
farmbuyer@73 3512 "unique", unique, "item", item, "count", count, "extratext", extratext)
farmbuyer@71 3513 if not addon.enabled then return end
farmbuyer@71 3514 adduser (sender, nil, true)
farmbuyer@71 3515 addon:CHAT_MSG_LOOT ("broadcast", recip, unique, item, count, sender, extratext)
farmbuyer@71 3516 end
farmbuyer@1 3517
farmbuyer@1 3518 OCR_funcs.boss = function (sender, _, reason, bossname, instancetag)
farmbuyer@56 3519 addon.dprint('comm', "DOTboss, sender", sender, "reason", reason,
farmbuyer@56 3520 "name", bossname, "it", instancetag)
farmbuyer@1 3521 if not addon.enabled then return end
farmbuyer@1 3522 adduser (sender, nil, true)
farmbuyer@56 3523 addon:on_boss_broadcast (reason, bossname, instancetag, --[[maxsize=]]0)
farmbuyer@56 3524 end
farmbuyer@56 3525 OCR_funcs['16boss'] = function (sender, _, reason, bossname, instancetag, maxsize)
farmbuyer@73 3526 addon.dprint('comm', "DOTboss/16,17, sender", sender, "reason", reason,
farmbuyer@56 3527 "name", bossname, "it", instancetag, "size", maxsize)
farmbuyer@56 3528 if not addon.enabled then return end
farmbuyer@56 3529 adduser (sender, nil, true)
farmbuyer@56 3530 addon:on_boss_broadcast (reason, bossname, instancetag, maxsize)
farmbuyer@1 3531 end
farmbuyer@71 3532 OCR_funcs['17boss'] = OCR_funcs['16boss']
farmbuyer@1 3533
farmbuyer@76 3534 local bcast_on = addon.format_hypertext ([[the red pill]], '|cffff4040',
farmbuyer@78 3535 function (self)
farmbuyer@78 3536 if not self.rebroadcast then
farmbuyer@78 3537 self:Activate(nil,true)
farmbuyer@76 3538 end
farmbuyer@78 3539 self:broadcast('bcast_responder')
farmbuyer@76 3540 end)
farmbuyer@76 3541 local waferthin = addon.format_hypertext ([[the blue pill]], '|cff0070dd',
farmbuyer@78 3542 function (self)
farmbuyer@76 3543 g_wafer_thin = true -- mint? it's wafer thin!
farmbuyer@78 3544 self:broadcast('bcast_denied') -- fuck off, I'm full
farmbuyer@76 3545 end)
farmbuyer@1 3546 OCR_funcs.bcast_req = function (sender)
farmbuyer@1 3547 if addon.debug.comm or ((not g_wafer_thin) and (not addon.rebroadcast))
farmbuyer@1 3548 then
farmbuyer@95 3549 addon:Print("%s has requested additional broadcasters! Click %s to enable rebroadcasting, or %s to remain off and also ignore rebroadcast requests for as long as you're logged in.",
farmbuyer@1 3550 sender,
farmbuyer@76 3551 tostring(bcast_on),
farmbuyer@76 3552 tostring(waferthin))
farmbuyer@1 3553 end
farmbuyer@18 3554 addon.popped = true
farmbuyer@1 3555 end
farmbuyer@1 3556
farmbuyer@1 3557 OCR_funcs.bcast_responder = function (sender)
farmbuyer@1 3558 if addon.debug.comm or addon.requesting or
farmbuyer@1 3559 ((not g_wafer_thin) and (not addon.rebroadcast))
farmbuyer@1 3560 then
farmbuyer@1 3561 addon:Print(sender, "has answered the call and is now broadcasting loot.")
farmbuyer@1 3562 end
farmbuyer@1 3563 end
farmbuyer@1 3564 -- remove this tag once it's all tested
farmbuyer@1 3565 OCR_funcs.bcast_denied = function (sender)
farmbuyer@1 3566 if addon.requesting then addon:Print(sender, "declines futher broadcast requests.") end
farmbuyer@1 3567 end
farmbuyer@1 3568
farmbuyer@73 3569 -- Incoming message disassembler and dispatcher. The static weak table
farmbuyer@73 3570 -- is not my favorite approach to handling ellipses, but it lets me loop
farmbuyer@73 3571 -- through potential nils easily without creating a ton of garbage.
farmbuyer@73 3572 local OCR_data = setmetatable({}, {__mode='v'})
farmbuyer@1 3573 local function dotdotdot (sender, tag, ...)
farmbuyer@1 3574 local f = OCR_funcs[tag]
farmbuyer@73 3575 if f then
farmbuyer@73 3576 --wipe(OCR_data) costs more than its worth here
farmbuyer@73 3577 local n = select('#',...)
farmbuyer@73 3578 for i = 1, n do
farmbuyer@73 3579 local d = select(i,...)
farmbuyer@73 3580 OCR_data[i] = (d ~= "") and d or nil
farmbuyer@73 3581 end
farmbuyer@89 3582 addon.dprint('comm', ":...processing", tag, "from", sender, "with arg count", n)
farmbuyer@73 3583 return f(sender,tag,unpack(OCR_data,1,n))
farmbuyer@73 3584 end
farmbuyer@73 3585 addon.dprint('comm', "unknown comm message", tag, "from", sender)
farmbuyer@1 3586 end
farmbuyer@73 3587 -- Recent message cache (this can be accessed via advanced options panel)
farmbuyer@1 3588 addon.recent_messages = create_new_cache ('comm', comm_cleanup_ttl)
farmbuyer@1 3589
farmbuyer@1 3590 function addon:OnCommReceived (prefix, msg, distribution, sender)
farmbuyer@1 3591 if prefix ~= self.ident then return end
farmbuyer@1 3592 if not self.debug.comm then
farmbuyer@1 3593 if distribution ~= "RAID" and distribution ~= "WHISPER" then return end
farmbuyer@1 3594 if sender == my_name then return end
farmbuyer@1 3595 end
farmbuyer@1 3596 self.dprint('comm', ":OCR from", sender, "message is", msg)
farmbuyer@1 3597
farmbuyer@1 3598 if self.recent_messages:test(msg) then
farmbuyer@40 3599 self.dprint('cache', "OCR message <",msg,"> already in cache, skipping")
farmbuyer@40 3600 return
farmbuyer@1 3601 end
farmbuyer@1 3602 self.recent_messages:add(msg)
farmbuyer@1 3603
farmbuyer@1 3604 -- Nothing is actually returned, just (ab)using tail calls.
farmbuyer@1 3605 return dotdotdot(sender,strsplit('\a',msg))
farmbuyer@1 3606 end
farmbuyer@1 3607
farmbuyer@1 3608 function addon:OnCommReceivedNocache (prefix, msg, distribution, sender)
farmbuyer@1 3609 if prefix ~= self.identTg then return end
farmbuyer@1 3610 if not self.debug.comm then
farmbuyer@1 3611 if distribution ~= "WHISPER" then return end
farmbuyer@1 3612 if sender == my_name then return end
farmbuyer@1 3613 end
farmbuyer@1 3614 self.dprint('comm', ":OCRN from", sender, "message is", msg)
farmbuyer@1 3615 return dotdotdot(sender,strsplit('\a',msg))
farmbuyer@1 3616 end
farmbuyer@1 3617 end
farmbuyer@1 3618
farmbuyer@1 3619 -- vim:noet