stupid globals
[QuestHelper.git] / director_quest.lua
blobe60386133c51c494fe2b6aef5d1d5633769ca71d
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 if v.sourcetype == "monster" then
62 table.insert(map_lines, QHFormat("OBJECTIVE_SLAY", dbgi.name or QHText("OBJECTIVE_UNKNOWN_MONSTER")))
63 table.insert(tooltip_lines, 1, QHFormat("TOOLTIP_SLAY", source.name or "nothing"))
64 licon = 1
65 elseif v.sourcetype == "item" then
66 table.insert(map_lines, QHFormat("OBJECTIVE_ACQUIRE", dbgi.name or QHText("OBJECTIVE_ITEM_UNKNOWN")))
67 table.insert(tooltip_lines, 1, QHFormat("TOOLTIP_LOOT", source.name or "nothing"))
68 licon = 2
69 else
70 table.insert(map_lines, string.format("unknown %s (%s/%s)", tostring(dbgi.name), tostring(v.sourcetype), tostring(v.sourceid)))
71 table.insert(tooltip_lines, 1, string.format("unknown %s (%s/%s)", tostring(last_name), tostring(v.sourcetype), tostring(v.sourceid)))
72 licon = 3
73 end
75 tooltips[string.format("%s@@%s", v.sourcetype, v.sourceid)] = copy_without_last(tooltip_lines)
77 AppendObjlinks(target, dbgi, tooltips, icon or licon, source.name, map_lines, tooltip_lines, seen)
78 table.remove(tooltip_lines, 1)
79 table.remove(map_lines)
81 DB_ReleaseItem(dbgi)
82 end
84 seen[source] = false
85 end
88 local function horribledupe(from)
89 if not from then return nil end
91 local rv = {}
92 for k, v in pairs(from) do
93 if k == "__owner" then
94 elseif type(v) == "table" then
95 rv[k] = horribledupe(v)
96 else
97 rv[k] = v
98 end
99 end
100 return rv
104 local quest_list = setmetatable({}, {__mode="k"})
106 local QuestCriteriaWarningBroadcast
108 local function GetQuestMetaobjective(questid, lbcount)
109 if not quest_list[questid] then
110 local q = DB_GetItem("quest", questid, true, true)
112 if not lbcount then
113 QuestHelper: TextOut("Missing lbcount, guessing wildly")
114 if q and q.criteria then
115 lbcount = 0
116 for k, v in ipairs(q.criteria) do
117 lbcount = math.max(lbcount, k)
119 else
120 lbcount = 0 -- heh
124 -- just doublechecking here
125 if not QuestCriteriaWarningBroadcast and q and q.criteria then for k, v in pairs(q.criteria) do
126 if type(k) == "number" and k > lbcount then
127 --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
128 QuestHelper_ErrorCatcher_ExplicitError(false, string.format("Too many stored objectives (%s %s %s)", questid, lbcount, k))
129 QuestCriteriaWarningBroadcast = true
131 end end
133 ite = {type_quest = {__backlink = ite}} -- we don't want to mutate the existing quest data. backlink exists only for nasty GC reasons
134 ite.desc = string.format("Quest %s", q and q.name or "(unknown)") -- this gets changed later anyway
136 for i = 1, lbcount do
137 local ttx = {}
138 --QuestHelper:TextOut(string.format("critty %d %d", k, c.loc and #c.loc or -1))
140 ttx.tooltip_canned = {}
142 if q and q.criteria and q.criteria[i] then
143 AppendObjlinks(ttx, q.criteria[i], ttx.tooltip_canned)
145 if debug_output and q.criteria[i].loc and #q.criteria[i] > 0 then
146 QuestHelper:TextOut(string.format("Wackyquest %d/%d", questid, i))
149 ttx.solid = horribledupe(q.criteria[i].solid)
152 if #ttx == 0 then
153 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
156 for idx, v in ipairs(ttx) do
157 v.desc = string.format("Criteria %d", i)
158 v.why = ite
159 v.cluster = ttx
160 v.type_quest = ite.type_quest
163 for k, v in pairs(ttx.tooltip_canned) do
164 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
167 ite[i] = ttx
171 local ttx = {type_quest_finish = true}
172 --QuestHelper:TextOut(string.format("finny %d", q.finish.loc and #q.finish.loc or -1))
173 if q and q.finish and q.finish.loc then
174 ttx.solid = horribledupe(q.finish.solid)
175 for m, v in ipairs(q.finish.loc) do
176 --print(v.rc, v.rz)
177 --print(QuestHelper_IndexLookup[v.rc])
178 --print(QuestHelper_IndexLookup[v.rc][v.rz])
179 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})
183 if #ttx == 0 then
184 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
187 ite.finish = ttx
190 quest_list[questid] = ite
192 if q then DB_ReleaseItem(q) end
195 return quest_list[questid]
199 local function GetQuestType(link)
200 return tonumber(string.match(link,
201 "^|cff%x%x%x%x%x%x|Hquest:(%d+):[%d-]+|h%[[^%]]*%]|h|r$"
202 )), tonumber(string.match(link,
203 "^|cff%x%x%x%x%x%x|Hquest:%d+:([%d-]+)|h%[[^%]]*%]|h|r$"
207 local update = true
208 local function UpdateTrigger()
209 update = true
212 -- 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.
213 local active_db = {}
215 local objective_parse_table = {
216 item = function (txt) return QuestHelper:convertPattern(QUEST_OBJECTS_FOUND)(txt) end,
217 object = function (txt) return QuestHelper:convertPattern(QUEST_OBJECTS_FOUND)(txt) end, -- why does this even exist
218 monster = function (txt) return QuestHelper:convertPattern(QUEST_MONSTERS_KILLED)(txt) end,
219 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.
220 reputation = function (txt) return QuestHelper:convertPattern(QUEST_FACTION_NEEDED)(txt) end, -- :ughh:
221 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.
224 local function objective_parse(typ, txt, done)
225 local pt, target, have, need = typ, objective_parse_table[typ](txt, done)
227 if not target then
228 -- well, that didn't work
229 target, have, need = string.match(txt, "^%s*(.-)%s*:%s*(.-)%s*/%s*(.-)%s*$")
230 pt = "fallback"
231 --QuestHelper:TextOut(string.format("%s rebecomes %s/%s/%s", tostring(title), tostring(target), tostring(have), tostring(need)))
234 if not target then
235 target, have, need = string.match(txt, "^%s*(.-)%s*$"), (done and 1 or 0), 1
236 --QuestHelper:TextOut(string.format("%s rerebecomes %s/%s/%s", tostring(title), tostring(target), tostring(have), tostring(need)))
239 QuestHelper: Assert(target) -- This will fail repeatedly. Come on. We all know it.
240 QuestHelper: Assert(have)
241 QuestHelper: Assert(need) -- As will these.
243 if tonumber(have) then have = tonumber(have) end
244 if tonumber(need) then need = tonumber(need) end
246 return pt, target, have, need
249 local function clamp(v)
250 if v < 0 then return 0 elseif v > 255 then return 255 else return v end
253 local function colorlerp(position, r1, g1, b1, r2, g2, b2)
254 local antip = 1 - position
255 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))
258 -- We're just gonna do the same thing QH originally did - red->yellow->green.
259 local function difficulty_color(position)
260 if position < 0 then position = 0 end
261 if position > 1 then position = 1 end
262 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)
265 local function MakeQuestTitle(title, level)
266 local plevel = UnitLevel("player") -- meh, should probably cache this, buuuuut
267 local grayd
269 if plevel >= 60 then
270 grayd = 9
271 elseif plevel >= 40 then
272 grayd = plevel / 5 + 1
273 else
274 grayd = plevel / 10 + 5
277 local isgray = (plevel - floor(grayd) >= level)
279 local ccode = isgray and "|cffb0b0b0" or difficulty_color(1 - ((level - plevel) / grayd + 1) / 2)
280 local qlevel = string.format("[%d] ", level)
282 local ret = title
283 if QuestHelper_Pref.track_level then ret = qlevel .. ret end
284 if QuestHelper_Pref.track_qcolour then ret = ccode .. ret end
286 return ret
289 local function MakeQuestObjectiveTitle(progress, target)
290 if not progress then return nil end
292 local player = UnitName("player")
294 local pt, pd = 0, 0
295 for _, v in pairs(progress) do
296 pt = pt + 1
297 if v[3] == 1 then pd = pd + 1 end
300 local ccode
301 local status
302 local party
303 local party_show = false
304 local party_compact = false
306 if progress[player] then
307 local have, need = tonumber(progress[player][1]), tonumber(progress[player][2])
309 ccode = difficulty_color(progress[player][3])
311 if have and need then
312 if need > 1 then
313 status = string.format("%d/%d", have, need)
314 party_compact = true
316 else
317 status = string.format("%s/%s", progress[player][1], progress[player][2])
318 party_compact = true
321 if pt > 1 then party_show = true end
322 elseif pt == 0 then
323 ccode = difficulty_color(1) -- probably just in the process of being removed from the tracker
324 status = "Complete"
325 else
326 ccode = difficulty_color(pd / pt)
328 party_show = true
331 if party_show then
332 if party_compact then
333 party = string.format("(P: %d/%d)", pd, pt)
334 else
335 party = string.format("Party %d/%d", pd, pt)
339 if QuestHelper_Pref.track_ocolour then
340 target = ccode .. target
343 if status or party then
344 target = target .. ":"
347 if status then
348 target = target .. " " .. status
351 if party then
352 target = target .. " " .. party
355 return target
358 local function Clicky(index)
359 ShowUIPanel(QuestLogFrame)
360 QuestLog_SetSelection(index)
361 QuestLog_Update()
364 local dontknow = {
365 name = "director_quest_unknown_objective",
366 no_exception = true,
367 no_disable = true,
368 friendly_reason = QHText("UNKNOWN_OBJ"),
371 -- InsertedItem[item] = {"list", "of", "reasons"}
372 local InsertedItems = {}
373 local TooltipType = {}
374 local Unknowning = {}
375 local in_pass = nil
377 local function SetTooltip(item, typ)
378 --print("stt", item, typ, item.tooltip_defer_questobjective)
379 if TooltipType[item] == typ and typ ~= "defer" and not item.tooltip_defer_questobjective_last then return end
380 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
382 if TooltipType[item] == "canned" then
383 QuestHelper: Assert(item.tooltip_canned)
384 QH_Tooltip_Canned_Remove(item.tooltip_canned)
385 elseif TooltipType[item] == "defer" then
386 QuestHelper: Assert(item.tooltip_defer_questname_last)
387 --print("remove", item.tooltip_defer_questname_last, item.tooltip_defer_questobjective_last, item.tooltip_defer_questobjective)
388 if item.tooltip_defer_questobjective_last then
389 QH_Tooltip_Defer_Remove(item.tooltip_defer_questname_last, item.tooltip_defer_questobjective_last, item.tooltip_defer_token_last)
390 else
391 QH_Tooltip_Defer_Remove(item.tooltip_defer_questname_last, item.tooltip_defer_questobjective, item.tooltip_defer_token_last)
393 elseif TooltipType[item] == nil then
394 else
395 QuestHelper: Assert(false)
398 item.tooltip_defer_questobjective_last = nil
399 item.tooltip_defer_questname_last = nil -- if it was anything, it is not now
400 item.tooltip_defer_token_last = nil
402 if typ == "canned" then
403 QuestHelper: Assert(item.tooltip_canned)
404 QH_Tooltip_Canned_Add(item.tooltip_canned)
405 elseif typ == "defer" then
406 QuestHelper: Assert(not not item.tooltip_defer_questobjective == not item.type_quest_finish) -- hmmm
407 --print("add", item.tooltip_defer_questname, item.tooltip_defer_questobjective)
408 QuestHelper: Assert(item.tooltip_defer_questname)
409 item.tooltip_defer_token_last = {{}, item}
410 QH_Tooltip_Defer_Add(item.tooltip_defer_questname, item.tooltip_defer_questobjective, item.tooltip_defer_token_last)
411 item.tooltip_defer_questname_last = item.tooltip_defer_questname
412 item.tooltip_defer_questobjective_last = item.tooltip_defer_questobjective
413 elseif typ == nil then
414 else
415 QuestHelper: Assert(false)
417 TooltipType[item] = typ
420 local function StartInsertionPass(id)
421 QuestHelper: Assert(not in_pass)
422 in_pass = id
423 for k, v in pairs(InsertedItems) do
424 v[id] = nil
426 if k.progress then
427 k.progress[id] = nil
428 local desc = MakeQuestObjectiveTitle(k.progress, k.target)
429 for _, v in ipairs(k) do
430 v.tracker_desc = desc or "(no description available)"
434 -- 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
435 if id == UnitName("player") then
436 k.tooltip_defer_questname = nil
437 k.tooltip_defer_questobjective = nil
441 local function RefreshItem(id, item, required)
442 --if not required and math.random() < 0.2 then return false end -- ha ha bzzzzt
444 QuestHelper: Assert(in_pass == id)
445 local added = false
446 if not InsertedItems[item] then
447 QH_Route_ClusterAdd(item)
448 --QH_Route_SetClusterPriority(item, math.random(5))
449 added = true
450 InsertedItems[item] = {}
452 InsertedItems[item][id] = true
454 if item.tooltip_defer_questname then
455 SetTooltip(item, "defer")
456 elseif item.tooltip_canned then
457 SetTooltip(item, "canned")
458 else
459 SetTooltip(item, nil)
462 if item.type_quest_unknown then table.insert(Unknowning, item) end
464 local desc = MakeQuestObjectiveTitle(item.progress, item.target)
465 for _, v in ipairs(item) do
466 v.tracker_desc = desc or "(no description available)"
469 return added
471 local function EndInsertionPass(id)
472 QuestHelper: Assert(in_pass == id)
473 local rem = QuestHelper:CreateTable("ip rem")
474 for k, v in pairs(InsertedItems) do
475 local has = false
476 for _, _ in pairs(v) do
477 has = true
478 break
480 if not has then
481 QH_Tracker_Unpin(k[1])
482 QH_Route_ClusterRemove(k)
483 rem[k] = true
485 SetTooltip(k, nil)
489 for k, _ in pairs(rem) do
490 InsertedItems[k] = nil
492 QuestHelper:ReleaseTable(rem)
494 for _, v in ipairs(Unknowning) do
495 QH_Route_IgnoreCluster(v, dontknow)
497 while table.remove(Unknowning) do end
499 in_pass = nil
501 --QH_Tooltip_Defer_Dump()
504 function QuestProcessor(user_id, db, title, level, group, variety, groupsize, watched, complete, lbcount, timed)
505 db.desc = title
506 db.tracker_desc = MakeQuestTitle(title, level)
508 db.type_quest.objectives = lbcount
509 db.type_quest.level = level
510 db.type_quest.done = (complete == 1)
511 db.type_quest.variety = variety
512 db.type_quest.groupsize = groupsize
513 db.type_quest.title = title
515 local turnin
516 local turnin_new
518 -- This is our "quest turnin" objective, which is currently being handled separately for no particularly good reason.
519 if db.finish and #db.finish > 0 then
520 for _, v in ipairs(db.finish) do
521 v.map_highlight = (complete == 1)
524 turnin = db.finish
525 --print("turnin:", turnin.tooltip_defer_questname)
526 if RefreshItem(user_id, turnin, true) then
527 turnin_new = true
528 for k, v in ipairs(turnin) do
529 v.tracker_clicked = function () Clicky(lindex) end
531 v.map_desc = {QHFormat("OBJECTIVE_REASON_TURNIN", title)}
534 if watched ~= "(ignore)" then QH_Tracker_SetPin(db.finish[1], watched) end
537 -- These are the individual criteria of the quest. Remember that each criteria can be represented by multiple routing objectives.
538 for i = 1, lbcount do
539 if db[i] then
540 local pt, pd, have, need = objective_parse(db[i].temp_typ, db[i].temp_desc, db[i].temp_done)
541 local dline
542 if pt == "item" or pt == "object" then
543 dline = QHFormat("OBJECTIVE_REASON", QHText("ACQUIRE_VERB"), pd, title)
544 elseif pt == "monster" then
545 dline = QHFormat("OBJECTIVE_REASON", QHText("SLAY_VERB"), pd, title)
546 else
547 dline = QHFormat("OBJECTIVE_REASON_FALLBACK", pd, title)
550 if not db[i].progress then
551 db[i].progress = {}
554 if type(have) == "number" and type(need) == "number" then
555 db[i].progress[db[i].temp_person] = {have, need, have / need}
556 else
557 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
560 local _, target = objective_parse(db[i].temp_typ, db[i].temp_desc)
561 db[i].target = target
563 db[i].desc = QHFormat("TOOLTIP_QUEST", title)
565 for k, v in ipairs(db[i]) do
566 v.desc = db[i].temp_desc
567 v.tracker_clicked = db.tracker_clicked
569 v.progress = db[i].progress
571 if v.path_desc then
572 v.map_desc = copy(v.path_desc)
573 v.map_desc[1] = dline
574 else
575 v.map_desc = {dline}
579 -- This is the snatch of code that actually adds it to routing.
580 if not db[i].temp_done and #db[i] > 0 then
581 if RefreshItem(user_id, db[i]) then
582 if turnin then QH_Route_ClusterRequires(turnin, db[i]) end
584 if watched ~= "(ignore)" then QH_Tracker_SetPin(db[i][1], watched) end
587 db[i].temp_desc, db[i].temp_typ, db[i].temp_done = nil, nil, nil
591 if turnin_new and timed then
592 QH_Route_SetClusterPriority(turnin, -1)
596 function SerItem(item)
597 local rtx
598 if type(item) == "boolean" then
599 rtx = "b" .. (item and "t" or "f")
600 elseif type(item) == "number" then
601 rtx = "n" .. tostring(item)
602 elseif type(item) == "string" then
603 rtx = "s" .. item:gsub("\\", "\\\\"):gsub(":", "\\;")
604 elseif type(item) == "nil" then
605 rtx = "0"
606 else
607 print(type(item), item)
608 QuestHelper: Assert()
610 return rtx
613 function DeSerItem(item)
614 local t = item:sub(1, 1)
615 local d = item:sub(2)
616 if t == "b" then
617 return (d == "t")
618 elseif t == "n" then
619 return tonumber(d)
620 elseif t == "s" then
621 return d:gsub("\\;", ":"):gsub("\\\\", "\\")
622 elseif t == "0" then
623 return nil
624 else
625 QuestHelper: Assert()
629 local function Serialize(...)
630 local sx
631 for i = 1, select("#", ...) do
632 if sx then sx = sx .. ":" else sx = "" end
633 sx = sx .. SerItem(select(i, ...))
635 QuestHelper: Assert(sx)
636 return sx
639 local function SAM(msg, chattype, target)
640 --QuestHelper: TextOut(string.format("%s/%s: %s", chattype, tostring(target), msg))
642 local thresh = 245
643 local msgsize = 240
644 if #msg > thresh then
645 for i = 1, #msg, msgsize do
646 local prefx = "x:"
647 if i == 1 then prefx = "v:" elseif i + msgsize > #msg then prefx = "X:" end
648 SAM(prefx .. msg:sub(i, i + msgsize - 1), chattype, target)
650 else
651 ChatThrottleLib:SendAddonMessage("BULK", "QHpr", msg, chattype, target, "QHpr")
655 -- sigh.
656 function is_uncached(typ, txt, done)
657 if not txt then return true end
658 if txt == "" then return true end
659 if txt:match("^ : %d+/%d+$") then return true end
660 local _, target = objective_parse(typ, txt, done)
661 if target == "" or target == " " then return true end
662 return false
665 -- qid, chunk
666 local current_chunks = {}
668 -- Here's the core update function
669 function QH_UpdateQuests(force)
670 if not DB_Ready() then return end
672 if update or force then -- Sometimes (usually) we don't actually update
673 local index = 1
675 local player = UnitName("player")
676 StartInsertionPass(player)
678 local next_chunks = {}
680 -- This begins the main update loop that loops through all of the quests
681 while true do
682 local title, level, variety, groupsize, _, _, complete = GetQuestLogTitle(index)
683 if not title then break end
685 title = title:match("%[.*%] (.*)") or title
687 local qlink = GetQuestLink(index)
688 if qlink then -- If we don't have a quest link, it's not really a quest
689 local id = GetQuestType(qlink)
690 if id then -- If we don't have a *valid* quest link, give up
691 local lbcount = GetNumQuestLeaderBoards(index)
692 local db = GetQuestMetaobjective(id, lbcount) -- This generates the above-mentioned metaobjective, including doing the database lookup.
694 QuestHelper: Assert(db)
696 local watched = IsQuestWatched(index)
698 -- 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)
699 local lindex = index
700 db.tracker_clicked = function () Clicky(lindex) end
702 db.type_quest.index = index
704 local timidx = 1
705 while true do
706 local timer = GetQuestIndexForTimer(timidx)
707 if not timer then timidx = nil break end
708 if timer == index then break end
709 timidx = timidx + 1
711 local timed = not not timidx
713 --print(id, title, level, groupsize, variety, groupsize, complete, timed)
714 local chunk = "q:" .. Serialize(id, title, level, groupsize, variety, groupsize, complete, timed)
715 for i = 1, lbcount do
716 QuestHelper: Assert(db[i])
717 db[i].temp_desc, db[i].temp_typ, db[i].temp_done = GetQuestLogLeaderBoard(i, index)
718 --[[if not db[i].temp_desc or is_uncached(db[i].temp_typ, db[i].temp_desc, db[i].temp_done) then
719 db[i].temp_desc = string.format("(missing description %d)", i)
720 end]]
721 db[i].temp_person = player
723 db[i].tooltip_defer_questname = title
724 db[i].tooltip_defer_questobjective = db[i].temp_desc -- yoink
725 QuestHelper: Assert(db[i].tooltip_defer_questobjective) -- hmmm
727 chunk = chunk .. ":" .. Serialize(db[i].temp_desc, db[i].temp_typ, db[i].temp_done)
730 db.finish.tooltip_defer_questname = title -- we're using this as our fallback right now
732 next_chunks[id] = chunk
734 QuestProcessor(player, db, title, level, groupsize, variety, groupsize, watched, complete, lbcount, timed)
737 index = index + 1
740 EndInsertionPass(player)
742 QH_Route_Filter_Rescan() -- 'cause filters may also change
744 if not QuestHelper_Pref.solo and QuestHelper_Pref.share then
745 for k, v in pairs(next_chunks) do
746 if current_chunks[k] ~= v then
747 SAM(v, "PARTY")
751 for k, v in pairs(current_chunks) do
752 if not next_chunks[k] then
753 SAM(string.format("q:n%d", k), "PARTY")
758 current_chunks = next_chunks
762 -- comm_packets[user][qid] = data
763 local comm_packets = {}
765 local function RefreshUserComms(user)
766 StartInsertionPass(user)
768 if comm_packets[user] then for _, dat in pairs(comm_packets[user]) do
769 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]
770 local objstart = 9
772 local obj = {}
773 while true do
774 if dat[#obj * 3 + objstart] == nil and dat[#obj * 3 + objstart + 1] == nil and dat[#obj * 3 + objstart + 2] == nil then break end
775 table.insert(obj, {dat[#obj * 3 + objstart], dat[#obj * 3 + objstart + 1], dat[#obj * 3 + objstart + 2]})
778 local lbcount = #obj
779 local db = GetQuestMetaobjective(id, lbcount) -- This generates the above-mentioned metaobjective, including doing the database lookup.
781 QuestHelper: Assert(db)
783 for i = 1, lbcount do
784 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
787 QuestProcessor(user, db, title, level, group, variety, groupsize, "(ignore)", complete, lbcount, false)
788 end end
790 EndInsertionPass(user)
792 QH_Route_Filter_Rescan() -- 'cause filters may also change
795 function QH_InsertCommPacket(user, data)
796 local q, chunk = data:match("([^:]+):(.*)")
797 if q ~= "q" then return end
799 local dat = {}
800 local idx = 1
801 for item in chunk:gmatch("([^:]+)") do
802 dat[idx] = DeSerItem(item)
803 idx = idx + 1
806 if not comm_packets[user] then comm_packets[user] = {} end
807 if idx == 2 then
808 comm_packets[user][dat[1]] = nil
809 else
810 comm_packets[user][dat[1]] = dat
813 -- refresh the comms
814 RefreshUserComms(user)
817 local function QH_DumpCommUser(user)
818 comm_packets[user] = nil
819 RefreshUserComms(user)
822 QH_Event("UNIT_QUEST_LOG_CHANGED", UpdateTrigger)
823 QH_Event("QUEST_LOG_UPDATE", QH_UpdateQuests)
825 -- 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.
826 QH_AddNotifier(GetTime() + 5, function ()
827 local aqw_orig = AddQuestWatch
828 AddQuestWatch = function(...)
829 aqw_orig(...)
830 QH_UpdateQuests(true)
832 local rqw_orig = RemoveQuestWatch
833 RemoveQuestWatch = function(...)
834 rqw_orig(...)
835 QH_UpdateQuests(true)
837 end)
839 local old_playerlist = {}
841 function QH_Questcomm_Sync()
842 if not (not QuestHelper_Pref.solo and QuestHelper_Pref.share) then
843 old_playerlist = {}
844 return
847 local playerlist = {}
848 --[[if GetNumRaidMembers() > 0 then
849 for i = 1, 40 do
850 local liv = UnitName(string.format("raid%d", i))
851 if liv then playerlist[liv] = true end
853 elseif]] if GetNumPartyMembers() > 0 then
854 -- we is in a party
855 for i = 1, 4 do
856 local targ = string.format("party%d", i)
857 local liv, relm = UnitName(targ)
858 if liv and not relm and liv ~= UNKNOWNOBJECT and UnitIsConnected(targ) then playerlist[liv] = true end
861 playerlist[UnitName("player")] = nil
863 local additions = {}
864 for k, v in pairs(playerlist) do
865 if not old_playerlist[k] then
866 --print("new player:", k)
867 table.insert(additions, k)
871 local removals = {}
872 for k, v in pairs(old_playerlist) do
873 if not playerlist[k] then
874 --print("lost player:", k)
875 table.insert(removals, k)
879 old_playerlist = playerlist
881 for _, v in ipairs(removals) do
882 QH_DumpCommUser(v)
885 if #additions == 0 then return end
887 if #additions == 1 then
888 SAM("syn:2", "WHISPER", additions[1])
889 else
890 SAM("syn:2", "PARTY")
894 local aku = {}
896 local newer_reported = false
897 local older_reported = false
898 function QH_Questcomm_Msg(data, from)
899 if data:match("syn:0") then
900 QH_DumpCommUser(from)
901 return
903 if QuestHelper_Pref.solo then return end
905 --print("received", from, data)
907 local cont = true
909 local key, value = data:match("(.):(.*)")
910 if key == "v" then
911 aku[from] = value
912 elseif key == "x" then
913 if aku[from] then
914 aku[from] = aku[from] .. value
916 elseif key == "X" then
917 if aku[from] then
918 aku[from] = aku[from] .. value
919 data = aku[from]
920 aku[from] = nil
921 cont = true
923 else
924 cont = true
927 if not cont then return end
930 --print("packet received", from, data)
931 if data:match("syn:.*") then
932 local synv = data:match("syn:([0-9]*)")
933 if synv then synv = tonumber(synv) end
934 if synv and synv ~= 2 then
935 if synv > 2 and not newer_reported then
936 QuestHelper:TextOut(QHFormat("PEER_NEWER", from))
937 newer_reported = true
938 elseif synv < 2 and not older_reported then
939 QuestHelper:TextOut(QHFormat("PEER_OLDER", from))
940 older_reported = true
944 if synv and synv >= 2 then
945 SAM("hello:2", "WHISPER", from)
947 elseif data == "hello:2" or data == "retrieve:2" then
948 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
950 for k, v in pairs(current_chunks) do
951 SAM(v, "WHISPER", from)
953 else
954 if old_playerlist[from] then
955 QH_InsertCommPacket(from, data)
960 function QuestHelper:SetShare(flag)
961 if flag then
962 QH_Questcomm_Sync()
963 else
964 SAM("syn:0", "PARTY")
965 local cpb = comm_packets
966 comm_packets = {}
967 for k in pairs(cpb) do RefreshUserComms(k) end