Merge branch 'master' of zorba@192.168.100.11:questhelper
[QuestHelper.git] / director_quest.lua
blobcaad762acab063be2b9dca5f059027718979c257
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 -- qid, chunk
619 local current_chunks = {}
621 -- Here's the core update function
622 function QH_UpdateQuests(force)
623 if not DB_Ready() then return end
625 if update or force then -- Sometimes (usually) we don't actually update
626 local index = 1
628 local player = UnitName("player")
629 StartInsertionPass(player)
631 local next_chunks = {}
633 -- This begins the main update loop that loops through all of the quests
634 while true do
635 local title, level, variety, groupsize, _, _, complete = GetQuestLogTitle(index)
636 if not title then break end
638 title = title:match("%[.*%] (.*)") or title
640 local qlink = GetQuestLink(index)
641 if qlink then -- If we don't have a quest link, it's not really a quest
642 local id = GetQuestType(qlink)
643 if id then -- If we don't have a *valid* quest link, give up
644 local lbcount = GetNumQuestLeaderBoards(index)
645 local db = GetQuestMetaobjective(id, lbcount) -- This generates the above-mentioned metaobjective, including doing the database lookup.
647 QuestHelper: Assert(db)
649 local watched = IsQuestWatched(index)
651 -- 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)
652 local lindex = index
653 db.tracker_clicked = function () Clicky(lindex) end
655 db.type_quest.index = index
657 local timidx = 1
658 while true do
659 local timer = GetQuestIndexForTimer(timidx)
660 if not timer then timidx = nil break end
661 if timer == index then break end
662 timidx = timidx + 1
664 local timed = not not timidx
666 --print(id, title, level, groupsize, variety, groupsize, complete, timed)
667 local chunk = "q:" .. Serialize(id, title, level, groupsize, variety, groupsize, complete, timed)
668 for i = 1, lbcount do
669 QuestHelper: Assert(db[i])
670 db[i].temp_desc, db[i].temp_typ, db[i].temp_done = GetQuestLogLeaderBoard(i, index)
671 db[i].temp_person = player
673 if db[i].temp_desc ~= db[i].tooltip_defer_questobjective then
674 db[i].tooltip_defer_questobjective_last = db[i].tooltip_defer_questobjective
676 db[i].tooltip_defer_questname = title
677 db[i].tooltip_defer_questobjective = db[i].temp_desc -- yoink
680 chunk = chunk .. ":" .. Serialize(db[i].temp_desc, db[i].temp_typ, db[i].temp_done)
683 db.finish.tooltip_defer_questname = title -- we're using this as our fallback right now
685 next_chunks[id] = chunk
687 QuestProcessor(player, db, title, level, groupsize, variety, groupsize, watched, complete, lbcount, timed)
690 index = index + 1
693 EndInsertionPass(player)
695 QH_Route_Filter_Rescan() -- 'cause filters may also change
697 if not QuestHelper_Pref.solo and QuestHelper_Pref.share then
698 for k, v in pairs(next_chunks) do
699 if current_chunks[k] ~= v then
700 SAM(v, "PARTY")
704 for k, v in pairs(current_chunks) do
705 if not next_chunks[k] then
706 SAM(string.format("q:n%d", k), "PARTY")
711 current_chunks = next_chunks
715 -- comm_packets[user][qid] = data
716 local comm_packets = {}
718 local function RefreshUserComms(user)
719 StartInsertionPass(user)
721 if comm_packets[user] then for _, dat in pairs(comm_packets[user]) do
722 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]
723 local objstart = 9
725 local obj = {}
726 while true do
727 if dat[#obj * 3 + objstart] == nil and dat[#obj * 3 + objstart + 1] == nil and dat[#obj * 3 + objstart + 2] == nil then break end
728 table.insert(obj, {dat[#obj * 3 + objstart], dat[#obj * 3 + objstart + 1], dat[#obj * 3 + objstart + 2]})
731 local lbcount = #obj
732 local db = GetQuestMetaobjective(id, lbcount) -- This generates the above-mentioned metaobjective, including doing the database lookup.
734 QuestHelper: Assert(db)
736 for i = 1, lbcount do
737 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
740 QuestProcessor(user, db, title, level, group, variety, groupsize, "(ignore)", complete, lbcount, false)
741 end end
743 EndInsertionPass(user)
745 QH_Route_Filter_Rescan() -- 'cause filters may also change
748 function QH_InsertCommPacket(user, data)
749 local q, chunk = data:match("([^:]+):(.*)")
750 if q ~= "q" then return end
752 local dat = {}
753 local idx = 1
754 for item in chunk:gmatch("([^:]+)") do
755 dat[idx] = DeSerItem(item)
756 idx = idx + 1
759 if not comm_packets[user] then comm_packets[user] = {} end
760 if idx == 2 then
761 comm_packets[user][dat[1]] = nil
762 else
763 comm_packets[user][dat[1]] = dat
766 -- refresh the comms
767 RefreshUserComms(user)
770 local function QH_DumpCommUser(user)
771 comm_packets[user] = nil
772 RefreshUserComms(user)
775 QH_Event("UNIT_QUEST_LOG_CHANGED", UpdateTrigger)
776 QH_Event("QUEST_LOG_UPDATE", QH_UpdateQuests)
778 -- 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.
779 QH_AddNotifier(GetTime() + 5, function ()
780 local aqw_orig = AddQuestWatch
781 AddQuestWatch = function(...)
782 aqw_orig(...)
783 QH_UpdateQuests(true)
785 local rqw_orig = RemoveQuestWatch
786 RemoveQuestWatch = function(...)
787 rqw_orig(...)
788 QH_UpdateQuests(true)
790 end)
792 local old_playerlist = {}
794 function QH_Questcomm_Sync()
795 if not (not QuestHelper_Pref.solo and QuestHelper_Pref.share) then
796 old_playerlist = {}
797 return
800 local playerlist = {}
801 --[[if GetNumRaidMembers() > 0 then
802 for i = 1, 40 do
803 local liv = UnitName(string.format("raid%d", i))
804 if liv then playerlist[liv] = true end
806 elseif]] if GetNumPartyMembers() > 0 then
807 -- we is in a party
808 for i = 1, 4 do
809 local targ = string.format("party%d", i)
810 local liv = UnitName(targ)
811 if liv and liv ~= UNKNOWNOBJECT and UnitIsConnected(targ) then playerlist[liv] = true end
814 playerlist[UnitName("player")] = nil
816 local additions = {}
817 for k, v in pairs(playerlist) do
818 if not old_playerlist[k] then
819 --print("new player:", k)
820 table.insert(additions, k)
824 local removals = {}
825 for k, v in pairs(old_playerlist) do
826 if not playerlist[k] then
827 --print("lost player:", k)
828 table.insert(removals, k)
832 old_playerlist = playerlist
834 for _, v in ipairs(removals) do
835 QH_DumpCommUser(v)
838 if #additions == 0 then return end
840 if #additions == 1 then
841 SAM("syn:2", "WHISPER", additions[1])
842 else
843 SAM("syn:2", "PARTY")
847 local aku = {}
849 local newer_reported = false
850 local older_reported = false
851 function QH_Questcomm_Msg(data, from)
852 if data:match("syn:0") then
853 QH_DumpCommUser(from)
854 return
856 if QuestHelper_Pref.solo then return end
858 --print("received", from, data)
860 local cont = true
862 local key, value = data:match("(.):(.*)")
863 if key == "v" then
864 aku[from] = value
865 elseif key == "x" then
866 if aku[from] then
867 aku[from] = aku[from] .. value
869 elseif key == "X" then
870 if aku[from] then
871 aku[from] = aku[from] .. value
872 data = aku[from]
873 aku[from] = nil
874 cont = true
876 else
877 cont = true
880 if not cont then return end
883 --print("packet received", from, data)
884 if data:match("syn:.*") then
885 local synv = data:match("syn:([0-9]*)")
886 if synv then synv = tonumber(synv) end
887 if synv and synv ~= 2 then
888 if synv > 2 and not newer_reported then
889 QuestHelper:TextOut(QHFormat("PEER_NEWER", from))
890 newer_reported = true
891 elseif synv < 2 and not older_reported then
892 QuestHelper:TextOut(QHFormat("PEER_OLDER", from))
893 older_reported = true
897 if synv and synv >= 2 then
898 SAM("hello:2", "WHISPER", from)
900 elseif data == "hello:2" or data == "retrieve:2" then
901 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
903 for k, v in pairs(current_chunks) do
904 SAM(v, "WHISPER", from)
906 else
907 if old_playerlist[from] then
908 QH_InsertCommPacket(from, data)
913 function QuestHelper:SetShare(flag)
914 if flag then
915 QH_Questcomm_Sync()
916 else
917 SAM("syn:0", "PARTY")
918 local cpb = comm_packets
919 comm_packets = {}
920 for k in pairs(cpb) do RefreshUserComms(k) end