fix that li'l spacing issue
[QuestHelper.git] / director_quest.lua
blob64b57b107b918bf61822b2e2049b7c7ad34691ec
1 QuestHelper_File["director_quest.lua"] = "Development Version"
2 QuestHelper_Loadtime["director_quest.lua"] = GetTime()
4 --[[
6 Little bit of explanation here.
8 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.
10 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.
14 local function copy(tab)
15 local tt = {}
16 for _, v in ipairs(tab) do
17 table.insert(tt, v)
18 end
19 return tt
20 end
22 local function copy_without_last(tab)
23 local tt = {}
24 for _, v in ipairs(tab) do
25 table.insert(tt, v)
26 end
27 table.remove(tt)
28 return tt
29 end
31 local function AppendObjlinks(target, source, tooltips, icon, last_name, map_lines, tooltip_lines, seen)
32 if not seen then seen = {} end
33 if not map_lines then map_lines = {} end
34 if not tooltip_lines then tooltip_lines = {} end
36 QuestHelper: Assert(not seen[source])
38 if seen[source] then return end
40 seen[source] = true
41 if source.loc then
42 for m, v in ipairs(source.loc) do
43 QuestHelper: Assert(#source == 0)
45 QuestHelper: Assert(target)
46 QuestHelper: Assert(QuestHelper_IndexLookup)
47 QuestHelper: Assert(QuestHelper_IndexLookup[v.rc], v.rc)
48 table.insert(target, {loc = {x = v.x, y = v.y, c = v.c, p = QuestHelper_IndexLookup[v.rc][v.rz]}, path_desc = copy(map_lines), icon_id = icon or 6})
49 end
50 else
51 for _, v in ipairs(source) do
52 local dbgi = DB_GetItem(v.sourcetype, v.sourceid, nil, true)
53 local licon
55 if v.sourcetype == "monster" then
56 table.insert(map_lines, QHFormat("OBJECTIVE_SLAY", dbgi.name or QHText("OBJECTIVE_UNKNOWN_MONSTER")))
57 table.insert(tooltip_lines, 1, QHFormat("TOOLTIP_SLAY", source.name or "nothing"))
58 licon = 1
59 elseif v.sourcetype == "item" then
60 table.insert(map_lines, QHFormat("OBJECTIVE_ACQUIRE", dbgi.name or QHText("OBJECTIVE_ITEM_UNKNOWN")))
61 table.insert(tooltip_lines, 1, QHFormat("TOOLTIP_LOOT", source.name or "nothing"))
62 licon = 2
63 else
64 table.insert(map_lines, string.format("unknown %s (%s/%s)", tostring(dbgi.name), tostring(v.sourcetype), tostring(v.sourceid)))
65 table.insert(tooltip_lines, 1, string.format("unknown %s (%s/%s)", tostring(last_name), tostring(v.sourcetype), tostring(v.sourceid)))
66 licon = 3
67 end
69 tooltips[string.format("%s@@%s", v.sourcetype, v.sourceid)] = copy_without_last(tooltip_lines)
71 AppendObjlinks(target, dbgi, tooltips, icon or licon, source.name, map_lines, tooltip_lines, seen)
72 table.remove(tooltip_lines, 1)
73 table.remove(map_lines)
75 DB_ReleaseItem(dbgi)
76 end
77 end
78 seen[source] = false
79 end
82 local quest_list = {}
83 local quest_list_used = {}
85 local QuestCriteriaWarningBroadcast
87 local function GetQuestMetaobjective(questid, lbcount)
88 if not quest_list[questid] then
89 local q = DB_GetItem("quest", questid, true, true)
91 if not lbcount then
92 QuestHelper: TextOut("Missing lbcount, guessing wildly")
93 if q and q.criteria then
94 lbcount = 0
95 for k, v in ipairs(q.criteria) do
96 lbcount = math.max(lbcount, k)
97 end
98 else
99 lbcount = 0 -- heh
103 -- just doublechecking here
104 if not QuestCriteriaWarningBroadcast and q and q.criteria then for k, v in pairs(q.criteria) do
105 if type(k) == "number" and k > lbcount then
106 QuestHelper:TextOut(string.format("Too many stored objectives for this quest, please report on the Questhelper homepage (%s %s %s)", questid, lbcount, k))
107 QuestHelper_ErrorCatcher_ExplicitError(false, string.format("Too many stored objectives (%s %s %s)", questid, lbcount, k))
108 QuestCriteriaWarningBroadcast = true
110 end end
112 ite = {type_quest = {}} -- we don't want to mutate the existing quest data
113 ite.desc = string.format("Quest %s", q and q.name or "(unknown)") -- this gets changed later anyway
115 for i = 1, lbcount do
116 local ttx = {}
117 --QuestHelper:TextOut(string.format("critty %d %d", k, c.loc and #c.loc or -1))
119 ttx.tooltip = {}
121 if q and q.criteria and q.criteria[i] then AppendObjlinks(ttx, q.criteria[i], ttx.tooltip) end
123 if #ttx == 0 then
124 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
127 for idx, v in ipairs(ttx) do
128 v.desc = string.format("Criteria %d", i)
129 v.why = ite
130 v.cluster = ttx
131 v.type_quest = ite.type_quest
134 for k, v in pairs(ttx.tooltip) do
135 ttx.tooltip[k] = {ttx.tooltip[k], ttx} -- we're gonna be handing out this table to other modules, so this isn't as dumb as it looks
138 ite[i] = ttx
142 local ttx = {}
143 --QuestHelper:TextOut(string.format("finny %d", q.finish.loc and #q.finish.loc or -1))
144 if q and q.finish and q.finish.loc then for m, v in ipairs(q.finish.loc) do
145 --print(v.rc, v.rz)
146 --print(QuestHelper_IndexLookup[v.rc])
147 --print(QuestHelper_IndexLookup[v.rc][v.rz])
148 table.insert(ttx, {desc = "Turn in quest", why = ite, loc = {x = v.x, y = v.y, c = v.c, p = QuestHelper_IndexLookup[v.rc][v.rz]}, tracker_hidden = true, cluster = ttx, icon_id = 7, type_quest = ite.type_quest})
149 end end
151 if #ttx == 0 then
152 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
155 ite.finish = ttx
158 quest_list[questid] = ite
160 if q then DB_ReleaseItem(q) end
163 quest_list_used[questid] = quest_list[questid]
164 return quest_list[questid]
168 local function GetQuestType(link)
169 return tonumber(string.match(link,
170 "^|cff%x%x%x%x%x%x|Hquest:(%d+):[%d-]+|h%[[^%]]*%]|h|r$"
171 )), tonumber(string.match(link,
172 "^|cff%x%x%x%x%x%x|Hquest:%d+:([%d-]+)|h%[[^%]]*%]|h|r$"
176 local update = true
177 local function UpdateTrigger()
178 update = true
181 local active = {}
183 -- 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.
184 local active_db = {}
186 local objective_parse_table = {
187 item = function (txt) return QuestHelper:convertPattern(QUEST_OBJECTS_FOUND)(txt) end,
188 object = function (txt) return QuestHelper:convertPattern(QUEST_OBJECTS_FOUND)(txt) end, -- why does this even exist
189 monster = function (txt) return QuestHelper:convertPattern(QUEST_MONSTERS_KILLED)(txt) end,
190 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.
191 reputation = function (txt) return QuestHelper:convertPattern(QUEST_FACTION_NEEDED)(txt) end, -- :ughh:
192 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.
195 local function objective_parse(typ, txt, done)
196 local pt, target, have, need = typ, objective_parse_table[typ](txt, done)
198 if not target then
199 -- well, that didn't work
200 target, have, need = string.match(txt, "^%s*(.-)%s*:%s*(.-)%s*/%s*(.-)%s*$")
201 pt = "fallback"
202 --QuestHelper:TextOut(string.format("%s rebecomes %s/%s/%s", tostring(title), tostring(target), tostring(have), tostring(need)))
205 if not target then
206 target, have, need = string.match(txt, "^%s*(.-)%s*$"), (done and 1 or 0), 1
207 --QuestHelper:TextOut(string.format("%s rerebecomes %s/%s/%s", tostring(title), tostring(target), tostring(have), tostring(need)))
210 QuestHelper: Assert(target) -- This will fail repeatedly. Come on. We all know it.
211 QuestHelper: Assert(have)
212 QuestHelper: Assert(need) -- As will these.
214 if tonumber(have) then have = tonumber(have) end
215 if tonumber(need) then need = tonumber(need) end
217 return pt, target, have, need
220 local function clamp(v)
221 if v < 0 then return 0 elseif v > 255 then return 255 else return v end
224 local function colorlerp(position, r1, g1, b1, r2, g2, b2)
225 local antip = 1 - position
226 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))
229 -- We're just gonna do the same thing QH originally did - red->yellow->green.
230 local function difficulty_color(position)
231 if position < 0 then position = 0 end
232 if position > 1 then position = 1 end
233 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)
236 local function MakeQuestTitle(title, level)
237 local plevel = UnitLevel("player") -- meh, should probably cache this, buuuuut
238 local grayd
240 if plevel >= 60 then
241 grayd = 9
242 elseif plevel >= 40 then
243 grayd = plevel / 5 + 1
244 else
245 grayd = plevel / 10 + 5
248 local isgray = (plevel - floor(grayd) >= level)
250 return string.format("%s[%d] %s", isgray and "|cffb0b0b0" or difficulty_color(1 - ((level - plevel) / grayd + 1) / 2), level, title)
253 local function MakeQuestObjectiveTitle(title, typ, done)
254 local _, target, have, need = objective_parse(typ, title, done)
256 local nhave, nneed = tonumber(have), tonumber(need)
257 if nhave and nneed then
258 have, need = nhave, nneed
260 local ccode = difficulty_color(have / need)
262 if need > 1 then target = string.format("%s: %d/%d", target, have, need) end
263 return ccode .. target
264 else
265 return string.format("|cffff0000%s: %s/%s", target, have, need)
269 local function Clicky(index)
270 ShowUIPanel(QuestLogFrame)
271 QuestLog_SetSelection(index)
272 QuestLog_Update()
275 local dontknow = {
276 name = "director_quest_unknown_objective",
277 no_exception = true,
278 no_disable = true,
279 friendly_reason = QHText("UNKNOWN_OBJ"),
282 -- Here's the core update function
283 function QH_UpdateQuests(force)
284 if not DB_Ready() then return end
286 if update or force then -- Sometimes (usually) we don't actually update
287 local index = 1
289 local nactive = {}
290 quest_list_used = {}
292 local unknown = {}
294 -- This begins the main update loop that loops through all of the quests
295 while true do
296 local title, level, variety, groupsize, _, _, complete = GetQuestLogTitle(index)
297 if not title then break end
299 title = title:match("%[.*%] (.*)") or title
301 local qlink = GetQuestLink(index)
302 if qlink then -- If we don't have a quest link, it's not really a quest
303 local id = GetQuestType(qlink)
304 if id then -- If we don't have a *valid* quest link, give up
305 local lbcount = GetNumQuestLeaderBoards(index)
306 local db = GetQuestMetaobjective(id, lbcount) -- This generates the above-mentioned metaobjective, including doing the database lookup.
308 if db then -- If we didn't get a database lookup, then we don't have a metaobjective either. Urgh. abort abort abort
310 -- 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! (six conditionals deep)
311 local lindex = index
312 db.desc = title
313 db.tracker_desc = MakeQuestTitle(title, level)
314 db.tracker_clicked = function () Clicky(lindex) end
316 local watched = IsQuestWatched(index)
318 db.type_quest.level = level
319 db.type_quest.done = (complete == 1)
320 db.type_quest.index = index
321 db.type_quest.variety = variety
322 db.type_quest.groupsize = groupsize
323 db.type_quest.title = title
324 db.type_quest.objectives = lbcount
325 QuestHelper: Assert(db.type_quest.index) -- why is this failing?
327 local turnin
328 local turnin_new
330 -- This is our "quest turnin" objective, which is currently being handled separately for no particularly good reason.
331 if db.finish and #db.finish > 0 then
332 for _, v in ipairs(db.finish) do
333 v.map_highlight = (complete == 1)
336 turnin = db.finish
337 nactive[turnin] = true
338 if not active[turnin] then
339 turnin_new = true
340 for k, v in ipairs(turnin) do
341 v.tracker_clicked = function () Clicky(lindex) end
343 v.map_desc = {QHFormat("OBJECTIVE_REASON_TURNIN", title)}
345 QH_Route_ClusterAdd(db.finish)
347 QH_Tracker_SetPin(db.finish[1], watched)
348 if db.finish[1].type_quest_unknown then table.insert(unknown, db.finish) end
351 -- These are the individual criteria of the quest. Remember that each criteria can be represented by multiple routing objectives.
352 for i = 1, lbcount do
353 if db[i] then
354 local desc, typ, done = GetQuestLogLeaderBoard(i, index)
355 local pt, pd, have, need = objective_parse(typ, desc, done)
356 local dline
357 if pt == "item" or pt == "object" then
358 dline = QHFormat("OBJECTIVE_REASON", QHText("ACQUIRE_VERB"), pd, title)
359 elseif pt == "monster" then
360 dline = QHFormat("OBJECTIVE_REASON", QHText("SLAY_VERB"), pd, title)
361 else
362 dline = QHFormat("OBJECTIVE_REASON_FALLBACK", pd, title)
365 if not db[i].progress then
366 db[i].progress = {}
369 if type(have) == "number" and type(need) == "number" then
370 db[i].progress[UnitName("player")] = {have, need, have / need}
371 else
372 db[i].progress[UnitName("player")] = {have, need, 0} -- it's only used for the coloring anyway
375 db[i].desc = QHFormat("TOOLTIP_QUEST", title)
377 for k, v in ipairs(db[i]) do
378 v.tracker_desc = MakeQuestObjectiveTitle(desc, typ, done)
379 v.desc = desc
380 v.tracker_clicked = function () Clicky(lindex) end
382 v.progress = db[i].progress
384 if v.path_desc then
385 v.map_desc = copy(v.path_desc)
386 v.map_desc[1] = dline
387 else
388 v.map_desc = {dline}
392 -- This is the snatch of code that actually adds it to routing.
393 if not done and #db[i] > 0 then
394 nactive[db[i]] = true
395 if not active[db[i]] then
396 QH_Route_ClusterAdd(db[i])
397 if db[i].tooltip then QH_Tooltip_Add(db[i].tooltip) end
398 if turnin then QH_Route_ClusterRequires(turnin, db[i]) end
400 QH_Tracker_SetPin(db[i][1], watched)
401 if db[i][1].type_quest_unknown then table.insert(unknown, db[i]) end
406 if turnin_new then
407 local timidx = 1
408 while true do
409 local timer = GetQuestIndexForTimer(timidx)
410 if not timer then break end
411 if timer == index then
412 QH_Route_SetClusterPriority(turnin, -1)
413 break
415 timidx = timidx + 1
421 index = index + 1
424 for _, v in ipairs(unknown) do
425 QH_Route_IgnoreCluster(v, dontknow)
428 for k, v in pairs(active) do
429 if not nactive[k] then
430 if k.tooltip then QH_Tooltip_Remove(k.tooltip) end
431 QH_Tracker_Unpin(k[1])
432 QH_Route_ClusterRemove(k)
436 active = nactive
438 quest_list = quest_list_used
442 QuestHelper.EventHookRegistrar("UNIT_QUEST_LOG_CHANGED", UpdateTrigger)
443 QuestHelper.EventHookRegistrar("QUEST_LOG_UPDATE", QH_UpdateQuests)
445 -- 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.
446 QH_AddNotifier(GetTime() + 5, function ()
447 local aqw_orig = AddQuestWatch
448 AddQuestWatch = function(...)
449 aqw_orig(...)
450 QH_UpdateQuests(true)
452 local rqw_orig = RemoveQuestWatch
453 RemoveQuestWatch = function(...)
454 rqw_orig(...)
455 QH_UpdateQuests(true)
457 end)