avoid a crash
[QuestHelper.git] / objective.lua
blobe051995978682b59c63df049b6a539780ae950dd
1 QuestHelper_File["objective.lua"] = "Development Version"
2 QuestHelper_Loadtime["objective.lua"] = GetTime()
4 local function ObjectiveCouldBeFirst(self)
5 if (self.user_ignore == nil and self.auto_ignore) or self.user_ignore then
6 return false
7 end
9 for i, j in pairs(self.after) do
10 if i.watched then
11 return false
12 end
13 end
15 return true
16 end
18 local function DefaultObjectiveKnown(self)
19 if self.user_ignore == nil then
20 if (self.filter_zone and QuestHelper_Pref.filter_zone) or
21 (self.filter_done and QuestHelper_Pref.filter_done) or
22 (self.filter_level and QuestHelper_Pref.filter_level) or
23 (self.filter_blocked and QuestHelper_Pref.filter_blocked) or
24 (self.filter_watched and QuestHelper_Pref.filter_watched) then
25 return false
26 end
27 elseif self.user_ignore then
28 return false
29 end
31 for i, j in pairs(self.after) do
32 if i.watched and not i:Known() then -- Need to know how to do everything before this objective.
33 return false
34 end
35 end
37 return true
38 end
40 local function ObjectiveReason(self, short)
41 local reason, rc = nil, 0
42 if self.reasons then
43 for r, c in pairs(self.reasons) do
44 if not reason or c > rc or (c == rc and r > reason) then
45 reason, rc = r, c
46 end
47 end
48 end
50 if not reason then reason = "Do some extremely secret unspecified something." end
52 if not short and self.pos and self.pos[6] then
53 reason = reason .. "\n" .. self.pos[6]
54 end
56 return reason
57 end
59 local function Uses(self, obj, text)
60 if self == obj then return end -- You cannot use yourself. A purse is not food.
61 local uses, used = self.uses, obj.used
63 if not uses then
64 uses = QuestHelper:CreateTable("uses")
65 self.uses = uses
66 end
68 if not used then
69 used = QuestHelper:CreateTable("used")
70 obj.used = used
71 end
73 if not uses[obj] then
74 uses[obj] = true
75 used[self] = text
76 obj:MarkUsed()
77 end
78 end
80 local function DoMarkUsed(self)
81 -- Objectives should call 'self:Uses(objective, text)' to mark objectives they use by don't directly depend on.
82 -- This information is used in tooltips.
83 -- text is passed to QHFormat with the name of the objective being used.
84 end
86 local function MarkUsed(self)
87 if not self.marked_used then
88 self.marked_used = 1
89 self:DoMarkUsed()
90 else
91 self.marked_used = self.marked_used + 1
92 end
93 end
95 local function MarkUnused(self)
96 assert(self.marked_used)
98 if self.marked_used == 1 then
99 local uses = self.uses
101 if uses then
102 for obj in pairs(uses) do
103 obj.used[self] = nil
104 obj:MarkUnused()
107 QuestHelper:ReleaseTable(uses)
108 self.uses = nil
111 if self.used then
112 assert(not next(self.used))
113 QuestHelper:ReleaseTable(self.used)
114 self.used = nil
117 self.marked_used = nil
118 else
119 self.marked_used = self.marked_used - 1
123 local function DummyObjectiveKnown(self)
124 return (self.o.pos or self.fb.pos) and DefaultObjectiveKnown(self)
127 local function ItemKnown(self)
128 if not DefaultObjectiveKnown(self) then return false end
130 if self.o.vendor then
131 for i, npc in ipairs(self.o.vendor) do
132 local n = self.qh:GetObjective("monster", npc)
133 local faction = n.o.faction or n.fb.faction
134 if (not faction or faction == self.qh.faction) and n:Known() then
135 return true
140 if self.fb.vendor then
141 for i, npc in ipairs(self.fb.vendor) do
142 local n = self.qh:GetObjective("monster", npc)
143 local faction = n.o.faction or n.fb.faction
144 if (not faction or faction == self.qh.faction) and n:Known() then
145 return true
150 if self.o.pos or self.fb.pos then
151 return true
154 if self.o.drop then for monster in pairs(self.o.drop) do
155 if self.qh:GetObjective("monster", monster):Known() then
156 return true
158 end end
160 if self.fb.drop then for monster in pairs(self.fb.drop) do
161 if self.qh:GetObjective("monster", monster):Known() then
162 return true
164 end end
166 if self.o.contained then for item in pairs(self.o.contained) do
167 if self.qh:GetObjective("item", item):Known() then
168 return true
170 end end
172 if self.fb.contained then for item in pairs(self.fb.contained) do
173 if self.qh:GetObjective("item", item):Known() then
174 return true
176 end end
178 if self.quest then
179 local item=self.quest.o.item
180 item = item and item[self.obj]
182 if item then
183 if item.pos then
184 return true
186 if item.drop then
187 for monster in pairs(item.drop) do
188 if self.qh:GetObjective("monster", monster):Known() then
189 return true
195 item=self.quest.fb.item
196 item = item and item[self.obj]
197 if item then
198 if item.pos then
199 return true
201 if item.drop then
202 for monster in pairs(item.drop) do
203 if self.qh:GetObjective("monster", monster):Known() then
204 return true
211 return false
214 local function ObjectiveAppendPositions(self, objective, weight, why, restrict)
215 local high = 0
217 if self.o.pos then for i, p in ipairs(self.o.pos) do
218 high = math.max(high, p[4])
219 end end
221 if self.fb.pos then for i, p in ipairs(self.fb.pos) do
222 high = math.max(high, p[4])
223 end end
225 high = weight/high
227 if self.o.pos then for i, p in ipairs(self.o.pos) do
228 if not restrict or not self.qh:Disallowed(p[1]) then
229 objective:AddLoc(p[1], p[2], p[3], p[4]*high, why)
231 end end
233 if self.fb.pos then for i, p in ipairs(self.fb.pos) do
234 if not restrict or not self.qh:Disallowed(p[1]) then
235 objective:AddLoc(p[1], p[2], p[3], p[4]*high, why)
237 end end
241 local function ObjectivePrepareRouting(self, anywhere)
242 self.setup_count = self.setup_count + 1
243 if not self.setup then
244 assert(not self.d)
245 assert(not self.p)
246 assert(not self.nm)
247 assert(not self.nm2)
248 assert(not self.nl)
250 self.d = QuestHelper:CreateTable("objective.d")
251 self.p = QuestHelper:CreateTable("objective.p")
252 self.nm = QuestHelper:CreateTable("objective.nm")
253 self.nm2 = QuestHelper:CreateTable("objective.nm2")
254 self.nl = QuestHelper:CreateTable("objective.nl")
255 self.distance_cache = QuestHelper:CreateTable("objective.distance_cache")
257 if not anywhere then
258 self:AppendPositions(self, 1, nil, true)
260 if not next(self.p) then
261 QuestHelper:TextOut(QHFormat("INACCESSIBLE_OBJ", self.obj or "whatever it was you just requested"))
262 anywhere = true
266 if anywhere then
267 self:AppendPositions(self, 1, nil, false)
270 self:FinishAddLoc(args)
274 local function ItemAppendPositions(self, objective, weight, why, restrict)
275 why2 = why and why.."\n" or ""
277 if self.o.vendor then for i, npc in ipairs(self.o.vendor) do
278 local n = self.qh:GetObjective("monster", npc)
279 local faction = n.o.faction or n.fb.faction
280 if (not faction or faction == self.qh.faction) then
281 n:AppendPositions(objective, 1, why2..QHFormat("OBJECTIVE_PURCHASE", npc), restrict)
283 end end
285 if self.fb.vendor then for i, npc in ipairs(self.fb.vendor) do
286 local n = self.qh:GetObjective("monster", npc)
287 local faction = n.o.faction or n.fb.faction
288 if (not faction or faction == self.qh.faction) then
289 n:AppendPositions(objective, 1, why2..QHFormat("OBJECTIVE_PURCHASE", npc), restrict)
291 end end
293 if next(objective.p, nil) then
294 -- If we have points from vendors, then always use vendors. I don't want it telling you to killing the
295 -- towns people just because you had to talk to them anyway, and it saves walking to the store.
296 return
299 if self.o.drop then for monster, count in pairs(self.o.drop) do
300 local m = self.qh:GetObjective("monster", monster)
301 m:AppendPositions(objective, m.o.looted and count/m.o.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster), restrict)
302 end end
304 if self.fb.drop then for monster, count in pairs(self.fb.drop) do
305 local m = self.qh:GetObjective("monster", monster)
306 m:AppendPositions(objective, m.fb.looted and count/m.fb.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster), restrict)
307 end end
309 if self.o.contained then for item, count in pairs(self.o.contained) do
310 local i = self.qh:GetObjective("item", item)
311 i:AppendPositions(objective, i.o.opened and count/i.o.opened or 1, why2..QHFormat("OBJECTIVE_LOOT", item), restrict)
312 end end
314 if self.fb.contained then for item, count in pairs(self.fb.contained) do
315 local i = self.qh:GetObjective("item", item)
316 i:AppendPositions(objective, i.fb.opened and count/i.fb.opened or 1, why2..QHFormat("OBJECTIVE_LOOT", item), restrict)
317 end end
319 if self.o.pos then for i, p in ipairs(self.o.pos) do
320 if not restrict or not self.qh:Disallowed(p[1]) then
321 objective:AddLoc(p[1], p[2], p[3], p[4], why)
323 end end
325 if self.fb.pos then for i, p in ipairs(self.fb.pos) do
326 if not restrict or not self.qh:Disallowed(p[1]) then
327 objective:AddLoc(p[1], p[2], p[3], p[4], why)
329 end end
331 if self.quest then
332 local item_list=self.quest.o.item
333 if item_list then
334 local data = item_list[self.obj]
335 if data and data.drop then
336 for monster, count in pairs(data.drop) do
337 local m = self.qh:GetObjective("monster", monster)
338 m:AppendPositions(objective, m.o.looted and count/m.o.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster), restrict)
340 elseif data and data.pos then
341 for i, p in ipairs(data.pos) do
342 if not restrict or not self.qh:Disallowed(p[1]) then
343 objective:AddLoc(p[1], p[2], p[3], p[4], why)
349 item_list=self.quest.fb.item
350 if item_list then
351 local data = item_list[self.obj]
352 if data and data.drop then
353 for monster, count in pairs(data.drop) do
354 local m = self.qh:GetObjective("monster", monster)
355 m:AppendPositions(objective, m.fb.looted and count/m.fb.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster), restrict)
357 elseif data and data.pos then
358 for i, p in ipairs(data.pos) do
359 if not restrict or not self.qh:Disallowed(p[1]) then
360 objective:AddLoc(p[1], p[2], p[3], p[4], why)
368 local function ItemDoMarkUsed(self)
369 if self.o.vendor then for i, npc in ipairs(self.o.vendor) do
370 local n = self.qh:GetObjective("monster", npc)
371 local faction = n.o.faction or n.fb.faction
372 if (not faction or faction == self.qh.faction) then
373 self:Uses(n, "TOOLTIP_PURCHASE")
375 end end
377 if self.fb.vendor then for i, npc in ipairs(self.fb.vendor) do
378 local n = self.qh:GetObjective("monster", npc)
379 local faction = n.o.faction or n.fb.faction
380 if (not faction or faction == self.qh.faction) then
381 self:Uses(n, "TOOLTIP_PURCHASE")
383 end end
385 if self.o.drop then for monster, count in pairs(self.o.drop) do
386 self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
387 end end
389 if self.fb.drop then for monster, count in pairs(self.fb.drop) do
390 self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
391 end end
393 if self.o.contained then for item, count in pairs(self.o.contained) do
394 self:Uses(self.qh:GetObjective("item", item), "TOOLTIP_LOOT")
395 end end
397 if self.fb.contained then for item, count in pairs(self.fb.contained) do
398 self:Uses(self.qh:GetObjective("item", item), "TOOLTIP_LOOT")
399 end end
401 if self.quest then
402 local item_list=self.quest.o.item
403 if item_list then
404 local data = item_list[self.obj]
405 if data and data.drop then
406 for monster, count in pairs(data.drop) do
407 self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
412 item_list=self.quest.fb.item
413 if item_list then
414 local data = item_list[self.obj]
415 if data and data.drop then
416 for monster, count in pairs(data.drop) do
417 self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
433 ---------------
447 local function AddLoc(self, index, x, y, w, why)
448 assert(not self.setup)
450 if w > 0 then
451 local pair = QuestHelper_ZoneLookup[index]
452 if not pair then return end -- that zone doesn't exist! We require more vespene gas. Not enough rage!
453 local c, z = pair[1], pair[2]
454 x, y = self.qh.Astrolabe:TranslateWorldMapPosition(c, z, x, y, c, 0)
456 x = x * self.qh.continent_scales_x[c]
457 y = y * self.qh.continent_scales_y[c]
458 local list = self.qh.zone_nodes[index]
460 local points = self.p[list]
461 if not points then
462 points = QuestHelper:CreateTable("objective.p[zone] (objective nodes per-zone)")
463 self.p[list] = points
466 for i, p in pairs(points) do
467 local u, v = x-p[3], y-p[4]
468 if u*u+v*v < 25 then -- Combine points within a threshold of 5 seconds travel time.
469 p[3] = (p[3]*p[5]+x*w)/(p[5]+w)
470 p[4] = (p[4]*p[5]+y*w)/(p[5]+w)
471 p[5] = p[5]+w
472 if w > p[7] then
473 p[6], p[7] = why, w
475 return
479 local new = QuestHelper:CreateTable("objective.p[zone] (possible objective node)")
480 new[1], new[2], new[3], new[4], new[5], new[6], new[7] = list, nil, x, y, w, why, w
481 table.insert(points, new)
485 local function FinishAddLoc(self, args)
486 local mx = 0
488 for z, pl in pairs(self.p) do
489 for i, p in ipairs(pl) do
490 if p[5] > mx then
491 self.location = p
492 mx = p[5]
497 if not self.zones then
498 -- Not using CreateTable, because it will not be released when routing is complete.
499 self.zones = {}
500 else
501 -- We could remove the already known zones, but I'm operating under the assumtion that locations will only be added,
502 -- not removed, so this isn't necessary.
505 -- Remove probably useless locations.
506 for z, pl in pairs(self.p) do
507 local remove_zone = true
508 local i = 1
509 while i <= #pl do
510 if pl[i][5] < mx*0.2 then
511 QuestHelper:ReleaseTable(pl[i])
512 table.remove(pl, i)
513 else
514 remove_zone = false
515 i = i + 1
518 if remove_zone then
519 QuestHelper:ReleaseTable(self.p[z])
520 self.p[z] = nil
521 else
522 self.zones[z.i] = true
526 local node_map = self.nm
527 local node_list = self.nl
529 for list, pl in pairs(self.p) do
530 local dist = self.d[list]
532 assert(not dist)
534 if not dist then
535 dist = QuestHelper:CreateTable("self.d[list]")
536 self.d[list] = dist
539 for i, point in ipairs(pl) do
540 point[5] = mx/point[5] -- Will become 1 for the most desired location, and become larger and larger for less desireable locations.
542 point[2] = QuestHelper:CreateTable("possible objective node to zone edge cache")
544 for i, node in ipairs(list) do
545 QuestHelper: Assert(type(point[3]) == "number", string.format("p3 %s", tostring(point[3])))
546 QuestHelper: Assert(type(point[4]) == "number", string.format("p4 %s", tostring(point[4])))
547 QuestHelper: Assert(type(node.x) == "number", string.format("nx %s", tostring(node.x)))
548 QuestHelper: Assert(type(node.y) == "number", string.format("ny %s", tostring(node.y)))
549 local u, v = point[3]-node.x, point[4]-node.y
550 local d = math.sqrt(u*u+v*v)
552 point[2][i] = d
554 if dist[i] then
555 if d*point[5] < dist[i][1]*dist[i][2] then
556 dist[i][1], dist[i][2] = d, point[5]
557 node_map[node] = point
559 else
560 local pair = QuestHelper:CreateTable()
561 pair[1], pair[2] = d, point[5]
562 dist[i] = pair
564 if not node_map[node] then
565 table.insert(node_list, node)
566 node_map[node] = point
567 else
568 u, v = node_map[node][3]-node.x, node_map[node][4]-node.y
570 if dist[i][1]*dist[i][2] < math.sqrt(u*u+v*v)*node_map[node][5] then
571 node_map[node] = point
579 -- 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.
580 --if not args or not args.failable then
581 -- if #node_list == 0 and QuestHelper:IsWrath() then QuestHelper:Error(self.cat.."/"..self.obj..": zero nodes!") end
582 --end
584 assert(not self.setup)
585 self.setup = true
586 table.insert(self.qh.prepared_objectives, self)
589 local function GetPosition(self)
590 assert(self.setup)
592 return self.location
595 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)
597 -- Note: Pos is the starting point, the objective is the destination. These are different data formats - "self" can be a set of points.
598 -- 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.
599 local function ObjectiveTravelTime(self, pos, nocache)
600 assert(self.setup)
602 -- The caching is pretty obvious.
603 local key, cached
604 if not nocache then
605 assert(pos ~= QuestHelper.pos)
606 if not pos.key then
607 pos.key = math.random()..""
609 key = pos.key
610 cached = self.distance_cache[key]
611 if cached then
612 if not QH_TESTCACHE then
613 return unpack(cached)
618 local graph = self.qh.world_graph
619 local nl = self.nl
621 graph:PrepareSearch()
623 -- 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.
624 for z, l in pairs(self.d) do
625 for i, n in ipairs(z) do
626 if n.s == 0 then
627 n.e, n.w = unpack(l[i])
628 n.s = 3
629 elseif n.e * n.w < l[i][1]*l[i][2] then
630 n.e, n.w = unpack(l[i])
635 local d = pos[2]
636 for i, n in ipairs(pos[1]) do
637 graph:AddStartNode(n, d[i], nl)
640 local e = graph:DoSearch(nl)
642 -- d changes datatype here. I hate this codebase. Hell, e probably changes datatype also! yaaaay. what does .nm mean? what does .d mean?
643 d = e.g+e.e
644 e = self.nm[e]
646 -- There's something going on with weighting here that I don't understand
647 local l = self.p[pos[1]]
648 if l then
649 local x, y = pos[3], pos[4]
650 local score = d*e[5]
652 for i, n in ipairs(l) do
653 local u, v = x-n[3], y-n[4]
654 local d2 = math.sqrt(u*u+v*v)
655 local s = d2*n[5]
656 if s < score then
657 d, e, score = d2, n, s
662 assert(e)
663 if not nocache then
664 assert( not cached or (cached[1] == d and cached[2] == e))
665 if not QH_TESTCACHE or not cached then
666 local new = self.qh:CreateTable()
667 new[1], new[2] = d, e
668 self.distance_cache[key] = new
669 self.qh:CacheRegister(self)
671 else
672 if self.distance_cache and self.distance_cache[key] then
673 assert(self.distance_cache[key][1] == d)
677 return d, e
680 -- Note: pos1 is the starting point, pos2 is the ending point, the objective is somewhere between them.
681 -- 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?
682 local function ObjectiveTravelTime2(self, pos1, pos2, nocache)
683 assert(self.setup)
685 -- caching is pretty simple as usual
686 local key, cached
687 if not nocache then
688 assert(pos1 ~= QuestHelper.pos)
689 assert(pos2 ~= QuestHelper.pos)
690 -- We don't want to cache distances involving the player's current position, as that would spam the table
691 if not pos1.key then
692 pos1.key = math.random()..""
694 if not pos2.key then
695 pos2.key = math.random()..""
697 key = pos1.key..pos2.key
698 cached = self.distance_cache[key]
699 if cached then
700 if not QH_TESTCACHE then
701 return unpack(cached)
706 local graph = self.qh.world_graph
707 local nl = self.nl
709 -- This is the standard pos1-to-self code that we're used to seeing . . .
710 graph:PrepareSearch()
712 for z, l in pairs(self.d) do
713 for i, n in ipairs(z) do
714 if n.s == 0 then
715 n.e, n.w = unpack(l[i])
716 n.s = 3
717 elseif n.e * n.w < l[i][1]*l[i][2] then
718 n.e, n.w = unpack(l[i])
723 local d = pos1[2]
724 for i, n in ipairs(pos1[1]) do
725 graph:AddStartNode(n, d[i], nl)
728 graph:DoFullSearch(nl)
730 graph:PrepareSearch()
732 -- . . . and here's where it gets wonky
733 -- Now, we need to figure out how long it takes to get to each node.
734 for z, point_list in pairs(self.p) do
735 if z == pos1[1] then
736 -- Will also consider min distance.
737 local x, y = pos1[3], pos1[4]
739 for i, p in ipairs(point_list) do
740 local a, b = p[3]-x, p[4]-y
741 local u, v = p[3], p[4]
742 local d = math.sqrt(a*a+b*b)
743 local w = p[5]
744 local score = d*w
745 for i, n in ipairs(z) do
746 a, b = n.x-u, n.y-v
747 local bleh = math.sqrt(a*a+b*b)+n.g
748 local s = bleh*w
749 if s < score then
750 d, score = bleh, d
753 p[7] = d
755 else
756 for i, p in ipairs(point_list) do
757 local x, y = p[3], p[4]
758 local w = p[5]
759 local d
760 local score
762 for i, n in ipairs(z) do
763 local a, b = n.x-x, n.y-y
764 local d2 = math.sqrt(a*a+b*b)+n.g
765 local s = d2*w
766 if not score or s < score then
767 d, score = d2, s
770 p[7] = d
775 d = pos2[2]
777 for i, n in ipairs(pos2[1]) do
778 n.e = d[i]
779 n.s = 3
782 local el = pos2[1]
783 local nm = self.nm2
785 for z, l in pairs(self.d) do
786 for i, n in ipairs(z) do
787 local x, y = n.x, n.y
788 local bp
789 local bg
790 local bs
791 for i, p in ipairs(self.p[z]) do
792 local a, b = x-p[3], y-p[4]
793 d = p[7]+math.sqrt(a*a+b*b)
794 s = d*p[5]
795 if not bs or s < bs then
796 bg, bp, bs = d, p, s
800 nm[n] = bp
801 -- Using score instead of distance, because we want nodes we're not really interested in to be less likely to get chosen.
802 graph:AddStartNode(n, bs, el)
806 local e = graph:DoSearch(pos2[1])
808 d = nm[e.p][7]
809 local d2 = e.g+e.e-e.p.g+(e.p.g/nm[e.p][5]-nm[e.p][7])
811 e = nm[e.p]
812 local total = (d+d2)*e[5]
814 if self.p[el] then
815 local x, y = pos2[3], pos2[4]
816 for i, p in ipairs(self.p[el]) do
817 local a, b = x-p[3], y-p[4]
818 local c = math.sqrt(a*a+b*b)
819 local t = (p[7]+c)*p[5]
820 if t < total then
821 total, d, d2, e = t, p[7], c, p
826 -- 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)
827 d = QuestHelper:ComputeTravelTime(pos1, e)
828 d2 = QuestHelper:ComputeTravelTime(e, pos2)
830 assert(e)
831 if not nocache then
832 assert( not cached or (cached[1] == d and cached[2] == d2 and cached[3] == e))
833 if not QH_TESTCACHE or not cached then
834 local new = self.qh:CreateTable("ObjectiveTravelTime2 cache")
835 new[1], new[2], new[3] = d, d2, e
836 self.distance_cache[key] = new
837 self.qh:CacheRegister(self)
839 else
840 if self.distance_cache and self.distance_cache[key] then
841 assert(self.distance_cache[key][1] == d and self.distance_cache[key][2] == d2)
845 --[[if pos1 and pos2 then -- Debug code so I can maybe actually fix the problems someday
846 QuestHelper:TextOut("Beginning dumping here")
848 local laxa = QuestHelper:ComputeTravelTime(pos1, e, true)
849 if math.abs(laxa-d) >= 0.0001 then
850 QuestHelper:TextOut(QuestHelper:StringizeTable(pos1))
851 QuestHelper:TextOut(QuestHelper:StringizeRecursive(pos1, 2))
852 QuestHelper:TextOut(QuestHelper:StringizeTable(e))
853 QuestHelper:TextOut(QuestHelper:StringizeTable(e[1]))
854 QuestHelper:TextOut(QuestHelper:StringizeTable(e[2]))
855 QuestHelper:TextOut(QuestHelper:StringizeRecursive(e[1], 2))]]
856 --QuestHelper:Assert(math.abs(laxa-d) < 0.0001, "Compare: "..laxa.." vs "..d) -- wonky commenting is thanks to the de-assert script, fix later
857 --[[end
858 local laxb = QuestHelper:ComputeTravelTime(e, pos2, true)
859 if math.abs(laxb-d2) >= 0.0001 then
860 QuestHelper:TextOut(QuestHelper:StringizeTable(pos2))
861 QuestHelper:TextOut(QuestHelper:StringizeTable(e))
862 QuestHelper:TextOut(QuestHelper:StringizeTable(e[1]))
863 QuestHelper:TextOut(QuestHelper:StringizeTable(e[2]))
864 QuestHelper:TextOut(QuestHelper:StringizeRecursive(e[1], 2))]]
865 --QuestHelper:Assert(math.abs(laxa-d) < 0.0001, "Compare: "..laxb.." vs "..d2)
866 --[[end
867 end]]
869 return d, d2, e
872 local function DoneRouting(self)
873 assert(self.setup_count > 0)
874 assert(self.setup)
876 if self.setup_count == 1 then
877 self.setup_count = 0
878 QuestHelper:ReleaseObjectivePathingInfo(self)
879 for i, obj in ipairs(self.qh.prepared_objectives) do
880 if o == obj then
881 table.remove(self.qh.prepared_objectives, i)
882 break
885 else
886 self.setup_count = self.setup_count - 1
890 local function IsObjectiveWatched(self)
891 -- Check if an objective is being watched. Note that this is an external query, not a simple Selector.
892 local info
894 if self.cat == "quest" then
895 info = QuestHelper.quest_log[self]
896 else
897 info = QuestHelper.quest_log[self.quest]
900 if info then
901 local index = info.index
902 if index then
903 if UberQuest then
904 -- UberQuest has it's own way of tracking quests.
905 local uq_settings = UberQuest_Config[UnitName("player")]
906 if uq_settings then
907 local list = uq_settings.selected
908 if list then
909 return list[GetQuestLogTitle(index)]
912 else
913 return IsQuestWatched(index)
918 return false
922 local next_objective_id = 0
924 local function ObjectiveShare(self)
925 self.want_share = true
928 local function ObjectiveUnshare(self)
929 self.want_share = false
932 QuestHelper.default_objective_param =
934 CouldBeFirst=ObjectiveCouldBeFirst,
936 Uses=Uses,
937 DoMarkUsed=DoMarkUsed,
938 MarkUsed=MarkUsed,
939 MarkUnused=MarkUnused,
941 DefaultKnown=DefaultObjectiveKnown,
942 Known=DummyObjectiveKnown,
943 Reason=ObjectiveReason,
945 AppendPositions=ObjectiveAppendPositions,
946 PrepareRouting=ObjectivePrepareRouting,
947 AddLoc=AddLoc,
948 FinishAddLoc=FinishAddLoc,
949 DoneRouting=DoneRouting,
951 Position=GetPosition,
952 TravelTime=ObjectiveTravelTime,
953 TravelTime2=ObjectiveTravelTime2,
955 IsWatched=IsObjectiveWatched,
957 Share=ObjectiveShare, -- Invoke to share this objective with your peers.
958 Unshare=ObjectiveUnshare, -- Invoke to stop sharing this objective.
961 QuestHelper.default_objective_item_param =
963 Known = ItemKnown,
964 AppendPositions = ItemAppendPositions,
965 DoMarkUsed = ItemDoMarkUsed
968 for key, value in pairs(QuestHelper.default_objective_param) do
969 if not QuestHelper.default_objective_item_param[key] then
970 QuestHelper.default_objective_item_param[key] = value
974 QuestHelper.default_objective_meta = { __index = QuestHelper.default_objective_param }
975 QuestHelper.default_objective_item_meta = { __index = QuestHelper.default_objective_item_param }
977 function QuestHelper:NewObjectiveObject()
978 next_objective_id = next_objective_id+1
979 return
980 setmetatable({
981 qh=self,
982 id=next_objective_id,
984 want_share=false, -- True if we want this objective shared.
985 is_sharing=false, -- Set to true if we've told other users about this objective.
987 user_ignore=nil, -- When nil, will use filters. Will ignore, when true, always show (if known).
989 priority=3, -- A hint as to what priority the quest should have. Should be 1, 2, 3, 4, or 5.
990 real_priority=3, -- This will be set to the priority routing actually decided to assign it.
992 setup_count=0,
994 icon_id=12,
995 icon_bg=14,
997 match_zone=false,
998 match_level=false,
999 match_done=false,
1001 before={}, -- List of objectives that this objective must appear before.
1002 after={}, -- List of objectives that this objective must appear after.
1004 -- Routing related junk.
1006 --[[ Will be created as needed.
1007 d=nil,
1008 p=nil,
1009 nm=nil, -- Maps nodes to their nearest zone/list/x/y position.
1010 nm2=nil, -- Maps nodes to their nears position, but dynamically set in TravelTime2.
1011 nl=nil, -- List of all the nodes we need to consider.
1012 location=nil, -- Will be set to the best position for the node.
1013 pos=nil, -- Zone node list, distance list, x, y, reason.
1014 sop=nil ]]
1015 }, QuestHelper.default_objective_meta)
1018 local explicit_support_warning_given = false
1020 function QuestHelper:GetObjective(category, objective)
1021 local objective_list = self.objective_objects[category]
1023 if not objective_list then
1024 objective_list = {}
1025 self.objective_objects[category] = objective_list
1028 local objective_object = objective_list[objective]
1030 if not objective_object then
1031 if category == "quest" then
1032 local level, hash, name = string.match(objective, "^(%d+)/(%d*)/(.*)$")
1033 if not level then
1034 level, name = string.match(objective, "^(%d+)/(.*)$")
1035 if not level then
1036 name = objective
1040 if hash == "" then hash = nil end
1041 objective_object = self:GetQuest(name, tonumber(level), tonumber(hash))
1042 objective_list[objective] = objective_object
1043 return objective_object
1046 objective_object = self:NewObjectiveObject()
1048 objective_object.cat = category
1049 objective_object.obj = objective
1051 if category == "item" then
1052 setmetatable(objective_object, QuestHelper.default_objective_item_meta)
1053 objective_object.icon_id = 2
1054 elseif category == "monster" then
1055 objective_object.icon_id = 1
1056 elseif category == "object" then
1057 objective_object.icon_id = 3
1058 elseif category == "event" then
1059 objective_object.icon_id = 4
1060 elseif category == "loc" then
1061 objective_object.icon_id = 6
1062 elseif category == "reputation" then
1063 objective_object.icon_id = 5
1064 elseif category == "player" then
1065 objective_object.icon_id = 1 -- not ideal, will improve later
1066 else
1067 if not explicit_support_warning_given then
1068 self:TextOut("FIXME: Objective type '"..category.."' for objective '"..objective.."' isn't explicitly supported yet; hopefully the dummy handler will do something sensible.")
1069 explicit_support_warning_given = true
1073 objective_list[objective] = objective_object
1075 if category == "loc" then
1076 -- Loc is special, we don't store it, and construct it from the string.
1077 -- Don't have any error checking here, will assume it's correct.
1078 local i
1079 local _, _, c, z, x, y = string.find(objective,"^(%d+),(%d+),([%d%.]+),([%d%.]+)$")
1081 if not y then
1082 _, _, i, x, y = string.find(objective,"^(%d+),([%d%.]+),([%d%.]+)$")
1083 else
1084 i = QuestHelper_IndexLookup[c][z]
1087 objective_object.o = {pos={{tonumber(i),tonumber(x),tonumber(y),1}}}
1088 objective_object.fb = {}
1089 else
1090 objective_list = QuestHelper_Objectives_Local[category]
1091 if not objective_list then
1092 objective_list = {}
1093 QuestHelper_Objectives_Local[category] = objective_list
1095 objective_object.o = objective_list[objective]
1096 if not objective_object.o then
1097 objective_object.o = {}
1098 objective_list[objective] = objective_object.o
1100 local l = QuestHelper_StaticData[self.locale]
1101 if l then
1102 objective_list = l.objective[category]
1103 if objective_list then
1104 objective_object.fb = objective_list[objective]
1107 if not objective_object.fb then
1108 objective_object.fb = {}
1111 -- TODO: If we have some other source of information (like LightHeaded) add its data to objective_object.fb
1116 return objective_object
1119 function QuestHelper:AppendObjectivePosition(objective, i, x, y, w)
1120 if not i then return end -- We don't have a player position. We have a pile of poop. Enjoy your poop.
1122 local pos = objective.o.pos
1123 if not pos then
1124 if objective.o.drop or objective.o.contained then
1125 return -- If it's dropped by a monster, don't record the position we got the item at.
1127 objective.o.pos = self:AppendPosition({}, i, x, y, w)
1128 else
1129 self:AppendPosition(pos, i, x, y, w)
1133 function QuestHelper:AppendObjectiveDrop(objective, monster, count)
1134 local drop = objective.o.drop
1135 if drop then
1136 drop[monster] = (drop[monster] or 0)+(count or 1)
1137 else
1138 objective.o.drop = {[monster] = count or 1}
1139 objective.o.pos = nil -- If it's dropped by a monster, then forget the position we found it at.
1143 function QuestHelper:AppendItemObjectiveDrop(item_object, item_name, monster_name, count)
1144 local quest = self:ItemIsForQuest(item_object, item_name)
1145 if quest and not item_object.o.vendor and not item_object.o.drop and not item_object.o.pos then
1146 self:AppendQuestDrop(quest, item_name, monster_name, count)
1147 else
1148 if not item_object.o.drop and not item_object.o.pos then
1149 self:PurgeQuestItem(item_object, item_name)
1151 self:AppendObjectiveDrop(item_object, monster_name, count)
1155 function QuestHelper:AppendItemObjectivePosition(item_object, item_name, i, x, y)
1156 local quest = self:ItemIsForQuest(item_object, item_name)
1157 if quest and not item_object.o.vendor and not item_object.o.drop and not item_object.o.pos then
1158 self:AppendQuestPosition(quest, item_name, i, x, y)
1159 else
1160 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
1161 -- Just learned that this item doesn't depend on a quest to drop, remove any quest references to it.
1162 self:PurgeQuestItem(item_object, item_name)
1164 self:AppendObjectivePosition(item_object, i, x, y)
1168 function QuestHelper:AppendItemObjectiveContainer(objective, container_name, count)
1169 local container = objective.o.contained
1170 if container then
1171 container[container_name] = (container[container_name] or 0)+(count or 1)
1172 else
1173 objective.o.contained = {[container_name] = count or 1}
1174 objective.o.pos = nil -- Forget the position.
1178 function QuestHelper:AddObjectiveWatch(objective, reason)
1179 if not objective.reasons then
1180 objective.reasons = {}
1183 if not next(objective.reasons, nil) then
1184 objective.watched = true
1185 objective:MarkUsed()
1187 objective.filter_blocked = false
1188 for obj in pairs(objective.swap_after or objective.after) do
1189 if obj.watched then
1190 objective.filter_blocked = true
1191 break
1195 for obj in pairs(objective.swap_before or objective.before) do
1196 if obj.watched then
1197 obj.filter_blocked = true
1201 if self.to_remove[objective] then
1202 self.to_remove[objective] = nil
1203 else
1204 self.to_add[objective] = true
1208 objective.reasons[reason] = (objective.reasons[reason] or 0) + 1
1211 function QuestHelper:RemoveObjectiveWatch(objective, reason)
1212 if objective.reasons[reason] == 1 then
1213 objective.reasons[reason] = nil
1214 if not next(objective.reasons, nil) then
1215 objective:MarkUnused()
1216 objective.watched = false
1218 for obj in pairs(objective.swap_before or objective.before) do
1219 if obj.watched then
1220 obj.filter_blocked = false
1221 for obj2 in pairs(obj.swap_after or obj.after) do
1222 if obj2.watched then
1223 obj.filter_blocked = true
1224 break
1230 if self.to_add[objective] then
1231 self.to_add[objective] = nil
1232 else
1233 self.to_remove[objective] = true
1236 else
1237 objective.reasons[reason] = objective.reasons[reason] - 1
1241 function QuestHelper:ObjectiveObjectDependsOn(objective, needs)
1242 assert(objective ~= needs) -- If this was true, ObjectiveIsKnown would get in an infinite loop.
1243 -- TODO: Needs sanity checking, especially now that dependencies can be assigned by remote users.
1246 -- We store the new relationships in objective.swap_[before|after],
1247 -- creating and copying them from objective.[before|after],
1248 -- the routing coroutine will check for those, swap them, and release the originals
1249 -- when it gets to a safe place to do so.
1251 if not (objective.swap_after or objective.after)[needs] then
1252 if objective.peer then
1253 for u, l in pairs(objective.peer) do
1254 -- Make sure other users know that the dependencies for this objective changed.
1255 objective.peer[u] = math.min(l, 1)
1259 if not objective.swap_after then
1260 objective.swap_after = self:CreateTable("swap_after")
1261 for key,value in pairs(objective.after) do objective.swap_after[key] = value end
1264 if not needs.swap_before then
1265 needs.swap_before = self:CreateTable("swap_before")
1266 for key,value in pairs(needs.before) do needs.swap_before[key] = value end
1269 if needs.watched then
1270 objective.filter_blocked = true
1273 objective.swap_after[needs] = true
1274 needs.swap_before[objective] = true
1278 local UserIgnored = {
1279 name = "user_manual_ignored",
1280 no_disable = true,
1281 friendly_reason = QHText("FILTERED_USER"),
1282 AddException = function(self, node)
1283 QH_Route_UnignoreNode(node, self) -- there isn't really state with this one
1287 function QuestHelper:AddObjectiveOptionsToMenu(obj, menu)
1288 local submenu = self:CreateMenu()
1290 local pri = (QH_Route_GetClusterPriority(obj.cluster) or 0) + 3
1291 for i = 1, 5 do
1292 local name = QHText("PRIORITY"..i)
1293 local item = self:CreateMenuItem(submenu, name)
1294 local tex
1296 if pri == i then
1297 tex = self:CreateIconTexture(item, 10)
1298 else
1299 tex = self:CreateIconTexture(item, 12)
1300 tex:SetVertexColor(1, 1, 1, 0)
1303 item:AddTexture(tex, true)
1304 item:SetFunction(QH_Route_SetClusterPriority, obj.cluster, i - 3)
1307 self:CreateMenuItem(menu, QHText("PRIORITY")):SetSubmenu(submenu)
1309 --[[if self.sharing then
1310 submenu = self:CreateMenu()
1311 local item = self:CreateMenuItem(submenu, QHText("SHARING_ENABLE"))
1312 local tex = self:CreateIconTexture(item, 10)
1313 if not obj.want_share then tex:SetVertexColor(1, 1, 1, 0) end
1314 item:AddTexture(tex, true)
1315 item:SetFunction(obj.Share, obj)
1317 local item = self:CreateMenuItem(submenu, QHText("SHARING_DISABLE"))
1318 local tex = self:CreateIconTexture(item, 10)
1319 if obj.want_share then tex:SetVertexColor(1, 1, 1, 0) end
1320 item:AddTexture(tex, true)
1321 item:SetFunction(obj.Unshare, obj)
1323 self:CreateMenuItem(menu, QHText("SHARING")):SetSubmenu(submenu)
1324 end]]
1326 --self:CreateMenuItem(menu, "(No options available)")
1328 if obj.cluster then
1329 self:CreateMenuItem(menu, QHText("IGNORE")):SetFunction(function () for _, v in ipairs(obj.cluster) do QH_Route_IgnoreNode(v, UserIgnored) end end)
1332 self:CreateMenuItem(menu, QHText("IGNORE_LOCATION")):SetFunction(QH_Route_IgnoreNode, obj, UserIgnored)
1335 function QuestHelper:IgnoreObjective(objective)
1336 if self.user_objectives[objective] then
1337 self:RemoveObjectiveWatch(objective, self.user_objectives[objective])
1338 self.user_objectives[objective] = nil
1339 else
1340 objective.user_ignore = true
1343 --self:ForceRouteUpdate()
1346 function QuestHelper:SetObjectivePriority(objective, level)
1347 level = math.min(5, math.max(1, math.floor((tonumber(level) or 3)+0.5)))
1348 if level ~= objective.priority then
1349 objective.priority = level
1350 if objective.peer then
1351 for u, l in pairs(objective.peer) do
1352 -- Peers don't know about this new priority.
1353 objective.peer[u] = math.min(l, 2)
1356 --self:ForceRouteUpdate()
1360 local function CalcObjectivePriority(obj)
1361 local priority = obj.priority
1363 for o in pairs(obj.before) do
1364 if o.watched then
1365 priority = math.min(priority, CalcObjectivePriority(o))
1369 return priority
1372 local function ApplyBlockPriority(obj, level)
1373 for o in pairs(obj.before) do
1374 if o.watched then
1375 ApplyBlockPriority(o, level)
1379 if obj.priority < level then QuestHelper:SetObjectivePriority(obj, level) end
1382 function QuestHelper:SetObjectivePriorityPrompt(objective, level)
1383 self:SetObjectivePriority(objective, level)
1384 if CalcObjectivePriority(objective) ~= level then
1385 local menu = self:CreateMenu()
1386 self:CreateMenuTitle(menu, QHText("IGNORED_PRIORITY_TITLE"))
1387 self:CreateMenuItem(menu, QHText("IGNORED_PRIORITY_FIX")):SetFunction(ApplyBlockPriority, objective, level)
1388 self:CreateMenuItem(menu, QHText("IGNORED_PRIORITY_IGNORE")):SetFunction(self.nop)
1389 menu:ShowAtCursor()
1393 function QuestHelper:SetObjectiveProgress(objective, user, have, need)
1394 if have and need then
1395 local list = objective.progress
1396 if not list then
1397 list = self:CreateTable("objective.progress")
1398 objective.progress = list
1401 local user_progress = list[user]
1402 if not user_progress then
1403 user_progress = self:CreateTable("objective.progress[user]")
1404 list[user] = user_progress
1407 local pct = 0
1408 local a, b = tonumber(have), tonumber(need)
1409 if a and b then
1410 if b ~= 0 then
1411 pct = a/b
1412 elseif a == 0 then
1413 pct = 1
1415 elseif a == b then
1416 pct = 1
1419 user_progress[1], user_progress[2], user_progress[3] = have, need, pct
1420 else
1421 if objective.progress then
1422 if objective.progress[user] then
1423 self:ReleaseTable(objective.progress[user])
1424 objective.progress[user] = nil
1426 if not next(objective.progress, nil) then
1427 self:ReleaseTable(objective.progress)
1428 objective.progress = nil