update repo
[QuestHelper.git] / objective.lua
blob53b1cbf5da1a586df22d66dd0dcf5edf5a3fa5e5
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)
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 objective:AddLoc(p[1], p[2], p[3], p[4]*high, why)
228 end end
230 if self.fb.pos then for i, p in ipairs(self.fb.pos) do
231 objective:AddLoc(p[1], p[2], p[3], p[4]*high, why)
232 end end
236 local function ObjectivePrepareRouting(self, args)
237 self.setup_count = self.setup_count + 1
238 if not self.setup then
239 assert(not self.d)
240 assert(not self.p)
241 assert(not self.nm)
242 assert(not self.nm2)
243 assert(not self.nl)
245 self.d = QuestHelper:CreateTable("objective.d")
246 self.p = QuestHelper:CreateTable("objective.p")
247 self.nm = QuestHelper:CreateTable("objective.nm")
248 self.nm2 = QuestHelper:CreateTable("objective.nm2")
249 self.nl = QuestHelper:CreateTable("objective.nl")
250 self.distance_cache = QuestHelper:CreateTable("objective.distance_cache")
252 self:AppendPositions(self, 1, nil)
253 self:FinishAddLoc(args)
257 local function ItemAppendPositions(self, objective, weight, why)
258 why2 = why and why.."\n" or ""
260 if self.o.vendor then for i, npc in ipairs(self.o.vendor) do
261 local n = self.qh:GetObjective("monster", npc)
262 local faction = n.o.faction or n.fb.faction
263 if (not faction or faction == self.qh.faction) then
264 n:AppendPositions(objective, 1, why2..QHFormat("OBJECTIVE_PURCHASE", npc))
266 end end
268 if self.fb.vendor then for i, npc in ipairs(self.fb.vendor) do
269 local n = self.qh:GetObjective("monster", npc)
270 local faction = n.o.faction or n.fb.faction
271 if (not faction or faction == self.qh.faction) then
272 n:AppendPositions(objective, 1, why2..QHFormat("OBJECTIVE_PURCHASE", npc))
274 end end
276 if next(objective.p, nil) then
277 -- If we have points from vendors, then always use vendors. I don't want it telling you to killing the
278 -- towns people just because you had to talk to them anyway, and it saves walking to the store.
279 return
282 if self.o.drop then for monster, count in pairs(self.o.drop) do
283 local m = self.qh:GetObjective("monster", monster)
284 m:AppendPositions(objective, m.o.looted and count/m.o.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster))
285 end end
287 if self.fb.drop then for monster, count in pairs(self.fb.drop) do
288 local m = self.qh:GetObjective("monster", monster)
289 m:AppendPositions(objective, m.fb.looted and count/m.fb.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster))
290 end end
292 if self.o.contained then for item, count in pairs(self.o.contained) do
293 local i = self.qh:GetObjective("item", item)
294 i:AppendPositions(objective, i.o.opened and count/i.o.opened or 1, why2..QHFormat("OBJECTIVE_LOOT", item))
295 end end
297 if self.fb.contained then for item, count in pairs(self.fb.contained) do
298 local i = self.qh:GetObjective("item", item)
299 i:AppendPositions(objective, i.fb.opened and count/i.fb.opened or 1, why2..QHFormat("OBJECTIVE_LOOT", item))
300 end end
302 if self.o.pos then for i, p in ipairs(self.o.pos) do
303 objective:AddLoc(p[1], p[2], p[3], p[4], why)
304 end end
306 if self.fb.pos then for i, p in ipairs(self.fb.pos) do
307 objective:AddLoc(p[1], p[2], p[3], p[4], why)
308 end end
310 if self.quest then
311 local item_list=self.quest.o.item
312 if item_list then
313 local data = item_list[self.obj]
314 if data and data.drop then
315 for monster, count in pairs(data.drop) do
316 local m = self.qh:GetObjective("monster", monster)
317 m:AppendPositions(objective, m.o.looted and count/m.o.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster))
319 elseif data and data.pos then
320 for i, p in ipairs(data.pos) do
321 objective:AddLoc(p[1], p[2], p[3], p[4], why)
326 item_list=self.quest.fb.item
327 if item_list then
328 local data = item_list[self.obj]
329 if data and data.drop then
330 for monster, count in pairs(data.drop) do
331 local m = self.qh:GetObjective("monster", monster)
332 m:AppendPositions(objective, m.fb.looted and count/m.fb.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster))
334 elseif data and data.pos then
335 for i, p in ipairs(data.pos) do
336 objective:AddLoc(p[1], p[2], p[3], p[4], why)
343 local function ItemDoMarkUsed(self)
344 if self.o.vendor then for i, npc in ipairs(self.o.vendor) do
345 local n = self.qh:GetObjective("monster", npc)
346 local faction = n.o.faction or n.fb.faction
347 if (not faction or faction == self.qh.faction) then
348 self:Uses(n, "TOOLTIP_PURCHASE")
350 end end
352 if self.fb.vendor then for i, npc in ipairs(self.fb.vendor) do
353 local n = self.qh:GetObjective("monster", npc)
354 local faction = n.o.faction or n.fb.faction
355 if (not faction or faction == self.qh.faction) then
356 self:Uses(n, "TOOLTIP_PURCHASE")
358 end end
360 if self.o.drop then for monster, count in pairs(self.o.drop) do
361 self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
362 end end
364 if self.fb.drop then for monster, count in pairs(self.fb.drop) do
365 self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
366 end end
368 if self.o.contained then for item, count in pairs(self.o.contained) do
369 self:Uses(self.qh:GetObjective("item", item), "TOOLTIP_LOOT")
370 end end
372 if self.fb.contained then for item, count in pairs(self.fb.contained) do
373 self:Uses(self.qh:GetObjective("item", item), "TOOLTIP_LOOT")
374 end end
376 if self.quest then
377 local item_list=self.quest.o.item
378 if item_list then
379 local data = item_list[self.obj]
380 if data and data.drop then
381 for monster, count in pairs(data.drop) do
382 self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
387 item_list=self.quest.fb.item
388 if item_list then
389 local data = item_list[self.obj]
390 if data and data.drop then
391 for monster, count in pairs(data.drop) do
392 self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
408 ---------------
422 local function AddLoc(self, index, x, y, w, why)
423 assert(not self.setup)
425 if w > 0 then
426 local pair = QuestHelper_ZoneLookup[index]
427 if not pair then return end -- that zone doesn't exist! We require more vespene gas. Not enough rage!
428 local c, z = pair[1], pair[2]
429 x, y = self.qh.Astrolabe:TranslateWorldMapPosition(c, z, x, y, c, 0)
431 x = x * self.qh.continent_scales_x[c]
432 y = y * self.qh.continent_scales_y[c]
433 local list = self.qh.zone_nodes[index]
435 local points = self.p[list]
436 if not points then
437 points = QuestHelper:CreateTable("objective.p[zone] (objective nodes per-zone)")
438 self.p[list] = points
441 for i, p in pairs(points) do
442 local u, v = x-p[3], y-p[4]
443 if u*u+v*v < 25 then -- Combine points within a threshold of 5 seconds travel time.
444 p[3] = (p[3]*p[5]+x*w)/(p[5]+w)
445 p[4] = (p[4]*p[5]+y*w)/(p[5]+w)
446 p[5] = p[5]+w
447 if w > p[7] then
448 p[6], p[7] = why, w
450 return
454 local new = QuestHelper:CreateTable("objective.p[zone] (possible objective node)")
455 new[1], new[2], new[3], new[4], new[5], new[6], new[7] = list, nil, x, y, w, why, w
456 table.insert(points, new)
460 local function FinishAddLoc(self, args)
461 local mx = 0
463 for z, pl in pairs(self.p) do
464 for i, p in ipairs(pl) do
465 if p[5] > mx then
466 self.location = p
467 mx = p[5]
472 if not self.zones then
473 -- Not using CreateTable, because it will not be released when routing is complete.
474 self.zones = {}
475 else
476 -- We could remove the already known zones, but I'm operating under the assumtion that locations will only be added,
477 -- not removed, so this isn't necessary.
480 -- Remove probably useless locations.
481 for z, pl in pairs(self.p) do
482 local remove_zone = true
483 local i = 1
484 while i <= #pl do
485 if pl[i][5] < mx*0.2 then
486 QuestHelper:ReleaseTable(pl[i])
487 table.remove(pl, i)
488 else
489 remove_zone = false
490 i = i + 1
493 if remove_zone then
494 QuestHelper:ReleaseTable(self.p[z])
495 self.p[z] = nil
496 else
497 self.zones[z.i] = true
501 local node_map = self.nm
502 local node_list = self.nl
504 for list, pl in pairs(self.p) do
505 local dist = self.d[list]
507 assert(not dist)
509 if not dist then
510 dist = QuestHelper:CreateTable("self.d[list]")
511 self.d[list] = dist
514 for i, point in ipairs(pl) do
515 point[5] = mx/point[5] -- Will become 1 for the most desired location, and become larger and larger for less desireable locations.
517 point[2] = QuestHelper:CreateTable("possible objective node to zone edge cache")
519 for i, node in ipairs(list) do
520 local u, v = point[3]-node.x, point[4]-node.y
521 local d = math.sqrt(u*u+v*v)
523 point[2][i] = d
525 if dist[i] then
526 if d*point[5] < dist[i][1]*dist[i][2] then
527 dist[i][1], dist[i][2] = d, point[5]
528 node_map[node] = point
530 else
531 local pair = QuestHelper:CreateTable()
532 pair[1], pair[2] = d, point[5]
533 dist[i] = pair
535 if not node_map[node] then
536 table.insert(node_list, node)
537 node_map[node] = point
538 else
539 u, v = node_map[node][3]-node.x, node_map[node][4]-node.y
541 if dist[i][1]*dist[i][2] < math.sqrt(u*u+v*v)*node_map[node][5] then
542 node_map[node] = point
550 if not args or not args.failable then
551 if #node_list == 0 then QuestHelper:Error(self.cat.."/"..self.obj..": zero nodes!") end
554 assert(not self.setup)
555 self.setup = true
556 table.insert(self.qh.prepared_objectives, self)
559 local function GetPosition(self)
560 assert(self.setup)
562 return self.location
565 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)
567 -- Note: Pos is the starting point, the objective is the destination. These are different data formats - "self" can be a set of points.
568 -- 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.
569 local function ObjectiveTravelTime(self, pos, nocache)
570 assert(self.setup)
572 -- The caching is pretty obvious.
573 local key, cached
574 if not nocache then
575 assert(pos ~= QuestHelper.pos)
576 if not pos.key then
577 pos.key = math.random()..""
579 key = pos.key
580 cached = self.distance_cache[key]
581 if cached then
582 if not QH_TESTCACHE then
583 return unpack(cached)
588 local graph = self.qh.world_graph
589 local nl = self.nl
591 graph:PrepareSearch()
593 -- 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.
594 for z, l in pairs(self.d) do
595 for i, n in ipairs(z) do
596 if n.s == 0 then
597 n.e, n.w = unpack(l[i])
598 n.s = 3
599 elseif n.e * n.w < l[i][1]*l[i][2] then
600 n.e, n.w = unpack(l[i])
605 local d = pos[2]
606 for i, n in ipairs(pos[1]) do
607 graph:AddStartNode(n, d[i], nl)
610 local e = graph:DoSearch(nl)
612 -- d changes datatype here. I hate this codebase. Hell, e probably changes datatype also! yaaaay. what does .nm mean? what does .d mean?
613 d = e.g+e.e
614 e = self.nm[e]
616 -- There's something going on with weighting here that I don't understand
617 local l = self.p[pos[1]]
618 if l then
619 local x, y = pos[3], pos[4]
620 local score = d*e[5]
622 for i, n in ipairs(l) do
623 local u, v = x-n[3], y-n[4]
624 local d2 = math.sqrt(u*u+v*v)
625 local s = d2*n[5]
626 if s < score then
627 d, e, score = d2, n, s
632 assert(e)
633 if not nocache then
634 assert( not cached or (cached[1] == d and cached[2] == e))
635 if not QH_TESTCACHE or not cached then
636 local new = self.qh:CreateTable()
637 new[1], new[2] = d, e
638 self.distance_cache[key] = new
639 self.qh:CacheRegister(self)
643 return d, e
646 -- Note: pos1 is the starting point, pos2 is the ending point, the objective is somewhere between them.
647 -- 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?
648 local function ObjectiveTravelTime2(self, pos1, pos2, nocache)
649 assert(self.setup)
651 -- caching is pretty simple as usual
652 local key, cached
653 if not nocache then
654 assert(pos1 ~= QuestHelper.pos)
655 assert(pos2 ~= QuestHelper.pos)
656 -- We don't want to cache distances involving the player's current position, as that would spam the table
657 if not pos1.key then
658 pos1.key = math.random()..""
660 if not pos2.key then
661 pos2.key = math.random()..""
663 key = pos1.key..pos2.key
664 cached = self.distance_cache[key]
665 if cached then
666 if not QH_TESTCACHE then
667 return unpack(cached)
672 local graph = self.qh.world_graph
673 local nl = self.nl
675 -- This is the standard pos1-to-self code that we're used to seeing . . .
676 graph:PrepareSearch()
678 for z, l in pairs(self.d) do
679 for i, n in ipairs(z) do
680 if n.s == 0 then
681 n.e, n.w = unpack(l[i])
682 n.s = 3
683 elseif n.e * n.w < l[i][1]*l[i][2] then
684 n.e, n.w = unpack(l[i])
689 local d = pos1[2]
690 for i, n in ipairs(pos1[1]) do
691 graph:AddStartNode(n, d[i], nl)
694 graph:DoFullSearch(nl)
696 graph:PrepareSearch()
698 -- . . . and here's where it gets wonky
699 -- Now, we need to figure out how long it takes to get to each node.
700 for z, point_list in pairs(self.p) do
701 if z == pos1[1] then
702 -- Will also consider min distance.
703 local x, y = pos1[3], pos1[4]
705 for i, p in ipairs(point_list) do
706 local a, b = p[3]-x, p[4]-y
707 local u, v = p[3], p[4]
708 local d = math.sqrt(a*a+b*b)
709 local w = p[5]
710 local score = d*w
711 for i, n in ipairs(z) do
712 a, b = n.x-u, n.y-v
713 local bleh = math.sqrt(a*a+b*b)+n.g
714 local s = bleh*w
715 if s < score then
716 d, score = bleh, d
719 p[7] = d
721 else
722 for i, p in ipairs(point_list) do
723 local x, y = p[3], p[4]
724 local w = p[5]
725 local d
726 local score
728 for i, n in ipairs(z) do
729 local a, b = n.x-x, n.y-y
730 local d2 = math.sqrt(a*a+b*b)+n.g
731 local s = d2*w
732 if not score or s < score then
733 d, score = d2, s
736 p[7] = d
741 d = pos2[2]
743 for i, n in ipairs(pos2[1]) do
744 n.e = d[i]
745 n.s = 3
748 local el = pos2[1]
749 local nm = self.nm2
751 for z, l in pairs(self.d) do
752 for i, n in ipairs(z) do
753 local x, y = n.x, n.y
754 local bp
755 local bg
756 local bs
757 for i, p in ipairs(self.p[z]) do
758 local a, b = x-p[3], y-p[4]
759 d = p[7]+math.sqrt(a*a+b*b)
760 s = d*p[5]
761 if not bs or s < bs then
762 bg, bp, bs = d, p, s
766 nm[n] = bp
767 -- Using score instead of distance, because we want nodes we're not really interested in to be less likely to get chosen.
768 graph:AddStartNode(n, bs, el)
772 local e = graph:DoSearch(pos2[1])
774 d = nm[e.p][7]
775 local d2 = e.g+e.e-e.p.g+(e.p.g/nm[e.p][5]-nm[e.p][7])
777 e = nm[e.p]
778 local total = (d+d2)*e[5]
780 if self.p[el] then
781 local x, y = pos2[3], pos2[4]
782 for i, p in ipairs(self.p[el]) do
783 local a, b = x-p[3], y-p[4]
784 local c = math.sqrt(a*a+b*b)
785 local t = (p[7]+c)*p[5]
786 if t < total then
787 total, d, d2, e = t, p[7], c, p
792 -- 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)
793 d = QuestHelper:ComputeTravelTime(pos1, e)
794 d2 = QuestHelper:ComputeTravelTime(e, pos2)
796 assert(e)
797 if not nocache then
798 assert( not cached or (cached[1] == d and cached[2] == d2 and cached[3] == e))
799 if not QH_TESTCACHE or not cached then
800 local new = self.qh:CreateTable("ObjectiveTravelTime2 cache")
801 new[1], new[2], new[3] = d, d2, e
802 self.distance_cache[key] = new
803 self.qh:CacheRegister(self)
807 --[[if pos1 and pos2 then -- Debug code so I can maybe actually fix the problems someday
808 QuestHelper:TextOut("Beginning dumping here")
810 local laxa = QuestHelper:ComputeTravelTime(pos1, e, true)
811 if math.abs(laxa-d) >= 0.0001 then
812 QuestHelper:TextOut(QuestHelper:StringizeTable(pos1))
813 QuestHelper:TextOut(QuestHelper:StringizeRecursive(pos1, 2))
814 QuestHelper:TextOut(QuestHelper:StringizeTable(e))
815 QuestHelper:TextOut(QuestHelper:StringizeTable(e[1]))
816 QuestHelper:TextOut(QuestHelper:StringizeTable(e[2]))
817 QuestHelper:TextOut(QuestHelper:StringizeRecursive(e[1], 2))]]
818 --QuestHelper:Assert(math.abs(laxa-d) < 0.0001, "Compare: "..laxa.." vs "..d) -- wonky commenting is thanks to the de-assert script, fix later
819 --[[end
820 local laxb = QuestHelper:ComputeTravelTime(e, pos2, true)
821 if math.abs(laxb-d2) >= 0.0001 then
822 QuestHelper:TextOut(QuestHelper:StringizeTable(pos2))
823 QuestHelper:TextOut(QuestHelper:StringizeTable(e))
824 QuestHelper:TextOut(QuestHelper:StringizeTable(e[1]))
825 QuestHelper:TextOut(QuestHelper:StringizeTable(e[2]))
826 QuestHelper:TextOut(QuestHelper:StringizeRecursive(e[1], 2))]]
827 --QuestHelper:Assert(math.abs(laxa-d) < 0.0001, "Compare: "..laxb.." vs "..d2)
828 --[[end
829 end]]
831 return d, d2, e
834 local function DoneRouting(self)
835 assert(self.setup_count > 0)
836 assert(self.setup)
838 if self.setup_count == 1 then
839 self.setup_count = 0
840 QuestHelper:ReleaseObjectivePathingInfo(self)
841 for i, obj in ipairs(self.qh.prepared_objectives) do
842 if o == obj then
843 table.remove(self.qh.prepared_objectives, i)
844 break
847 else
848 self.setup_count = self.setup_count - 1
852 local function IsObjectiveWatched(self)
853 -- Check if an objective is being watched. Note that this is an external query, not a simple Selector.
854 local info
856 if self.cat == "quest" then
857 info = QuestHelper.quest_log[self]
858 else
859 info = QuestHelper.quest_log[self.quest]
862 if info then
863 local index = info.index
864 if index then
865 if UberQuest then
866 -- UberQuest has it's own way of tracking quests.
867 local uq_settings = UberQuest_Config[UnitName("player")]
868 if uq_settings then
869 local list = uq_settings.selected
870 if list then
871 return list[GetQuestLogTitle(index)]
874 else
875 return IsQuestWatched(index)
880 return false
884 local next_objective_id = 0
886 local function ObjectiveShare(self)
887 self.want_share = true
890 local function ObjectiveUnshare(self)
891 self.want_share = false
894 QuestHelper.default_objective_param =
896 CouldBeFirst=ObjectiveCouldBeFirst,
898 Uses=Uses,
899 DoMarkUsed=DoMarkUsed,
900 MarkUsed=MarkUsed,
901 MarkUnused=MarkUnused,
903 DefaultKnown=DefaultObjectiveKnown,
904 Known=DummyObjectiveKnown,
905 Reason=ObjectiveReason,
907 AppendPositions=ObjectiveAppendPositions,
908 PrepareRouting=ObjectivePrepareRouting,
909 AddLoc=AddLoc,
910 FinishAddLoc=FinishAddLoc,
911 DoneRouting=DoneRouting,
913 Position=GetPosition,
914 TravelTime=ObjectiveTravelTime,
915 TravelTime2=ObjectiveTravelTime2,
917 IsWatched=IsObjectiveWatched,
919 Share=ObjectiveShare, -- Invoke to share this objective with your peers.
920 Unshare=ObjectiveUnshare, -- Invoke to stop sharing this objective.
923 QuestHelper.default_objective_item_param =
925 Known = ItemKnown,
926 AppendPositions = ItemAppendPositions,
927 DoMarkUsed = ItemDoMarkUsed
930 for key, value in pairs(QuestHelper.default_objective_param) do
931 if not QuestHelper.default_objective_item_param[key] then
932 QuestHelper.default_objective_item_param[key] = value
936 QuestHelper.default_objective_meta = { __index = QuestHelper.default_objective_param }
937 QuestHelper.default_objective_item_meta = { __index = QuestHelper.default_objective_item_param }
939 function QuestHelper:NewObjectiveObject()
940 next_objective_id = next_objective_id+1
941 return
942 setmetatable({
943 qh=self,
944 id=next_objective_id,
946 want_share=false, -- True if we want this objective shared.
947 is_sharing=false, -- Set to true if we've told other users about this objective.
949 user_ignore=nil, -- When nil, will use filters. Will ignore, when true, always show (if known).
951 priority=3, -- A hint as to what priority the quest should have. Should be 1, 2, 3, 4, or 5.
952 real_priority=3, -- This will be set to the priority routing actually decided to assign it.
954 setup_count=0,
956 icon_id=12,
957 icon_bg=14,
959 match_zone=false,
960 match_level=false,
961 match_done=false,
963 before={}, -- List of objectives that this objective must appear before.
964 after={}, -- List of objectives that this objective must appear after.
966 -- Routing related junk.
968 --[[ Will be created as needed.
969 d=nil,
970 p=nil,
971 nm=nil, -- Maps nodes to their nearest zone/list/x/y position.
972 nm2=nil, -- Maps nodes to their nears position, but dynamically set in TravelTime2.
973 nl=nil, -- List of all the nodes we need to consider.
974 location=nil, -- Will be set to the best position for the node.
975 pos=nil, -- Zone node list, distance list, x, y, reason.
976 sop=nil ]]
977 }, QuestHelper.default_objective_meta)
980 function QuestHelper:GetObjective(category, objective)
981 local objective_list = self.objective_objects[category]
983 if not objective_list then
984 objective_list = {}
985 self.objective_objects[category] = objective_list
988 local objective_object = objective_list[objective]
990 if not objective_object then
991 if category == "quest" then
992 local level, hash, name = string.match(objective, "^(%d+)/(%d*)/(.*)$")
993 if not level then
994 level, name = string.match(objective, "^(%d+)/(.*)$")
995 if not level then
996 name = objective
1000 if hash == "" then hash = nil end
1001 objective_object = self:GetQuest(name, tonumber(level), tonumber(hash))
1002 objective_list[objective] = objective_object
1003 return objective_object
1006 objective_object = self:NewObjectiveObject()
1008 objective_object.cat = category
1009 objective_object.obj = objective
1011 if category == "item" then
1012 setmetatable(objective_object, QuestHelper.default_objective_item_meta)
1013 objective_object.icon_id = 2
1014 elseif category == "monster" then
1015 objective_object.icon_id = 1
1016 elseif category == "object" then
1017 objective_object.icon_id = 3
1018 elseif category == "event" then
1019 objective_object.icon_id = 4
1020 elseif category == "loc" then
1021 objective_object.icon_id = 6
1022 elseif category == "reputation" then
1023 objective_object.icon_id = 5
1024 else
1025 self:TextOut("FIXME: Objective type '"..category.."' for objective '"..objective.."' isn't explicitly supported yet; hopefully the dummy handler will do something sensible.")
1028 objective_list[objective] = objective_object
1030 if category == "loc" then
1031 -- Loc is special, we don't store it, and construct it from the string.
1032 -- Don't have any error checking here, will assume it's correct.
1033 local i
1034 local _, _, c, z, x, y = string.find(objective,"^(%d+),(%d+),([%d%.]+),([%d%.]+)$")
1036 if not y then
1037 _, _, i, x, y = string.find(objective,"^(%d+),([%d%.]+),([%d%.]+)$")
1038 else
1039 i = QuestHelper_IndexLookup[c][z]
1042 objective_object.o = {pos={{tonumber(i),tonumber(x),tonumber(y),1}}}
1043 objective_object.fb = {}
1044 else
1045 objective_list = QuestHelper_Objectives_Local[category]
1046 if not objective_list then
1047 objective_list = {}
1048 QuestHelper_Objectives_Local[category] = objective_list
1050 objective_object.o = objective_list[objective]
1051 if not objective_object.o then
1052 objective_object.o = {}
1053 objective_list[objective] = objective_object.o
1055 local l = QuestHelper_StaticData[self.locale]
1056 if l then
1057 objective_list = l.objective[category]
1058 if objective_list then
1059 objective_object.fb = objective_list[objective]
1062 if not objective_object.fb then
1063 objective_object.fb = {}
1066 -- TODO: If we have some other source of information (like LightHeaded) add its data to objective_object.fb
1071 return objective_object
1074 function QuestHelper:AppendObjectivePosition(objective, i, x, y, w)
1075 local pos = objective.o.pos
1076 if not pos then
1077 if objective.o.drop or objective.o.contained then
1078 return -- If it's dropped by a monster, don't record the position we got the item at.
1080 objective.o.pos = self:AppendPosition({}, i, x, y, w)
1081 else
1082 self:AppendPosition(pos, i, x, y, w)
1086 function QuestHelper:AppendObjectiveDrop(objective, monster, count)
1087 local drop = objective.o.drop
1088 if drop then
1089 drop[monster] = (drop[monster] or 0)+(count or 1)
1090 else
1091 objective.o.drop = {[monster] = count or 1}
1092 objective.o.pos = nil -- If it's dropped by a monster, then forget the position we found it at.
1096 function QuestHelper:AppendItemObjectiveDrop(item_object, item_name, monster_name, count)
1097 local quest = self:ItemIsForQuest(item_object, item_name)
1098 if quest and not item_object.o.vendor and not item_object.o.drop and not item_object.o.pos then
1099 self:AppendQuestDrop(quest, item_name, monster_name, count)
1100 else
1101 if not item_object.o.drop and not item_object.o.pos then
1102 self:PurgeQuestItem(item_object, item_name)
1104 self:AppendObjectiveDrop(item_object, monster_name, count)
1108 function QuestHelper:AppendItemObjectivePosition(item_object, item_name, i, x, y)
1109 local quest = self:ItemIsForQuest(item_object, item_name)
1110 if quest and not item_object.o.vendor and not item_object.o.drop and not item_object.o.pos then
1111 self:AppendQuestPosition(quest, item_name, i, x, y)
1112 else
1113 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
1114 -- Just learned that this item doesn't depend on a quest to drop, remove any quest references to it.
1115 self:PurgeQuestItem(item_object, item_name)
1117 self:AppendObjectivePosition(item_object, i, x, y)
1121 function QuestHelper:AppendItemObjectiveContainer(objective, container_name, count)
1122 local container = objective.o.contained
1123 if container then
1124 container[container_name] = (container[container_name] or 0)+(count or 1)
1125 else
1126 objective.o.contained = {[container_name] = count or 1}
1127 objective.o.pos = nil -- Forget the position.
1131 function QuestHelper:AddObjectiveWatch(objective, reason)
1132 if not objective.reasons then
1133 objective.reasons = {}
1136 if not next(objective.reasons, nil) then
1137 objective.watched = true
1138 objective:MarkUsed()
1140 objective.filter_blocked = false
1141 for obj in pairs(objective.swap_after or objective.after) do
1142 if obj.watched then
1143 objective.filter_blocked = true
1144 break
1148 for obj in pairs(objective.swap_before or objective.before) do
1149 if obj.watched then
1150 obj.filter_blocked = true
1154 if self.to_remove[objective] then
1155 self.to_remove[objective] = nil
1156 else
1157 self.to_add[objective] = true
1161 objective.reasons[reason] = (objective.reasons[reason] or 0) + 1
1164 function QuestHelper:RemoveObjectiveWatch(objective, reason)
1165 if objective.reasons[reason] == 1 then
1166 objective.reasons[reason] = nil
1167 if not next(objective.reasons, nil) then
1168 objective:MarkUnused()
1169 objective.watched = false
1171 for obj in pairs(objective.swap_before or objective.before) do
1172 if obj.watched then
1173 obj.filter_blocked = false
1174 for obj2 in pairs(obj.swap_after or obj.after) do
1175 if obj2.watched then
1176 obj.filter_blocked = true
1177 break
1183 if self.to_add[objective] then
1184 self.to_add[objective] = nil
1185 else
1186 self.to_remove[objective] = true
1189 else
1190 objective.reasons[reason] = objective.reasons[reason] - 1
1194 function QuestHelper:ObjectiveObjectDependsOn(objective, needs)
1195 assert(objective ~= needs) -- If this was true, ObjectiveIsKnown would get in an infinite loop.
1196 -- TODO: Needs sanity checking, especially now that dependencies can be assigned by remote users.
1199 -- We store the new relationships in objective.swap_[before|after],
1200 -- creating and copying them from objective.[before|after],
1201 -- the routing coroutine will check for those, swap them, and release the originals
1202 -- when it gets to a safe place to do so.
1204 if not (objective.swap_after or objective.after)[needs] then
1205 if objective.peer then
1206 for u, l in pairs(objective.peer) do
1207 -- Make sure other users know that the dependencies for this objective changed.
1208 objective.peer[u] = math.min(l, 1)
1212 if not objective.swap_after then
1213 objective.swap_after = self:CreateTable("swap_after")
1214 for key,value in pairs(objective.after) do objective.swap_after[key] = value end
1217 if not needs.swap_before then
1218 needs.swap_before = self:CreateTable("swap_before")
1219 for key,value in pairs(needs.before) do needs.swap_before[key] = value end
1222 if needs.watched then
1223 objective.filter_blocked = true
1226 objective.swap_after[needs] = true
1227 needs.swap_before[objective] = true
1231 function QuestHelper:AddObjectiveOptionsToMenu(obj, menu)
1232 local submenu = self:CreateMenu()
1234 for i = 1,5 do
1235 local name = QHText("PRIORITY"..i)
1236 local item = self:CreateMenuItem(submenu, name)
1237 local tex
1239 if obj.priority == i then
1240 tex = self:CreateIconTexture(item, 10)
1241 elseif obj.real_priority == i then
1242 tex = self:CreateIconTexture(item, 8)
1243 else
1244 tex = self:CreateIconTexture(item, 12)
1245 tex:SetVertexColor(1, 1, 1, 0)
1248 item:AddTexture(tex, true)
1249 item:SetFunction(self.SetObjectivePriorityPrompt, self, obj, i)
1252 self:CreateMenuItem(menu, QHText("PRIORITY")):SetSubmenu(submenu)
1254 if self.sharing then
1255 submenu = self:CreateMenu()
1256 local item = self:CreateMenuItem(submenu, QHText("SHARING_ENABLE"))
1257 local tex = self:CreateIconTexture(item, 10)
1258 if not obj.want_share then tex:SetVertexColor(1, 1, 1, 0) end
1259 item:AddTexture(tex, true)
1260 item:SetFunction(obj.Share, obj)
1262 local item = self:CreateMenuItem(submenu, QHText("SHARING_DISABLE"))
1263 local tex = self:CreateIconTexture(item, 10)
1264 if obj.want_share then tex:SetVertexColor(1, 1, 1, 0) end
1265 item:AddTexture(tex, true)
1266 item:SetFunction(obj.Unshare, obj)
1268 self:CreateMenuItem(menu, QHText("SHARING")):SetSubmenu(submenu)
1271 self:CreateMenuItem(menu, QHText("IGNORE")):SetFunction(self.IgnoreObjective, self, obj)
1274 function QuestHelper:IgnoreObjective(objective)
1275 if self.user_objectives[objective] then
1276 self:RemoveObjectiveWatch(objective, self.user_objectives[objective])
1277 self.user_objectives[objective] = nil
1278 else
1279 objective.user_ignore = true
1282 --self:ForceRouteUpdate()
1285 function QuestHelper:SetObjectivePriority(objective, level)
1286 level = math.min(5, math.max(1, math.floor((tonumber(level) or 3)+0.5)))
1287 if level ~= objective.priority then
1288 objective.priority = level
1289 if objective.peer then
1290 for u, l in pairs(objective.peer) do
1291 -- Peers don't know about this new priority.
1292 objective.peer[u] = math.min(l, 2)
1295 --self:ForceRouteUpdate()
1299 local function CalcObjectivePriority(obj)
1300 local priority = obj.priority
1302 for o in pairs(obj.before) do
1303 if o.watched then
1304 priority = math.min(priority, CalcObjectivePriority(o))
1308 return priority
1311 local function ApplyBlockPriority(obj, level)
1312 for o in pairs(obj.before) do
1313 if o.watched then
1314 ApplyBlockPriority(o, level)
1318 if obj.priority < level then QuestHelper:SetObjectivePriority(obj, level) end
1321 function QuestHelper:SetObjectivePriorityPrompt(objective, level)
1322 self:SetObjectivePriority(objective, level)
1323 if CalcObjectivePriority(objective) ~= level then
1324 local menu = self:CreateMenu()
1325 self:CreateMenuTitle(menu, QHText("IGNORED_PRIORITY_TITLE"))
1326 self:CreateMenuItem(menu, QHText("IGNORED_PRIORITY_FIX")):SetFunction(ApplyBlockPriority, objective, level)
1327 self:CreateMenuItem(menu, QHText("IGNORED_PRIORITY_IGNORE")):SetFunction(self.nop)
1328 menu:ShowAtCursor()
1332 function QuestHelper:SetObjectiveProgress(objective, user, have, need)
1333 if have and need then
1334 local list = objective.progress
1335 if not list then
1336 list = self:CreateTable("objective.progress")
1337 objective.progress = list
1340 local user_progress = list[user]
1341 if not user_progress then
1342 user_progress = self:CreateTable("objective.progress[user]")
1343 list[user] = user_progress
1346 local pct = 0
1347 local a, b = tonumber(have), tonumber(need)
1348 if a and b then
1349 if b ~= 0 then
1350 pct = a/b
1351 elseif a == 0 then
1352 pct = 1
1354 elseif a == b then
1355 pct = 1
1358 user_progress[1], user_progress[2], user_progress[3] = have, need, pct
1359 else
1360 if objective.progress then
1361 if objective.progress[user] then
1362 self:ReleaseTable(objective.progress[user])
1363 objective.progress[user] = nil
1365 if not next(objective.progress, nil) then
1366 self:ReleaseTable(objective.progress)
1367 objective.progress = nil