Fixed a bug where an objective could be added to the route multiple times if inserted...
[QuestHelper.git] / flightpath.lua
blob973bcdd182111eedb33ab7ba5f7f7df6be3551ff
1 local real_TakeTaxiNode = TakeTaxiNode
2 local real_TaxiNodeOnButtonEnter= TaxiNodeOnButtonEnter
4 assert(type(real_TakeTaxiNode) == "function")
5 assert(type(real_TaxiNodeOnButtonEnter) == "function")
7 local function LookupName(x, y)
8 local best, d2
9 for i = 1,NumTaxiNodes() do
10 local u, v = TaxiNodePosition(i)
11 u = u - x
12 v = v - y
13 u = u*u+v*v
14 if not best or u < d2 then
15 best, d2 = TaxiNodeName(i), u
16 end
17 end
19 return best
20 end
22 local function getRoute(id)
23 for i = 1,NumTaxiNodes() do
24 if GetNumRoutes(i) == 0 then
25 local routes = GetNumRoutes(id)
26 if routes and routes > 0 and routes < 100 then
27 local origin, dest = TaxiNodeName(i), TaxiNodeName(id)
28 local path_hash = 0
30 if routes > 1 then
31 local path_str = ""
33 for j = 1,routes-1 do
34 path_str = string.format("%s/%s", path_str, LookupName(TaxiGetDestX(id, j), TaxiGetDestY(id, j)))
35 end
37 path_hash = QuestHelper:HashString(path_str)
38 end
40 return origin, dest, path_hash
41 end
42 end
43 end
44 end
46 TaxiNodeOnButtonEnter = function(btn)
47 real_TaxiNodeOnButtonEnter(btn)
49 if QuestHelper_Pref.flight_time then
50 local index = btn:GetID()
51 if TaxiNodeGetType(index) == "REACHABLE" then
52 local origin, dest, hash = getRoute(index)
53 local eta, estimate = nil, false
54 if origin then
55 eta = QuestHelper:computeLinkTime(origin, dest, hash, false)
56 if not eta then
57 eta = QuestHelper.flight_times[origin] and QuestHelper.flight_times[origin][dest]
58 estimate = true
59 end
60 end
62 if eta then -- Going to replace the tooltip.
63 GameTooltip:SetOwner(btn, "ANCHOR_RIGHT")
64 GameTooltip:ClearLines()
65 GameTooltip:AddLine(dest, "", 1.0, 1.0, 1.0)
66 GameTooltip:AddDoubleLine(QHText("TRAVEL_ESTIMATE"), (estimate and "|cffffffff≈|r " or "")..QHFormat("TRAVEL_ESTIMATE_VALUE", eta))
67 local cost = TaxiNodeCost(index)
68 if cost > 0 then
69 SetTooltipMoney(GameTooltip, cost)
70 end
71 GameTooltip:Show()
72 end
73 end
74 end
75 end
77 TakeTaxiNode = function(id)
78 local origin, dest, hash = getRoute(id)
80 if origin then
81 local flight_data = QuestHelper.flight_data
82 if not flight_data then
83 flight_data = QuestHelper:CreateTable()
84 QuestHelper.flight_data = flight_data
85 end
87 flight_data.origin = origin
88 flight_data.dest = dest
89 flight_data.hash = hash
90 flight_data.start_time = nil
91 flight_data.end_time = nil
92 flight_data.end_time_estimate = nil
93 end
95 real_TakeTaxiNode(id)
96 end
98 function QuestHelper:processFlightData(data)
99 local npc = self:getFlightInstructor(data.dest)
100 if not npc then
101 self:TextOut(QHText("TALK_TO_FLIGHT_MASTER"))
102 return false
105 local npc_obj = self:GetObjective("monster", npc)
106 npc_obj:PrepareRouting()
108 local pos = npc_obj:Position()
109 if not pos then
110 -- Don't know te location of the flight instructor.
111 self:TextOut(QHText("TALK_TO_FLIGHT_MASTER"))
112 npc_obj:DoneRouting()
113 return false
116 local correct = true
118 if pos[1].c ~= self.c then
119 correct = false
120 else
121 local x, y = self.Astrolabe:TranslateWorldMapPosition(self.c, self.z, self.x, self.y, self.c, 0)
122 x = x * self.continent_scales_x[self.c]
123 y = y * self.continent_scales_y[self.c]
124 local t = (x-pos[3])*(x-pos[3])+(y-pos[4])*(y-pos[4])
126 if t > 5*5 then
127 correct = false
131 npc_obj:DoneRouting()
133 if not correct then
134 return true
137 if data.start_time and data.end_time and data.end_time > data.start_time then
138 local routes = QuestHelper_FlightRoutes[self.faction]
139 if not routes then
140 routes = {}
141 QuestHelper_FlightRoutes[self.faction] = routes
144 local origin = routes[data.origin]
145 if not origin then
146 origin = {}
147 routes[data.origin] = origin
150 local dest = origin[data.dest]
151 if not dest then
152 dest = {}
153 origin[data.dest] = dest
156 dest[data.hash] = data.end_time - data.start_time
159 return true
162 function QuestHelper:getFlightInstructor(area)
163 local fi_table = QuestHelper_FlightInstructors[self.faction]
164 if fi_table then
165 local npc = fi_table[area]
166 if npc then
167 return npc
171 local static = QuestHelper_StaticData[QuestHelper_Locale]
173 if static then
174 fi_table = static.flight_instructors and static.flight_instructors[self.faction]
175 if fi_table then
176 return fi_table[area]
181 local function getTime(tbl, orig, dest, hash)
182 tbl = tbl and tbl[orig]
183 tbl = tbl and tbl[dest]
184 return tbl and tbl[hash] ~= true and tbl[hash]
187 local function getWalkToFlight(tbl, fi1, fi2)
188 local f, w = 0, 0
190 if tbl then
191 for origin, list in pairs(tbl) do
192 for dest, hashlist in pairs(list) do
193 if type(hashlist[0]) == "number" then
194 local npc1, npc2 = (fi1 and fi1[origin]) or (fi2 and fi2[origin]), (fi1 and fi1[dest]) or (fi2 and fi2[dest])
195 if npc1 and npc2 then
196 local obj1, obj2 = QuestHelper:GetObjective("monster", npc1), QuestHelper:GetObjective("monster", npc2)
197 obj1:PrepareRouting()
198 obj2:PrepareRouting()
200 local pos1, pos2 = obj1:Position(), obj2:Position()
202 if pos1 and pos2 then
203 local x, y = pos1[3]-pos2[3], pos1[4]-pos2[4]
204 w = w + math.sqrt(x*x+y*y)
205 f = f + hashlist[0]
208 obj2:DoneRouting()
209 obj1:DoneRouting()
216 return f, w
219 function QuestHelper:computeWalkToFlightMult()
220 local l = QuestHelper_FlightRoutes[self.faction]
221 local s = QuestHelper_StaticData[self.locale]
222 s = s and s.flight_routes
223 s = s and s[self.faction]
225 local fi1 = QuestHelper_FlightInstructors[self.faction]
226 local fi2 = QuestHelper_StaticData[self.locale]
227 fi2 = fi2 and fi2.flight_instructors
228 fi2 = fi2 and fi2[self.faction]
230 local f1, w1 = getWalkToFlight(l, fi1, fi2)
231 local f2, w2 = getWalkToFlight(s, fi1, fi2)
232 return (f1+f2+0.032876)/(w1+w2+0.1)
235 function QuestHelper:computeLinkTime(origin, dest, hash, fallback)
236 -- Only works for directly connected flight points.
238 if origin == dest then
239 return 0
242 local l = QuestHelper_FlightRoutes[self.faction]
243 local s = QuestHelper_StaticData[self.locale]
244 s = s and s.flight_routes
245 s = s and s[self.faction]
247 hash = hash or 0
249 -- Will try to lookup flight time there, failing that, will use the time from there to here.
250 local t = getTime(l, origin, dest, hash) or getTime(s, origin, dest, hash) or
251 getTime(l, dest, origin, hash) or getTime(s, dest, origin, hash) or fallback
253 if t == nil then -- Don't have any recored information on this flight time, will estimate based on distances.
254 l = QuestHelper_FlightInstructors[self.faction]
255 s = QuestHelper_StaticData[self.locale]
256 s = s and s.flight_instructors
257 s = s and s[self.faction]
259 local npc1, npc2 = (l and l[origin]) or (s and s[origin]),
260 (l and l[dest]) or (s and s[dest])
262 if npc1 and npc2 then
263 local obj1, obj2 = self:GetObjective("monster", npc1), self:GetObjective("monster", npc2)
264 obj1:PrepareRouting()
265 obj2:PrepareRouting()
267 local pos1, pos2 = obj1:Position(), obj2:Position()
269 if pos1 and pos2 then
270 local x, y = pos1[3]-pos2[3], pos1[4]-pos2[4]
272 t = math.sqrt(x*x+y*y)*self.flight_scalar
275 obj2:DoneRouting()
276 obj1:DoneRouting()
280 return t
283 local moonglade_fp = nil
285 function QuestHelper:addLinkInfo(data, flight_times)
286 if data then
287 local ignored_fp = nil
289 if select(2, UnitClass("player")) ~= "DRUID" then
290 -- As only druids can use the flight point in moonglade, we need to figure out
291 -- where it is so we can ignore it.
293 if not moonglade_fp then
295 local fi_table = QuestHelper_FlightInstructors[self.faction]
297 if fi_table then for area, npc in pairs(fi_table) do
298 local npc_obj = self:GetObjective("monster", npc)
299 npc_obj:PrepareRouting()
300 local pos = npc_obj:Position()
301 if pos and QuestHelper_IndexLookup[pos[1].c][pos[1].z] == 20 then
302 moonglade_fp = area
303 npc_obj:DoneRouting()
304 break
306 npc_obj:DoneRouting()
307 end end
309 if not moonglade_fp then
310 fi_table = QuestHelper_StaticData[QuestHelper_Locale]
311 fi_table = fi_table and fi_table.flight_instructors and fi_table.flight_instructors[self.faction]
313 if fi_table then for area, npc in pairs(fi_table) do
314 local npc_obj = self:GetObjective("monster", npc)
315 npc_obj:PrepareRouting()
316 local pos = npc_obj:Position()
317 if pos and QuestHelper_IndexLookup[pos[1].c][pos[1].z] == 20 then
318 moonglade_fp = area
319 npc_obj:DoneRouting()
320 break
322 npc_obj:DoneRouting()
323 end end
326 if not moonglade_fp then
327 -- This will always be unknown for the session, even if you call buildFlightTimes again
328 -- but if it's unknown then you won't be able to
329 -- get the waypoint this session since you're not a druid
330 -- so its all good.
331 moonglade_fp = "unknown"
335 ignored_fp = moonglade_fp
338 for origin, list in pairs(data) do
339 local tbl = flight_times[origin]
340 if not tbl then
341 tbl = self:CreateTable()
342 flight_times[origin] = tbl
345 for dest, hashs in pairs(list) do
346 if origin ~= ignored_fp and QuestHelper_KnownFlightRoutes[dest] and hashs[0] then
347 local tbl2 = tbl[dest]
348 if not tbl2 then
349 local t = self:computeLinkTime(origin, dest)
350 if t then
351 tbl2 = self:CreateTable()
352 tbl[dest] = tbl2
353 tbl2[1] = t
354 tbl2[2] = dest
363 local visited = {}
365 local function getDataTime(ft, origin, dest)
366 local str = nil
367 local data = ft[origin][dest]
368 local t = data[1]
370 for key in pairs(visited) do visited[key] = nil end
372 while true do
373 local n = data[2]
375 -- We might be asked about a route that visits the same point multiple times, and
376 -- since this is effectively a linked list, we need to check for this to avoid
377 -- infinite loops.
378 if visited[n] then return end
379 visited[n] = true
381 local temp = QuestHelper:computeLinkTime(origin, n, str and QuestHelper:HashString(str) or 0, false)
383 if temp then
384 t = temp + (n == dest and 0 or ft[n][dest][1])
387 if n == dest then break end
388 str = string.format("%s/%s", str or "", n)
389 data = ft[n][dest]
392 return t
395 function QuestHelper:buildFlightTimes()
396 self.flight_scalar = self:computeWalkToFlightMult()
398 local flight_times = self.flight_times
399 if not flight_times then
400 flight_times = self:CreateTable()
401 self.flight_times = flight_times
404 for key, list in pairs(flight_times) do
405 self:ReleaseTable(list)
406 flight_times[key] = nil
409 local l = QuestHelper_FlightRoutes[self.faction]
410 local s = QuestHelper_StaticData[self.locale]
411 s = s and s.flight_routes
412 s = s and s[self.faction]
414 self:addLinkInfo(l, flight_times)
415 self:addLinkInfo(s, flight_times)
417 local cont = true
418 while cont do
419 cont = false
420 local origin = nil
421 while true do
422 origin = next(flight_times, origin)
423 if not origin then break end
424 local list = flight_times[origin]
426 for dest, data in pairs(list) do
427 if flight_times[dest] then for dest2, data2 in pairs(flight_times[dest]) do
428 if dest2 ~= origin then
429 local dat = list[dest2]
431 if not dat then
432 dat = self:CreateTable()
433 dat[1], dat[2] = data[1]+data2[1], dest
434 list[dest2] = dat
435 dat[1] = getDataTime(flight_times, origin, dest2)
437 if not dat[1] then
438 self:ReleaseTable(dat)
439 list[dest2] = nil
440 else
441 cont = true
443 else
444 local o1, o2 = dat[1], dat[2] -- Temporarly replace old data for the sake of looking up its time.
445 if o2 ~= dest then
446 dat[1], dat[2] = data[1]+data2[1], dest
447 local t2 = getDataTime(flight_times, origin, dest2)
449 if t2 and t2 < o1 then
450 dat[1] = t2
451 cont = true
452 else
453 dat[1], dat[2] = o1, o2
458 end end
463 -- Replace the tables with simple times.
464 for orig, list in pairs(flight_times) do
465 for dest, data in pairs(list) do
466 local t = data[1]
467 self:ReleaseTable(data)
468 list[dest] = t
473 function QuestHelper:taxiMapOpened()
474 local routes = QuestHelper_FlightRoutes[self.faction]
476 if not routes then
477 routes = {}
478 QuestHelper_FlightRoutes[self.faction] = routes
481 local sroutes = QuestHelper_StaticData[self.locale]
482 sroutes = sroutes and sroutes.flight_routes
483 sroutes = sroutes and sroutes[self.faction]
485 local origin, altered = nil, false
487 for i = 1,NumTaxiNodes() do
488 local name = TaxiNodeName(i)
489 if not QuestHelper_KnownFlightRoutes[name] then
490 QuestHelper_KnownFlightRoutes[name] = true
491 altered = true
494 if GetNumRoutes(i) == 0 then -- Zero hops from this location, must be where we are.
495 origin = name
499 if origin then
500 if not QuestHelper_KnownFlightRoutes[origin] then
501 -- Player didn't previously have this flight point, will need to recalculate pathing data to account for it.
502 QuestHelper_KnownFlightRoutes[origin] = true
503 altered = true
506 local npc = UnitName("npc")
508 if npc then
509 -- Record who the flight instructor for this location is.
510 local fi_table = QuestHelper_FlightInstructors[self.faction]
511 if not fi_table then
512 fi_table = {}
513 QuestHelper_FlightInstructors[self.faction] = fi_table
516 fi_table[origin] = npc
519 if not self.flight_times[origin] then
520 -- If this is true, then we probably either didn't who the flight instructor here was,
521 -- or did know but didn't know where.
522 -- As we should now know, the flight times should be updated.
523 altered = true
526 if self.flight_data and self:processFlightData(self.flight_data) then
527 self:TextOut(QHText("TALK_TO_FLIGHT_MASTER_COMPLETE"))
528 self:ReleaseTable(self.flight_data)
529 self.flight_data = nil
532 for j = 1,NumTaxiNodes() do
533 local node_count = GetNumRoutes(j)
534 if node_count and i ~= j and node_count > 0 and node_count < 100 then
535 for k = 1,node_count do
536 local n1, n2 = LookupName(TaxiGetSrcX(j, k), TaxiGetSrcY(j, k)), LookupName(TaxiGetDestX(j, k), TaxiGetDestY(j, k))
538 assert(n1 and n2 and n1 ~= n2)
540 local dest1, dest2 = routes[n1], routes[n2]
542 if not dest1 then
543 dest1 = {}
544 routes[n1] = dest1
547 if not dest2 then
548 dest2 = {}
549 routes[n2] = dest2
552 local hash1, hash2 = dest1[n2], dest2[n1]
554 if not hash1 then
555 hash1 = {}
556 dest1[n2] = hash1
559 if not hash2 then
560 hash2 = {}
561 dest2[n1] = hash2
564 if not hash1[0] then
565 if not (slinks and slinks[n1] and slinks[n1][n2] and slinks[n1][n2][0]) then
566 -- hadn't been considering this link in pathing.
567 altered = true
569 hash1[0] = true
572 if not hash2[0] then
573 if not (slinks and slinks[n2] and slinks[n2][n1] and slinks[n2][n1][0]) then
574 -- hadn't been considering this link in pathing.
575 altered = true
577 hash2[0] = true
584 if altered then
585 self:TextOut(QHText("ROUTES_CHANGED"))
586 self:TextOut(QHText("WILL_RESET_PATH"))
587 self.defered_graph_reset = true
588 self:buildFlightTimes()
592 local elapsed = 0
593 local function flight_updater(frame, delta)
594 elapsed = elapsed + delta
595 if elapsed > 1 then
596 elapsed = elapsed - 1
597 local data = QuestHelper.flight_data
598 if data then
599 frame:SetText(string.format("%s: %s", QuestHelper:HighlightText(select(3, string.find(data.dest, "^(.-),")) or data.dest),
600 QuestHelper:TimeString(math.max(0, data.end_time_estimate-time()))))
601 else
602 frame:Hide()
603 frame:SetScript("OnUpdate", nil)
608 function QuestHelper:flightBegan()
609 if self.flight_data and not self.flight_data.start_time then
610 self.flight_data.start_time = GetTime()
611 local origin, dest = self.flight_data.origin, self.flight_data.dest
612 local eta = self:computeLinkTime(origin, dest, self.flight_data.hash,
613 self.flight_times[origin] and self.flight_times[origin][dest]) or 0
615 local npc = self:getFlightInstructor(self.flight_data.dest) -- Will inform QuestHelper that we're going to be at this NPC in whenever.
616 if npc then
617 local npc_obj = self:GetObjective("monster", npc)
618 npc_obj:PrepareRouting()
619 local pos = npc_obj:Position()
620 if pos then
621 local c, z = pos[1].c, pos[1].z
622 local x, y = self.Astrolabe:TranslateWorldMapPosition(c, 0,
623 pos[3]/self.continent_scales_x[c],
624 pos[4]/self.continent_scales_y[c], c, z)
626 self:SetTargetLocation(QuestHelper_IndexLookup[c][z], x, y, eta)
629 npc_obj:DoneRouting()
632 if QuestHelper_Pref.flight_time then
633 self.flight_data.end_time_estimate = time()+eta
634 self:PerformCustomSearch(flight_updater) -- Reusing the search status indicator to display ETA for flight.
639 function QuestHelper:flightEnded()
640 local flight_data = self.flight_data
641 if flight_data and not flight_data.end_time then
642 flight_data.end_time = GetTime()
644 if self:processFlightData(flight_data) then
645 self:ReleaseTable(flight_data)
646 self.flight_data = nil
649 self:UnsetTargetLocation()
650 self:StopCustomSearch()