debugging, fixing
[QuestHelper.git] / director_quest.lua
blobb656b78fc9776da66f3f761f2f8d3ca8d3139dfc
1 QuestHelper_File["director_quest.lua"] = "Development Version"
2 QuestHelper_Loadtime["director_quest.lua"] = GetTime()
4 local debug_output = false
5 if QuestHelper_File["director_quest.lua"] == "Development Version" then debug_output = true end
7 --[[
9 Little bit of explanation here.
11 The db layer dumps things out in DB format. This isn't immediately usable for our routing engine. We convert this to an intermediate "metaobjective" format that the routing engine can use, as well as copying anything that needs to be copied. This also allows us to modify our metaobjective tables as we see fit, rather than doing nasty stuff to keep the original objectives intact.
13 It's worth mentioning that, completely accidentally, everything it requests from the DB is deallocated rapidly - it doesn't keep any references to the original DB objects around. This is unintentional, but kind of neat. It's not worth preserving, but it doesn't really have to be "fixed" either.
17 local function copy(tab)
18 local tt = {}
19 for _, v in ipairs(tab) do
20 table.insert(tt, v)
21 end
22 return tt
23 end
25 local function copy_without_last(tab)
26 local tt = {}
27 for _, v in ipairs(tab) do
28 table.insert(tt, v)
29 end
30 table.remove(tt)
31 return tt
32 end
34 local function AppendObjlinks(target, source, tooltips, icon, last_name, map_lines, tooltip_lines, seen)
35 if not seen then seen = {} end
36 if not map_lines then map_lines = {} end
37 if not tooltip_lines then tooltip_lines = {} end
39 QuestHelper: Assert(not seen[source])
41 if seen[source] then return end
43 seen[source] = true
44 if source.loc then
45 if target then
46 for m, v in ipairs(source.loc) do
47 QuestHelper: Assert(target)
48 QuestHelper: Assert(QuestHelper_ParentLookup)
49 QuestHelper: Assert(QuestHelper_ParentLookup[v.p], v.p)
50 table.insert(target, {loc = {x = v.x, y = v.y, c = QuestHelper_ParentLookup[v.p], p = v.p}, path_desc = copy(map_lines), icon_id = icon or 6})
51 end
52 end
54 target = nil -- if we have a "source" as well, then we want to plow through it for tooltip data, but we don't want to add targets for it
55 end
57 for _, v in ipairs(source) do
58 local dbgi = DB_GetItem(v.sourcetype, v.sourceid, nil, true)
59 local licon
61 --print(v.sourcetype, v.sourceid, v.type)
63 if v.sourcetype == "monster" then
64 table.insert(map_lines, QHFormat("OBJECTIVE_SLAY", dbgi.name or QHText("OBJECTIVE_UNKNOWN_MONSTER")))
65 table.insert(tooltip_lines, 1, QHFormat("TOOLTIP_SLAY", source.name or "nothing"))
66 licon = 1
67 elseif v.sourcetype == "item" then
68 table.insert(map_lines, QHFormat("OBJECTIVE_LOOT", dbgi.name or QHText("OBJECTIVE_ITEM_UNKNOWN")))
69 table.insert(tooltip_lines, 1, QHFormat("TOOLTIP_LOOT", source.name or "nothing"))
70 licon = 2
71 elseif v.sourcetype == "object" then
72 table.insert(map_lines, QHFormat("OBJECTIVE_OPEN", dbgi.name or QHText("OBJECTIVE_ITEM_UNKNOWN")))
73 table.insert(tooltip_lines, 1, QHFormat("TOOLTIP_OPEN", source.name or "nothing"))
74 licon = 2
75 else
76 table.insert(map_lines, string.format("unknown %s (%s/%s)", tostring(dbgi.name), tostring(v.sourcetype), tostring(v.sourceid)))
77 table.insert(tooltip_lines, 1, string.format("unknown %s (%s/%s)", tostring(last_name), tostring(v.sourcetype), tostring(v.sourceid)))
78 licon = 3
79 end
81 tooltips[string.format("%s@@%s", v.sourcetype, v.sourceid)] = copy_without_last(tooltip_lines)
83 AppendObjlinks(target, dbgi, tooltips, icon or licon, source.name, map_lines, tooltip_lines, seen)
84 table.remove(tooltip_lines, 1)
85 table.remove(map_lines)
87 DB_ReleaseItem(dbgi)
88 end
90 seen[source] = false
91 end
94 local function horribledupe(from)
95 if not from then return nil end
97 local rv = {}
98 for k, v in pairs(from) do
99 if k == "__owner" then
100 elseif type(v) == "table" then
101 rv[k] = horribledupe(v)
102 else
103 rv[k] = v
106 return rv
110 local quest_list = setmetatable({}, {__mode="k"})
112 local QuestCriteriaWarningBroadcast
114 local function GetQuestMetaobjective(questid, lbcount)
115 if not quest_list[questid] then
116 local q = DB_GetItem("quest", questid, true, true)
118 if not lbcount then
119 QuestHelper: TextOut("Missing lbcount, guessing wildly")
120 if q and q.criteria then
121 lbcount = 0
122 for k, v in ipairs(q.criteria) do
123 lbcount = math.max(lbcount, k)
125 else
126 lbcount = 0 -- heh
130 -- just doublechecking here
131 if not QuestCriteriaWarningBroadcast and q and q.criteria then for k, v in pairs(q.criteria) do
132 if type(k) == "number" and k > lbcount then
133 --QuestHelper:TextOut(string.format("Too many stored objectives for this quest, please report on the Questhelper homepage (%s %s %s)", questid, lbcount, k)) -- we're just going to hide this for now
134 QuestHelper_ErrorCatcher_ExplicitError(false, string.format("Too many stored objectives (%s %s %s)", questid, lbcount, k))
135 QuestCriteriaWarningBroadcast = true
137 end end
139 local ite = {type_quest = {__backlink = ite}} -- we don't want to mutate the existing quest data. backlink exists only for nasty GC reasons
140 ite.desc = string.format("Quest %s", q and q.name or "(unknown)") -- this gets changed later anyway
142 for i = 1, lbcount do
143 local ttx = {}
144 --QuestHelper:TextOut(string.format("critty %d %d", k, c.loc and #c.loc or -1))
146 ttx.tooltip_canned = {}
148 if q and q.criteria and q.criteria[i] then
149 --print("Appending criteria", questid, i)
150 AppendObjlinks(ttx, q.criteria[i], ttx.tooltip_canned)
151 --print("Done")
153 if debug_output and q.criteria[i].loc and #q.criteria[i] > 0 then
154 QuestHelper:TextOut(string.format("Wackyquest %d/%d", questid, i))
157 ttx.solid = horribledupe(q.criteria[i].solid)
160 if #ttx == 0 then
161 table.insert(ttx, {loc = {x = 5000, y = 5000, c = 0, p = 2}, icon_id = 7, type_quest_unknown = true, map_desc = {"Unknown"}}) -- this is Ashenvale, for no particularly good reason
162 ttx.type_quest_unknown = true
165 for idx, v in ipairs(ttx) do
166 v.desc = string.format("Criteria %d", i)
167 v.why = ite
168 v.cluster = ttx
169 v.type_quest = ite.type_quest
172 for k, v in pairs(ttx.tooltip_canned) do
173 ttx.tooltip_canned[k] = {ttx.tooltip_canned[k], ttx} -- we're gonna be handing out this table to other modules, so this isn't as dumb as it looks
176 ite[i] = ttx
180 local ttx = {type_quest_finish = true}
181 --QuestHelper:TextOut(string.format("finny %d", q.finish.loc and #q.finish.loc or -1))
182 if q and q.finish and q.finish.loc then
183 ttx.solid = horribledupe(q.finish.solid)
184 for m, v in ipairs(q.finish.loc) do
185 --print(v.rc, v.rz)
186 --print(QuestHelper_IndexLookup[v.rc])
187 --print(QuestHelper_IndexLookup[v.rc][v.rz])
188 table.insert(ttx, {desc = "Turn in quest", why = ite, loc = {x = v.x, y = v.y, c = QuestHelper_ParentLookup[v.p], p = v.p}, tracker_hidden = true, cluster = ttx, icon_id = 7, type_quest = ite.type_quest})
192 if #ttx == 0 then
193 table.insert(ttx, {desc = "Turn in quest", why = ite, loc = {x = 5000, y = 5000, c = 0, p = 2}, tracker_hidden = true, cluster = ttx, icon_id = 7, type_quest = ite.type_quest, type_quest_unknown = true}) -- this is Ashenvale, for no particularly good reason
194 ttx.type_quest_unknown = true
197 ite.finish = ttx
200 quest_list[questid] = ite
202 if q then DB_ReleaseItem(q) end
205 return quest_list[questid]
209 local function GetQuestType(link)
210 return tonumber(string.match(link,
211 "^|cff%x%x%x%x%x%x|Hquest:(%d+):[%d-]+|h%[[^%]]*%]|h|r$"
212 )), tonumber(string.match(link,
213 "^|cff%x%x%x%x%x%x|Hquest:%d+:([%d-]+)|h%[[^%]]*%]|h|r$"
217 local update = true
218 local function UpdateTrigger()
219 update = true
222 -- It's possible that things end up garbage-collected and we end up with different tables than we expect. This is something that the entire system is kind of prone to. The solution's pretty easy - we just have to store them ourselves while we're using them.
223 local active_db = {}
225 local objective_parse_table = {
226 item = function (txt) return QuestHelper:convertPattern(QUEST_OBJECTS_FOUND)(txt) end,
227 object = function (txt) return QuestHelper:convertPattern(QUEST_OBJECTS_FOUND)(txt) end, -- why does this even exist
228 monster = function (txt) return QuestHelper:convertPattern(QUEST_MONSTERS_KILLED)(txt) end,
229 event = function (txt, done) return txt, (done and 1 or 0), 1 end, -- It appears that events are only used for things which can only happen once.
230 reputation = function (txt) return QuestHelper:convertPattern(QUEST_FACTION_NEEDED)(txt) end, -- :ughh:
231 player = function (txt) return QuestHelper:convertPattern(QUEST_MONSTERS_KILLED)(txt) end, -- We're using monsters here in the hopes that it follows the same pattern. I'd rather not try to find the exact right version of "player" in the locale files, though PLAYER might be it.
234 local function objective_parse(typ, txt, done)
235 local pt, target, have, need = typ, objective_parse_table[typ](txt, done)
237 if not target then
238 -- well, that didn't work
239 target, have, need = string.match(txt, "^%s*(.-)%s*:%s*(.-)%s*/%s*(.-)%s*$")
240 pt = "fallback"
241 --QuestHelper:TextOut(string.format("%s rebecomes %s/%s/%s", tostring(title), tostring(target), tostring(have), tostring(need)))
244 if not target then
245 target, have, need = string.match(txt, "^%s*(.-)%s*$"), (done and 1 or 0), 1
246 --QuestHelper:TextOut(string.format("%s rerebecomes %s/%s/%s", tostring(title), tostring(target), tostring(have), tostring(need)))
249 QuestHelper: Assert(target) -- This will fail repeatedly. Come on. We all know it.
250 QuestHelper: Assert(have)
251 QuestHelper: Assert(need) -- As will these.
253 if tonumber(have) then have = tonumber(have) end
254 if tonumber(need) then need = tonumber(need) end
256 return pt, target, have, need
259 local function clamp(v)
260 if v < 0 then return 0 elseif v > 255 then return 255 else return v end
263 local function colorlerp(position, r1, g1, b1, r2, g2, b2)
264 local antip = 1 - position
265 return string.format("|cff%02x%02x%02x", clamp((r1 * antip + r2 * position) * 255), clamp((g1 * antip + g2 * position) * 255), clamp((b1 * antip + b2 * position) * 255))
268 -- We're just gonna do the same thing QH originally did - red->yellow->green.
269 local function difficulty_color(position)
270 if position < 0 then position = 0 end
271 if position > 1 then position = 1 end
272 return (position < 0.5) and colorlerp(position * 2, 1, 0, 0, 1, 1, 0) or colorlerp(position * 2 - 1, 1, 1, 0, 0, 1, 0)
275 local function MakeQuestTitle(title, level)
276 local plevel = UnitLevel("player") -- meh, should probably cache this, buuuuut
277 local grayd
279 if plevel >= 60 then
280 grayd = 9
281 elseif plevel >= 40 then
282 grayd = plevel / 5 + 1
283 else
284 grayd = plevel / 10 + 5
287 local isgray = (plevel - floor(grayd) >= level)
289 local ccode = isgray and "|cffb0b0b0" or difficulty_color(1 - ((level - plevel) / grayd + 1) / 2)
290 local qlevel = string.format("[%d] ", level)
292 local ret = title
293 if QuestHelper_Pref.track_level then ret = qlevel .. ret end
294 if QuestHelper_Pref.track_qcolour then ret = ccode .. ret end
296 return ret
299 local function MakeQuestObjectiveTitle(progress, target)
300 if not progress then return nil end
302 local player = UnitName("player")
304 local pt, pd = 0, 0
305 for _, v in pairs(progress) do
306 pt = pt + 1
307 if v[3] == 1 then pd = pd + 1 end
310 local ccode
311 local status
312 local party
313 local party_show = false
314 local party_compact = false
316 if progress[player] then
317 local have, need = tonumber(progress[player][1]), tonumber(progress[player][2])
319 ccode = difficulty_color(progress[player][3])
321 if have and need then
322 if need > 1 then
323 status = string.format("%d/%d", have, need)
324 party_compact = true
326 else
327 status = string.format("%s/%s", progress[player][1], progress[player][2])
328 party_compact = true
331 if pt > 1 then party_show = true end
332 elseif pt == 0 then
333 ccode = difficulty_color(1) -- probably just in the process of being removed from the tracker
334 status = "Complete"
335 else
336 ccode = difficulty_color(pd / pt)
338 party_show = true
341 if party_show then
342 if party_compact then
343 party = string.format("(P: %d/%d)", pd, pt)
344 else
345 party = string.format("Party %d/%d", pd, pt)
349 if QuestHelper_Pref.track_ocolour then
350 target = ccode .. target
353 if status or party then
354 target = target .. ":"
357 if status then
358 target = target .. " " .. status
361 if party then
362 target = target .. " " .. party
365 return target
368 local function Clicky(index)
369 ShowUIPanel(QuestLogFrame)
370 QuestLog_SetSelection(index)
371 QuestLog_Update()
374 local dontknow = {
375 name = "director_quest_unknown_objective",
376 no_exception = true,
377 no_disable = true,
378 friendly_reason = QHText("UNKNOWN_OBJ"),
381 -- InsertedItem[item] = {"list", "of", "reasons"}
382 local InsertedItems = {}
383 local TooltipType = {}
384 local Unknowning = {}
385 local Unknowned = {}
386 local in_pass = nil
388 local function SetTooltip(item, typ)
389 --print("stt", item, typ, item.tooltip_defer_questobjective)
390 if TooltipType[item] == typ and typ ~= "defer" and not item.tooltip_defer_questobjective_last then return end
391 if TooltipType[item] == "defer" and typ == "defer" and (not item.tooltip_defer_questobjective_last or item.tooltip_defer_questobjective_last == item.tooltip_defer_questobjective) then return end -- sigh
393 if TooltipType[item] == "canned" then
394 QuestHelper: Assert(item.tooltip_canned)
395 QH_Tooltip_Canned_Remove(item.tooltip_canned)
396 elseif TooltipType[item] == "defer" then
397 QuestHelper: Assert(item.tooltip_defer_questname_last)
398 --print("remove", item.tooltip_defer_questname_last, item.tooltip_defer_questobjective_last, item.tooltip_defer_questobjective)
399 if item.tooltip_defer_questobjective_last then
400 QH_Tooltip_Defer_Remove(item.tooltip_defer_questname_last, item.tooltip_defer_questobjective_last, item.tooltip_defer_token_last)
401 else
402 QH_Tooltip_Defer_Remove(item.tooltip_defer_questname_last, item.tooltip_defer_questobjective, item.tooltip_defer_token_last)
404 elseif TooltipType[item] == nil then
405 else
406 QuestHelper: Assert(false)
409 item.tooltip_defer_questobjective_last = nil
410 item.tooltip_defer_questname_last = nil -- if it was anything, it is not now
411 item.tooltip_defer_token_last = nil
413 if typ == "canned" then
414 QuestHelper: Assert(item.tooltip_canned)
415 QH_Tooltip_Canned_Add(item.tooltip_canned)
416 elseif typ == "defer" then
417 QuestHelper: Assert(not not item.tooltip_defer_questobjective == not item.type_quest_finish) -- hmmm
418 --print("add", item.tooltip_defer_questname, item.tooltip_defer_questobjective)
419 QuestHelper: Assert(item.tooltip_defer_questname)
420 item.tooltip_defer_token_last = {{}, item}
421 QH_Tooltip_Defer_Add(item.tooltip_defer_questname, item.tooltip_defer_questobjective, item.tooltip_defer_token_last)
422 item.tooltip_defer_questname_last = item.tooltip_defer_questname
423 item.tooltip_defer_questobjective_last = item.tooltip_defer_questobjective
424 elseif typ == nil then
425 else
426 QuestHelper: Assert(false)
428 TooltipType[item] = typ
431 local function StartInsertionPass(id)
432 QuestHelper: Assert(not in_pass)
433 in_pass = id
434 QH_Timeslice_PushUnyieldable()
435 for k, v in pairs(InsertedItems) do
436 v[id] = nil
438 if k.progress then
439 k.progress[id] = nil
440 local desc = MakeQuestObjectiveTitle(k.progress, k.target)
441 for _, v in ipairs(k) do
442 v.tracker_desc = desc or "(no description available)"
446 -- if these are needed to remove, they'll be stored in last, and this way they'll be obliterated if the user doesn't have that actual quest
447 if id == UnitName("player") then
448 k.tooltip_defer_questname = nil
449 k.tooltip_defer_questobjective = nil
453 local function RefreshItem(id, item, required)
454 --if not required and math.random() < 0.2 then return false end -- ha ha bzzzzt
456 QuestHelper: Assert(in_pass == id)
457 local added = false
458 if not InsertedItems[item] then
459 QH_Route_ClusterAdd(item)
460 --QH_Route_SetClusterPriority(item, math.random(5))
461 added = true
462 InsertedItems[item] = {}
464 InsertedItems[item][id] = true
466 if item.tooltip_defer_questname then
467 SetTooltip(item, "defer")
468 elseif item.tooltip_canned then
469 SetTooltip(item, "canned")
470 else
471 SetTooltip(item, nil)
474 if item.type_quest_unknown then table.insert(Unknowning, item) end
476 local desc = MakeQuestObjectiveTitle(item.progress, item.target)
477 for _, v in ipairs(item) do
478 v.tracker_desc = desc or "(no description available)"
481 return added
483 local function EndInsertionPass(id)
484 QuestHelper: Assert(in_pass == id)
485 local rem = QuestHelper:CreateTable("ip rem")
486 for k, v in pairs(InsertedItems) do
487 local has = false
488 for _, _ in pairs(v) do
489 has = true
490 break
492 if not has then
493 QH_Tracker_Unpin(k[1], true)
494 QH_Route_ClusterRemove(k)
495 rem[k] = true
497 SetTooltip(k, nil)
501 QH_Tracker_Rescan()
503 for k, _ in pairs(rem) do
504 InsertedItems[k] = nil
506 QuestHelper:ReleaseTable(rem)
508 -- this is all so we don't spam the system with multiple ignores, since that currently causes an early routing exit
509 for k in pairs(Unknowned) do
510 Unknowned[k] = false
512 for _, v in ipairs(Unknowning) do
513 if Unknowned[v] == nil then
514 QH_Route_IgnoreCluster(v, dontknow)
516 Unknowned[v] = true
518 while table.remove(Unknowning) do end
519 local need_rescan = false
520 local new_unknowned = QuestHelper:CreateTable("unk")
521 for k, v in pairs(Unknowned) do
522 if v then new_unknowned[k] = true end
524 QuestHelper:ReleaseTable(Unknowned)
525 Unknowned = new_unknowned
527 QH_Timeslice_PopUnyieldable()
528 in_pass = nil
530 --QH_Tooltip_Defer_Dump()
533 function QuestProcessor(user_id, db, title, level, group, variety, groupsize, watched, complete, lbcount, timed)
534 db.desc = title
535 db.tracker_desc = MakeQuestTitle(title, level)
537 db.type_quest.objectives = lbcount
538 db.type_quest.level = level
539 db.type_quest.done = (complete == 1)
540 db.type_quest.variety = variety
541 db.type_quest.groupsize = groupsize
542 db.type_quest.title = title
544 local turnin
545 local turnin_new
547 -- This is our "quest turnin" objective, which is currently being handled separately for no particularly good reason.
548 if db.finish and #db.finish > 0 then
549 for _, v in ipairs(db.finish) do
550 v.map_highlight = (complete == 1)
553 turnin = db.finish
554 --print("turnin:", turnin.tooltip_defer_questname)
555 if RefreshItem(user_id, turnin, true) then
556 turnin_new = true
557 for k, v in ipairs(turnin) do
558 v.tracker_clicked = function () Clicky(lindex) end
560 v.map_desc = {QHFormat("OBJECTIVE_REASON_TURNIN", title)}
563 if watched ~= "(ignore)" then QH_Tracker_SetPin(db.finish[1], watched, true) end
566 -- These are the individual criteria of the quest. Remember that each criteria can be represented by multiple routing objectives.
567 for i = 1, lbcount do
568 if db[i] then
569 local pt, pd, have, need = objective_parse(db[i].temp_typ, db[i].temp_desc, db[i].temp_done)
570 local dline
571 if pt == "item" or pt == "object" then
572 dline = QHFormat("OBJECTIVE_REASON", QHText("ACQUIRE_VERB"), pd, title)
573 elseif pt == "monster" then
574 dline = QHFormat("OBJECTIVE_REASON", QHText("SLAY_VERB"), pd, title)
575 else
576 dline = QHFormat("OBJECTIVE_REASON_FALLBACK", pd, title)
579 if not db[i].progress then
580 db[i].progress = {}
583 if type(have) == "number" and type(need) == "number" then
584 db[i].progress[db[i].temp_person] = {have, need, have / need}
585 else
586 db[i].progress[db[i].temp_person] = {have, need, db[i].temp_done and 1 or 0} -- it's only used for the coloring anyway
589 local _, target = objective_parse(db[i].temp_typ, db[i].temp_desc)
590 db[i].target = target
592 db[i].desc = QHFormat("TOOLTIP_QUEST", title)
594 for k, v in ipairs(db[i]) do
595 v.desc = db[i].temp_desc
596 v.tracker_clicked = db.tracker_clicked
598 v.progress = db[i].progress
600 if v.path_desc then
601 v.map_desc = copy(v.path_desc)
602 v.map_desc[1] = dline
603 else
604 v.map_desc = {dline}
608 -- This is the snatch of code that actually adds it to routing.
609 if not db[i].temp_done and #db[i] > 0 then
610 if RefreshItem(user_id, db[i]) then
611 if turnin then QH_Route_ClusterRequires(turnin, db[i]) end
613 if watched ~= "(ignore)" then QH_Tracker_SetPin(db[i][1], watched, true) end
616 db[i].temp_desc, db[i].temp_typ, db[i].temp_done = nil, nil, nil
620 if turnin_new and timed then
621 QH_Route_SetClusterPriority(turnin, -1)
625 function SerItem(item)
626 local rtx
627 if type(item) == "boolean" then
628 rtx = "b" .. (item and "t" or "f")
629 elseif type(item) == "number" then
630 rtx = "n" .. tostring(item)
631 elseif type(item) == "string" then
632 rtx = "s" .. item:gsub("\\", "\\\\"):gsub(":", "\\;")
633 elseif type(item) == "nil" then
634 rtx = "0"
635 else
636 print(type(item), item)
637 QuestHelper: Assert()
639 return rtx
642 function DeSerItem(item)
643 local t = item:sub(1, 1)
644 local d = item:sub(2)
645 if t == "b" then
646 return (d == "t")
647 elseif t == "n" then
648 return tonumber(d)
649 elseif t == "s" then
650 return d:gsub("\\;", ":"):gsub("\\\\", "\\")
651 elseif t == "0" then
652 return nil
653 else
654 QuestHelper: Assert()
658 local function Serialize(...)
659 local sx
660 for i = 1, select("#", ...) do
661 if sx then sx = sx .. ":" else sx = "" end
662 sx = sx .. SerItem(select(i, ...))
664 QuestHelper: Assert(sx)
665 return sx
668 local function SAM(msg, chattype, target)
669 --QuestHelper: TextOut(string.format("%s/%s: %s", chattype, tostring(target), msg))
671 local thresh = 245
672 local msgsize = 240
673 if #msg > thresh then
674 for i = 1, #msg, msgsize do
675 local prefx = "x:"
676 if i == 1 then prefx = "v:" elseif i + msgsize > #msg then prefx = "X:" end
677 SAM(prefx .. msg:sub(i, i + msgsize - 1), chattype, target)
679 else
680 ChatThrottleLib:SendAddonMessage("BULK", "QHpr", msg, chattype, target, "QHpr")
684 -- sigh.
685 function is_uncached(typ, txt, done)
686 if not txt then return true end
687 if txt == "" then return true end
688 if txt:match("^ : %d+/%d+$") then return true end
689 local _, target = objective_parse(typ, txt, done)
690 if target == "" or target == " " then return true end
691 return false
694 -- qid, chunk
695 local current_chunks = {}
697 -- "log" is a synthetic objective that Blizzard tossed in for god only knows what reason, so we just pretend it doesn't exist
698 local function GetEffectiveNumQuestLeaderBoards(index)
699 local v = GetNumQuestLeaderBoards(index)
700 if v ~= 1 then return v end
701 if select(2, GetQuestLogLeaderBoard(1, index)) == "log" then return 0 end
702 return 1
705 -- Here's the core update function
706 function QH_UpdateQuests(force)
707 if not DB_Ready() then return end
708 QH_Timeslice_PushUnyieldable()
710 if update or force then -- Sometimes (usually) we don't actually update
711 local index = 1
713 local player = UnitName("player")
714 if not player then return end -- bzzt, try again later
715 StartInsertionPass(player)
717 local next_chunks = {}
719 local first = true
721 -- This begins the main update loop that loops through all of the quests
722 while true do
723 local title, level, variety, groupsize, _, _, complete = GetQuestLogTitle(index)
724 if not title then break end
726 title = title:match("%[.*%] (.*)") or title
728 local qlink = GetQuestLink(index)
729 if qlink then -- If we don't have a quest link, it's not really a quest
730 local id = GetQuestType(qlink)
731 --if first then id = 13836 else id = nil end
732 if id then -- If we don't have a *valid* quest link, give up
733 local lbcount = GetEffectiveNumQuestLeaderBoards(index)
734 local db = GetQuestMetaobjective(id, lbcount) -- This generates the above-mentioned metaobjective, including doing the database lookup.
736 QuestHelper: Assert(db)
738 local watched = IsQuestWatched(index)
740 -- The section in here, in other words, is: we have a metaobjective (which may be a new one, or may not be), and we're trying to attach things to our routing engine. Now is where the real work begins! (many conditionals deep)
741 local lindex = index
742 db.tracker_clicked = function () Clicky(lindex) end
744 db.type_quest.index = index
746 local timidx = 1
747 while true do
748 local timer = GetQuestIndexForTimer(timidx)
749 if not timer then timidx = nil break end
750 if timer == index then break end
751 timidx = timidx + 1
753 local timed = not not timidx
755 --print(id, title, level, groupsize, variety, groupsize, complete, timed)
756 local chunk = "q:" .. Serialize(id, title, level, groupsize, variety, groupsize, complete, timed)
757 for i = 1, lbcount do
758 QuestHelper: Assert(db[i])
759 db[i].temp_desc, db[i].temp_typ, db[i].temp_done = GetQuestLogLeaderBoard(i, index)
760 --[[if not db[i].temp_desc or is_uncached(db[i].temp_typ, db[i].temp_desc, db[i].temp_done) then
761 db[i].temp_desc = string.format("(missing description %d)", i)
762 end]]
763 db[i].temp_person = player
765 db[i].tooltip_defer_questname = title
766 db[i].tooltip_defer_questobjective = db[i].temp_desc -- yoink
767 QuestHelper: Assert(db[i].tooltip_defer_questobjective) -- hmmm
769 chunk = chunk .. ":" .. Serialize(db[i].temp_desc, db[i].temp_typ, db[i].temp_done)
772 db.finish.tooltip_defer_questname = title -- we're using this as our fallback right now
774 next_chunks[id] = chunk
776 QuestProcessor(player, db, title, level, groupsize, variety, groupsize, watched, complete, lbcount, timed)
778 first = false
780 index = index + 1
783 EndInsertionPass(player)
785 QH_Route_Filter_Rescan(nil, true) -- 'cause filters may also change, but let's not bother getting too excited about it
787 if not QuestHelper_Pref.solo and QuestHelper_Pref.share then
788 for k, v in pairs(next_chunks) do
789 if current_chunks[k] ~= v then
790 SAM(v, "PARTY")
794 for k, v in pairs(current_chunks) do
795 if not next_chunks[k] then
796 SAM(string.format("q:n%d", k), "PARTY")
801 current_chunks = next_chunks
802 --update = false
805 QH_Timeslice_PopUnyieldable()
808 -- comm_packets[user][qid] = data
809 local comm_packets = {}
811 local function RefreshUserComms(user)
812 StartInsertionPass(user)
814 if comm_packets[user] then for _, dat in pairs(comm_packets[user]) do
815 local id, title, level, group, variety, groupsize, complete, timed = dat[1], dat[2], dat[3], dat[4], dat[5], dat[6], dat[7], dat[8]
816 local objstart = 9
818 local obj = {}
819 while true do
820 if dat[#obj * 3 + objstart] == nil and dat[#obj * 3 + objstart + 1] == nil and dat[#obj * 3 + objstart + 2] == nil then break end
821 table.insert(obj, {dat[#obj * 3 + objstart], dat[#obj * 3 + objstart + 1], dat[#obj * 3 + objstart + 2]})
824 local lbcount = #obj
825 local db = GetQuestMetaobjective(id, lbcount) -- This generates the above-mentioned metaobjective, including doing the database lookup.
827 QuestHelper: Assert(db)
829 for i = 1, lbcount do
830 db[i].temp_desc, db[i].temp_typ, db[i].temp_done, db[i].temp_person = obj[i][1], obj[i][2], obj[i][3], user
833 QuestProcessor(user, db, title, level, group, variety, groupsize, "(ignore)", complete, lbcount, false)
834 end end
836 EndInsertionPass(user)
838 QH_Route_Filter_Rescan() -- 'cause filters may also change
841 function QH_InsertCommPacket(user, data)
842 local q, chunk = data:match("([^:]+):(.*)")
843 if q ~= "q" then return end
845 local dat = {}
846 local idx = 1
847 for item in chunk:gmatch("([^:]+)") do
848 dat[idx] = DeSerItem(item)
849 idx = idx + 1
852 if not comm_packets[user] then comm_packets[user] = {} end
853 if idx == 2 then
854 comm_packets[user][dat[1]] = nil
855 else
856 comm_packets[user][dat[1]] = dat
859 -- refresh the comms
860 RefreshUserComms(user)
863 local function QH_DumpCommUser(user)
864 comm_packets[user] = nil
865 RefreshUserComms(user)
868 QH_Event("PLAYER_ENTERING_WORLD", UpdateTrigger)
869 QH_Event("UNIT_QUEST_LOG_CHANGED", UpdateTrigger)
870 QH_Event("QUEST_LOG_UPDATE", QH_UpdateQuests)
872 -- We don't return anything here, but I don't think that's actually an issue - those functions don't return anything anyway. Someday I'll regret writing this. Delay because of beql which is a bitch.
873 QH_AddNotifier(GetTime() + 5, function ()
874 local aqw_orig = AddQuestWatch
875 AddQuestWatch = function(...)
876 aqw_orig(...)
877 QH_UpdateQuests(true)
879 local rqw_orig = RemoveQuestWatch
880 RemoveQuestWatch = function(...)
881 rqw_orig(...)
882 QH_UpdateQuests(true)
884 end)
886 -- We seem to end up out of sync sometimes. Why? I'm not sure. Maybe my current events aren't reliable. So let's just scan every five seconds and see what happens, scanning is fast and efficient anyway.
887 --[[local function autonotify()
888 QH_UpdateQuests(true)
889 QH_AddNotifier(GetTime() + 5, autonotify)
891 QH_AddNotifier(GetTime() + 30, autonotify)]]
893 local old_playerlist = {}
895 function QH_Questcomm_Sync()
896 if not (not QuestHelper_Pref.solo and QuestHelper_Pref.share) then
897 old_playerlist = {}
898 return
901 local playerlist = {}
902 --[[if GetNumRaidMembers() > 0 then
903 for i = 1, 40 do
904 local liv = UnitName(string.format("raid%d", i))
905 if liv then playerlist[liv] = true end
907 elseif]] if GetNumPartyMembers() > 0 then
908 -- we is in a party
909 for i = 1, 4 do
910 local targ = string.format("party%d", i)
911 local liv, relm = UnitName(targ)
912 if liv and not relm and liv ~= UNKNOWNOBJECT and UnitIsConnected(targ) then playerlist[liv] = true end
915 playerlist[UnitName("player")] = nil
917 local additions = {}
918 for k, v in pairs(playerlist) do
919 if not old_playerlist[k] then
920 --print("new player:", k)
921 table.insert(additions, k)
925 local removals = {}
926 for k, v in pairs(old_playerlist) do
927 if not playerlist[k] then
928 --print("lost player:", k)
929 table.insert(removals, k)
933 old_playerlist = playerlist
935 for _, v in ipairs(removals) do
936 QH_DumpCommUser(v)
939 if #additions == 0 then return end
941 if #additions == 1 then
942 SAM("syn:2", "WHISPER", additions[1])
943 else
944 SAM("syn:2", "PARTY")
948 local aku = {}
950 local newer_reported = false
951 local older_reported = false
952 function QH_Questcomm_Msg(data, from)
953 if data:match("syn:0") then
954 QH_DumpCommUser(from)
955 return
957 if QuestHelper_Pref.solo then return end
959 --print("received", from, data)
961 local cont = true
963 local key, value = data:match("(.):(.*)")
964 if key == "v" then
965 aku[from] = value
966 elseif key == "x" then
967 if aku[from] then
968 aku[from] = aku[from] .. value
970 elseif key == "X" then
971 if aku[from] then
972 aku[from] = aku[from] .. value
973 data = aku[from]
974 aku[from] = nil
975 cont = true
977 else
978 cont = true
981 if not cont then return end
984 --print("packet received", from, data)
985 if data:match("syn:.*") then
986 local synv = data:match("syn:([0-9]*)")
987 if synv then synv = tonumber(synv) end
988 if synv and synv ~= 2 then
989 if synv > 2 and not newer_reported then
990 QuestHelper:TextOut(QHFormat("PEER_NEWER", from))
991 newer_reported = true
992 elseif synv < 2 and not older_reported then
993 QuestHelper:TextOut(QHFormat("PEER_OLDER", from))
994 older_reported = true
998 if synv and synv >= 2 then
999 SAM("hello:2", "WHISPER", from)
1001 elseif data == "hello:2" or data == "retrieve:2" then
1002 if data == "hello:2" then SAM("retrieve:2", "WHISPER", from) end -- requests their info as well, needed to deal with UI reloading/logon/logoff properly
1004 for k, v in pairs(current_chunks) do
1005 SAM(v, "WHISPER", from)
1007 else
1008 if old_playerlist[from] then
1009 QH_InsertCommPacket(from, data)
1014 function QuestHelper:SetShare(flag)
1015 if flag then
1016 QH_Questcomm_Sync()
1017 else
1018 SAM("syn:0", "PARTY")
1019 local cpb = comm_packets
1020 comm_packets = {}
1021 for k in pairs(cpb) do RefreshUserComms(k) end