comparison core.lua @ 1:822b6ca3ef89

Import of 2.15, moving to wowace svn.
author Farmbuyer of US-Kilrogg <farmbuyer@gmail.com>
date Sat, 16 Apr 2011 06:03:29 +0000
parents
children fe437e761ef8
comparison
equal deleted inserted replaced
0:0f14a1e5364d 1:822b6ca3ef89
1 local addon = select(2,...)
2
3 --[==[
4 g_loot's numeric indices are loot entries (including titles, separators,
5 etc); its named indices are:
6 - forum: saved text from forum markup window, default nil
7 - attend: saved text from raid attendence window, default nil
8 - printed.FOO: last index formatted into text window FOO, default 0
9 - saved: table of copies of saved texts, default nil; keys are numeric
10 indices of tables, subkeys of those are name/forum/attend/date
11 - autoshard: optional name of disenchanting player, default nil
12 - threshold: optional loot threshold, default nil
13
14 Functions arranged like this, with these lables (for jumping to). As a
15 rule, member functions with UpperCamelCase names are called directly by
16 user-facing code, ones with lowercase names are "one step removed", and
17 names with leading underscores are strictly internal helper functions.
18 ------ Saved variables
19 ------ Constants
20 ------ Addon member data
21 ------ Locals
22 ------ Expiring caches
23 ------ Ace3 framework stuff
24 ------ Event handlers
25 ------ Slash command handler
26 ------ On/off
27 ------ Behind the scenes routines
28 ------ Saved texts
29 ------ Loot histories
30 ------ Player communication
31
32 This started off as part of a raid addon package written by somebody else.
33 After he retired, I began modifying the code. Eventually I set aside the
34 entire package and rewrote the loot tracker module from scratch. Many of the
35 variable/function naming conventions (sv_*, g_*, and family) stayed across the
36 rewrite. Some variables are needlessly initialized to nil just to look uniform.
37
38 ]==]
39
40 ------ Saved variables
41 OuroLootSV = nil -- possible copy of g_loot
42 OuroLootSV_opts = nil -- same as option_defaults until changed
43 OuroLootSV_hist = nil
44
45
46 ------ Constants
47 local option_defaults = {
48 ['popup_on_join'] = true,
49 ['register_slashloot'] = true,
50 ['scroll_to_bottom'] = true,
51 ['chatty_on_kill'] = false,
52 ['no_tracking_wipes'] = false,
53 ['snarky_boss'] = true,
54 ['keybinding'] = false,
55 ['keybinding_text'] = 'CTRL-SHIFT-O',
56 ['forum'] = {
57 ['[url]'] = '[url=http://www.wowhead.com/?item=$I]$N[/url]$X - $T',
58 ['[item] by name'] = '[item]$N[/item]$X - $T',
59 ['[item] by ID'] = '[item]$I[/item]$X - $T',
60 ['Custom...'] = '',
61 },
62 ['forum_current'] = '[item] by name',
63 }
64 local virgin = "First time loaded? Hi! Use the /ouroloot or /loot command"
65 .." to show the main display. You should probably browse the instructions"
66 .." if you've never used this before; %s to display the help window. This"
67 .." welcome message will not intrude again."
68 local qualnames = {
69 ['gray'] = 0, ['grey'] = 0, ['poor'] = 0, ['trash'] = 0,
70 ['white'] = 1, ['common'] = 1,
71 ['green'] = 2, ['uncommon'] = 2,
72 ['blue'] = 3, ['rare'] = 3,
73 ['epic'] = 4, ['purple'] = 4,
74 ['legendary'] = 5, ['orange'] = 5,
75 ['artifact'] = 6,
76 --['heirloom'] = 7,
77 }
78 local my_name = UnitName('player')
79 local comm_cleanup_ttl = 5 -- seconds in the cache
80
81
82 ------ Addon member data
83 local flib = LibStub("LibFarmbuyer")
84 addon.author_debug = flib.author_debug
85
86 -- Play cute games with namespaces here just to save typing.
87 do local _G = _G setfenv (1, addon)
88
89 revision = 15
90 ident = "OuroLoot2"
91 identTg = "OuroLoot2Tg"
92 status_text = nil
93
94 DEBUG_PRINT = false
95 debug = {
96 comm = false,
97 loot = false,
98 flow = false,
99 notraid = false,
100 cache = false,
101 }
102 function dprint (t,...)
103 if DEBUG_PRINT and debug[t] then return _G.print("<"..t.."> ",...) end
104 end
105
106 if author_debug then
107 function pprint(t,...)
108 return _G.print("<<"..t..">> ",...)
109 end
110 else
111 pprint = flib.nullfunc
112 end
113
114 enabled = false
115 rebroadcast = false
116 display = nil -- display frame, when visible
117 loot_clean = nil -- index of last GUI entry with known-current visual data
118 sender_list = {active={},names={}} -- this should be reworked
119 threshold = debug.loot and 0 or 3 -- rare by default
120 sharder = nil -- name of person whose loot is marked as shards
121
122 -- The rest is also used in the GUI:
123
124 popped = nil -- non-nil when reminder has been shown, actual value unimportant
125
126 -- This is an amalgamation of all four LOOT_ITEM_* patterns.
127 -- Captures: 1 person/You, 2 itemstring, 3 rest of string after final |r until '.'
128 -- Can change 'loot' to 'item' to trigger on, e.g., extracting stuff from mail.
129 loot_pattern = "(%S+) receives? loot:.*|cff%x+|H(.-)|h.*|r(.*)%.$"
130
131 dbm_registered = nil
132 requesting = nil -- for prompting for additional rebroadcasters
133
134 thresholds, quality_hexes = {}, {}
135 for i = 0,6 do
136 local hex = _G.select(4,_G.GetItemQualityColor(i))
137 local desc = _G["ITEM_QUALITY"..i.."_DESC"]
138 quality_hexes[i] = hex
139 thresholds[i] = hex .. desc .. "|r"
140 end
141
142 _G.setfenv (1, _G)
143 end
144
145 addon = LibStub("AceAddon-3.0"):NewAddon(addon, "Ouro Loot",
146 "AceTimer-3.0", "AceComm-3.0", "AceConsole-3.0", "AceEvent-3.0")
147
148
149 ------ Locals
150 local g_loot = nil
151 local g_restore_p = nil
152 local g_saved_tmp = nil -- restoring across a clear
153 local g_wafer_thin = nil -- for prompting for additional rebroadcasters
154 local g_today = nil -- "today" entry in g_loot
155 local opts = nil
156
157 local pairs, ipairs, tinsert, tremove, tonumber = pairs, ipairs, table.insert, table.remove, tonumber
158
159 local pprint, tabledump = addon.pprint, flib.tabledump
160
161 -- En masse forward decls of symbols defined inside local blocks
162 local _registerDBM -- break out into separate file
163 local makedate, create_new_cache, _init
164
165 -- Hypertext support, inspired by DBM broadcast pizza timers
166 do
167 local hypertext_format_str = "|HOuroRaid:%s|h%s[%s]|r|h"
168
169 function addon.format_hypertext (code, text, color)
170 return hypertext_format_str:format (code,
171 type(color)=='number' and addon.quality_hexes[color] or color,
172 text)
173 end
174
175 DEFAULT_CHAT_FRAME:HookScript("OnHyperlinkClick", function(self, link, string, mousebutton)
176 local ltype, arg = strsplit(":",link)
177 if ltype ~= "OuroRaid" then return end
178 if arg == 'openloot' then
179 addon:BuildMainDisplay()
180 elseif arg == 'help' then
181 addon:BuildMainDisplay('help')
182 elseif arg == 'bcaston' then
183 if not addon.rebroadcast then
184 addon:Activate(nil,true)
185 end
186 addon:broadcast('bcast_responder')
187 elseif arg == 'waferthin' then -- mint? it's wafer thin!
188 g_wafer_thin = true -- fuck off, I'm full
189 addon:broadcast('bcast_denied') -- remove once tested
190 end
191 end)
192
193 local old = ItemRefTooltip.SetHyperlink
194 function ItemRefTooltip:SetHyperlink (link, ...)
195 if link:match("^OuroRaid") then return end
196 return old (self, link, ...)
197 end
198 end
199
200 do
201 -- copied here because it's declared local to the calendar ui, thanks blizz ><
202 local CALENDAR_FULLDATE_MONTH_NAMES = {
203 FULLDATE_MONTH_JANUARY, FULLDATE_MONTH_FEBRUARY, FULLDATE_MONTH_MARCH,
204 FULLDATE_MONTH_APRIL, FULLDATE_MONTH_MAY, FULLDATE_MONTH_JUNE,
205 FULLDATE_MONTH_JULY, FULLDATE_MONTH_AUGUST, FULLDATE_MONTH_SEPTEMBER,
206 FULLDATE_MONTH_OCTOBER, FULLDATE_MONTH_NOVEMBER, FULLDATE_MONTH_DECEMBER,
207 }
208 -- returns "dd Month yyyy", mm, dd, yyyy
209 function makedate()
210 Calendar_LoadUI()
211 local _, M, D, Y = CalendarGetDate()
212 local text = ("%d %s %d"):format(D, CALENDAR_FULLDATE_MONTH_NAMES[M], Y)
213 return text, M, D, Y
214 end
215 end
216
217 -- Returns an instance name or abbreviation
218 local function instance_tag()
219 local name, typeof, diffcode, diffstr, _, perbossheroic, isdynamic = GetInstanceInfo()
220 local t
221 name = addon.instance_abbrev[name] or name
222 if typeof == "none" then return name end
223 -- diffstr is "5 Player", "10 Player (Heroic)", etc. ugh.
224 if diffcode == 1 then
225 t = ((GetNumRaidMembers()>0) and "10" or "5")
226 elseif diffcode == 2 then
227 t = ((GetNumRaidMembers()>0) and "25" or "5h")
228 elseif diffcode == 3 then
229 t = "10h"
230 elseif diffcode == 4 then
231 t = "25h"
232 end
233 -- dynamic difficulties always return normal "codes"
234 if isdynamic and perbossheroic == 1 then
235 t = t .. "h"
236 end
237 return name .. "(" .. t .. ")"
238 end
239 addon.instance_tag = instance_tag -- grumble
240
241
242 ------ Expiring caches
243 --[[
244 foo = create_new_cache("myfoo",15[,cleanup]) -- ttl
245 foo:add("blah")
246 foo:test("blah") -- returns true
247 ]]
248 do
249 local caches = {}
250 local cleanup_group = AnimTimerFrame:CreateAnimationGroup()
251 cleanup_group:SetLooping("REPEAT")
252 cleanup_group:SetScript("OnLoop", function(cg)
253 addon.dprint('cache',"OnLoop firing")
254 local now = GetTime()
255 local alldone = true
256 -- this is ass-ugly
257 for _,c in ipairs(caches) do
258 while (#c > 0) and (now - c[1].t > c.ttl) do
259 addon.dprint('cache', c.name, "cache removing",c[1].t, c[1].m)
260 tremove(c,1)
261 end
262 alldone = alldone and (#c == 0)
263 end
264 if alldone then
265 addon.dprint('cache',"OnLoop finishing animation group")
266 cleanup_group:Finish()
267 for _,c in ipairs(caches) do
268 if c.func then c:func() end
269 end
270 end
271 addon.dprint('cache',"OnLoop done")
272 end)
273
274 local function _add (cache, x)
275 tinsert(cache, {t=GetTime(),m=x})
276 if not cleanup_group:IsPlaying() then
277 addon.dprint('cache', cache.name, "STARTING animation group")
278 cache.cleanup:SetDuration(2) -- hmmm
279 cleanup_group:Play()
280 end
281 end
282 local function _test (cache, x)
283 for _,v in ipairs(cache) do
284 if v.m == x then return true end
285 end
286 end
287 function create_new_cache (name, ttl, on_alldone)
288 local c = {
289 ttl = ttl,
290 name = name,
291 add = _add,
292 test = _test,
293 cleanup = cleanup_group:CreateAnimation("Animation"),
294 func = on_alldone,
295 }
296 c.cleanup:SetOrder(1)
297 -- setting OnFinished for cleanup fires at the end of each inner loop,
298 -- with no 'requested' argument to distinguish cases. thus, on_alldone.
299 tinsert (caches, c)
300 return c
301 end
302 end
303
304
305 ------ Ace3 framework stuff
306 function addon:OnInitialize()
307 -- VARIABLES_LOADED has fired by this point; test if we're doing something like
308 -- relogging during a raid and already have collected loot data
309 g_restore_p = OuroLootSV ~= nil
310 self.dprint('flow', "oninit sets restore as", g_restore_p)
311
312 if OuroLootSV_opts == nil then
313 OuroLootSV_opts = {}
314 self:ScheduleTimer(function(s)
315 s:Print(virgin, s.format_hypertext('help',"click here",ITEM_QUALITY_UNCOMMON))
316 virgin = nil
317 end,10,self)
318 end
319 opts = OuroLootSV_opts
320 for opt,default in pairs(option_defaults) do
321 if opts[opt] == nil then
322 opts[opt] = default
323 end
324 end
325 option_defaults = nil
326 -- transition/remove old options
327 opts["forum_use_itemid"] = nil
328 if opts["forum_format"] then
329 opts.forum["Custom..."] = opts["forum_format"]
330 opts["forum_format"] = nil
331 end
332 -- get item filter table if needed
333 if opts.itemfilter == nil then
334 opts.itemfilter = addon.default_itemfilter
335 end
336 addon.default_itemfilter = nil
337
338 self:RegisterChatCommand("ouroloot", "OnSlash")
339 -- maybe try to detect if this command is already in use...
340 if opts.register_slashloot then
341 SLASH_ACECONSOLE_OUROLOOT2 = "/loot"
342 end
343
344 self.history_all = self.history_all or OuroLootSV_hist or {}
345 local r = GetRealmName()
346 self.history_all[r] = self:_prep_new_history_category (self.history_all[r], r)
347 self.history = self.history_all[r]
348
349 _init(self)
350 self.OnInitialize = nil
351 end
352
353 function addon:OnEnable()
354 self:RegisterEvent "PLAYER_LOGOUT"
355 self:RegisterEvent "RAID_ROSTER_UPDATE"
356
357 -- Cribbed from Talented. I like the way jerry thinks: the first argument
358 -- can be a format spec for the remainder of the arguments. (The new
359 -- AceConsole:Printf isn't used because we can't specify a prefix without
360 -- jumping through ridonkulous hoops.) The part about overriding :Print
361 -- with a version using prefix hyperlinks is my fault.
362 do
363 local AC = LibStub("AceConsole-3.0")
364 local chat_prefix = self.format_hypertext('openloot',"Ouro Loot",--[[legendary]]5)
365 function addon:Print (str, ...)
366 if type(str) == 'string' and str:find("%", nil, --[[plainmatch=]]true) then
367 return AC:Print (chat_prefix, str:format(...))
368 else
369 return AC:Print (chat_prefix, str, ...)
370 end
371 end
372 end
373
374 if opts.keybinding then
375 local btn = CreateFrame("Button", "OuroLootBindingOpen", nil, "SecureActionButtonTemplate")
376 btn:SetAttribute("type", "macro")
377 btn:SetAttribute("macrotext", "/ouroloot toggle")
378 if SetBindingClick(opts.keybinding_text, "OuroLootBindingOpen") then
379 SaveBindings(GetCurrentBindingSet())
380 else
381 self:Print("Error registering '%s' as a keybinding, check spelling!",
382 opts.keybinding_text)
383 end
384 end
385
386 if self.debug.flow then self:Print"is in control-flow debug mode." end
387 end
388 --function addon:OnDisable() end
389
390
391 ------ Event handlers
392 function addon:_clear_SVs()
393 g_loot = {} -- not saved, just fooling PLAYER_LOGOUT tests
394 OuroLootSV = nil
395 OuroLootSV_opts = nil
396 OuroLootSV_hist = nil
397 end
398 function addon:PLAYER_LOGOUT()
399 if (#g_loot > 0) or g_loot.saved
400 or (g_loot.forum and g_loot.forum ~= "")
401 or (g_loot.attend and g_loot.attend ~= "")
402 then
403 g_loot.autoshard = self.sharder
404 g_loot.threshold = self.threshold
405 --OuroLootSV = g_loot
406 --for i,e in ipairs(OuroLootSV) do
407 for i,e in ipairs(g_loot) do
408 e.cols = nil
409 end
410 OuroLootSV = g_loot
411 end
412 self.history.kind = nil
413 self.history.st = nil
414 self.history.byname = nil
415 OuroLootSV_hist = self.history_all
416 end
417
418 function addon:RAID_ROSTER_UPDATE (event)
419 if GetNumRaidMembers() > 0 then
420 local inside,whatkind = IsInInstance()
421 if inside and (whatkind == "pvp" or whatkind == "arena") then
422 return self.dprint('flow', "got RRU event but in pvp zone, bailing")
423 end
424 if event == "Activate" then
425 -- dispatched manually from Activate
426 self:RegisterEvent "CHAT_MSG_LOOT"
427 _registerDBM(self)
428 elseif event == "RAID_ROSTER_UPDATE" then
429 -- event registration from onload, joined a raid, maybe show popup
430 if opts.popup_on_join and not self.popped then
431 self.popped = StaticPopup_Show "OUROL_REMIND"
432 self.popped.data = self
433 end
434 end
435 else
436 self:UnregisterEvent "CHAT_MSG_LOOT"
437 self.popped = nil
438 end
439 end
440
441 -- helper for CHAT_MSG_LOOT handler
442 do
443 -- Recent loot cache
444 addon.recent_loot = create_new_cache ('loot', comm_cleanup_ttl)
445
446 local GetItemInfo = GetItemInfo
447
448 -- 'from' and onwards only present if this is triggered by a broadcast
449 function addon:_do_loot (local_override, recipient, itemid, count, from, extratext)
450 local iname, ilink, iquality, _,_,_,_,_,_, itexture = GetItemInfo(itemid)
451 if not iname then return end -- sigh
452 self.dprint('loot',">>_do_loot, R:", recipient, "I:", itemid, "C:", count, "frm:", from, "ex:", extratext)
453
454 local i
455 itemid = tonumber(ilink:match("item:(%d+)"))
456 if local_override or ((iquality >= self.threshold) and not opts.itemfilter[itemid]) then
457 if (self.rebroadcast and (not from)) and not local_override then
458 self:broadcast('loot', recipient, itemid, count)
459 end
460 if self.enabled or local_override then
461 local signature = recipient .. iname .. (count or "")
462 if self.recent_loot:test(signature) then
463 self.dprint('cache', "loot <",signature,"> already in cache, skipping")
464 else
465 self.recent_loot:add(signature)
466 i = self._addLootEntry{ -- There is some redundancy here...
467 kind = 'loot',
468 person = recipient,
469 person_class= select(2,UnitClass(recipient)),
470 quality = iquality,
471 itemname = iname,
472 id = itemid,
473 itemlink = ilink,
474 itexture = itexture,
475 disposition = (recipient == self.sharder) and 'shard' or nil,
476 count = count,
477 bcast_from = from,
478 extratext = extratext,
479 is_heroic = self:is_heroic_item(ilink),
480 }
481 self.dprint('loot', "added entry", i)
482 self:_addHistoryEntry(i)
483 if self.display then
484 self:redisplay()
485 --[[
486 local st = self.display:GetUserData("eoiST")
487 if st and st.frame:IsVisible() then
488 st:OuroLoot_Refresh()
489 end
490 ]]
491 end
492 end
493 end
494 end
495 self.dprint('loot',"<<_do_loot out")
496 return i
497 end
498
499 function addon:CHAT_MSG_LOOT (event, ...)
500 if (not self.rebroadcast) and (not self.enabled) and (event ~= "manual") then return end
501
502 --[[
503 iname: Hearthstone
504 iquality: integer
505 ilink: clickable formatted link
506 itemstring: item:6948:....
507 itexture: inventory icon texture
508 ]]
509
510 if event == "CHAT_MSG_LOOT" then
511 local msg = ...
512 --ChatFrame2:AddMessage("original string: >"..(msg:gsub("\124","\124\124")).."<")
513 local person, itemstring, remainder = msg:match(self.loot_pattern)
514 self.dprint('loot', "CHAT_MSG_LOOT, person is", person, ", itemstring is", itemstring, ", rest is", remainder)
515 if not person then return end -- "So-and-So selected Greed", etc, not actual looting
516 local count = remainder and remainder:match(".*(x%d+)$")
517
518 -- Name might be colorized, remove the highlighting
519 local p = person:match("|c%x%x%x%x%x%x%x%x(%S+)")
520 person = p or person
521 person = (person == UNIT_YOU) and my_name or person
522
523 local id = tonumber((select(2, strsplit(":", itemstring))))
524
525 return self:_do_loot (false, person, id, count)
526
527 elseif event == "broadcast" then
528 return self:_do_loot(false, ...)
529
530 elseif event == "manual" then
531 local r,i,n = ...
532 return self:_do_loot(true, r,i,nil,nil,n)
533 end
534 end
535 end
536
537
538 ------ Slash command handler
539 -- Thought about breaking this up into a table-driven dispatcher. But
540 -- that would result in a pile of teensy functions, most of which would
541 -- never be called. Too much overhead. (2.0: Most of these removed now
542 -- that GUI is in place.)
543 function addon:OnSlash (txt) --, editbox)
544 txt = strtrim(txt:lower())
545 local cmd, arg = ""
546 do
547 local s,e = txt:find("^%a+")
548 if s then
549 cmd = txt:sub(s,e)
550 s = txt:find("%S", e+2)
551 if s then arg = txt:sub(s,-1) end
552 end
553 end
554
555 if cmd == "" then
556 if InCombatLockdown() then
557 return self:Print("Can't display window in combat.")
558 else
559 return self:BuildMainDisplay()
560 end
561
562 elseif cmd:find("^thre") then
563 self:SetThreshold(arg)
564
565 elseif cmd == "on" then self:Activate(arg)
566 elseif cmd == "off" then self:Deactivate()
567 elseif cmd == "broadcast" or cmd == "bcast" then self:Activate(nil,true)
568
569 elseif cmd == "fake" then -- maybe comment this out for real users
570 self:_mark_boss_kill (self._addLootEntry{
571 kind='boss',reason='kill',bosskill="Baron Steamroller",instance=instance_tag(),duration=0
572 })
573 self:CHAT_MSG_LOOT ('manual', my_name, 54797)
574 if self.display then
575 self:redisplay()
576 end
577 self:Print "Baron Steamroller has been slain. Congratulations on your rug."
578
579 elseif cmd == "debug" then
580 if arg then
581 self.debug[arg] = not self.debug[arg]
582 _G.print(arg,self.debug[arg])
583 if self.debug[arg] then self.DEBUG_PRINT = true end
584 else
585 self.DEBUG_PRINT = not self.DEBUG_PRINT
586 end
587
588 elseif cmd == "save" and arg and arg:len() > 0 then
589 self:save_saveas(arg)
590 elseif cmd == "list" then
591 self:save_list()
592 elseif cmd == "restore" and arg and arg:len() > 0 then
593 self:save_restore(tonumber(arg))
594 elseif cmd == "delete" and arg and arg:len() > 0 then
595 self:save_delete(tonumber(arg))
596
597 elseif cmd == "help" then
598 self:BuildMainDisplay('help')
599 elseif cmd == "toggle" then
600 if self.display then
601 self.display:Hide()
602 else
603 return self:BuildMainDisplay()
604 end
605
606 else
607 if self:OpenMainDisplayToTab(cmd) then
608 return
609 end
610 self:Print("Unknown command '%s'. %s to see the help window.",
611 cmd, self.format_hypertext('help',"Click here",ITEM_QUALITY_UNCOMMON))
612 end
613 end
614
615 function addon:SetThreshold (arg, quiet_p)
616 local q = tonumber(arg)
617 if q then
618 q = math.floor(q+0.001)
619 if q<0 or q>6 then
620 return self:Print("Threshold must be 0-6.")
621 end
622 else
623 q = qualnames[arg]
624 if not q then
625 return self:Print("Unrecognized item quality argument.")
626 end
627 end
628 self.threshold = q
629 if not quiet_p then self:Print("Threshold now set to %s.", self.thresholds[q]) end
630 end
631
632
633 ------ On/off
634 function addon:Activate (opt_threshold, opt_bcast_only)
635 self:RegisterEvent "RAID_ROSTER_UPDATE"
636 self.popped = true
637 if GetNumRaidMembers() > 0 then
638 self:RAID_ROSTER_UPDATE("Activate")
639 elseif self.debug.notraid then
640 self:RegisterEvent "CHAT_MSG_LOOT"
641 _registerDBM(self)
642 elseif g_restore_p then
643 g_restore_p = nil
644 if #g_loot == 0 then return end -- only saved texts, not worth verbage
645 self:Print("Ouro Raid Loot restored previous data, but not in a raid",
646 "and 5-person mode not active. |cffff0505NOT tracking loot|r;",
647 "use 'enable' to activate loot tracking, or 'clear' to erase",
648 "previous data, or 'help' to read about saved-texts commands.")
649 self.popped = nil -- get the reminder if later joining a raid
650 return
651 end
652 self.rebroadcast = true -- hardcode to true; this used to be more complicated
653 self.enabled = not opt_bcast_only
654 if opt_threshold then
655 self:SetThreshold (opt_threshold, --[[quiet_p=]]true)
656 end
657 self:Print("Ouro Raid Loot is %s. Threshold currently %s.",
658 self.enabled and "tracking" or "only broadcasting",
659 self.thresholds[self.threshold])
660 end
661
662 -- Note: running '/loot off' will also avoid the popup reminder when
663 -- joining a raid, but will not change the saved option setting.
664 function addon:Deactivate()
665 self.enabled = false
666 self.rebroadcast = false
667 self:UnregisterEvent "RAID_ROSTER_UPDATE"
668 self:UnregisterEvent "CHAT_MSG_LOOT"
669 self:Print("Ouro Raid Loot deactivated.")
670 end
671
672 function addon:Clear(verbose_p)
673 local repopup, st
674 if self.display then
675 -- in the new version, this is likely to always be the case
676 repopup = true
677 st = self.display:GetUserData("eoiST")
678 if not st then
679 self.dprint('flow', "Clear: display visible but eoiST not set??")
680 end
681 self.display:Hide()
682 end
683 g_restore_p = nil
684 OuroLootSV = nil
685 self:_reset_timestamps()
686 g_saved_tmp = g_loot.saved
687 if verbose_p then
688 if (g_saved_tmp and #g_saved_tmp>0) then
689 self:Print("Current loot data cleared, %d saved sets remaining.", #g_saved_tmp)
690 else
691 self:Print("Current loot data cleared.")
692 end
693 end
694 _init(self,st)
695 if repopup then
696 addon:BuildMainDisplay()
697 end
698 end
699
700
701 ------ Behind the scenes routines
702 -- Adds indices to traverse the tables in a nice sorted order.
703 do
704 local byindex, temp = {}, {}
705 local function sort (src, dest)
706 for k in pairs(src) do
707 temp[#temp+1] = k
708 end
709 table.sort(temp)
710 table.wipe(dest)
711 for i = 1, #temp do
712 dest[i] = src[temp[i]]
713 end
714 end
715
716 function addon.sender_list.sort()
717 sort (addon.sender_list.active, byindex)
718 table.wipe(temp)
719 addon.sender_list.activeI = #byindex
720 sort (addon.sender_list.names, byindex)
721 table.wipe(temp)
722 end
723 addon.sender_list.namesI = byindex
724 end
725
726 -- Message sending.
727 -- See OCR_funcs.tag at the end of this file for incoming message treatment.
728 do
729 local function assemble(...)
730 local msg = ...
731 for i = 2, select('#',...) do
732 msg = msg .. '\a' .. (select(i,...) or "")
733 end
734 return msg
735 end
736
737 -- broadcast('tag', <stuff>)
738 function addon:broadcast(...)
739 local msg = assemble(...)
740 self.dprint('comm', "<broadcast>:", msg)
741 -- the "GUILD" here is just so that we can also pick up on it
742 self:SendCommMessage(self.ident, msg, self.debug.comm and "GUILD" or "RAID")
743 end
744 -- whispercast(<to>, 'tag', <stuff>)
745 function addon:whispercast(to,...)
746 local msg = assemble(...)
747 self.dprint('comm', "<whispercast>@", to, ":", msg)
748 self:SendCommMessage(self.identTg, msg, "WHISPER", to)
749 end
750 end
751
752 -- Generic helpers
753 function addon._find_next_after (kind, index)
754 index = index + 1
755 while index <= #g_loot do
756 if g_loot[index].kind == kind then
757 return index, g_loot[index]
758 end
759 index = index + 1
760 end
761 end
762
763 -- Iterate through g_loot entries according to the KIND field. Loop variables
764 -- are g_loot indices and the corresponding entries (essentially ipairs + some
765 -- conditionals).
766 function addon:filtered_loot_iter (filter_kind)
767 return self._find_next_after, filter_kind, 0
768 end
769
770 do
771 local itt
772 local function create()
773 local tip, lefts = CreateFrame("GameTooltip"), {}
774 for i = 1, 2 do -- scanning idea here also snagged from Talented
775 local L,R = tip:CreateFontString(), tip:CreateFontString()
776 L:SetFontObject(GameFontNormal)
777 R:SetFontObject(GameFontNormal)
778 tip:AddFontStrings(L,R)
779 lefts[i] = L
780 end
781 tip.lefts = lefts
782 return tip
783 end
784 function addon:is_heroic_item(item) -- returns true or *nil*
785 itt = itt or create()
786 itt:SetOwner(UIParent,"ANCHOR_NONE")
787 itt:ClearLines()
788 itt:SetHyperlink(item)
789 local t = itt.lefts[2]:GetText()
790 itt:Hide()
791 return (t == ITEM_HEROIC) or nil
792 end
793 end
794
795 -- Called when first loading up, and then also when a 'clear' is being
796 -- performed. If SV's are present then restore_p will be true.
797 function _init (self, possible_st)
798 self.dprint('flow',"_init running")
799 self.loot_clean = nil
800 self.hist_clean = nil
801 if g_restore_p then
802 g_loot = OuroLootSV
803 self.popped = true
804 self.dprint('flow', "restoring", #g_loot, "entries")
805 self:ScheduleTimer("Activate", 8, g_loot.threshold)
806 -- FIXME printed could be too large if entries were deleted, how much do we care?
807 self.sharder = g_loot.autoshard
808 else
809 g_loot = { printed = {} }
810 g_loot.saved = g_saved_tmp; g_saved_tmp = nil -- potentially restore across a clear
811 end
812
813 self.threshold = g_loot.threshold or self.threshold -- in the case of restoring but not tracking
814 self:gui_init(g_loot)
815
816 if g_restore_p then
817 self:zero_printed_fenceposts() -- g_loot.printed.* = previous/safe values
818 else
819 self:zero_printed_fenceposts(0) -- g_loot.printed.* = 0
820 end
821 if possible_st then
822 possible_st:SetData(g_loot)
823 end
824
825 self.status_text = ("v2r%d communicating as ident %s"):format(self.revision,self.ident)
826 self:RegisterComm(self.ident)
827 self:RegisterComm(self.identTg, "OnCommReceivedNocache")
828
829 if self.author_debug then
830 _G.OL = self
831 _G.Oloot = g_loot
832 end
833 end
834
835 -- Tie-ins with Deadly Boss Mods
836 do
837 local candidates, location
838 local function fixup_durations (cache)
839 if candidates == nil then return end -- this is called for *all* cache expirations, including non-boss
840 local boss, bossi
841 boss = candidates[1]
842 if #candidates == 1 then
843 -- (1) or (2)
844 boss.duration = boss.duration or 0
845 addon.dprint('loot', "only one candidate")
846 else
847 -- (3), should only be one 'cast entry and our local entry
848 if #candidates ~= 2 then
849 -- could get a bunch of 'cast entries on the heels of one another
850 -- before the local one ever fires, apparently... sigh
851 --addon:Print("<warning> s3 cache has %d entries, does that seem right to you?", #candidates)
852 end
853 if candidates[2].duration == nil then
854 --addon:Print("<warning> s3's second entry is not the local trigger, does that seem right to you?")
855 end
856 -- try and be generic anyhow
857 for i,c in ipairs(candidates) do
858 if c.duration then
859 boss = c
860 addon.dprint('loot', "fixup found candidate", i, "duration", c.duration)
861 break
862 end
863 end
864 end
865 bossi = addon._addLootEntry(boss)
866 addon.dprint('loot', "added entry", bossi)
867 if boss.reason == 'kill' then
868 addon:_mark_boss_kill (bossi)
869 if opts.chatty_on_kill then
870 addon:Print("Registered kill for '%s' in %s!", boss.bosskill, boss.instance)
871 end
872 end
873 candidates = nil
874 end
875 addon.recent_boss = create_new_cache ('boss', 10, fixup_durations)
876
877 -- Similar to _do_loot, but duration+ parms only present when locally generated.
878 local function _do_boss (self, reason, bossname, intag, duration, raiders)
879 self.dprint('loot',">>_do_boss, R:", reason, "B:", bossname, "T:", intag,
880 "D:", duration, "RL:", (raiders and #raiders or 'nil'))
881 if self.rebroadcast and duration then
882 self:broadcast('boss', reason, bossname, intag)
883 end
884 -- This is only a loop to make jumping out of it easy, and still do cleanup below.
885 while self.enabled do
886 if reason == 'wipe' and opts.no_tracking_wipes then break end
887 bossname = (opts.snarky_boss and self.boss_abbrev[bossname] or bossname) or bossname
888 local not_from_local = duration == nil
889 local signature = bossname .. reason
890 if not_from_local and self.recent_boss:test(signature) then
891 self.dprint('cache', "boss <",signature,"> already in cache, skipping")
892 else
893 self.recent_boss:add(signature)
894 -- Possible scenarios: (1) we don't see a boss event at all (e.g., we're
895 -- outside the instance) and so this only happens once as a non-local event,
896 -- (2) we see a local event first and all non-local events are filtered
897 -- by the cache, (3) we happen to get some non-local events before doing
898 -- our local event (not because of network weirdness but because our local
899 -- DBM might not trigger for a while).
900 local c = {
901 kind = 'boss',
902 bosskill = bossname, -- minor misnomer, might not actually be a kill
903 reason = reason,
904 instance = intag,
905 duration = duration, -- these two deliberately may be nil
906 raiderlist = raiders and table.concat(raiders, ", ")
907 }
908 candidates = candidates or {}
909 tinsert(candidates,c)
910 break
911 end
912 end
913 self.dprint('loot',"<<_do_boss out")
914 end
915 -- No wrapping layer for now
916 addon.on_boss_broadcast = _do_boss
917
918 function addon:_mark_boss_kill (index)
919 local e = g_loot[index]
920 if not e.bosskill then
921 return self:Print("Something horribly wrong;", index, "is not a boss entry!")
922 end
923 if e.reason ~= 'wipe' then
924 -- enh, bail
925 self.loot_clean = index-1
926 end
927 local attempts = 1
928 local first
929
930 local i,d = 1,g_loot[1]
931 while d ~= e do
932 if d.bosskill and
933 d.bosskill == e.bosskill and
934 d.reason == 'wipe'
935 then
936 first = first or i
937 attempts = attempts + 1
938 assert(tremove(g_loot,i)==d,"_mark_boss_kill screwed up data badly")
939 else
940 i = i + 1
941 end
942 d = g_loot[i]
943 end
944 e.reason = 'kill'
945 e.attempts = attempts
946 self.loot_clean = first or index-1
947 end
948
949 local GetRaidRosterInfo = GetRaidRosterInfo
950 function addon:DBMBossCallback (reason, mod, ...)
951 if (not self.rebroadcast) and (not self.enabled) then return end
952
953 local name
954 if mod.combatInfo and mod.combatInfo.name then
955 name = mod.combatInfo.name
956 elseif mod.id then
957 name = mod.id
958 else
959 name = "Unknown Boss"
960 end
961
962 local it = location or instance_tag()
963 location = nil
964
965 local duration = 0
966 if mod.combatInfo and mod.combatInfo.pull then
967 duration = math.floor (GetTime() - mod.combatInfo.pull)
968 end
969
970 -- attendance: maybe put people in groups 6,7,8 into a "backup/standby"
971 -- list? probably too specific to guild practices.
972 local raiders = {}
973 for i = 1, GetNumRaidMembers() do
974 tinsert(raiders, (GetRaidRosterInfo(i)))
975 end
976 table.sort(raiders)
977
978 return _do_boss (self, reason, name, it, duration, raiders)
979 end
980
981 local callback = function(...) addon:DBMBossCallback(...) end
982 function _registerDBM(self)
983 if DBM then
984 if not self.dbm_registered then
985 local rev = tonumber(DBM.Revision) or 0
986 if rev < 1503 then
987 self.status_text = "|cffff1010Deadly Boss Mods must be version 1.26 or newer to work with Ouro Loot.|r"
988 return
989 end
990 local r = DBM:RegisterCallback("kill", callback)
991 DBM:RegisterCallback("wipe", callback)
992 DBM:RegisterCallback("pull", function() location = instance_tag() end)
993 self.dbm_registered = r > 0
994 end
995 else
996 self.status_text = "|cffff1010Ouro Loot cannot find Deadly Boss Mods, loot will not be grouped by boss.|r"
997 end
998 end
999 end -- DBM tie-ins
1000
1001 -- Adding entries to the loot record, and tracking the corresponding timestamp.
1002 do
1003 -- This shouldn't be required. /sadface
1004 local loot_entry_mt = {
1005 __index = function (e,key)
1006 if key == 'cols' then
1007 pprint('mt', e.kind)
1008 --tabledump(e) -- not actually that useful
1009 addon:_fill_out_eoi_data(1)
1010 end
1011 return rawget(e,key)
1012 end
1013 }
1014
1015 -- Given a loot index, searches backwards for a timestamp. Returns that
1016 -- index and the time entry, or nil if it falls off the beginning. Pass an
1017 -- optional second index to search no earlier than it.
1018 -- May also be able to make good use of this in forum-generation routine.
1019 function addon:find_previous_time_entry(i,stop)
1020 local stop = stop or 0
1021 while i > stop do
1022 if g_loot[i].kind == 'time' then
1023 return i, g_loot[i]
1024 end
1025 i = i - 1
1026 end
1027 end
1028
1029 -- format_timestamp (["format_string"], Day, [Loot])
1030 -- DAY is a loot entry with kind=='time', and controls the date printed.
1031 -- LOOT may be any kind of entry in the g_loot table. If present, it
1032 -- overrides the hour and minute printed; if absent, those values are
1033 -- taken from the DAY entry.
1034 -- FORMAT_STRING may contain $x (x in Y/M/D/h/m) tokens.
1035 local format_timestamp_values, point2dee = {}, "%.2d"
1036 function addon:format_timestamp (fmt_opt, day_entry, time_entry_opt)
1037 if not time_entry_opt then
1038 if type(fmt_opt) == 'table' then -- Two entries, default format
1039 time_entry_opt, day_entry = day_entry, fmt_opt
1040 fmt_opt = "$Y/$M/$D $h:$m"
1041 --elseif type(fmt_opt) == "string" then -- Day entry only, specified format
1042 end
1043 end
1044 --format_timestamp_values.Y = point2dee:format (day_entry.startday.year % 100)
1045 format_timestamp_values.Y = ("%.4d"):format (day_entry.startday.year)
1046 format_timestamp_values.M = point2dee:format (day_entry.startday.month)
1047 format_timestamp_values.D = point2dee:format (day_entry.startday.day)
1048 format_timestamp_values.h = point2dee:format ((time_entry_opt or day_entry).hour)
1049 format_timestamp_values.m = point2dee:format ((time_entry_opt or day_entry).minute)
1050 return fmt_opt:gsub ('%$([YMDhm])', format_timestamp_values)
1051 end
1052
1053 local done_todays_date
1054 function addon:_reset_timestamps()
1055 done_todays_date = nil
1056 end
1057 local function do_todays_date()
1058 local text, M, D, Y = makedate()
1059 local found,ts = #g_loot+1
1060 repeat
1061 found,ts = addon:find_previous_time_entry(found-1)
1062 if found and ts.startday.text == text then
1063 done_todays_date = true
1064 end
1065 until done_todays_date or (not found)
1066 if done_todays_date then
1067 g_today = ts
1068 else
1069 done_todays_date = true
1070 g_today = g_loot[addon._addLootEntry{
1071 kind = 'time',
1072 startday = {
1073 text = text, month = M, day = D, year = Y
1074 }
1075 }]
1076 end
1077 addon:_fill_out_eoi_data(1)
1078 end
1079
1080 -- Adding anything original to g_loot goes through this routine.
1081 function addon._addLootEntry (e)
1082 setmetatable(e,loot_entry_mt)
1083
1084 if not done_todays_date then do_todays_date() end
1085
1086 local h, m = GetGameTime()
1087 local localuptime = math.floor(GetTime())
1088 e.hour = h
1089 e.minute = m
1090 e.stamp = localuptime
1091 local index = #g_loot + 1
1092 g_loot[index] = e
1093 return index
1094 end
1095 end
1096
1097
1098 ------ Saved texts
1099 function addon:check_saved_table(silent_p)
1100 local s = g_loot.saved
1101 if s and (#s > 0) then return s end
1102 g_loot.saved = nil
1103 if not silent_p then self:Print("There are no saved loot texts.") end
1104 end
1105
1106 function addon:save_list()
1107 local s = self:check_saved_table(); if not s then return end;
1108 for i,t in ipairs(s) do
1109 self:Print("#%d %s %d entries %s", i, t.date, t.count, t.name)
1110 end
1111 end
1112
1113 function addon:save_saveas(name)
1114 g_loot.saved = g_loot.saved or {}
1115 local n = #(g_loot.saved) + 1
1116 local save = {
1117 name = name,
1118 date = makedate(),
1119 count = #g_loot,
1120 forum = g_loot.forum,
1121 attend = g_loot.attend,
1122 }
1123 self:Print("Saving current loot texts to #%d '%s'", n, name)
1124 g_loot.saved[n] = save
1125 return self:save_list()
1126 end
1127
1128 function addon:save_restore(num)
1129 local s = self:check_saved_table(); if not s then return end;
1130 if (not num) or (num > #s) then
1131 return self:Print("Saved text number must be 1 - "..#s)
1132 end
1133 local save = s[num]
1134 self:Print("Overwriting current loot data with saved text #%d '%s'", num, save.name)
1135 self:Clear(--[[verbose_p=]]false)
1136 -- Clear will already have displayed the window, and re-selected the first
1137 -- tab. Set these up for when the text tabs are clicked.
1138 g_loot.forum = save.forum
1139 g_loot.attend = save.attend
1140 end
1141
1142 function addon:save_delete(num)
1143 local s = self:check_saved_table(); if not s then return end;
1144 if (not num) or (num > #s) then
1145 return self:Print("Saved text number must be 1 - "..#s)
1146 end
1147 self:Print("Deleting saved text #"..num)
1148 tremove(s,num)
1149 return self:save_list()
1150 end
1151
1152
1153 ------ Loot histories
1154 -- history_all = {
1155 -- ["Kilrogg"] = {
1156 -- ["realm"] = "Kilrogg", -- not saved
1157 -- ["st"] = { lib-st display table }, -- not saved
1158 -- ["byname"] = { -- not saved
1159 -- ["OtherPlayer"] = 2,
1160 -- ["Farmbuyer"] = 1,
1161 -- }
1162 -- [1] = {
1163 -- ["name"] = "Farmbuyer",
1164 -- [1] = { id = nnnnn, when = "formatted timestamp for displaying" } -- most recent loot
1165 -- [2] = { ......., [count = "x3"] } -- previous loot
1166 -- },
1167 -- [2] = {
1168 -- ["name"] = "OtherPlayer",
1169 -- ......
1170 -- }, ......
1171 -- },
1172 -- ["OtherRealm"] = ......
1173 -- }
1174 do
1175 -- Builds the map of names to array indices.
1176 function addon:_build_history_names (opt_hist)
1177 local hist = opt_hist or self.history
1178 local m = {}
1179 for i = 1, #hist do
1180 m[hist[i].name] = i
1181 end
1182 hist.byname = m
1183 end
1184
1185 -- Maps a name to an array index, creating new tables if needed. Returns
1186 function addon:get_loot_history (name)
1187 local i
1188 i = self.history.byname[name]
1189 if not i then
1190 i = #self.history + 1
1191 self.history[i] = { name=name }
1192 self.history.byname[name] = i
1193 end
1194 return self.history[i]
1195 end
1196
1197 -- Prepares and returns table to be used as self.history.
1198 function addon:_prep_new_history_category (prev_table, realmname)
1199 local t = prev_table or {
1200 --kind = 'realm',
1201 realm = realmname,
1202 }
1203
1204 --[[
1205 t.cols = setmetatable({
1206 { value = realmname },
1207 }, self.time_column1_used_mt)
1208 ]]
1209
1210 if not t.byname then
1211 self:_build_history_names (t)
1212 end
1213
1214 return t
1215 end
1216
1217 function addon:_addHistoryEntry (lootindex)
1218 local e = g_loot[lootindex]
1219 local h = self:get_loot_history(e.person)
1220 local n = {
1221 id = e.id,
1222 when = self:format_timestamp (g_today, e),
1223 count = e.count,
1224 }
1225 h[#h+1] = n
1226 end
1227 end
1228
1229
1230 ------ Player communication
1231 do
1232 local function adduser (name, status, active)
1233 if status then addon.sender_list.names[name] = status end
1234 if active then addon.sender_list.active[name] = active end
1235 end
1236
1237 -- Incoming handler functions. All take the sender name and the incoming
1238 -- tag as the first two arguments. All of these are active even when the
1239 -- player is not tracking loot, so test for that when appropriate.
1240 local OCR_funcs = {}
1241
1242 OCR_funcs.ping = function (sender)
1243 pprint('comm', "incoming ping from", sender)
1244 addon:whispercast (sender, 'pong', addon.revision,
1245 addon.enabled and "tracking" or (addon.rebroadcast and "broadcasting" or "disabled"))
1246 end
1247 OCR_funcs.pong = function (sender, _, rev, status)
1248 local s = ("|cff00ff00%s|r v2r%s is |cff00ffff%s|r"):format(sender,rev,status)
1249 addon:Print("Echo: ", s)
1250 adduser (sender, s, status=="tracking" or status=="broadcasting" or nil)
1251 end
1252
1253 OCR_funcs.loot = function (sender, _, recip, item, count, extratext)
1254 addon.dprint('comm', "DOTloot, sender", sender, "recip", recip, "item", item, "count", count)
1255 if not addon.enabled then return end
1256 adduser (sender, nil, true)
1257 addon:CHAT_MSG_LOOT ("broadcast", recip, item, count, sender, extratext)
1258 end
1259
1260 OCR_funcs.boss = function (sender, _, reason, bossname, instancetag)
1261 addon.dprint('comm', "DOTboss, sender", sender, "reason", reason, "name", bossname, "it", instancetag)
1262 if not addon.enabled then return end
1263 adduser (sender, nil, true)
1264 addon:on_boss_broadcast (reason, bossname, instancetag)
1265 end
1266
1267 OCR_funcs.bcast_req = function (sender)
1268 if addon.debug.comm or ((not g_wafer_thin) and (not addon.rebroadcast))
1269 then
1270 addon:Print("%s has requested additional broadcasters! Choose %s to enable rebroadcasting, or %s to remain off and also ignore rebroadcast requests for as long as you're logged in. Or do nothing for now to see if other requests arrive.",
1271 sender,
1272 addon.format_hypertext('bcaston',"the red pill",'|cffff4040'),
1273 addon.format_hypertext('waferthin',"the blue pill",'|cff0070dd'))
1274 end
1275 self.popped = true
1276 end
1277
1278 OCR_funcs.bcast_responder = function (sender)
1279 if addon.debug.comm or addon.requesting or
1280 ((not g_wafer_thin) and (not addon.rebroadcast))
1281 then
1282 addon:Print(sender, "has answered the call and is now broadcasting loot.")
1283 end
1284 end
1285 -- remove this tag once it's all tested
1286 OCR_funcs.bcast_denied = function (sender)
1287 if addon.requesting then addon:Print(sender, "declines futher broadcast requests.") end
1288 end
1289
1290 -- Incoming message dispatcher
1291 local function dotdotdot (sender, tag, ...)
1292 local f = OCR_funcs[tag]
1293 addon.dprint('comm', ":... processing",tag,"from",sender)
1294 if f then return f(sender,tag,...) end
1295 addon.dprint('comm', "unknown comm message",tag",from", sender)
1296 end
1297 -- Recent message cache
1298 addon.recent_messages = create_new_cache ('comm', comm_cleanup_ttl)
1299
1300 function addon:OnCommReceived (prefix, msg, distribution, sender)
1301 if prefix ~= self.ident then return end
1302 if not self.debug.comm then
1303 if distribution ~= "RAID" and distribution ~= "WHISPER" then return end
1304 if sender == my_name then return end
1305 end
1306 self.dprint('comm', ":OCR from", sender, "message is", msg)
1307
1308 if self.recent_messages:test(msg) then
1309 return self.dprint('cache', "message <",msg,"> already in cache, skipping")
1310 end
1311 self.recent_messages:add(msg)
1312
1313 -- Nothing is actually returned, just (ab)using tail calls.
1314 return dotdotdot(sender,strsplit('\a',msg))
1315 end
1316
1317 function addon:OnCommReceivedNocache (prefix, msg, distribution, sender)
1318 if prefix ~= self.identTg then return end
1319 if not self.debug.comm then
1320 if distribution ~= "WHISPER" then return end
1321 if sender == my_name then return end
1322 end
1323 self.dprint('comm', ":OCRN from", sender, "message is", msg)
1324 return dotdotdot(sender,strsplit('\a',msg))
1325 end
1326 end
1327
1328 -- vim:noet