1 QuestHelper_File
["director_quest.lua"] = "Development Version"
2 QuestHelper_Loadtime
["director_quest.lua"] = GetTime()
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
)
16 for _
, v
in ipairs(tab
) do
22 local function copy_without_last(tab
)
24 for _
, v
in ipairs(tab
) do
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
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})
51 for _
, v
in ipairs(source
) do
52 local dbgi
= DB_GetItem(v
.sourcetype
, v
.sourceid
, nil, true)
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"))
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"))
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
)))
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
)
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)
92 QuestHelper
: TextOut("Missing lbcount, guessing wildly")
93 if q
and q
.criteria
then
95 for k
, v
in ipairs(q
.criteria
) do
96 lbcount
= math
.max(lbcount
, k
)
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
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
117 --QuestHelper:TextOut(string.format("critty %d %d", k, c.loc and #c.loc or -1))
121 if q
and q
.criteria
and q
.criteria
[i
] then AppendObjlinks(ttx
, q
.criteria
[i
], ttx
.tooltip
) end
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
)
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
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
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
})
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
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$"
177 local function UpdateTrigger()
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.
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
)
199 -- well, that didn't work
200 target
, have
, need
= string.match(txt
, "^%s*(.-)%s*:%s*(.-)%s*/%s*(.-)%s*$")
202 --QuestHelper:TextOut(string.format("%s rebecomes %s/%s/%s", tostring(title), tostring(target), tostring(have), tostring(need)))
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
242 elseif plevel
>= 40 then
243 grayd
= plevel
/ 5 + 1
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
265 return string.format("|cffff0000%s: %s/%s", target
, have
, need
)
269 local function Clicky(index
)
270 ShowUIPanel(QuestLogFrame
)
271 QuestLog_SetSelection(index
)
276 name
= "director_quest_unknown_objective",
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
294 -- This begins the main update loop that loops through all of the quests
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)
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?
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)
337 nactive
[turnin
] = true
338 if not active
[turnin
] then
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
354 local desc
, typ
, done
= GetQuestLogLeaderBoard(i
, index
)
355 local pt
, pd
, have
, need
= objective_parse(typ
, desc
, done
)
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
)
362 dline
= QHFormat("OBJECTIVE_REASON_FALLBACK", pd
, title
)
365 if not db
[i
].progress
then
369 if type(have
) == "number" and type(need
) == "number" then
370 db
[i
].progress
[UnitName("player")] = {have
, need
, have
/ need
}
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
)
380 v
.tracker_clicked
= function () Clicky(lindex
) end
382 v
.progress
= db
[i
].progress
385 v
.map_desc
= copy(v
.path_desc
)
386 v
.map_desc
[1] = 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
409 local timer
= GetQuestIndexForTimer(timidx
)
410 if not timer
then break end
411 if timer
== index
then
412 QH_Route_SetClusterPriority(turnin
, -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
)
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(...)
450 QH_UpdateQuests(true)
452 local rqw_orig
= RemoveQuestWatch
453 RemoveQuestWatch
= function(...)
455 QH_UpdateQuests(true)