remove extra +x
[QuestHelper.git] / objective.lua
blob946a09e3be67806a61702673df5af52c6c2d9517
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 function QuestHelper:GetObjective(category, objective)
1014 local objective_list = self.objective_objects[category]
1016 if not objective_list then
1017 objective_list = {}
1018 self.objective_objects[category] = objective_list
1021 local objective_object = objective_list[objective]
1023 if not objective_object then
1024 if category == "quest" then
1025 local level, hash, name = string.match(objective, "^(%d+)/(%d*)/(.*)$")
1026 if not level then
1027 level, name = string.match(objective, "^(%d+)/(.*)$")
1028 if not level then
1029 name = objective
1033 if hash == "" then hash = nil end
1034 objective_object = self:GetQuest(name, tonumber(level), tonumber(hash))
1035 objective_list[objective] = objective_object
1036 return objective_object
1039 objective_object = self:NewObjectiveObject()
1041 objective_object.cat = category
1042 objective_object.obj = objective
1044 if category == "item" then
1045 setmetatable(objective_object, QuestHelper.default_objective_item_meta)
1046 objective_object.icon_id = 2
1047 elseif category == "monster" then
1048 objective_object.icon_id = 1
1049 elseif category == "object" then
1050 objective_object.icon_id = 3
1051 elseif category == "event" then
1052 objective_object.icon_id = 4
1053 elseif category == "loc" then
1054 objective_object.icon_id = 6
1055 elseif category == "reputation" then
1056 objective_object.icon_id = 5
1057 else
1058 self:TextOut("FIXME: Objective type '"..category.."' for objective '"..objective.."' isn't explicitly supported yet; hopefully the dummy handler will do something sensible.")
1061 objective_list[objective] = objective_object
1063 if category == "loc" then
1064 -- Loc is special, we don't store it, and construct it from the string.
1065 -- Don't have any error checking here, will assume it's correct.
1066 local i
1067 local _, _, c, z, x, y = string.find(objective,"^(%d+),(%d+),([%d%.]+),([%d%.]+)$")
1069 if not y then
1070 _, _, i, x, y = string.find(objective,"^(%d+),([%d%.]+),([%d%.]+)$")
1071 else
1072 i = QuestHelper_IndexLookup[c][z]
1075 objective_object.o = {pos={{tonumber(i),tonumber(x),tonumber(y),1}}}
1076 objective_object.fb = {}
1077 else
1078 objective_list = QuestHelper_Objectives_Local[category]
1079 if not objective_list then
1080 objective_list = {}
1081 QuestHelper_Objectives_Local[category] = objective_list
1083 objective_object.o = objective_list[objective]
1084 if not objective_object.o then
1085 objective_object.o = {}
1086 objective_list[objective] = objective_object.o
1088 local l = QuestHelper_StaticData[self.locale]
1089 if l then
1090 objective_list = l.objective[category]
1091 if objective_list then
1092 objective_object.fb = objective_list[objective]
1095 if not objective_object.fb then
1096 objective_object.fb = {}
1099 -- TODO: If we have some other source of information (like LightHeaded) add its data to objective_object.fb
1104 return objective_object
1107 function QuestHelper:AppendObjectivePosition(objective, i, x, y, w)
1108 local pos = objective.o.pos
1109 if not pos then
1110 if objective.o.drop or objective.o.contained then
1111 return -- If it's dropped by a monster, don't record the position we got the item at.
1113 objective.o.pos = self:AppendPosition({}, i, x, y, w)
1114 else
1115 self:AppendPosition(pos, i, x, y, w)
1119 function QuestHelper:AppendObjectiveDrop(objective, monster, count)
1120 local drop = objective.o.drop
1121 if drop then
1122 drop[monster] = (drop[monster] or 0)+(count or 1)
1123 else
1124 objective.o.drop = {[monster] = count or 1}
1125 objective.o.pos = nil -- If it's dropped by a monster, then forget the position we found it at.
1129 function QuestHelper:AppendItemObjectiveDrop(item_object, item_name, monster_name, count)
1130 local quest = self:ItemIsForQuest(item_object, item_name)
1131 if quest and not item_object.o.vendor and not item_object.o.drop and not item_object.o.pos then
1132 self:AppendQuestDrop(quest, item_name, monster_name, count)
1133 else
1134 if not item_object.o.drop and not item_object.o.pos then
1135 self:PurgeQuestItem(item_object, item_name)
1137 self:AppendObjectiveDrop(item_object, monster_name, count)
1141 function QuestHelper:AppendItemObjectivePosition(item_object, item_name, i, x, y)
1142 local quest = self:ItemIsForQuest(item_object, item_name)
1143 if quest and not item_object.o.vendor and not item_object.o.drop and not item_object.o.pos then
1144 self:AppendQuestPosition(quest, item_name, i, x, y)
1145 else
1146 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
1147 -- Just learned that this item doesn't depend on a quest to drop, remove any quest references to it.
1148 self:PurgeQuestItem(item_object, item_name)
1150 self:AppendObjectivePosition(item_object, i, x, y)
1154 function QuestHelper:AppendItemObjectiveContainer(objective, container_name, count)
1155 local container = objective.o.contained
1156 if container then
1157 container[container_name] = (container[container_name] or 0)+(count or 1)
1158 else
1159 objective.o.contained = {[container_name] = count or 1}
1160 objective.o.pos = nil -- Forget the position.
1164 function QuestHelper:AddObjectiveWatch(objective, reason)
1165 if not objective.reasons then
1166 objective.reasons = {}
1169 if not next(objective.reasons, nil) then
1170 objective.watched = true
1171 objective:MarkUsed()
1173 objective.filter_blocked = false
1174 for obj in pairs(objective.swap_after or objective.after) do
1175 if obj.watched then
1176 objective.filter_blocked = true
1177 break
1181 for obj in pairs(objective.swap_before or objective.before) do
1182 if obj.watched then
1183 obj.filter_blocked = true
1187 if self.to_remove[objective] then
1188 self.to_remove[objective] = nil
1189 else
1190 self.to_add[objective] = true
1194 objective.reasons[reason] = (objective.reasons[reason] or 0) + 1
1197 function QuestHelper:RemoveObjectiveWatch(objective, reason)
1198 if objective.reasons[reason] == 1 then
1199 objective.reasons[reason] = nil
1200 if not next(objective.reasons, nil) then
1201 objective:MarkUnused()
1202 objective.watched = false
1204 for obj in pairs(objective.swap_before or objective.before) do
1205 if obj.watched then
1206 obj.filter_blocked = false
1207 for obj2 in pairs(obj.swap_after or obj.after) do
1208 if obj2.watched then
1209 obj.filter_blocked = true
1210 break
1216 if self.to_add[objective] then
1217 self.to_add[objective] = nil
1218 else
1219 self.to_remove[objective] = true
1222 else
1223 objective.reasons[reason] = objective.reasons[reason] - 1
1227 function QuestHelper:ObjectiveObjectDependsOn(objective, needs)
1228 assert(objective ~= needs) -- If this was true, ObjectiveIsKnown would get in an infinite loop.
1229 -- TODO: Needs sanity checking, especially now that dependencies can be assigned by remote users.
1232 -- We store the new relationships in objective.swap_[before|after],
1233 -- creating and copying them from objective.[before|after],
1234 -- the routing coroutine will check for those, swap them, and release the originals
1235 -- when it gets to a safe place to do so.
1237 if not (objective.swap_after or objective.after)[needs] then
1238 if objective.peer then
1239 for u, l in pairs(objective.peer) do
1240 -- Make sure other users know that the dependencies for this objective changed.
1241 objective.peer[u] = math.min(l, 1)
1245 if not objective.swap_after then
1246 objective.swap_after = self:CreateTable("swap_after")
1247 for key,value in pairs(objective.after) do objective.swap_after[key] = value end
1250 if not needs.swap_before then
1251 needs.swap_before = self:CreateTable("swap_before")
1252 for key,value in pairs(needs.before) do needs.swap_before[key] = value end
1255 if needs.watched then
1256 objective.filter_blocked = true
1259 objective.swap_after[needs] = true
1260 needs.swap_before[objective] = true
1264 function QuestHelper:AddObjectiveOptionsToMenu(obj, menu)
1265 local submenu = self:CreateMenu()
1267 for i = 1,5 do
1268 local name = QHText("PRIORITY"..i)
1269 local item = self:CreateMenuItem(submenu, name)
1270 local tex
1272 if obj.priority == i then
1273 tex = self:CreateIconTexture(item, 10)
1274 elseif obj.real_priority == i then
1275 tex = self:CreateIconTexture(item, 8)
1276 else
1277 tex = self:CreateIconTexture(item, 12)
1278 tex:SetVertexColor(1, 1, 1, 0)
1281 item:AddTexture(tex, true)
1282 item:SetFunction(self.SetObjectivePriorityPrompt, self, obj, i)
1285 self:CreateMenuItem(menu, QHText("PRIORITY")):SetSubmenu(submenu)
1287 if self.sharing then
1288 submenu = self:CreateMenu()
1289 local item = self:CreateMenuItem(submenu, QHText("SHARING_ENABLE"))
1290 local tex = self:CreateIconTexture(item, 10)
1291 if not obj.want_share then tex:SetVertexColor(1, 1, 1, 0) end
1292 item:AddTexture(tex, true)
1293 item:SetFunction(obj.Share, obj)
1295 local item = self:CreateMenuItem(submenu, QHText("SHARING_DISABLE"))
1296 local tex = self:CreateIconTexture(item, 10)
1297 if obj.want_share then tex:SetVertexColor(1, 1, 1, 0) end
1298 item:AddTexture(tex, true)
1299 item:SetFunction(obj.Unshare, obj)
1301 self:CreateMenuItem(menu, QHText("SHARING")):SetSubmenu(submenu)
1304 self:CreateMenuItem(menu, QHText("IGNORE")):SetFunction(self.IgnoreObjective, self, obj)
1307 function QuestHelper:IgnoreObjective(objective)
1308 if self.user_objectives[objective] then
1309 self:RemoveObjectiveWatch(objective, self.user_objectives[objective])
1310 self.user_objectives[objective] = nil
1311 else
1312 objective.user_ignore = true
1315 --self:ForceRouteUpdate()
1318 function QuestHelper:SetObjectivePriority(objective, level)
1319 level = math.min(5, math.max(1, math.floor((tonumber(level) or 3)+0.5)))
1320 if level ~= objective.priority then
1321 objective.priority = level
1322 if objective.peer then
1323 for u, l in pairs(objective.peer) do
1324 -- Peers don't know about this new priority.
1325 objective.peer[u] = math.min(l, 2)
1328 --self:ForceRouteUpdate()
1332 local function CalcObjectivePriority(obj)
1333 local priority = obj.priority
1335 for o in pairs(obj.before) do
1336 if o.watched then
1337 priority = math.min(priority, CalcObjectivePriority(o))
1341 return priority
1344 local function ApplyBlockPriority(obj, level)
1345 for o in pairs(obj.before) do
1346 if o.watched then
1347 ApplyBlockPriority(o, level)
1351 if obj.priority < level then QuestHelper:SetObjectivePriority(obj, level) end
1354 function QuestHelper:SetObjectivePriorityPrompt(objective, level)
1355 self:SetObjectivePriority(objective, level)
1356 if CalcObjectivePriority(objective) ~= level then
1357 local menu = self:CreateMenu()
1358 self:CreateMenuTitle(menu, QHText("IGNORED_PRIORITY_TITLE"))
1359 self:CreateMenuItem(menu, QHText("IGNORED_PRIORITY_FIX")):SetFunction(ApplyBlockPriority, objective, level)
1360 self:CreateMenuItem(menu, QHText("IGNORED_PRIORITY_IGNORE")):SetFunction(self.nop)
1361 menu:ShowAtCursor()
1365 function QuestHelper:SetObjectiveProgress(objective, user, have, need)
1366 if have and need then
1367 local list = objective.progress
1368 if not list then
1369 list = self:CreateTable("objective.progress")
1370 objective.progress = list
1373 local user_progress = list[user]
1374 if not user_progress then
1375 user_progress = self:CreateTable("objective.progress[user]")
1376 list[user] = user_progress
1379 local pct = 0
1380 local a, b = tonumber(have), tonumber(need)
1381 if a and b then
1382 if b ~= 0 then
1383 pct = a/b
1384 elseif a == 0 then
1385 pct = 1
1387 elseif a == b then
1388 pct = 1
1391 user_progress[1], user_progress[2], user_progress[3] = have, need, pct
1392 else
1393 if objective.progress then
1394 if objective.progress[user] then
1395 self:ReleaseTable(objective.progress[user])
1396 objective.progress[user] = nil
1398 if not next(objective.progress, nil) then
1399 self:ReleaseTable(objective.progress)
1400 objective.progress = nil