grumble
[QuestHelper.git] / director_quest.lua
blobe9f713c58e921b9cca6eda3a54138724b5e42985
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 local 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 Unknowned = {}
382 local in_pass = nil
384 local function SetTooltip(item, typ)
385 --print("stt", item, typ, item.tooltip_defer_questobjective)
386 if TooltipType[item] == typ and typ ~= "defer" and not item.tooltip_defer_questobjective_last then return end
387 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
389 if TooltipType[item] == "canned" then
390 QuestHelper: Assert(item.tooltip_canned)
391 QH_Tooltip_Canned_Remove(item.tooltip_canned)
392 elseif TooltipType[item] == "defer" then
393 QuestHelper: Assert(item.tooltip_defer_questname_last)
394 --print("remove", item.tooltip_defer_questname_last, item.tooltip_defer_questobjective_last, item.tooltip_defer_questobjective)
395 if item.tooltip_defer_questobjective_last then
396 QH_Tooltip_Defer_Remove(item.tooltip_defer_questname_last, item.tooltip_defer_questobjective_last, item.tooltip_defer_token_last)
397 else
398 QH_Tooltip_Defer_Remove(item.tooltip_defer_questname_last, item.tooltip_defer_questobjective, item.tooltip_defer_token_last)
400 elseif TooltipType[item] == nil then
401 else
402 QuestHelper: Assert(false)
405 item.tooltip_defer_questobjective_last = nil
406 item.tooltip_defer_questname_last = nil -- if it was anything, it is not now
407 item.tooltip_defer_token_last = nil
409 if typ == "canned" then
410 QuestHelper: Assert(item.tooltip_canned)
411 QH_Tooltip_Canned_Add(item.tooltip_canned)
412 elseif typ == "defer" then
413 QuestHelper: Assert(not not item.tooltip_defer_questobjective == not item.type_quest_finish) -- hmmm
414 --print("add", item.tooltip_defer_questname, item.tooltip_defer_questobjective)
415 QuestHelper: Assert(item.tooltip_defer_questname)
416 item.tooltip_defer_token_last = {{}, item}
417 QH_Tooltip_Defer_Add(item.tooltip_defer_questname, item.tooltip_defer_questobjective, item.tooltip_defer_token_last)
418 item.tooltip_defer_questname_last = item.tooltip_defer_questname
419 item.tooltip_defer_questobjective_last = item.tooltip_defer_questobjective
420 elseif typ == nil then
421 else
422 QuestHelper: Assert(false)
424 TooltipType[item] = typ
427 local function StartInsertionPass(id)
428 QuestHelper: Assert(not in_pass)
429 in_pass = id
430 QH_Timeslice_PushUnyieldable()
431 for k, v in pairs(InsertedItems) do
432 v[id] = nil
434 if k.progress then
435 k.progress[id] = nil
436 local desc = MakeQuestObjectiveTitle(k.progress, k.target)
437 for _, v in ipairs(k) do
438 v.tracker_desc = desc or "(no description available)"
442 -- 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
443 if id == UnitName("player") then
444 k.tooltip_defer_questname = nil
445 k.tooltip_defer_questobjective = nil
449 local function RefreshItem(id, item, required)
450 --if not required and math.random() < 0.2 then return false end -- ha ha bzzzzt
452 QuestHelper: Assert(in_pass == id)
453 local added = false
454 if not InsertedItems[item] then
455 QH_Route_ClusterAdd(item)
456 --QH_Route_SetClusterPriority(item, math.random(5))
457 added = true
458 InsertedItems[item] = {}
460 InsertedItems[item][id] = true
462 if item.tooltip_defer_questname then
463 SetTooltip(item, "defer")
464 elseif item.tooltip_canned then
465 SetTooltip(item, "canned")
466 else
467 SetTooltip(item, nil)
470 if item.type_quest_unknown then table.insert(Unknowning, item) end
472 local desc = MakeQuestObjectiveTitle(item.progress, item.target)
473 for _, v in ipairs(item) do
474 v.tracker_desc = desc or "(no description available)"
477 return added
479 local function EndInsertionPass(id)
480 QuestHelper: Assert(in_pass == id)
481 local rem = QuestHelper:CreateTable("ip rem")
482 for k, v in pairs(InsertedItems) do
483 local has = false
484 for _, _ in pairs(v) do
485 has = true
486 break
488 if not has then
489 QH_Tracker_Unpin(k[1], true)
490 QH_Route_ClusterRemove(k)
491 rem[k] = true
493 SetTooltip(k, nil)
497 QH_Tracker_Rescan()
499 for k, _ in pairs(rem) do
500 InsertedItems[k] = nil
502 QuestHelper:ReleaseTable(rem)
504 -- this is all so we don't spam the system with multiple ignores, since that currently causes an early routing exit
505 for k in pairs(Unknowned) do
506 Unknowned[k] = false
508 for _, v in ipairs(Unknowning) do
509 if Unknowned[v] == nil then
510 QH_Route_IgnoreCluster(v, dontknow)
512 Unknowned[v] = true
514 while table.remove(Unknowning) do end
515 local need_rescan = false
516 local new_unknowned = QuestHelper:CreateTable("unk")
517 for k, v in pairs(Unknowned) do
518 if v then new_unknowned[k] = true end
520 QuestHelper:ReleaseTable(Unknowned)
521 Unknowned = new_unknowned
523 QH_Timeslice_PopUnyieldable()
524 in_pass = nil
526 --QH_Tooltip_Defer_Dump()
529 function QuestProcessor(user_id, db, title, level, group, variety, groupsize, watched, complete, lbcount, timed)
530 db.desc = title
531 db.tracker_desc = MakeQuestTitle(title, level)
533 db.type_quest.objectives = lbcount
534 db.type_quest.level = level
535 db.type_quest.done = (complete == 1)
536 db.type_quest.variety = variety
537 db.type_quest.groupsize = groupsize
538 db.type_quest.title = title
540 local turnin
541 local turnin_new
543 -- This is our "quest turnin" objective, which is currently being handled separately for no particularly good reason.
544 if db.finish and #db.finish > 0 then
545 for _, v in ipairs(db.finish) do
546 v.map_highlight = (complete == 1)
549 turnin = db.finish
550 --print("turnin:", turnin.tooltip_defer_questname)
551 if RefreshItem(user_id, turnin, true) then
552 turnin_new = true
553 for k, v in ipairs(turnin) do
554 v.tracker_clicked = function () Clicky(lindex) end
556 v.map_desc = {QHFormat("OBJECTIVE_REASON_TURNIN", title)}
559 if watched ~= "(ignore)" then QH_Tracker_SetPin(db.finish[1], watched, true) end
562 -- These are the individual criteria of the quest. Remember that each criteria can be represented by multiple routing objectives.
563 for i = 1, lbcount do
564 if db[i] then
565 local pt, pd, have, need = objective_parse(db[i].temp_typ, db[i].temp_desc, db[i].temp_done)
566 local dline
567 if pt == "item" or pt == "object" then
568 dline = QHFormat("OBJECTIVE_REASON", QHText("ACQUIRE_VERB"), pd, title)
569 elseif pt == "monster" then
570 dline = QHFormat("OBJECTIVE_REASON", QHText("SLAY_VERB"), pd, title)
571 else
572 dline = QHFormat("OBJECTIVE_REASON_FALLBACK", pd, title)
575 if not db[i].progress then
576 db[i].progress = {}
579 if type(have) == "number" and type(need) == "number" then
580 db[i].progress[db[i].temp_person] = {have, need, have / need}
581 else
582 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
585 local _, target = objective_parse(db[i].temp_typ, db[i].temp_desc)
586 db[i].target = target
588 db[i].desc = QHFormat("TOOLTIP_QUEST", title)
590 for k, v in ipairs(db[i]) do
591 v.desc = db[i].temp_desc
592 v.tracker_clicked = db.tracker_clicked
594 v.progress = db[i].progress
596 if v.path_desc then
597 v.map_desc = copy(v.path_desc)
598 v.map_desc[1] = dline
599 else
600 v.map_desc = {dline}
604 -- This is the snatch of code that actually adds it to routing.
605 if not db[i].temp_done and #db[i] > 0 then
606 if RefreshItem(user_id, db[i]) then
607 if turnin then QH_Route_ClusterRequires(turnin, db[i]) end
609 if watched ~= "(ignore)" then QH_Tracker_SetPin(db[i][1], watched, true) end
612 db[i].temp_desc, db[i].temp_typ, db[i].temp_done = nil, nil, nil
616 if turnin_new and timed then
617 QH_Route_SetClusterPriority(turnin, -1)
621 function SerItem(item)
622 local rtx
623 if type(item) == "boolean" then
624 rtx = "b" .. (item and "t" or "f")
625 elseif type(item) == "number" then
626 rtx = "n" .. tostring(item)
627 elseif type(item) == "string" then
628 rtx = "s" .. item:gsub("\\", "\\\\"):gsub(":", "\\;")
629 elseif type(item) == "nil" then
630 rtx = "0"
631 else
632 print(type(item), item)
633 QuestHelper: Assert()
635 return rtx
638 function DeSerItem(item)
639 local t = item:sub(1, 1)
640 local d = item:sub(2)
641 if t == "b" then
642 return (d == "t")
643 elseif t == "n" then
644 return tonumber(d)
645 elseif t == "s" then
646 return d:gsub("\\;", ":"):gsub("\\\\", "\\")
647 elseif t == "0" then
648 return nil
649 else
650 QuestHelper: Assert()
654 local function Serialize(...)
655 local sx
656 for i = 1, select("#", ...) do
657 if sx then sx = sx .. ":" else sx = "" end
658 sx = sx .. SerItem(select(i, ...))
660 QuestHelper: Assert(sx)
661 return sx
664 local function SAM(msg, chattype, target)
665 --QuestHelper: TextOut(string.format("%s/%s: %s", chattype, tostring(target), msg))
667 local thresh = 245
668 local msgsize = 240
669 if #msg > thresh then
670 for i = 1, #msg, msgsize do
671 local prefx = "x:"
672 if i == 1 then prefx = "v:" elseif i + msgsize > #msg then prefx = "X:" end
673 SAM(prefx .. msg:sub(i, i + msgsize - 1), chattype, target)
675 else
676 ChatThrottleLib:SendAddonMessage("BULK", "QHpr", msg, chattype, target, "QHpr")
680 -- sigh.
681 function is_uncached(typ, txt, done)
682 if not txt then return true end
683 if txt == "" then return true end
684 if txt:match("^ : %d+/%d+$") then return true end
685 local _, target = objective_parse(typ, txt, done)
686 if target == "" or target == " " then return true end
687 return false
690 -- qid, chunk
691 local current_chunks = {}
693 -- "log" is a synthetic objective that Blizzard tossed in for god only knows what reason, so we just pretend it doesn't exist
694 local function GetEffectiveNumQuestLeaderBoards(index)
695 local v = GetNumQuestLeaderBoards(index)
696 if v ~= 1 then return v end
697 if select(2, GetQuestLogLeaderBoard(1, index)) == "log" then return 0 end
698 return 1
701 -- Here's the core update function
702 function QH_UpdateQuests(force)
703 if not DB_Ready() then return end
704 QH_Timeslice_PushUnyieldable()
706 if update or force then -- Sometimes (usually) we don't actually update
707 local index = 1
709 local player = UnitName("player")
710 StartInsertionPass(player)
712 local next_chunks = {}
714 local first = true
716 -- This begins the main update loop that loops through all of the quests
717 while true do
718 local title, level, variety, groupsize, _, _, complete = GetQuestLogTitle(index)
719 if not title then break end
721 title = title:match("%[.*%] (.*)") or title
723 local qlink = GetQuestLink(index)
724 if qlink then -- If we don't have a quest link, it's not really a quest
725 local id = GetQuestType(qlink)
726 --if first then id = 13836 else id = nil end
727 if id then -- If we don't have a *valid* quest link, give up
728 local lbcount = GetEffectiveNumQuestLeaderBoards(index)
729 local db = GetQuestMetaobjective(id, lbcount) -- This generates the above-mentioned metaobjective, including doing the database lookup.
731 QuestHelper: Assert(db)
733 local watched = IsQuestWatched(index)
735 -- 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)
736 local lindex = index
737 db.tracker_clicked = function () Clicky(lindex) end
739 db.type_quest.index = index
741 local timidx = 1
742 while true do
743 local timer = GetQuestIndexForTimer(timidx)
744 if not timer then timidx = nil break end
745 if timer == index then break end
746 timidx = timidx + 1
748 local timed = not not timidx
750 --print(id, title, level, groupsize, variety, groupsize, complete, timed)
751 local chunk = "q:" .. Serialize(id, title, level, groupsize, variety, groupsize, complete, timed)
752 for i = 1, lbcount do
753 QuestHelper: Assert(db[i])
754 db[i].temp_desc, db[i].temp_typ, db[i].temp_done = GetQuestLogLeaderBoard(i, index)
755 --[[if not db[i].temp_desc or is_uncached(db[i].temp_typ, db[i].temp_desc, db[i].temp_done) then
756 db[i].temp_desc = string.format("(missing description %d)", i)
757 end]]
758 db[i].temp_person = player
760 db[i].tooltip_defer_questname = title
761 db[i].tooltip_defer_questobjective = db[i].temp_desc -- yoink
762 QuestHelper: Assert(db[i].tooltip_defer_questobjective) -- hmmm
764 chunk = chunk .. ":" .. Serialize(db[i].temp_desc, db[i].temp_typ, db[i].temp_done)
767 db.finish.tooltip_defer_questname = title -- we're using this as our fallback right now
769 next_chunks[id] = chunk
771 QuestProcessor(player, db, title, level, groupsize, variety, groupsize, watched, complete, lbcount, timed)
773 first = false
775 index = index + 1
778 EndInsertionPass(player)
780 QH_Route_Filter_Rescan(nil, true) -- 'cause filters may also change, but let's not bother getting too excited about it
782 if not QuestHelper_Pref.solo and QuestHelper_Pref.share then
783 for k, v in pairs(next_chunks) do
784 if current_chunks[k] ~= v then
785 SAM(v, "PARTY")
789 for k, v in pairs(current_chunks) do
790 if not next_chunks[k] then
791 SAM(string.format("q:n%d", k), "PARTY")
796 current_chunks = next_chunks
799 QH_Timeslice_PopUnyieldable()
802 -- comm_packets[user][qid] = data
803 local comm_packets = {}
805 local function RefreshUserComms(user)
806 StartInsertionPass(user)
808 if comm_packets[user] then for _, dat in pairs(comm_packets[user]) do
809 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]
810 local objstart = 9
812 local obj = {}
813 while true do
814 if dat[#obj * 3 + objstart] == nil and dat[#obj * 3 + objstart + 1] == nil and dat[#obj * 3 + objstart + 2] == nil then break end
815 table.insert(obj, {dat[#obj * 3 + objstart], dat[#obj * 3 + objstart + 1], dat[#obj * 3 + objstart + 2]})
818 local lbcount = #obj
819 local db = GetQuestMetaobjective(id, lbcount) -- This generates the above-mentioned metaobjective, including doing the database lookup.
821 QuestHelper: Assert(db)
823 for i = 1, lbcount do
824 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
827 QuestProcessor(user, db, title, level, group, variety, groupsize, "(ignore)", complete, lbcount, false)
828 end end
830 EndInsertionPass(user)
832 QH_Route_Filter_Rescan() -- 'cause filters may also change
835 function QH_InsertCommPacket(user, data)
836 local q, chunk = data:match("([^:]+):(.*)")
837 if q ~= "q" then return end
839 local dat = {}
840 local idx = 1
841 for item in chunk:gmatch("([^:]+)") do
842 dat[idx] = DeSerItem(item)
843 idx = idx + 1
846 if not comm_packets[user] then comm_packets[user] = {} end
847 if idx == 2 then
848 comm_packets[user][dat[1]] = nil
849 else
850 comm_packets[user][dat[1]] = dat
853 -- refresh the comms
854 RefreshUserComms(user)
857 local function QH_DumpCommUser(user)
858 comm_packets[user] = nil
859 RefreshUserComms(user)
862 QH_Event("PLAYER_ENTERING_WORLD", UpdateTrigger)
863 QH_Event("UNIT_QUEST_LOG_CHANGED", UpdateTrigger)
864 QH_Event("QUEST_LOG_UPDATE", QH_UpdateQuests)
866 -- 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.
867 QH_AddNotifier(GetTime() + 5, function ()
868 local aqw_orig = AddQuestWatch
869 AddQuestWatch = function(...)
870 aqw_orig(...)
871 QH_UpdateQuests(true)
873 local rqw_orig = RemoveQuestWatch
874 RemoveQuestWatch = function(...)
875 rqw_orig(...)
876 QH_UpdateQuests(true)
878 end)
880 -- 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.
881 --[[local function autonotify()
882 QH_UpdateQuests(true)
883 QH_AddNotifier(GetTime() + 5, autonotify)
885 QH_AddNotifier(GetTime() + 30, autonotify)]]
887 local old_playerlist = {}
889 function QH_Questcomm_Sync()
890 if not (not QuestHelper_Pref.solo and QuestHelper_Pref.share) then
891 old_playerlist = {}
892 return
895 local playerlist = {}
896 --[[if GetNumRaidMembers() > 0 then
897 for i = 1, 40 do
898 local liv = UnitName(string.format("raid%d", i))
899 if liv then playerlist[liv] = true end
901 elseif]] if GetNumPartyMembers() > 0 then
902 -- we is in a party
903 for i = 1, 4 do
904 local targ = string.format("party%d", i)
905 local liv, relm = UnitName(targ)
906 if liv and not relm and liv ~= UNKNOWNOBJECT and UnitIsConnected(targ) then playerlist[liv] = true end
909 playerlist[UnitName("player")] = nil
911 local additions = {}
912 for k, v in pairs(playerlist) do
913 if not old_playerlist[k] then
914 --print("new player:", k)
915 table.insert(additions, k)
919 local removals = {}
920 for k, v in pairs(old_playerlist) do
921 if not playerlist[k] then
922 --print("lost player:", k)
923 table.insert(removals, k)
927 old_playerlist = playerlist
929 for _, v in ipairs(removals) do
930 QH_DumpCommUser(v)
933 if #additions == 0 then return end
935 if #additions == 1 then
936 SAM("syn:2", "WHISPER", additions[1])
937 else
938 SAM("syn:2", "PARTY")
942 local aku = {}
944 local newer_reported = false
945 local older_reported = false
946 function QH_Questcomm_Msg(data, from)
947 if data:match("syn:0") then
948 QH_DumpCommUser(from)
949 return
951 if QuestHelper_Pref.solo then return end
953 --print("received", from, data)
955 local cont = true
957 local key, value = data:match("(.):(.*)")
958 if key == "v" then
959 aku[from] = value
960 elseif key == "x" then
961 if aku[from] then
962 aku[from] = aku[from] .. value
964 elseif key == "X" then
965 if aku[from] then
966 aku[from] = aku[from] .. value
967 data = aku[from]
968 aku[from] = nil
969 cont = true
971 else
972 cont = true
975 if not cont then return end
978 --print("packet received", from, data)
979 if data:match("syn:.*") then
980 local synv = data:match("syn:([0-9]*)")
981 if synv then synv = tonumber(synv) end
982 if synv and synv ~= 2 then
983 if synv > 2 and not newer_reported then
984 QuestHelper:TextOut(QHFormat("PEER_NEWER", from))
985 newer_reported = true
986 elseif synv < 2 and not older_reported then
987 QuestHelper:TextOut(QHFormat("PEER_OLDER", from))
988 older_reported = true
992 if synv and synv >= 2 then
993 SAM("hello:2", "WHISPER", from)
995 elseif data == "hello:2" or data == "retrieve:2" then
996 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
998 for k, v in pairs(current_chunks) do
999 SAM(v, "WHISPER", from)
1001 else
1002 if old_playerlist[from] then
1003 QH_InsertCommPacket(from, data)
1008 function QuestHelper:SetShare(flag)
1009 if flag then
1010 QH_Questcomm_Sync()
1011 else
1012 SAM("syn:0", "PARTY")
1013 local cpb = comm_packets
1014 comm_packets = {}
1015 for k in pairs(cpb) do RefreshUserComms(k) end