Merge branch 'master' of git://cams.pavlovian.net/questhelper
[QuestHelper.git] / director_quest.lua
blobcebd93094a99930abd7dc95a644291db965b1847
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 quest_list = setmetatable({}, {__mode="k"})
90 local QuestCriteriaWarningBroadcast
92 local function GetQuestMetaobjective(questid, lbcount)
93 if not quest_list[questid] then
94 local q = DB_GetItem("quest", questid, true, true)
96 if not lbcount then
97 QuestHelper: TextOut("Missing lbcount, guessing wildly")
98 if q and q.criteria then
99 lbcount = 0
100 for k, v in ipairs(q.criteria) do
101 lbcount = math.max(lbcount, k)
103 else
104 lbcount = 0 -- heh
108 -- just doublechecking here
109 if not QuestCriteriaWarningBroadcast and q and q.criteria then for k, v in pairs(q.criteria) do
110 if type(k) == "number" and k > lbcount then
111 --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
112 QuestHelper_ErrorCatcher_ExplicitError(false, string.format("Too many stored objectives (%s %s %s)", questid, lbcount, k))
113 QuestCriteriaWarningBroadcast = true
115 end end
117 ite = {type_quest = {__backlink = ite}} -- we don't want to mutate the existing quest data. backlink exists only for nasty GC reasons
118 ite.desc = string.format("Quest %s", q and q.name or "(unknown)") -- this gets changed later anyway
120 for i = 1, lbcount do
121 local ttx = {}
122 --QuestHelper:TextOut(string.format("critty %d %d", k, c.loc and #c.loc or -1))
124 ttx.tooltip_canned = {}
126 if q and q.criteria and q.criteria[i] then
127 AppendObjlinks(ttx, q.criteria[i], ttx.tooltip_canned)
129 if debug_output and q.criteria[i].loc and #q.criteria[i] > 0 then
130 QuestHelper:TextOut(string.format("Wackyquest %d/%d", questid, i))
134 if #ttx == 0 then
135 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
138 for idx, v in ipairs(ttx) do
139 v.desc = string.format("Criteria %d", i)
140 v.why = ite
141 v.cluster = ttx
142 v.type_quest = ite.type_quest
145 for k, v in pairs(ttx.tooltip_canned) do
146 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
149 ite[i] = ttx
153 local ttx = {type_quest_finish = true}
154 --QuestHelper:TextOut(string.format("finny %d", q.finish.loc and #q.finish.loc or -1))
155 if q and q.finish and q.finish.loc then for m, v in ipairs(q.finish.loc) do
156 --print(v.rc, v.rz)
157 --print(QuestHelper_IndexLookup[v.rc])
158 --print(QuestHelper_IndexLookup[v.rc][v.rz])
159 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})
160 end end
162 if #ttx == 0 then
163 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
166 ite.finish = ttx
169 quest_list[questid] = ite
171 if q then DB_ReleaseItem(q) end
174 return quest_list[questid]
178 local function GetQuestType(link)
179 return tonumber(string.match(link,
180 "^|cff%x%x%x%x%x%x|Hquest:(%d+):[%d-]+|h%[[^%]]*%]|h|r$"
181 )), tonumber(string.match(link,
182 "^|cff%x%x%x%x%x%x|Hquest:%d+:([%d-]+)|h%[[^%]]*%]|h|r$"
186 local update = true
187 local function UpdateTrigger()
188 update = true
191 -- 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.
192 local active_db = {}
194 local objective_parse_table = {
195 item = function (txt) return QuestHelper:convertPattern(QUEST_OBJECTS_FOUND)(txt) end,
196 object = function (txt) return QuestHelper:convertPattern(QUEST_OBJECTS_FOUND)(txt) end, -- why does this even exist
197 monster = function (txt) return QuestHelper:convertPattern(QUEST_MONSTERS_KILLED)(txt) end,
198 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.
199 reputation = function (txt) return QuestHelper:convertPattern(QUEST_FACTION_NEEDED)(txt) end, -- :ughh:
200 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.
203 local function objective_parse(typ, txt, done)
204 local pt, target, have, need = typ, objective_parse_table[typ](txt, done)
206 if not target then
207 -- well, that didn't work
208 target, have, need = string.match(txt, "^%s*(.-)%s*:%s*(.-)%s*/%s*(.-)%s*$")
209 pt = "fallback"
210 --QuestHelper:TextOut(string.format("%s rebecomes %s/%s/%s", tostring(title), tostring(target), tostring(have), tostring(need)))
213 if not target then
214 target, have, need = string.match(txt, "^%s*(.-)%s*$"), (done and 1 or 0), 1
215 --QuestHelper:TextOut(string.format("%s rerebecomes %s/%s/%s", tostring(title), tostring(target), tostring(have), tostring(need)))
218 QuestHelper: Assert(target) -- This will fail repeatedly. Come on. We all know it.
219 QuestHelper: Assert(have)
220 QuestHelper: Assert(need) -- As will these.
222 if tonumber(have) then have = tonumber(have) end
223 if tonumber(need) then need = tonumber(need) end
225 return pt, target, have, need
228 local function clamp(v)
229 if v < 0 then return 0 elseif v > 255 then return 255 else return v end
232 local function colorlerp(position, r1, g1, b1, r2, g2, b2)
233 local antip = 1 - position
234 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))
237 -- We're just gonna do the same thing QH originally did - red->yellow->green.
238 local function difficulty_color(position)
239 if position < 0 then position = 0 end
240 if position > 1 then position = 1 end
241 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)
244 local function MakeQuestTitle(title, level)
245 local plevel = UnitLevel("player") -- meh, should probably cache this, buuuuut
246 local grayd
248 if plevel >= 60 then
249 grayd = 9
250 elseif plevel >= 40 then
251 grayd = plevel / 5 + 1
252 else
253 grayd = plevel / 10 + 5
256 local isgray = (plevel - floor(grayd) >= level)
258 local ccode = isgray and "|cffb0b0b0" or difficulty_color(1 - ((level - plevel) / grayd + 1) / 2)
259 local qlevel = string.format("[%d] ", level)
261 local ret = title
262 if QuestHelper_Pref.track_level then ret = qlevel .. ret end
263 if QuestHelper_Pref.track_qcolour then ret = ccode .. ret end
265 return ret
268 local function MakeQuestObjectiveTitle(progress, target)
269 if not progress then return nil end
271 local player = UnitName("player")
273 local pt, pd = 0, 0
274 for _, v in pairs(progress) do
275 pt = pt + 1
276 if v[3] == 1 then pd = pd + 1 end
279 local ccode
280 local status
281 local party
282 local party_show = false
283 local party_compact = false
285 if progress[player] then
286 local have, need = tonumber(progress[player][1]), tonumber(progress[player][2])
288 ccode = difficulty_color(progress[player][3])
290 if have and need then
291 if need > 1 then
292 status = string.format("%d/%d", have, need)
293 party_compact = true
295 else
296 status = string.format("%s/%s", progress[player][1], progress[player][2])
297 party_compact = true
300 if pt > 1 then party_show = true end
301 elseif pt == 0 then
302 ccode = difficulty_color(1) -- probably just in the process of being removed from the tracker
303 status = "Complete"
304 else
305 ccode = difficulty_color(pd / pt)
307 party_show = true
310 if party_show then
311 if party_compact then
312 party = string.format("(P: %d/%d)", pd, pt)
313 else
314 party = string.format("Party %d/%d", pd, pt)
318 if QuestHelper_Pref.track_ocolour then
319 target = ccode .. target
322 if status or party then
323 target = target .. ":"
326 if status then
327 target = target .. " " .. status
330 if party then
331 target = target .. " " .. party
334 return target
337 local function Clicky(index)
338 ShowUIPanel(QuestLogFrame)
339 QuestLog_SetSelection(index)
340 QuestLog_Update()
343 local dontknow = {
344 name = "director_quest_unknown_objective",
345 no_exception = true,
346 no_disable = true,
347 friendly_reason = QHText("UNKNOWN_OBJ"),
350 -- InsertedItem[item] = {"list", "of", "reasons"}
351 local InsertedItems = {}
352 local TooltipType = {}
353 local Unknowning = {}
354 local in_pass = nil
356 local function SetTooltip(item, typ)
357 --print("stt", item, typ, item.tooltip_defer_questobjective)
358 if TooltipType[item] == typ and typ ~= "defer" and not item.tooltip_defer_questobjective_last then return end
359 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
361 if TooltipType[item] == "canned" then
362 QuestHelper: Assert(item.tooltip_canned)
363 QH_Tooltip_Canned_Remove(item.tooltip_canned)
364 elseif TooltipType[item] == "defer" then
365 QuestHelper: Assert(item.tooltip_defer_questname)
366 if item.tooltip_defer_questobjective_last then
367 QH_Tooltip_Defer_Remove(item.tooltip_defer_questname, item.tooltip_defer_questobjective_last)
368 else
369 QH_Tooltip_Defer_Remove(item.tooltip_defer_questname, item.tooltip_defer_questobjective)
371 elseif TooltipType[item] == nil then
372 else
373 QuestHelper: Assert(false)
376 if typ == "canned" then
377 QuestHelper: Assert(item.tooltip_canned)
378 QH_Tooltip_Canned_Add(item.tooltip_canned)
379 elseif typ == "defer" then
380 QuestHelper: Assert(item.tooltip_defer_questname)
381 QH_Tooltip_Defer_Add(item.tooltip_defer_questname, item.tooltip_defer_questobjective, {{}, item})
382 elseif typ == nil then
383 else
384 QuestHelper: Assert(false)
387 item.tooltip_defer_questobjective_last = nil -- if it was anything, it is not now
388 TooltipType[item] = typ
391 local function StartInsertionPass(id)
392 QuestHelper: Assert(not in_pass)
393 in_pass = id
394 for k, v in pairs(InsertedItems) do
395 v[id] = nil
397 if k.progress then
398 k.progress[id] = nil
399 local desc = MakeQuestObjectiveTitle(k.progress, k.target)
400 for _, v in ipairs(k) do
401 v.tracker_desc = desc or "(no description available)"
406 local function RefreshItem(id, item, required)
407 --if not required and math.random() < 0.2 then return false end -- ha ha bzzzzt
409 QuestHelper: Assert(in_pass == id)
410 local added = false
411 if not InsertedItems[item] then
412 QH_Route_ClusterAdd(item)
413 --QH_Route_SetClusterPriority(item, math.random(5))
414 added = true
415 InsertedItems[item] = {}
417 InsertedItems[item][id] = true
419 if item.tooltip_defer_questname then
420 SetTooltip(item, "defer")
421 elseif item.tooltip_canned then
422 SetTooltip(item, "canned")
423 else
424 SetTooltip(item, nil)
427 if item.type_quest_unknown then table.insert(Unknowning, item) end
429 local desc = MakeQuestObjectiveTitle(item.progress, item.target)
430 for _, v in ipairs(item) do
431 v.tracker_desc = desc or "(no description available)"
434 return added
436 local function EndInsertionPass(id)
437 QuestHelper: Assert(in_pass == id)
438 local rem = QuestHelper:CreateTable("ip rem")
439 for k, v in pairs(InsertedItems) do
440 local has = false
441 for _, _ in pairs(v) do
442 has = true
443 break
445 if not has then
446 QH_Tracker_Unpin(k[1])
447 QH_Route_ClusterRemove(k)
448 rem[k] = true
450 SetTooltip(k, nil)
454 for k, _ in pairs(rem) do
455 InsertedItems[k] = nil
457 QuestHelper:ReleaseTable(rem)
459 for _, v in ipairs(Unknowning) do
460 QH_Route_IgnoreCluster(v, dontknow)
462 while table.remove(Unknowning) do end
464 in_pass = nil
467 function QuestProcessor(user_id, db, title, level, group, variety, groupsize, watched, complete, lbcount, timed)
468 db.desc = title
469 db.tracker_desc = MakeQuestTitle(title, level)
471 db.type_quest.objectives = lbcount
472 db.type_quest.level = level
473 db.type_quest.done = (complete == 1)
474 db.type_quest.variety = variety
475 db.type_quest.groupsize = groupsize
476 db.type_quest.title = title
478 local turnin
479 local turnin_new
481 -- This is our "quest turnin" objective, which is currently being handled separately for no particularly good reason.
482 if db.finish and #db.finish > 0 then
483 for _, v in ipairs(db.finish) do
484 v.map_highlight = (complete == 1)
487 turnin = db.finish
488 --print("turnin:", turnin.tooltip_defer_questname)
489 if RefreshItem(user_id, turnin, true) then
490 turnin_new = true
491 for k, v in ipairs(turnin) do
492 v.tracker_clicked = function () Clicky(lindex) end
494 v.map_desc = {QHFormat("OBJECTIVE_REASON_TURNIN", title)}
497 if watched ~= "(ignore)" then QH_Tracker_SetPin(db.finish[1], watched) end
500 -- These are the individual criteria of the quest. Remember that each criteria can be represented by multiple routing objectives.
501 for i = 1, lbcount do
502 if db[i] then
503 local pt, pd, have, need = objective_parse(db[i].temp_typ, db[i].temp_desc, db[i].temp_done)
504 local dline
505 if pt == "item" or pt == "object" then
506 dline = QHFormat("OBJECTIVE_REASON", QHText("ACQUIRE_VERB"), pd, title)
507 elseif pt == "monster" then
508 dline = QHFormat("OBJECTIVE_REASON", QHText("SLAY_VERB"), pd, title)
509 else
510 dline = QHFormat("OBJECTIVE_REASON_FALLBACK", pd, title)
513 if not db[i].progress then
514 db[i].progress = {}
517 if type(have) == "number" and type(need) == "number" then
518 db[i].progress[db[i].temp_person] = {have, need, have / need}
519 else
520 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
523 local _, target = objective_parse(db[i].temp_typ, db[i].temp_desc)
524 db[i].target = target
526 db[i].desc = QHFormat("TOOLTIP_QUEST", title)
528 for k, v in ipairs(db[i]) do
529 v.desc = db[i].temp_desc
530 v.tracker_clicked = db.tracker_clicked
532 v.progress = db[i].progress
534 if v.path_desc then
535 v.map_desc = copy(v.path_desc)
536 v.map_desc[1] = dline
537 else
538 v.map_desc = {dline}
542 -- This is the snatch of code that actually adds it to routing.
543 if not db[i].temp_done and #db[i] > 0 then
544 if RefreshItem(user_id, db[i]) then
545 if turnin then QH_Route_ClusterRequires(turnin, db[i]) end
547 if watched ~= "(ignore)" then QH_Tracker_SetPin(db[i][1], watched) end
550 db[i].temp_desc, db[i].temp_typ, db[i].temp_done = nil, nil, nil
554 if turnin_new and timed then
555 QH_Route_SetClusterPriority(turnin, -1)
559 function SerItem(item)
560 local rtx
561 if type(item) == "boolean" then
562 rtx = "b" .. (item and "t" or "f")
563 elseif type(item) == "number" then
564 rtx = "n" .. tostring(item)
565 elseif type(item) == "string" then
566 rtx = "s" .. item:gsub("\\", "\\\\"):gsub(":", "\\;")
567 elseif type(item) == "nil" then
568 rtx = "0"
569 else
570 print(type(item), item)
571 QuestHelper: Assert()
573 return rtx
576 function DeSerItem(item)
577 local t = item:sub(1, 1)
578 local d = item:sub(2)
579 if t == "b" then
580 return (d == "t")
581 elseif t == "n" then
582 return tonumber(d)
583 elseif t == "s" then
584 return d:gsub("\\;", ":"):gsub("\\\\", "\\")
585 elseif t == "0" then
586 return nil
587 else
588 QuestHelper: Assert()
592 local function Serialize(...)
593 local sx
594 for i = 1, select("#", ...) do
595 if sx then sx = sx .. ":" else sx = "" end
596 sx = sx .. SerItem(select(i, ...))
598 QuestHelper: Assert(sx)
599 return sx
602 local function SAM(msg, chattype, target)
603 --QuestHelper: TextOut(string.format("%s/%s: %s", chattype, tostring(target), msg))
605 local thresh = 245
606 local msgsize = 240
607 if #msg > thresh then
608 for i = 1, #msg, msgsize do
609 local prefx = "x:"
610 if i == 1 then prefx = "v:" elseif i + msgsize > #msg then prefx = "X:" end
611 SAM(prefx .. msg:sub(i, i + msgsize - 1), chattype, target)
613 else
614 ChatThrottleLib:SendAddonMessage("BULK", "QHpr", msg, chattype, target, "QHpr")
618 -- sigh.
619 function is_uncached(typ, txt, done)
620 if not txt then return true end
621 if txt == "" then return true end
622 if txt:match("^ : %d+/%d+$") then return true end
623 local _, target = objective_parse(typ, txt, done)
624 if target == "" or target == " " then return true end
625 return false
628 -- qid, chunk
629 local current_chunks = {}
631 -- Here's the core update function
632 function QH_UpdateQuests(force)
633 if not DB_Ready() then return end
635 if update or force then -- Sometimes (usually) we don't actually update
636 local index = 1
638 local player = UnitName("player")
639 StartInsertionPass(player)
641 local next_chunks = {}
643 -- This begins the main update loop that loops through all of the quests
644 while true do
645 local title, level, variety, groupsize, _, _, complete = GetQuestLogTitle(index)
646 if not title then break end
648 title = title:match("%[.*%] (.*)") or title
650 local qlink = GetQuestLink(index)
651 if qlink then -- If we don't have a quest link, it's not really a quest
652 local id = GetQuestType(qlink)
653 if id then -- If we don't have a *valid* quest link, give up
654 local lbcount = GetNumQuestLeaderBoards(index)
655 local db = GetQuestMetaobjective(id, lbcount) -- This generates the above-mentioned metaobjective, including doing the database lookup.
657 QuestHelper: Assert(db)
659 local watched = IsQuestWatched(index)
661 -- 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)
662 local lindex = index
663 db.tracker_clicked = function () Clicky(lindex) end
665 db.type_quest.index = index
667 local timidx = 1
668 while true do
669 local timer = GetQuestIndexForTimer(timidx)
670 if not timer then timidx = nil break end
671 if timer == index then break end
672 timidx = timidx + 1
674 local timed = not not timidx
676 --print(id, title, level, groupsize, variety, groupsize, complete, timed)
677 local chunk = "q:" .. Serialize(id, title, level, groupsize, variety, groupsize, complete, timed)
678 for i = 1, lbcount do
679 QuestHelper: Assert(db[i])
680 db[i].temp_desc, db[i].temp_typ, db[i].temp_done = GetQuestLogLeaderBoard(i, index)
681 if is_uncached(db[i].temp_typ, db[i].temp_desc, db[i].temp_done) then
682 db[i].temp_desc = string.format("(missing description %d)", i)
684 db[i].temp_person = player
686 if db[i].temp_desc ~= db[i].tooltip_defer_questobjective then
687 db[i].tooltip_defer_questobjective_last = db[i].tooltip_defer_questobjective
689 db[i].tooltip_defer_questname = title
690 db[i].tooltip_defer_questobjective = db[i].temp_desc -- yoink
693 chunk = chunk .. ":" .. Serialize(db[i].temp_desc, db[i].temp_typ, db[i].temp_done)
696 db.finish.tooltip_defer_questname = title -- we're using this as our fallback right now
698 next_chunks[id] = chunk
700 QuestProcessor(player, db, title, level, groupsize, variety, groupsize, watched, complete, lbcount, timed)
703 index = index + 1
706 EndInsertionPass(player)
708 QH_Route_Filter_Rescan() -- 'cause filters may also change
710 if not QuestHelper_Pref.solo and QuestHelper_Pref.share then
711 for k, v in pairs(next_chunks) do
712 if current_chunks[k] ~= v then
713 SAM(v, "PARTY")
717 for k, v in pairs(current_chunks) do
718 if not next_chunks[k] then
719 SAM(string.format("q:n%d", k), "PARTY")
724 current_chunks = next_chunks
728 -- comm_packets[user][qid] = data
729 local comm_packets = {}
731 local function RefreshUserComms(user)
732 StartInsertionPass(user)
734 if comm_packets[user] then for _, dat in pairs(comm_packets[user]) do
735 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]
736 local objstart = 9
738 local obj = {}
739 while true do
740 if dat[#obj * 3 + objstart] == nil and dat[#obj * 3 + objstart + 1] == nil and dat[#obj * 3 + objstart + 2] == nil then break end
741 table.insert(obj, {dat[#obj * 3 + objstart], dat[#obj * 3 + objstart + 1], dat[#obj * 3 + objstart + 2]})
744 local lbcount = #obj
745 local db = GetQuestMetaobjective(id, lbcount) -- This generates the above-mentioned metaobjective, including doing the database lookup.
747 QuestHelper: Assert(db)
749 for i = 1, lbcount do
750 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
753 QuestProcessor(user, db, title, level, group, variety, groupsize, "(ignore)", complete, lbcount, false)
754 end end
756 EndInsertionPass(user)
758 QH_Route_Filter_Rescan() -- 'cause filters may also change
761 function QH_InsertCommPacket(user, data)
762 local q, chunk = data:match("([^:]+):(.*)")
763 if q ~= "q" then return end
765 local dat = {}
766 local idx = 1
767 for item in chunk:gmatch("([^:]+)") do
768 dat[idx] = DeSerItem(item)
769 idx = idx + 1
772 if not comm_packets[user] then comm_packets[user] = {} end
773 if idx == 2 then
774 comm_packets[user][dat[1]] = nil
775 else
776 comm_packets[user][dat[1]] = dat
779 -- refresh the comms
780 RefreshUserComms(user)
783 local function QH_DumpCommUser(user)
784 comm_packets[user] = nil
785 RefreshUserComms(user)
788 QH_Event("UNIT_QUEST_LOG_CHANGED", UpdateTrigger)
789 QH_Event("QUEST_LOG_UPDATE", QH_UpdateQuests)
791 -- 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.
792 QH_AddNotifier(GetTime() + 5, function ()
793 local aqw_orig = AddQuestWatch
794 AddQuestWatch = function(...)
795 aqw_orig(...)
796 QH_UpdateQuests(true)
798 local rqw_orig = RemoveQuestWatch
799 RemoveQuestWatch = function(...)
800 rqw_orig(...)
801 QH_UpdateQuests(true)
803 end)
805 local old_playerlist = {}
807 function QH_Questcomm_Sync()
808 if not (not QuestHelper_Pref.solo and QuestHelper_Pref.share) then
809 old_playerlist = {}
810 return
813 local playerlist = {}
814 --[[if GetNumRaidMembers() > 0 then
815 for i = 1, 40 do
816 local liv = UnitName(string.format("raid%d", i))
817 if liv then playerlist[liv] = true end
819 elseif]] if GetNumPartyMembers() > 0 then
820 -- we is in a party
821 for i = 1, 4 do
822 local targ = string.format("party%d", i)
823 local liv = UnitName(targ)
824 if liv and liv ~= UNKNOWNOBJECT and UnitIsConnected(targ) then playerlist[liv] = true end
827 playerlist[UnitName("player")] = nil
829 local additions = {}
830 for k, v in pairs(playerlist) do
831 if not old_playerlist[k] then
832 --print("new player:", k)
833 table.insert(additions, k)
837 local removals = {}
838 for k, v in pairs(old_playerlist) do
839 if not playerlist[k] then
840 --print("lost player:", k)
841 table.insert(removals, k)
845 old_playerlist = playerlist
847 for _, v in ipairs(removals) do
848 QH_DumpCommUser(v)
851 if #additions == 0 then return end
853 if #additions == 1 then
854 SAM("syn:2", "WHISPER", additions[1])
855 else
856 SAM("syn:2", "PARTY")
860 local aku = {}
862 local newer_reported = false
863 local older_reported = false
864 function QH_Questcomm_Msg(data, from)
865 if data:match("syn:0") then
866 QH_DumpCommUser(from)
867 return
869 if QuestHelper_Pref.solo then return end
871 --print("received", from, data)
873 local cont = true
875 local key, value = data:match("(.):(.*)")
876 if key == "v" then
877 aku[from] = value
878 elseif key == "x" then
879 if aku[from] then
880 aku[from] = aku[from] .. value
882 elseif key == "X" then
883 if aku[from] then
884 aku[from] = aku[from] .. value
885 data = aku[from]
886 aku[from] = nil
887 cont = true
889 else
890 cont = true
893 if not cont then return end
896 --print("packet received", from, data)
897 if data:match("syn:.*") then
898 local synv = data:match("syn:([0-9]*)")
899 if synv then synv = tonumber(synv) end
900 if synv and synv ~= 2 then
901 if synv > 2 and not newer_reported then
902 QuestHelper:TextOut(QHFormat("PEER_NEWER", from))
903 newer_reported = true
904 elseif synv < 2 and not older_reported then
905 QuestHelper:TextOut(QHFormat("PEER_OLDER", from))
906 older_reported = true
910 if synv and synv >= 2 then
911 SAM("hello:2", "WHISPER", from)
913 elseif data == "hello:2" or data == "retrieve:2" then
914 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
916 for k, v in pairs(current_chunks) do
917 SAM(v, "WHISPER", from)
919 else
920 if old_playerlist[from] then
921 QH_InsertCommPacket(from, data)
926 function QuestHelper:SetShare(flag)
927 if flag then
928 QH_Questcomm_Sync()
929 else
930 SAM("syn:0", "PARTY")
931 local cpb = comm_packets
932 comm_packets = {}
933 for k in pairs(cpb) do RefreshUserComms(k) end