arrrgh
[QuestHelper.git] / objective.lua
blobc65a12b400ad04d20fd1a6165e14bc741ae09c5c
1 QuestHelper_File["objective.lua"] = "Development Version"
3 local function ObjectiveCouldBeFirst(self)
4 if (self.user_ignore == nil and self.auto_ignore) or self.user_ignore then
5 return false
6 end
8 for i, j in pairs(self.after) do
9 if i.watched then
10 return false
11 end
12 end
14 return true
15 end
17 local function DefaultObjectiveKnown(self)
18 if self.user_ignore == nil then
19 if (self.filter_zone and QuestHelper_Pref.filter_zone) or
20 (self.filter_done and QuestHelper_Pref.filter_done) or
21 (self.filter_level and QuestHelper_Pref.filter_level) or
22 (self.filter_blocked and QuestHelper_Pref.filter_blocked) or
23 (self.filter_watched and QuestHelper_Pref.filter_watched) then
24 return false
25 end
26 elseif self.user_ignore then
27 return false
28 end
30 for i, j in pairs(self.after) do
31 if i.watched and not i:Known() then -- Need to know how to do everything before this objective.
32 return false
33 end
34 end
36 return true
37 end
39 local function ObjectiveReason(self, short)
40 local reason, rc = nil, 0
41 if self.reasons then
42 for r, c in pairs(self.reasons) do
43 if not reason or c > rc or (c == rc and r > reason) then
44 reason, rc = r, c
45 end
46 end
47 end
49 if not reason then reason = "Do some extremely secret unspecified something." end
51 if not short and self.pos and self.pos[6] then
52 reason = reason .. "\n" .. self.pos[6]
53 end
55 return reason
56 end
58 local function Uses(self, obj, text)
59 if self == obj then return end -- You cannot use yourself. A purse is not food.
60 local uses, used = self.uses, obj.used
62 if not uses then
63 uses = QuestHelper:CreateTable("uses")
64 self.uses = uses
65 end
67 if not used then
68 used = QuestHelper:CreateTable("used")
69 obj.used = used
70 end
72 if not uses[obj] then
73 uses[obj] = true
74 used[self] = text
75 obj:MarkUsed()
76 end
77 end
79 local function DoMarkUsed(self)
80 -- Objectives should call 'self:Uses(objective, text)' to mark objectives they use by don't directly depend on.
81 -- This information is used in tooltips.
82 -- text is passed to QHFormat with the name of the objective being used.
83 end
85 local function MarkUsed(self)
86 if not self.marked_used then
87 self.marked_used = 1
88 self:DoMarkUsed()
89 else
90 self.marked_used = self.marked_used + 1
91 end
92 end
94 local function MarkUnused(self)
95 assert(self.marked_used)
97 if self.marked_used == 1 then
98 local uses = self.uses
100 if uses then
101 for obj in pairs(uses) do
102 obj.used[self] = nil
103 obj:MarkUnused()
106 QuestHelper:ReleaseTable(uses)
107 self.uses = nil
110 if self.used then
111 assert(not next(self.used))
112 QuestHelper:ReleaseTable(self.used)
113 self.used = nil
116 self.marked_used = nil
117 else
118 self.marked_used = self.marked_used - 1
122 local function DummyObjectiveKnown(self)
123 return (self.o.pos or self.fb.pos) and DefaultObjectiveKnown(self)
126 local function ItemKnown(self)
127 if not DefaultObjectiveKnown(self) then return false end
129 if self.o.vendor then
130 for i, npc in ipairs(self.o.vendor) do
131 local n = self.qh:GetObjective("monster", npc)
132 local faction = n.o.faction or n.fb.faction
133 if (not faction or faction == self.qh.faction) and n:Known() then
134 return true
139 if self.fb.vendor then
140 for i, npc in ipairs(self.fb.vendor) do
141 local n = self.qh:GetObjective("monster", npc)
142 local faction = n.o.faction or n.fb.faction
143 if (not faction or faction == self.qh.faction) and n:Known() then
144 return true
149 if self.o.pos or self.fb.pos then
150 return true
153 if self.o.drop then for monster in pairs(self.o.drop) do
154 if self.qh:GetObjective("monster", monster):Known() then
155 return true
157 end end
159 if self.fb.drop then for monster in pairs(self.fb.drop) do
160 if self.qh:GetObjective("monster", monster):Known() then
161 return true
163 end end
165 if self.o.contained then for item in pairs(self.o.contained) do
166 if self.qh:GetObjective("item", item):Known() then
167 return true
169 end end
171 if self.fb.contained then for item in pairs(self.fb.contained) do
172 if self.qh:GetObjective("item", item):Known() then
173 return true
175 end end
177 if self.quest then
178 local item=self.quest.o.item
179 item = item and item[self.obj]
181 if item then
182 if item.pos then
183 return true
185 if item.drop then
186 for monster in pairs(item.drop) do
187 if self.qh:GetObjective("monster", monster):Known() then
188 return true
194 item=self.quest.fb.item
195 item = item and item[self.obj]
196 if item then
197 if item.pos then
198 return true
200 if item.drop then
201 for monster in pairs(item.drop) do
202 if self.qh:GetObjective("monster", monster):Known() then
203 return true
210 return false
213 local function ObjectiveAppendPositions(self, objective, weight, why, restrict)
214 local high = 0
216 if self.o.pos then for i, p in ipairs(self.o.pos) do
217 high = math.max(high, p[4])
218 end end
220 if self.fb.pos then for i, p in ipairs(self.fb.pos) do
221 high = math.max(high, p[4])
222 end end
224 high = weight/high
226 if self.o.pos then for i, p in ipairs(self.o.pos) do
227 if not restrict or not self.qh:Disallowed(p[1]) then
228 objective:AddLoc(p[1], p[2], p[3], p[4]*high, why)
230 end end
232 if self.fb.pos then for i, p in ipairs(self.fb.pos) do
233 if not restrict or not self.qh:Disallowed(p[1]) then
234 objective:AddLoc(p[1], p[2], p[3], p[4]*high, why)
236 end end
240 local function ObjectivePrepareRouting(self, anywhere)
241 self.setup_count = self.setup_count + 1
242 if not self.setup then
243 assert(not self.d)
244 assert(not self.p)
245 assert(not self.nm)
246 assert(not self.nm2)
247 assert(not self.nl)
249 self.d = QuestHelper:CreateTable("objective.d")
250 self.p = QuestHelper:CreateTable("objective.p")
251 self.nm = QuestHelper:CreateTable("objective.nm")
252 self.nm2 = QuestHelper:CreateTable("objective.nm2")
253 self.nl = QuestHelper:CreateTable("objective.nl")
254 self.distance_cache = QuestHelper:CreateTable("objective.distance_cache")
256 if not anywhere then
257 self:AppendPositions(self, 1, nil, true)
259 if not next(self.p) then
260 QuestHelper:TextOut(QHFormat("INACCESSIBLE_OBJ", self.obj or "whatever it was you just requested"))
261 anywhere = true
265 if anywhere then
266 self:AppendPositions(self, 1, nil, false)
269 self:FinishAddLoc(args)
273 local function ItemAppendPositions(self, objective, weight, why, restrict)
274 why2 = why and why.."\n" or ""
276 if self.o.vendor then for i, npc in ipairs(self.o.vendor) do
277 local n = self.qh:GetObjective("monster", npc)
278 local faction = n.o.faction or n.fb.faction
279 if (not faction or faction == self.qh.faction) then
280 n:AppendPositions(objective, 1, why2..QHFormat("OBJECTIVE_PURCHASE", npc), restrict)
282 end end
284 if self.fb.vendor then for i, npc in ipairs(self.fb.vendor) do
285 local n = self.qh:GetObjective("monster", npc)
286 local faction = n.o.faction or n.fb.faction
287 if (not faction or faction == self.qh.faction) then
288 n:AppendPositions(objective, 1, why2..QHFormat("OBJECTIVE_PURCHASE", npc), restrict)
290 end end
292 if next(objective.p, nil) then
293 -- If we have points from vendors, then always use vendors. I don't want it telling you to killing the
294 -- towns people just because you had to talk to them anyway, and it saves walking to the store.
295 return
298 if self.o.drop then for monster, count in pairs(self.o.drop) do
299 local m = self.qh:GetObjective("monster", monster)
300 m:AppendPositions(objective, m.o.looted and count/m.o.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster), restrict)
301 end end
303 if self.fb.drop then for monster, count in pairs(self.fb.drop) do
304 local m = self.qh:GetObjective("monster", monster)
305 m:AppendPositions(objective, m.fb.looted and count/m.fb.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster), restrict)
306 end end
308 if self.o.contained then for item, count in pairs(self.o.contained) do
309 local i = self.qh:GetObjective("item", item)
310 i:AppendPositions(objective, i.o.opened and count/i.o.opened or 1, why2..QHFormat("OBJECTIVE_LOOT", item), restrict)
311 end end
313 if self.fb.contained then for item, count in pairs(self.fb.contained) do
314 local i = self.qh:GetObjective("item", item)
315 i:AppendPositions(objective, i.fb.opened and count/i.fb.opened or 1, why2..QHFormat("OBJECTIVE_LOOT", item), restrict)
316 end end
318 if self.o.pos then for i, p in ipairs(self.o.pos) do
319 if not restrict or not self.qh:Disallowed(p[1]) then
320 objective:AddLoc(p[1], p[2], p[3], p[4], why)
322 end end
324 if self.fb.pos then for i, p in ipairs(self.fb.pos) do
325 if not restrict or not self.qh:Disallowed(p[1]) then
326 objective:AddLoc(p[1], p[2], p[3], p[4], why)
328 end end
330 if self.quest then
331 local item_list=self.quest.o.item
332 if item_list then
333 local data = item_list[self.obj]
334 if data and data.drop then
335 for monster, count in pairs(data.drop) do
336 local m = self.qh:GetObjective("monster", monster)
337 m:AppendPositions(objective, m.o.looted and count/m.o.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster), restrict)
339 elseif data and data.pos then
340 for i, p in ipairs(data.pos) do
341 if not restrict or not self.qh:Disallowed(p[1]) then
342 objective:AddLoc(p[1], p[2], p[3], p[4], why)
348 item_list=self.quest.fb.item
349 if item_list then
350 local data = item_list[self.obj]
351 if data and data.drop then
352 for monster, count in pairs(data.drop) do
353 local m = self.qh:GetObjective("monster", monster)
354 m:AppendPositions(objective, m.fb.looted and count/m.fb.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster), restrict)
356 elseif data and data.pos then
357 for i, p in ipairs(data.pos) do
358 if not restrict or not self.qh:Disallowed(p[1]) then
359 objective:AddLoc(p[1], p[2], p[3], p[4], why)
367 local function ItemDoMarkUsed(self)
368 if self.o.vendor then for i, npc in ipairs(self.o.vendor) do
369 local n = self.qh:GetObjective("monster", npc)
370 local faction = n.o.faction or n.fb.faction
371 if (not faction or faction == self.qh.faction) then
372 self:Uses(n, "TOOLTIP_PURCHASE")
374 end end
376 if self.fb.vendor then for i, npc in ipairs(self.fb.vendor) do
377 local n = self.qh:GetObjective("monster", npc)
378 local faction = n.o.faction or n.fb.faction
379 if (not faction or faction == self.qh.faction) then
380 self:Uses(n, "TOOLTIP_PURCHASE")
382 end end
384 if self.o.drop then for monster, count in pairs(self.o.drop) do
385 self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
386 end end
388 if self.fb.drop then for monster, count in pairs(self.fb.drop) do
389 self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
390 end end
392 if self.o.contained then for item, count in pairs(self.o.contained) do
393 self:Uses(self.qh:GetObjective("item", item), "TOOLTIP_LOOT")
394 end end
396 if self.fb.contained then for item, count in pairs(self.fb.contained) do
397 self:Uses(self.qh:GetObjective("item", item), "TOOLTIP_LOOT")
398 end end
400 if self.quest then
401 local item_list=self.quest.o.item
402 if item_list then
403 local data = item_list[self.obj]
404 if data and data.drop then
405 for monster, count in pairs(data.drop) do
406 self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
411 item_list=self.quest.fb.item
412 if item_list then
413 local data = item_list[self.obj]
414 if data and data.drop then
415 for monster, count in pairs(data.drop) do
416 self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
432 ---------------
446 local function AddLoc(self, index, x, y, w, why)
447 assert(not self.setup)
449 if w > 0 then
450 local pair = QuestHelper_ZoneLookup[index]
451 if not pair then return end -- that zone doesn't exist! We require more vespene gas. Not enough rage!
452 local c, z = pair[1], pair[2]
453 x, y = self.qh.Astrolabe:TranslateWorldMapPosition(c, z, x, y, c, 0)
455 x = x * self.qh.continent_scales_x[c]
456 y = y * self.qh.continent_scales_y[c]
457 local list = self.qh.zone_nodes[index]
459 local points = self.p[list]
460 if not points then
461 points = QuestHelper:CreateTable("objective.p[zone] (objective nodes per-zone)")
462 self.p[list] = points
465 for i, p in pairs(points) do
466 local u, v = x-p[3], y-p[4]
467 if u*u+v*v < 25 then -- Combine points within a threshold of 5 seconds travel time.
468 p[3] = (p[3]*p[5]+x*w)/(p[5]+w)
469 p[4] = (p[4]*p[5]+y*w)/(p[5]+w)
470 p[5] = p[5]+w
471 if w > p[7] then
472 p[6], p[7] = why, w
474 return
478 local new = QuestHelper:CreateTable("objective.p[zone] (possible objective node)")
479 new[1], new[2], new[3], new[4], new[5], new[6], new[7] = list, nil, x, y, w, why, w
480 table.insert(points, new)
484 local function FinishAddLoc(self, args)
485 local mx = 0
487 for z, pl in pairs(self.p) do
488 for i, p in ipairs(pl) do
489 if p[5] > mx then
490 self.location = p
491 mx = p[5]
496 if not self.zones then
497 -- Not using CreateTable, because it will not be released when routing is complete.
498 self.zones = {}
499 else
500 -- We could remove the already known zones, but I'm operating under the assumtion that locations will only be added,
501 -- not removed, so this isn't necessary.
504 -- Remove probably useless locations.
505 for z, pl in pairs(self.p) do
506 local remove_zone = true
507 local i = 1
508 while i <= #pl do
509 if pl[i][5] < mx*0.2 then
510 QuestHelper:ReleaseTable(pl[i])
511 table.remove(pl, i)
512 else
513 remove_zone = false
514 i = i + 1
517 if remove_zone then
518 QuestHelper:ReleaseTable(self.p[z])
519 self.p[z] = nil
520 else
521 self.zones[z.i] = true
525 local node_map = self.nm
526 local node_list = self.nl
528 for list, pl in pairs(self.p) do
529 local dist = self.d[list]
531 assert(not dist)
533 if not dist then
534 dist = QuestHelper:CreateTable("self.d[list]")
535 self.d[list] = dist
538 for i, point in ipairs(pl) do
539 point[5] = mx/point[5] -- Will become 1 for the most desired location, and become larger and larger for less desireable locations.
541 point[2] = QuestHelper:CreateTable("possible objective node to zone edge cache")
543 for i, node in ipairs(list) do
544 local u, v = point[3]-node.x, point[4]-node.y
545 local d = math.sqrt(u*u+v*v)
547 point[2][i] = d
549 if dist[i] then
550 if d*point[5] < dist[i][1]*dist[i][2] then
551 dist[i][1], dist[i][2] = d, point[5]
552 node_map[node] = point
554 else
555 local pair = QuestHelper:CreateTable()
556 pair[1], pair[2] = d, point[5]
557 dist[i] = pair
559 if not node_map[node] then
560 table.insert(node_list, node)
561 node_map[node] = point
562 else
563 u, v = node_map[node][3]-node.x, node_map[node][4]-node.y
565 if dist[i][1]*dist[i][2] < math.sqrt(u*u+v*v)*node_map[node][5] then
566 node_map[node] = point
574 -- Disabled because we're having some data sanity issues. This should be solved at buildtime, but I'm leery of mucking with the build system right now, so it isn't. Re-enable later.
575 --if not args or not args.failable then
576 -- if #node_list == 0 and QuestHelper:IsWrath() then QuestHelper:Error(self.cat.."/"..self.obj..": zero nodes!") end
577 --end
579 assert(not self.setup)
580 self.setup = true
581 table.insert(self.qh.prepared_objectives, self)
584 local function GetPosition(self)
585 assert(self.setup)
587 return self.location
590 local QH_TESTCACHE = nil -- make this "true" or something if you want to test caching (i.e. recalculate everything, then verify that the cache is valid)
592 -- Note: Pos is the starting point, the objective is the destination. These are different data formats - "self" can be a set of points.
593 -- More annotation here, if you're trying to learn the codebase. This function is a more complicated version of QH:ComputeTravelTime, so refer to that for information first before reading this one.
594 local function ObjectiveTravelTime(self, pos, nocache)
595 assert(self.setup)
597 -- The caching is pretty obvious.
598 local key, cached
599 if not nocache then
600 assert(pos ~= QuestHelper.pos)
601 if not pos.key then
602 pos.key = math.random()..""
604 key = pos.key
605 cached = self.distance_cache[key]
606 if cached then
607 if not QH_TESTCACHE then
608 return unpack(cached)
613 local graph = self.qh.world_graph
614 local nl = self.nl
616 graph:PrepareSearch()
618 -- This is quite similar to the same "create nodes for all zone links" in ComputeTravelTime except that it's creating nodes for all zone links for a set of possible destinations. I'm not sure if the weighting is backwards. It might be.
619 for z, l in pairs(self.d) do
620 for i, n in ipairs(z) do
621 if n.s == 0 then
622 n.e, n.w = unpack(l[i])
623 n.s = 3
624 elseif n.e * n.w < l[i][1]*l[i][2] then
625 n.e, n.w = unpack(l[i])
630 local d = pos[2]
631 for i, n in ipairs(pos[1]) do
632 graph:AddStartNode(n, d[i], nl)
635 local e = graph:DoSearch(nl)
637 -- d changes datatype here. I hate this codebase. Hell, e probably changes datatype also! yaaaay. what does .nm mean? what does .d mean?
638 d = e.g+e.e
639 e = self.nm[e]
641 -- There's something going on with weighting here that I don't understand
642 local l = self.p[pos[1]]
643 if l then
644 local x, y = pos[3], pos[4]
645 local score = d*e[5]
647 for i, n in ipairs(l) do
648 local u, v = x-n[3], y-n[4]
649 local d2 = math.sqrt(u*u+v*v)
650 local s = d2*n[5]
651 if s < score then
652 d, e, score = d2, n, s
657 assert(e)
658 if not nocache then
659 assert( not cached or (cached[1] == d and cached[2] == e))
660 if not QH_TESTCACHE or not cached then
661 local new = self.qh:CreateTable()
662 new[1], new[2] = d, e
663 self.distance_cache[key] = new
664 self.qh:CacheRegister(self)
666 else
667 if self.distance_cache and self.distance_cache[key] then
668 assert(self.distance_cache[key][1] == d)
672 return d, e
675 -- Note: pos1 is the starting point, pos2 is the ending point, the objective is somewhere between them.
676 -- Yet more annotation! This one is based off ObjectiveTravelTime. Yes, it's nasty that there are three (edit: four) functions with basically the same goal. Have I mentioned this codebase kind of sucks?
677 local function ObjectiveTravelTime2(self, pos1, pos2, nocache)
678 assert(self.setup)
680 -- caching is pretty simple as usual
681 local key, cached
682 if not nocache then
683 assert(pos1 ~= QuestHelper.pos)
684 assert(pos2 ~= QuestHelper.pos)
685 -- We don't want to cache distances involving the player's current position, as that would spam the table
686 if not pos1.key then
687 pos1.key = math.random()..""
689 if not pos2.key then
690 pos2.key = math.random()..""
692 key = pos1.key..pos2.key
693 cached = self.distance_cache[key]
694 if cached then
695 if not QH_TESTCACHE then
696 return unpack(cached)
701 local graph = self.qh.world_graph
702 local nl = self.nl
704 -- This is the standard pos1-to-self code that we're used to seeing . . .
705 graph:PrepareSearch()
707 for z, l in pairs(self.d) do
708 for i, n in ipairs(z) do
709 if n.s == 0 then
710 n.e, n.w = unpack(l[i])
711 n.s = 3
712 elseif n.e * n.w < l[i][1]*l[i][2] then
713 n.e, n.w = unpack(l[i])
718 local d = pos1[2]
719 for i, n in ipairs(pos1[1]) do
720 graph:AddStartNode(n, d[i], nl)
723 graph:DoFullSearch(nl)
725 graph:PrepareSearch()
727 -- . . . and here's where it gets wonky
728 -- Now, we need to figure out how long it takes to get to each node.
729 for z, point_list in pairs(self.p) do
730 if z == pos1[1] then
731 -- Will also consider min distance.
732 local x, y = pos1[3], pos1[4]
734 for i, p in ipairs(point_list) do
735 local a, b = p[3]-x, p[4]-y
736 local u, v = p[3], p[4]
737 local d = math.sqrt(a*a+b*b)
738 local w = p[5]
739 local score = d*w
740 for i, n in ipairs(z) do
741 a, b = n.x-u, n.y-v
742 local bleh = math.sqrt(a*a+b*b)+n.g
743 local s = bleh*w
744 if s < score then
745 d, score = bleh, d
748 p[7] = d
750 else
751 for i, p in ipairs(point_list) do
752 local x, y = p[3], p[4]
753 local w = p[5]
754 local d
755 local score
757 for i, n in ipairs(z) do
758 local a, b = n.x-x, n.y-y
759 local d2 = math.sqrt(a*a+b*b)+n.g
760 local s = d2*w
761 if not score or s < score then
762 d, score = d2, s
765 p[7] = d
770 d = pos2[2]
772 for i, n in ipairs(pos2[1]) do
773 n.e = d[i]
774 n.s = 3
777 local el = pos2[1]
778 local nm = self.nm2
780 for z, l in pairs(self.d) do
781 for i, n in ipairs(z) do
782 local x, y = n.x, n.y
783 local bp
784 local bg
785 local bs
786 for i, p in ipairs(self.p[z]) do
787 local a, b = x-p[3], y-p[4]
788 d = p[7]+math.sqrt(a*a+b*b)
789 s = d*p[5]
790 if not bs or s < bs then
791 bg, bp, bs = d, p, s
795 nm[n] = bp
796 -- Using score instead of distance, because we want nodes we're not really interested in to be less likely to get chosen.
797 graph:AddStartNode(n, bs, el)
801 local e = graph:DoSearch(pos2[1])
803 d = nm[e.p][7]
804 local d2 = e.g+e.e-e.p.g+(e.p.g/nm[e.p][5]-nm[e.p][7])
806 e = nm[e.p]
807 local total = (d+d2)*e[5]
809 if self.p[el] then
810 local x, y = pos2[3], pos2[4]
811 for i, p in ipairs(self.p[el]) do
812 local a, b = x-p[3], y-p[4]
813 local c = math.sqrt(a*a+b*b)
814 local t = (p[7]+c)*p[5]
815 if t < total then
816 total, d, d2, e = t, p[7], c, p
821 -- grim stabilization hack, since obviously the numbers it generates are only vaguely based in reality. This should be fixed and removed ASAP (you know, once I figure out WTF this thing is doing)
822 d = QuestHelper:ComputeTravelTime(pos1, e)
823 d2 = QuestHelper:ComputeTravelTime(e, pos2)
825 assert(e)
826 if not nocache then
827 assert( not cached or (cached[1] == d and cached[2] == d2 and cached[3] == e))
828 if not QH_TESTCACHE or not cached then
829 local new = self.qh:CreateTable("ObjectiveTravelTime2 cache")
830 new[1], new[2], new[3] = d, d2, e
831 self.distance_cache[key] = new
832 self.qh:CacheRegister(self)
834 else
835 if self.distance_cache and self.distance_cache[key] then
836 assert(self.distance_cache[key][1] == d and self.distance_cache[key][2] == d2)
840 --[[if pos1 and pos2 then -- Debug code so I can maybe actually fix the problems someday
841 QuestHelper:TextOut("Beginning dumping here")
843 local laxa = QuestHelper:ComputeTravelTime(pos1, e, true)
844 if math.abs(laxa-d) >= 0.0001 then
845 QuestHelper:TextOut(QuestHelper:StringizeTable(pos1))
846 QuestHelper:TextOut(QuestHelper:StringizeRecursive(pos1, 2))
847 QuestHelper:TextOut(QuestHelper:StringizeTable(e))
848 QuestHelper:TextOut(QuestHelper:StringizeTable(e[1]))
849 QuestHelper:TextOut(QuestHelper:StringizeTable(e[2]))
850 QuestHelper:TextOut(QuestHelper:StringizeRecursive(e[1], 2))]]
851 --QuestHelper:Assert(math.abs(laxa-d) < 0.0001, "Compare: "..laxa.." vs "..d) -- wonky commenting is thanks to the de-assert script, fix later
852 --[[end
853 local laxb = QuestHelper:ComputeTravelTime(e, pos2, true)
854 if math.abs(laxb-d2) >= 0.0001 then
855 QuestHelper:TextOut(QuestHelper:StringizeTable(pos2))
856 QuestHelper:TextOut(QuestHelper:StringizeTable(e))
857 QuestHelper:TextOut(QuestHelper:StringizeTable(e[1]))
858 QuestHelper:TextOut(QuestHelper:StringizeTable(e[2]))
859 QuestHelper:TextOut(QuestHelper:StringizeRecursive(e[1], 2))]]
860 --QuestHelper:Assert(math.abs(laxa-d) < 0.0001, "Compare: "..laxb.." vs "..d2)
861 --[[end
862 end]]
864 return d, d2, e
867 local function DoneRouting(self)
868 assert(self.setup_count > 0)
869 assert(self.setup)
871 if self.setup_count == 1 then
872 self.setup_count = 0
873 QuestHelper:ReleaseObjectivePathingInfo(self)
874 for i, obj in ipairs(self.qh.prepared_objectives) do
875 if o == obj then
876 table.remove(self.qh.prepared_objectives, i)
877 break
880 else
881 self.setup_count = self.setup_count - 1
885 local function IsObjectiveWatched(self)
886 -- Check if an objective is being watched. Note that this is an external query, not a simple Selector.
887 local info
889 if self.cat == "quest" then
890 info = QuestHelper.quest_log[self]
891 else
892 info = QuestHelper.quest_log[self.quest]
895 if info then
896 local index = info.index
897 if index then
898 if UberQuest then
899 -- UberQuest has it's own way of tracking quests.
900 local uq_settings = UberQuest_Config[UnitName("player")]
901 if uq_settings then
902 local list = uq_settings.selected
903 if list then
904 return list[GetQuestLogTitle(index)]
907 else
908 return IsQuestWatched(index)
913 return false
917 local next_objective_id = 0
919 local function ObjectiveShare(self)
920 self.want_share = true
923 local function ObjectiveUnshare(self)
924 self.want_share = false
927 QuestHelper.default_objective_param =
929 CouldBeFirst=ObjectiveCouldBeFirst,
931 Uses=Uses,
932 DoMarkUsed=DoMarkUsed,
933 MarkUsed=MarkUsed,
934 MarkUnused=MarkUnused,
936 DefaultKnown=DefaultObjectiveKnown,
937 Known=DummyObjectiveKnown,
938 Reason=ObjectiveReason,
940 AppendPositions=ObjectiveAppendPositions,
941 PrepareRouting=ObjectivePrepareRouting,
942 AddLoc=AddLoc,
943 FinishAddLoc=FinishAddLoc,
944 DoneRouting=DoneRouting,
946 Position=GetPosition,
947 TravelTime=ObjectiveTravelTime,
948 TravelTime2=ObjectiveTravelTime2,
950 IsWatched=IsObjectiveWatched,
952 Share=ObjectiveShare, -- Invoke to share this objective with your peers.
953 Unshare=ObjectiveUnshare, -- Invoke to stop sharing this objective.
956 QuestHelper.default_objective_item_param =
958 Known = ItemKnown,
959 AppendPositions = ItemAppendPositions,
960 DoMarkUsed = ItemDoMarkUsed
963 for key, value in pairs(QuestHelper.default_objective_param) do
964 if not QuestHelper.default_objective_item_param[key] then
965 QuestHelper.default_objective_item_param[key] = value
969 QuestHelper.default_objective_meta = { __index = QuestHelper.default_objective_param }
970 QuestHelper.default_objective_item_meta = { __index = QuestHelper.default_objective_item_param }
972 function QuestHelper:NewObjectiveObject()
973 next_objective_id = next_objective_id+1
974 return
975 setmetatable({
976 qh=self,
977 id=next_objective_id,
979 want_share=false, -- True if we want this objective shared.
980 is_sharing=false, -- Set to true if we've told other users about this objective.
982 user_ignore=nil, -- When nil, will use filters. Will ignore, when true, always show (if known).
984 priority=3, -- A hint as to what priority the quest should have. Should be 1, 2, 3, 4, or 5.
985 real_priority=3, -- This will be set to the priority routing actually decided to assign it.
987 setup_count=0,
989 icon_id=12,
990 icon_bg=14,
992 match_zone=false,
993 match_level=false,
994 match_done=false,
996 before={}, -- List of objectives that this objective must appear before.
997 after={}, -- List of objectives that this objective must appear after.
999 -- Routing related junk.
1001 --[[ Will be created as needed.
1002 d=nil,
1003 p=nil,
1004 nm=nil, -- Maps nodes to their nearest zone/list/x/y position.
1005 nm2=nil, -- Maps nodes to their nears position, but dynamically set in TravelTime2.
1006 nl=nil, -- List of all the nodes we need to consider.
1007 location=nil, -- Will be set to the best position for the node.
1008 pos=nil, -- Zone node list, distance list, x, y, reason.
1009 sop=nil ]]
1010 }, QuestHelper.default_objective_meta)
1013 local explicit_support_warning_given = false
1015 function QuestHelper:GetObjective(category, objective)
1016 local objective_list = self.objective_objects[category]
1018 if not objective_list then
1019 objective_list = {}
1020 self.objective_objects[category] = objective_list
1023 local objective_object = objective_list[objective]
1025 if not objective_object then
1026 if category == "quest" then
1027 local level, hash, name = string.match(objective, "^(%d+)/(%d*)/(.*)$")
1028 if not level then
1029 level, name = string.match(objective, "^(%d+)/(.*)$")
1030 if not level then
1031 name = objective
1035 if hash == "" then hash = nil end
1036 objective_object = self:GetQuest(name, tonumber(level), tonumber(hash))
1037 objective_list[objective] = objective_object
1038 return objective_object
1041 objective_object = self:NewObjectiveObject()
1043 objective_object.cat = category
1044 objective_object.obj = objective
1046 if category == "item" then
1047 setmetatable(objective_object, QuestHelper.default_objective_item_meta)
1048 objective_object.icon_id = 2
1049 elseif category == "monster" then
1050 objective_object.icon_id = 1
1051 elseif category == "object" then
1052 objective_object.icon_id = 3
1053 elseif category == "event" then
1054 objective_object.icon_id = 4
1055 elseif category == "loc" then
1056 objective_object.icon_id = 6
1057 elseif category == "reputation" then
1058 objective_object.icon_id = 5
1059 elseif category == "player" then
1060 objective_object.icon_id = 1 -- not ideal, will improve later
1061 else
1062 if not explicit_support_warning_given then
1063 self:TextOut("FIXME: Objective type '"..category.."' for objective '"..objective.."' isn't explicitly supported yet; hopefully the dummy handler will do something sensible.")
1064 explicit_support_warning_given = true
1068 objective_list[objective] = objective_object
1070 if category == "loc" then
1071 -- Loc is special, we don't store it, and construct it from the string.
1072 -- Don't have any error checking here, will assume it's correct.
1073 local i
1074 local _, _, c, z, x, y = string.find(objective,"^(%d+),(%d+),([%d%.]+),([%d%.]+)$")
1076 if not y then
1077 _, _, i, x, y = string.find(objective,"^(%d+),([%d%.]+),([%d%.]+)$")
1078 else
1079 i = QuestHelper_IndexLookup[c][z]
1082 objective_object.o = {pos={{tonumber(i),tonumber(x),tonumber(y),1}}}
1083 objective_object.fb = {}
1084 else
1085 objective_list = QuestHelper_Objectives_Local[category]
1086 if not objective_list then
1087 objective_list = {}
1088 QuestHelper_Objectives_Local[category] = objective_list
1090 objective_object.o = objective_list[objective]
1091 if not objective_object.o then
1092 objective_object.o = {}
1093 objective_list[objective] = objective_object.o
1095 local l = QuestHelper_StaticData[self.locale]
1096 if l then
1097 objective_list = l.objective[category]
1098 if objective_list then
1099 objective_object.fb = objective_list[objective]
1102 if not objective_object.fb then
1103 objective_object.fb = {}
1106 -- TODO: If we have some other source of information (like LightHeaded) add its data to objective_object.fb
1111 return objective_object
1114 function QuestHelper:AppendObjectivePosition(objective, i, x, y, w)
1115 local pos = objective.o.pos
1116 if not pos then
1117 if objective.o.drop or objective.o.contained then
1118 return -- If it's dropped by a monster, don't record the position we got the item at.
1120 objective.o.pos = self:AppendPosition({}, i, x, y, w)
1121 else
1122 self:AppendPosition(pos, i, x, y, w)
1126 function QuestHelper:AppendObjectiveDrop(objective, monster, count)
1127 local drop = objective.o.drop
1128 if drop then
1129 drop[monster] = (drop[monster] or 0)+(count or 1)
1130 else
1131 objective.o.drop = {[monster] = count or 1}
1132 objective.o.pos = nil -- If it's dropped by a monster, then forget the position we found it at.
1136 function QuestHelper:AppendItemObjectiveDrop(item_object, item_name, monster_name, count)
1137 local quest = self:ItemIsForQuest(item_object, item_name)
1138 if quest and not item_object.o.vendor and not item_object.o.drop and not item_object.o.pos then
1139 self:AppendQuestDrop(quest, item_name, monster_name, count)
1140 else
1141 if not item_object.o.drop and not item_object.o.pos then
1142 self:PurgeQuestItem(item_object, item_name)
1144 self:AppendObjectiveDrop(item_object, monster_name, count)
1148 function QuestHelper:AppendItemObjectivePosition(item_object, item_name, i, x, y)
1149 local quest = self:ItemIsForQuest(item_object, item_name)
1150 if quest and not item_object.o.vendor and not item_object.o.drop and not item_object.o.pos then
1151 self:AppendQuestPosition(quest, item_name, i, x, y)
1152 else
1153 if not item_object.o.vendor and not item_object.o.drop and not item_object.o.contained and not item_object.o.pos then
1154 -- Just learned that this item doesn't depend on a quest to drop, remove any quest references to it.
1155 self:PurgeQuestItem(item_object, item_name)
1157 self:AppendObjectivePosition(item_object, i, x, y)
1161 function QuestHelper:AppendItemObjectiveContainer(objective, container_name, count)
1162 local container = objective.o.contained
1163 if container then
1164 container[container_name] = (container[container_name] or 0)+(count or 1)
1165 else
1166 objective.o.contained = {[container_name] = count or 1}
1167 objective.o.pos = nil -- Forget the position.
1171 function QuestHelper:AddObjectiveWatch(objective, reason)
1172 if not objective.reasons then
1173 objective.reasons = {}
1176 if not next(objective.reasons, nil) then
1177 objective.watched = true
1178 objective:MarkUsed()
1180 objective.filter_blocked = false
1181 for obj in pairs(objective.swap_after or objective.after) do
1182 if obj.watched then
1183 objective.filter_blocked = true
1184 break
1188 for obj in pairs(objective.swap_before or objective.before) do
1189 if obj.watched then
1190 obj.filter_blocked = true
1194 if self.to_remove[objective] then
1195 self.to_remove[objective] = nil
1196 else
1197 self.to_add[objective] = true
1201 objective.reasons[reason] = (objective.reasons[reason] or 0) + 1
1204 function QuestHelper:RemoveObjectiveWatch(objective, reason)
1205 if objective.reasons[reason] == 1 then
1206 objective.reasons[reason] = nil
1207 if not next(objective.reasons, nil) then
1208 objective:MarkUnused()
1209 objective.watched = false
1211 for obj in pairs(objective.swap_before or objective.before) do
1212 if obj.watched then
1213 obj.filter_blocked = false
1214 for obj2 in pairs(obj.swap_after or obj.after) do
1215 if obj2.watched then
1216 obj.filter_blocked = true
1217 break
1223 if self.to_add[objective] then
1224 self.to_add[objective] = nil
1225 else
1226 self.to_remove[objective] = true
1229 else
1230 objective.reasons[reason] = objective.reasons[reason] - 1
1234 function QuestHelper:ObjectiveObjectDependsOn(objective, needs)
1235 assert(objective ~= needs) -- If this was true, ObjectiveIsKnown would get in an infinite loop.
1236 -- TODO: Needs sanity checking, especially now that dependencies can be assigned by remote users.
1239 -- We store the new relationships in objective.swap_[before|after],
1240 -- creating and copying them from objective.[before|after],
1241 -- the routing coroutine will check for those, swap them, and release the originals
1242 -- when it gets to a safe place to do so.
1244 if not (objective.swap_after or objective.after)[needs] then
1245 if objective.peer then
1246 for u, l in pairs(objective.peer) do
1247 -- Make sure other users know that the dependencies for this objective changed.
1248 objective.peer[u] = math.min(l, 1)
1252 if not objective.swap_after then
1253 objective.swap_after = self:CreateTable("swap_after")
1254 for key,value in pairs(objective.after) do objective.swap_after[key] = value end
1257 if not needs.swap_before then
1258 needs.swap_before = self:CreateTable("swap_before")
1259 for key,value in pairs(needs.before) do needs.swap_before[key] = value end
1262 if needs.watched then
1263 objective.filter_blocked = true
1266 objective.swap_after[needs] = true
1267 needs.swap_before[objective] = true
1271 function QuestHelper:AddObjectiveOptionsToMenu(obj, menu)
1272 local submenu = self:CreateMenu()
1274 for i = 1,5 do
1275 local name = QHText("PRIORITY"..i)
1276 local item = self:CreateMenuItem(submenu, name)
1277 local tex
1279 if obj.priority == i then
1280 tex = self:CreateIconTexture(item, 10)
1281 elseif obj.real_priority == i then
1282 tex = self:CreateIconTexture(item, 8)
1283 else
1284 tex = self:CreateIconTexture(item, 12)
1285 tex:SetVertexColor(1, 1, 1, 0)
1288 item:AddTexture(tex, true)
1289 item:SetFunction(self.SetObjectivePriorityPrompt, self, obj, i)
1292 self:CreateMenuItem(menu, QHText("PRIORITY")):SetSubmenu(submenu)
1294 if self.sharing then
1295 submenu = self:CreateMenu()
1296 local item = self:CreateMenuItem(submenu, QHText("SHARING_ENABLE"))
1297 local tex = self:CreateIconTexture(item, 10)
1298 if not obj.want_share then tex:SetVertexColor(1, 1, 1, 0) end
1299 item:AddTexture(tex, true)
1300 item:SetFunction(obj.Share, obj)
1302 local item = self:CreateMenuItem(submenu, QHText("SHARING_DISABLE"))
1303 local tex = self:CreateIconTexture(item, 10)
1304 if obj.want_share then tex:SetVertexColor(1, 1, 1, 0) end
1305 item:AddTexture(tex, true)
1306 item:SetFunction(obj.Unshare, obj)
1308 self:CreateMenuItem(menu, QHText("SHARING")):SetSubmenu(submenu)
1311 self:CreateMenuItem(menu, QHText("IGNORE")):SetFunction(self.IgnoreObjective, self, obj)
1314 function QuestHelper:IgnoreObjective(objective)
1315 if self.user_objectives[objective] then
1316 self:RemoveObjectiveWatch(objective, self.user_objectives[objective])
1317 self.user_objectives[objective] = nil
1318 else
1319 objective.user_ignore = true
1322 --self:ForceRouteUpdate()
1325 function QuestHelper:SetObjectivePriority(objective, level)
1326 level = math.min(5, math.max(1, math.floor((tonumber(level) or 3)+0.5)))
1327 if level ~= objective.priority then
1328 objective.priority = level
1329 if objective.peer then
1330 for u, l in pairs(objective.peer) do
1331 -- Peers don't know about this new priority.
1332 objective.peer[u] = math.min(l, 2)
1335 --self:ForceRouteUpdate()
1339 local function CalcObjectivePriority(obj)
1340 local priority = obj.priority
1342 for o in pairs(obj.before) do
1343 if o.watched then
1344 priority = math.min(priority, CalcObjectivePriority(o))
1348 return priority
1351 local function ApplyBlockPriority(obj, level)
1352 for o in pairs(obj.before) do
1353 if o.watched then
1354 ApplyBlockPriority(o, level)
1358 if obj.priority < level then QuestHelper:SetObjectivePriority(obj, level) end
1361 function QuestHelper:SetObjectivePriorityPrompt(objective, level)
1362 self:SetObjectivePriority(objective, level)
1363 if CalcObjectivePriority(objective) ~= level then
1364 local menu = self:CreateMenu()
1365 self:CreateMenuTitle(menu, QHText("IGNORED_PRIORITY_TITLE"))
1366 self:CreateMenuItem(menu, QHText("IGNORED_PRIORITY_FIX")):SetFunction(ApplyBlockPriority, objective, level)
1367 self:CreateMenuItem(menu, QHText("IGNORED_PRIORITY_IGNORE")):SetFunction(self.nop)
1368 menu:ShowAtCursor()
1372 function QuestHelper:SetObjectiveProgress(objective, user, have, need)
1373 if have and need then
1374 local list = objective.progress
1375 if not list then
1376 list = self:CreateTable("objective.progress")
1377 objective.progress = list
1380 local user_progress = list[user]
1381 if not user_progress then
1382 user_progress = self:CreateTable("objective.progress[user]")
1383 list[user] = user_progress
1386 local pct = 0
1387 local a, b = tonumber(have), tonumber(need)
1388 if a and b then
1389 if b ~= 0 then
1390 pct = a/b
1391 elseif a == 0 then
1392 pct = 1
1394 elseif a == b then
1395 pct = 1
1398 user_progress[1], user_progress[2], user_progress[3] = have, need, pct
1399 else
1400 if objective.progress then
1401 if objective.progress[user] then
1402 self:ReleaseTable(objective.progress[user])
1403 objective.progress[user] = nil
1405 if not next(objective.progress, nil) then
1406 self:ReleaseTable(objective.progress)
1407 objective.progress = nil