Arrgh
[QuestHelper.git] / director_quest.lua
blob2ac2afa8f842deb8a989c8e0add0752879e88714
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 else
72 table.insert(map_lines, string.format("unknown %s (%s/%s)", tostring(dbgi.name), tostring(v.sourcetype), tostring(v.sourceid)))
73 table.insert(tooltip_lines, 1, string.format("unknown %s (%s/%s)", tostring(last_name), tostring(v.sourcetype), tostring(v.sourceid)))
74 licon = 3
75 end
77 tooltips[string.format("%s@@%s", v.sourcetype, v.sourceid)] = copy_without_last(tooltip_lines)
79 AppendObjlinks(target, dbgi, tooltips, icon or licon, source.name, map_lines, tooltip_lines, seen)
80 table.remove(tooltip_lines, 1)
81 table.remove(map_lines)
83 DB_ReleaseItem(dbgi)
84 end
86 seen[source] = false
87 end
90 local function horribledupe(from)
91 if not from then return nil end
93 local rv = {}
94 for k, v in pairs(from) do
95 if k == "__owner" then
96 elseif type(v) == "table" then
97 rv[k] = horribledupe(v)
98 else
99 rv[k] = v
102 return rv
106 local quest_list = setmetatable({}, {__mode="k"})
108 local QuestCriteriaWarningBroadcast
110 local function GetQuestMetaobjective(questid, lbcount)
111 if not quest_list[questid] then
112 local q = DB_GetItem("quest", questid, true, true)
114 if not lbcount then
115 QuestHelper: TextOut("Missing lbcount, guessing wildly")
116 if q and q.criteria then
117 lbcount = 0
118 for k, v in ipairs(q.criteria) do
119 lbcount = math.max(lbcount, k)
121 else
122 lbcount = 0 -- heh
126 -- just doublechecking here
127 if not QuestCriteriaWarningBroadcast and q and q.criteria then for k, v in pairs(q.criteria) do
128 if type(k) == "number" and k > lbcount then
129 --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
130 QuestHelper_ErrorCatcher_ExplicitError(false, string.format("Too many stored objectives (%s %s %s)", questid, lbcount, k))
131 QuestCriteriaWarningBroadcast = true
133 end end
135 ite = {type_quest = {__backlink = ite}} -- we don't want to mutate the existing quest data. backlink exists only for nasty GC reasons
136 ite.desc = string.format("Quest %s", q and q.name or "(unknown)") -- this gets changed later anyway
138 for i = 1, lbcount do
139 local ttx = {}
140 --QuestHelper:TextOut(string.format("critty %d %d", k, c.loc and #c.loc or -1))
142 ttx.tooltip_canned = {}
144 if q and q.criteria and q.criteria[i] then
145 --print("Appending criteria", questid, i)
146 AppendObjlinks(ttx, q.criteria[i], ttx.tooltip_canned)
147 --print("Done")
149 if debug_output and q.criteria[i].loc and #q.criteria[i] > 0 then
150 QuestHelper:TextOut(string.format("Wackyquest %d/%d", questid, i))
153 ttx.solid = horribledupe(q.criteria[i].solid)
156 if #ttx == 0 then
157 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
158 ttx.type_quest_unknown = true
161 for idx, v in ipairs(ttx) do
162 v.desc = string.format("Criteria %d", i)
163 v.why = ite
164 v.cluster = ttx
165 v.type_quest = ite.type_quest
168 for k, v in pairs(ttx.tooltip_canned) do
169 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
172 ite[i] = ttx
176 local ttx = {type_quest_finish = true}
177 --QuestHelper:TextOut(string.format("finny %d", q.finish.loc and #q.finish.loc or -1))
178 if q and q.finish and q.finish.loc then
179 ttx.solid = horribledupe(q.finish.solid)
180 for m, v in ipairs(q.finish.loc) do
181 --print(v.rc, v.rz)
182 --print(QuestHelper_IndexLookup[v.rc])
183 --print(QuestHelper_IndexLookup[v.rc][v.rz])
184 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})
188 if #ttx == 0 then
189 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
190 ttx.type_quest_unknown = true
193 ite.finish = ttx
196 quest_list[questid] = ite
198 if q then DB_ReleaseItem(q) end
201 return quest_list[questid]
205 local function GetQuestType(link)
206 return tonumber(string.match(link,
207 "^|cff%x%x%x%x%x%x|Hquest:(%d+):[%d-]+|h%[[^%]]*%]|h|r$"
208 )), tonumber(string.match(link,
209 "^|cff%x%x%x%x%x%x|Hquest:%d+:([%d-]+)|h%[[^%]]*%]|h|r$"
213 local update = true
214 local function UpdateTrigger()
215 update = true
218 -- 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.
219 local active_db = {}
221 local objective_parse_table = {
222 item = function (txt) return QuestHelper:convertPattern(QUEST_OBJECTS_FOUND)(txt) end,
223 object = function (txt) return QuestHelper:convertPattern(QUEST_OBJECTS_FOUND)(txt) end, -- why does this even exist
224 monster = function (txt) return QuestHelper:convertPattern(QUEST_MONSTERS_KILLED)(txt) end,
225 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.
226 reputation = function (txt) return QuestHelper:convertPattern(QUEST_FACTION_NEEDED)(txt) end, -- :ughh:
227 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.
230 local function objective_parse(typ, txt, done)
231 local pt, target, have, need = typ, objective_parse_table[typ](txt, done)
233 if not target then
234 -- well, that didn't work
235 target, have, need = string.match(txt, "^%s*(.-)%s*:%s*(.-)%s*/%s*(.-)%s*$")
236 pt = "fallback"
237 --QuestHelper:TextOut(string.format("%s rebecomes %s/%s/%s", tostring(title), tostring(target), tostring(have), tostring(need)))
240 if not target then
241 target, have, need = string.match(txt, "^%s*(.-)%s*$"), (done and 1 or 0), 1
242 --QuestHelper:TextOut(string.format("%s rerebecomes %s/%s/%s", tostring(title), tostring(target), tostring(have), tostring(need)))
245 QuestHelper: Assert(target) -- This will fail repeatedly. Come on. We all know it.
246 QuestHelper: Assert(have)
247 QuestHelper: Assert(need) -- As will these.
249 if tonumber(have) then have = tonumber(have) end
250 if tonumber(need) then need = tonumber(need) end
252 return pt, target, have, need
255 local function clamp(v)
256 if v < 0 then return 0 elseif v > 255 then return 255 else return v end
259 local function colorlerp(position, r1, g1, b1, r2, g2, b2)
260 local antip = 1 - position
261 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))
264 -- We're just gonna do the same thing QH originally did - red->yellow->green.
265 local function difficulty_color(position)
266 if position < 0 then position = 0 end
267 if position > 1 then position = 1 end
268 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)
271 local function MakeQuestTitle(title, level)
272 local plevel = UnitLevel("player") -- meh, should probably cache this, buuuuut
273 local grayd
275 if plevel >= 60 then
276 grayd = 9
277 elseif plevel >= 40 then
278 grayd = plevel / 5 + 1
279 else
280 grayd = plevel / 10 + 5
283 local isgray = (plevel - floor(grayd) >= level)
285 local ccode = isgray and "|cffb0b0b0" or difficulty_color(1 - ((level - plevel) / grayd + 1) / 2)
286 local qlevel = string.format("[%d] ", level)
288 local ret = title
289 if QuestHelper_Pref.track_level then ret = qlevel .. ret end
290 if QuestHelper_Pref.track_qcolour then ret = ccode .. ret end
292 return ret
295 local function MakeQuestObjectiveTitle(progress, target)
296 if not progress then return nil end
298 local player = UnitName("player")
300 local pt, pd = 0, 0
301 for _, v in pairs(progress) do
302 pt = pt + 1
303 if v[3] == 1 then pd = pd + 1 end
306 local ccode
307 local status
308 local party
309 local party_show = false
310 local party_compact = false
312 if progress[player] then
313 local have, need = tonumber(progress[player][1]), tonumber(progress[player][2])
315 ccode = difficulty_color(progress[player][3])
317 if have and need then
318 if need > 1 then
319 status = string.format("%d/%d", have, need)
320 party_compact = true
322 else
323 status = string.format("%s/%s", progress[player][1], progress[player][2])
324 party_compact = true
327 if pt > 1 then party_show = true end
328 elseif pt == 0 then
329 ccode = difficulty_color(1) -- probably just in the process of being removed from the tracker
330 status = "Complete"
331 else
332 ccode = difficulty_color(pd / pt)
334 party_show = true
337 if party_show then
338 if party_compact then
339 party = string.format("(P: %d/%d)", pd, pt)
340 else
341 party = string.format("Party %d/%d", pd, pt)
345 if QuestHelper_Pref.track_ocolour then
346 target = ccode .. target
349 if status or party then
350 target = target .. ":"
353 if status then
354 target = target .. " " .. status
357 if party then
358 target = target .. " " .. party
361 return target
364 local function Clicky(index)
365 ShowUIPanel(QuestLogFrame)
366 QuestLog_SetSelection(index)
367 QuestLog_Update()
370 local dontknow = {
371 name = "director_quest_unknown_objective",
372 no_exception = true,
373 no_disable = true,
374 friendly_reason = QHText("UNKNOWN_OBJ"),
377 -- InsertedItem[item] = {"list", "of", "reasons"}
378 local InsertedItems = {}
379 local TooltipType = {}
380 local Unknowning = {}
381 local in_pass = nil
383 local function SetTooltip(item, typ)
384 --print("stt", item, typ, item.tooltip_defer_questobjective)
385 if TooltipType[item] == typ and typ ~= "defer" and not item.tooltip_defer_questobjective_last then return end
386 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
388 if TooltipType[item] == "canned" then
389 QuestHelper: Assert(item.tooltip_canned)
390 QH_Tooltip_Canned_Remove(item.tooltip_canned)
391 elseif TooltipType[item] == "defer" then
392 QuestHelper: Assert(item.tooltip_defer_questname_last)
393 --print("remove", item.tooltip_defer_questname_last, item.tooltip_defer_questobjective_last, item.tooltip_defer_questobjective)
394 if item.tooltip_defer_questobjective_last then
395 QH_Tooltip_Defer_Remove(item.tooltip_defer_questname_last, item.tooltip_defer_questobjective_last, item.tooltip_defer_token_last)
396 else
397 QH_Tooltip_Defer_Remove(item.tooltip_defer_questname_last, item.tooltip_defer_questobjective, item.tooltip_defer_token_last)
399 elseif TooltipType[item] == nil then
400 else
401 QuestHelper: Assert(false)
404 item.tooltip_defer_questobjective_last = nil
405 item.tooltip_defer_questname_last = nil -- if it was anything, it is not now
406 item.tooltip_defer_token_last = nil
408 if typ == "canned" then
409 QuestHelper: Assert(item.tooltip_canned)
410 QH_Tooltip_Canned_Add(item.tooltip_canned)
411 elseif typ == "defer" then
412 QuestHelper: Assert(not not item.tooltip_defer_questobjective == not item.type_quest_finish) -- hmmm
413 --print("add", item.tooltip_defer_questname, item.tooltip_defer_questobjective)
414 QuestHelper: Assert(item.tooltip_defer_questname)
415 item.tooltip_defer_token_last = {{}, item}
416 QH_Tooltip_Defer_Add(item.tooltip_defer_questname, item.tooltip_defer_questobjective, item.tooltip_defer_token_last)
417 item.tooltip_defer_questname_last = item.tooltip_defer_questname
418 item.tooltip_defer_questobjective_last = item.tooltip_defer_questobjective
419 elseif typ == nil then
420 else
421 QuestHelper: Assert(false)
423 TooltipType[item] = typ
426 local function StartInsertionPass(id)
427 QuestHelper: Assert(not in_pass)
428 in_pass = id
429 QH_Timeslice_PushUnyieldable()
430 for k, v in pairs(InsertedItems) do
431 v[id] = nil
433 if k.progress then
434 k.progress[id] = nil
435 local desc = MakeQuestObjectiveTitle(k.progress, k.target)
436 for _, v in ipairs(k) do
437 v.tracker_desc = desc or "(no description available)"
441 -- 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
442 if id == UnitName("player") then
443 k.tooltip_defer_questname = nil
444 k.tooltip_defer_questobjective = nil
448 local function RefreshItem(id, item, required)
449 --if not required and math.random() < 0.2 then return false end -- ha ha bzzzzt
451 QuestHelper: Assert(in_pass == id)
452 local added = false
453 if not InsertedItems[item] then
454 QH_Route_ClusterAdd(item)
455 --QH_Route_SetClusterPriority(item, math.random(5))
456 added = true
457 InsertedItems[item] = {}
459 InsertedItems[item][id] = true
461 if item.tooltip_defer_questname then
462 SetTooltip(item, "defer")
463 elseif item.tooltip_canned then
464 SetTooltip(item, "canned")
465 else
466 SetTooltip(item, nil)
469 if item.type_quest_unknown then table.insert(Unknowning, item) end
471 local desc = MakeQuestObjectiveTitle(item.progress, item.target)
472 for _, v in ipairs(item) do
473 v.tracker_desc = desc or "(no description available)"
476 return added
478 local function EndInsertionPass(id)
479 QuestHelper: Assert(in_pass == id)
480 local rem = QuestHelper:CreateTable("ip rem")
481 for k, v in pairs(InsertedItems) do
482 local has = false
483 for _, _ in pairs(v) do
484 has = true
485 break
487 if not has then
488 QH_Tracker_Unpin(k[1], true)
489 QH_Route_ClusterRemove(k)
490 rem[k] = true
492 SetTooltip(k, nil)
496 QH_Tracker_Rescan()
498 for k, _ in pairs(rem) do
499 InsertedItems[k] = nil
501 QuestHelper:ReleaseTable(rem)
503 for _, v in ipairs(Unknowning) do
504 QH_Route_IgnoreCluster(v, dontknow)
506 while table.remove(Unknowning) do end
508 QH_Timeslice_PopUnyieldable()
509 in_pass = nil
511 --QH_Tooltip_Defer_Dump()
514 function QuestProcessor(user_id, db, title, level, group, variety, groupsize, watched, complete, lbcount, timed)
515 db.desc = title
516 db.tracker_desc = MakeQuestTitle(title, level)
518 db.type_quest.objectives = lbcount
519 db.type_quest.level = level
520 db.type_quest.done = (complete == 1)
521 db.type_quest.variety = variety
522 db.type_quest.groupsize = groupsize
523 db.type_quest.title = title
525 local turnin
526 local turnin_new
528 -- This is our "quest turnin" objective, which is currently being handled separately for no particularly good reason.
529 if db.finish and #db.finish > 0 then
530 for _, v in ipairs(db.finish) do
531 v.map_highlight = (complete == 1)
534 turnin = db.finish
535 --print("turnin:", turnin.tooltip_defer_questname)
536 if RefreshItem(user_id, turnin, true) then
537 turnin_new = true
538 for k, v in ipairs(turnin) do
539 v.tracker_clicked = function () Clicky(lindex) end
541 v.map_desc = {QHFormat("OBJECTIVE_REASON_TURNIN", title)}
544 if watched ~= "(ignore)" then QH_Tracker_SetPin(db.finish[1], watched, true) end
547 -- These are the individual criteria of the quest. Remember that each criteria can be represented by multiple routing objectives.
548 for i = 1, lbcount do
549 if db[i] then
550 local pt, pd, have, need = objective_parse(db[i].temp_typ, db[i].temp_desc, db[i].temp_done)
551 local dline
552 if pt == "item" or pt == "object" then
553 dline = QHFormat("OBJECTIVE_REASON", QHText("ACQUIRE_VERB"), pd, title)
554 elseif pt == "monster" then
555 dline = QHFormat("OBJECTIVE_REASON", QHText("SLAY_VERB"), pd, title)
556 else
557 dline = QHFormat("OBJECTIVE_REASON_FALLBACK", pd, title)
560 if not db[i].progress then
561 db[i].progress = {}
564 if type(have) == "number" and type(need) == "number" then
565 db[i].progress[db[i].temp_person] = {have, need, have / need}
566 else
567 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
570 local _, target = objective_parse(db[i].temp_typ, db[i].temp_desc)
571 db[i].target = target
573 db[i].desc = QHFormat("TOOLTIP_QUEST", title)
575 for k, v in ipairs(db[i]) do
576 v.desc = db[i].temp_desc
577 v.tracker_clicked = db.tracker_clicked
579 v.progress = db[i].progress
581 if v.path_desc then
582 v.map_desc = copy(v.path_desc)
583 v.map_desc[1] = dline
584 else
585 v.map_desc = {dline}
589 -- This is the snatch of code that actually adds it to routing.
590 if not db[i].temp_done and #db[i] > 0 then
591 if RefreshItem(user_id, db[i]) then
592 if turnin then QH_Route_ClusterRequires(turnin, db[i]) end
594 if watched ~= "(ignore)" then QH_Tracker_SetPin(db[i][1], watched, true) end
597 db[i].temp_desc, db[i].temp_typ, db[i].temp_done = nil, nil, nil
601 if turnin_new and timed then
602 QH_Route_SetClusterPriority(turnin, -1)
606 function SerItem(item)
607 local rtx
608 if type(item) == "boolean" then
609 rtx = "b" .. (item and "t" or "f")
610 elseif type(item) == "number" then
611 rtx = "n" .. tostring(item)
612 elseif type(item) == "string" then
613 rtx = "s" .. item:gsub("\\", "\\\\"):gsub(":", "\\;")
614 elseif type(item) == "nil" then
615 rtx = "0"
616 else
617 print(type(item), item)
618 QuestHelper: Assert()
620 return rtx
623 function DeSerItem(item)
624 local t = item:sub(1, 1)
625 local d = item:sub(2)
626 if t == "b" then
627 return (d == "t")
628 elseif t == "n" then
629 return tonumber(d)
630 elseif t == "s" then
631 return d:gsub("\\;", ":"):gsub("\\\\", "\\")
632 elseif t == "0" then
633 return nil
634 else
635 QuestHelper: Assert()
639 local function Serialize(...)
640 local sx
641 for i = 1, select("#", ...) do
642 if sx then sx = sx .. ":" else sx = "" end
643 sx = sx .. SerItem(select(i, ...))
645 QuestHelper: Assert(sx)
646 return sx
649 local function SAM(msg, chattype, target)
650 --QuestHelper: TextOut(string.format("%s/%s: %s", chattype, tostring(target), msg))
652 local thresh = 245
653 local msgsize = 240
654 if #msg > thresh then
655 for i = 1, #msg, msgsize do
656 local prefx = "x:"
657 if i == 1 then prefx = "v:" elseif i + msgsize > #msg then prefx = "X:" end
658 SAM(prefx .. msg:sub(i, i + msgsize - 1), chattype, target)
660 else
661 ChatThrottleLib:SendAddonMessage("BULK", "QHpr", msg, chattype, target, "QHpr")
665 -- sigh.
666 function is_uncached(typ, txt, done)
667 if not txt then return true end
668 if txt == "" then return true end
669 if txt:match("^ : %d+/%d+$") then return true end
670 local _, target = objective_parse(typ, txt, done)
671 if target == "" or target == " " then return true end
672 return false
675 -- qid, chunk
676 local current_chunks = {}
678 -- "log" is a synthetic objective that Blizzard tossed in for god only knows what reason, so we just pretend it doesn't exist
679 local function GetEffectiveNumQuestLeaderBoards(index)
680 local v = GetNumQuestLeaderBoards(index)
681 if v ~= 1 then return v end
682 if select(2, GetQuestLogLeaderBoard(1, index)) == "log" then return 0 end
683 return 1
686 -- Here's the core update function
687 function QH_UpdateQuests(force)
688 if not DB_Ready() then return end
689 QH_Timeslice_PushUnyieldable()
691 if update or force then -- Sometimes (usually) we don't actually update
692 local index = 1
694 local player = UnitName("player")
695 StartInsertionPass(player)
697 local next_chunks = {}
699 local first = true
701 -- This begins the main update loop that loops through all of the quests
702 while true do
703 local title, level, variety, groupsize, _, _, complete = GetQuestLogTitle(index)
704 if not title then break end
706 title = title:match("%[.*%] (.*)") or title
708 local qlink = GetQuestLink(index)
709 if qlink then -- If we don't have a quest link, it's not really a quest
710 local id = GetQuestType(qlink)
711 --if first then id = 13836 else id = nil end
712 if id then -- If we don't have a *valid* quest link, give up
713 local lbcount = GetEffectiveNumQuestLeaderBoards(index)
714 local db = GetQuestMetaobjective(id, lbcount) -- This generates the above-mentioned metaobjective, including doing the database lookup.
716 QuestHelper: Assert(db)
718 local watched = IsQuestWatched(index)
720 -- 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)
721 local lindex = index
722 db.tracker_clicked = function () Clicky(lindex) end
724 db.type_quest.index = index
726 local timidx = 1
727 while true do
728 local timer = GetQuestIndexForTimer(timidx)
729 if not timer then timidx = nil break end
730 if timer == index then break end
731 timidx = timidx + 1
733 local timed = not not timidx
735 --print(id, title, level, groupsize, variety, groupsize, complete, timed)
736 local chunk = "q:" .. Serialize(id, title, level, groupsize, variety, groupsize, complete, timed)
737 for i = 1, lbcount do
738 QuestHelper: Assert(db[i])
739 db[i].temp_desc, db[i].temp_typ, db[i].temp_done = GetQuestLogLeaderBoard(i, index)
740 --[[if not db[i].temp_desc or is_uncached(db[i].temp_typ, db[i].temp_desc, db[i].temp_done) then
741 db[i].temp_desc = string.format("(missing description %d)", i)
742 end]]
743 db[i].temp_person = player
745 db[i].tooltip_defer_questname = title
746 db[i].tooltip_defer_questobjective = db[i].temp_desc -- yoink
747 QuestHelper: Assert(db[i].tooltip_defer_questobjective) -- hmmm
749 chunk = chunk .. ":" .. Serialize(db[i].temp_desc, db[i].temp_typ, db[i].temp_done)
752 db.finish.tooltip_defer_questname = title -- we're using this as our fallback right now
754 next_chunks[id] = chunk
756 QuestProcessor(player, db, title, level, groupsize, variety, groupsize, watched, complete, lbcount, timed)
758 first = false
760 index = index + 1
763 EndInsertionPass(player)
765 QH_Route_Filter_Rescan() -- 'cause filters may also change
767 if not QuestHelper_Pref.solo and QuestHelper_Pref.share then
768 for k, v in pairs(next_chunks) do
769 if current_chunks[k] ~= v then
770 SAM(v, "PARTY")
774 for k, v in pairs(current_chunks) do
775 if not next_chunks[k] then
776 SAM(string.format("q:n%d", k), "PARTY")
781 current_chunks = next_chunks
784 QH_Timeslice_PopUnyieldable()
787 -- comm_packets[user][qid] = data
788 local comm_packets = {}
790 local function RefreshUserComms(user)
791 StartInsertionPass(user)
793 if comm_packets[user] then for _, dat in pairs(comm_packets[user]) do
794 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]
795 local objstart = 9
797 local obj = {}
798 while true do
799 if dat[#obj * 3 + objstart] == nil and dat[#obj * 3 + objstart + 1] == nil and dat[#obj * 3 + objstart + 2] == nil then break end
800 table.insert(obj, {dat[#obj * 3 + objstart], dat[#obj * 3 + objstart + 1], dat[#obj * 3 + objstart + 2]})
803 local lbcount = #obj
804 local db = GetQuestMetaobjective(id, lbcount) -- This generates the above-mentioned metaobjective, including doing the database lookup.
806 QuestHelper: Assert(db)
808 for i = 1, lbcount do
809 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
812 QuestProcessor(user, db, title, level, group, variety, groupsize, "(ignore)", complete, lbcount, false)
813 end end
815 EndInsertionPass(user)
817 QH_Route_Filter_Rescan() -- 'cause filters may also change
820 function QH_InsertCommPacket(user, data)
821 local q, chunk = data:match("([^:]+):(.*)")
822 if q ~= "q" then return end
824 local dat = {}
825 local idx = 1
826 for item in chunk:gmatch("([^:]+)") do
827 dat[idx] = DeSerItem(item)
828 idx = idx + 1
831 if not comm_packets[user] then comm_packets[user] = {} end
832 if idx == 2 then
833 comm_packets[user][dat[1]] = nil
834 else
835 comm_packets[user][dat[1]] = dat
838 -- refresh the comms
839 RefreshUserComms(user)
842 local function QH_DumpCommUser(user)
843 comm_packets[user] = nil
844 RefreshUserComms(user)
847 QH_Event("PLAYER_ENTERING_WORLD", UpdateTrigger)
848 QH_Event("UNIT_QUEST_LOG_CHANGED", UpdateTrigger)
849 QH_Event("QUEST_LOG_UPDATE", QH_UpdateQuests)
851 -- 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.
852 QH_AddNotifier(GetTime() + 5, function ()
853 local aqw_orig = AddQuestWatch
854 AddQuestWatch = function(...)
855 aqw_orig(...)
856 QH_UpdateQuests(true)
858 local rqw_orig = RemoveQuestWatch
859 RemoveQuestWatch = function(...)
860 rqw_orig(...)
861 QH_UpdateQuests(true)
863 end)
865 -- 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.
866 --[[local function autonotify()
867 QH_UpdateQuests(true)
868 QH_AddNotifier(GetTime() + 5, autonotify)
870 QH_AddNotifier(GetTime() + 30, autonotify)]]
872 local old_playerlist = {}
874 function QH_Questcomm_Sync()
875 if not (not QuestHelper_Pref.solo and QuestHelper_Pref.share) then
876 old_playerlist = {}
877 return
880 local playerlist = {}
881 --[[if GetNumRaidMembers() > 0 then
882 for i = 1, 40 do
883 local liv = UnitName(string.format("raid%d", i))
884 if liv then playerlist[liv] = true end
886 elseif]] if GetNumPartyMembers() > 0 then
887 -- we is in a party
888 for i = 1, 4 do
889 local targ = string.format("party%d", i)
890 local liv, relm = UnitName(targ)
891 if liv and not relm and liv ~= UNKNOWNOBJECT and UnitIsConnected(targ) then playerlist[liv] = true end
894 playerlist[UnitName("player")] = nil
896 local additions = {}
897 for k, v in pairs(playerlist) do
898 if not old_playerlist[k] then
899 --print("new player:", k)
900 table.insert(additions, k)
904 local removals = {}
905 for k, v in pairs(old_playerlist) do
906 if not playerlist[k] then
907 --print("lost player:", k)
908 table.insert(removals, k)
912 old_playerlist = playerlist
914 for _, v in ipairs(removals) do
915 QH_DumpCommUser(v)
918 if #additions == 0 then return end
920 if #additions == 1 then
921 SAM("syn:2", "WHISPER", additions[1])
922 else
923 SAM("syn:2", "PARTY")
927 local aku = {}
929 local newer_reported = false
930 local older_reported = false
931 function QH_Questcomm_Msg(data, from)
932 if data:match("syn:0") then
933 QH_DumpCommUser(from)
934 return
936 if QuestHelper_Pref.solo then return end
938 --print("received", from, data)
940 local cont = true
942 local key, value = data:match("(.):(.*)")
943 if key == "v" then
944 aku[from] = value
945 elseif key == "x" then
946 if aku[from] then
947 aku[from] = aku[from] .. value
949 elseif key == "X" then
950 if aku[from] then
951 aku[from] = aku[from] .. value
952 data = aku[from]
953 aku[from] = nil
954 cont = true
956 else
957 cont = true
960 if not cont then return end
963 --print("packet received", from, data)
964 if data:match("syn:.*") then
965 local synv = data:match("syn:([0-9]*)")
966 if synv then synv = tonumber(synv) end
967 if synv and synv ~= 2 then
968 if synv > 2 and not newer_reported then
969 QuestHelper:TextOut(QHFormat("PEER_NEWER", from))
970 newer_reported = true
971 elseif synv < 2 and not older_reported then
972 QuestHelper:TextOut(QHFormat("PEER_OLDER", from))
973 older_reported = true
977 if synv and synv >= 2 then
978 SAM("hello:2", "WHISPER", from)
980 elseif data == "hello:2" or data == "retrieve:2" then
981 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
983 for k, v in pairs(current_chunks) do
984 SAM(v, "WHISPER", from)
986 else
987 if old_playerlist[from] then
988 QH_InsertCommPacket(from, data)
993 function QuestHelper:SetShare(flag)
994 if flag then
995 QH_Questcomm_Sync()
996 else
997 SAM("syn:0", "PARTY")
998 local cpb = comm_packets
999 comm_packets = {}
1000 for k in pairs(cpb) do RefreshUserComms(k) end