update repo
[QuestHelper.git] / flightpath.lua
blob960215444ccf37ee6f4e05020cf9999a25ad1f9c
1 QuestHelper_File["flightpath.lua"] = "Development Version"
3 local real_TakeTaxiNode = TakeTaxiNode
4 local real_TaxiNodeOnButtonEnter= TaxiNodeOnButtonEnter
6 assert(type(real_TakeTaxiNode) == "function")
7 assert(type(real_TaxiNodeOnButtonEnter) == "function")
9 local function LookupName(x, y)
10 local best, d2
11 for i = 1,NumTaxiNodes() do
12 local u, v = TaxiNodePosition(i)
13 u = u - x
14 v = v - y
15 u = u*u+v*v
16 if not best or u < d2 then
17 best, d2 = TaxiNodeName(i), u
18 end
19 end
21 return best
22 end
24 local function getRoute(id)
25 for i = 1,NumTaxiNodes() do
26 if GetNumRoutes(i) == 0 then
27 local routes = GetNumRoutes(id)
28 if routes and routes > 0 and routes < 100 then
29 local origin, dest = TaxiNodeName(i), TaxiNodeName(id)
30 local path_hash = 0
32 if routes > 1 then
33 local path_str = ""
35 for j = 1,routes-1 do
36 path_str = string.format("%s/%s", path_str, LookupName(TaxiGetDestX(id, j), TaxiGetDestY(id, j)))
37 end
39 path_hash = QuestHelper:HashString(path_str)
40 end
42 return origin, dest, path_hash
43 end
44 end
45 end
46 end
48 TaxiNodeOnButtonEnter = function(btn)
49 real_TaxiNodeOnButtonEnter(btn)
51 if QuestHelper_Pref.flight_time then
52 local index = btn:GetID()
53 if TaxiNodeGetType(index) == "REACHABLE" then
54 local origin, dest, hash = getRoute(index)
55 local eta, estimate = nil, false
56 if origin then
57 eta = QuestHelper:computeLinkTime(origin, dest, hash, false)
58 if not eta then
59 eta = QuestHelper.flight_times[origin] and QuestHelper.flight_times[origin][dest]
60 estimate = true
61 end
62 end
64 if eta then -- Going to replace the tooltip.
65 GameTooltip:SetOwner(btn, "ANCHOR_RIGHT")
66 GameTooltip:ClearLines()
67 GameTooltip:AddLine(dest, "", 1.0, 1.0, 1.0)
68 GameTooltip:AddDoubleLine(QHText("TRAVEL_ESTIMATE"), (estimate and "|cffffffff≈|r " or "")..QHFormat("TRAVEL_ESTIMATE_VALUE", eta))
69 local cost = TaxiNodeCost(index)
70 if cost > 0 then
71 SetTooltipMoney(GameTooltip, cost)
72 end
73 GameTooltip:Show()
74 end
75 end
76 end
77 end
79 TakeTaxiNode = function(id)
80 local origin, dest, hash = getRoute(id)
82 if origin then
83 local flight_data = QuestHelper.flight_data
84 if not flight_data then
85 flight_data = QuestHelper:CreateTable()
86 QuestHelper.flight_data = flight_data
87 end
89 flight_data.origin = origin
90 flight_data.dest = dest
91 flight_data.hash = hash
92 flight_data.start_time = nil
93 flight_data.end_time = nil
94 flight_data.end_time_estimate = nil
95 end
97 real_TakeTaxiNode(id)
98 end
100 function QuestHelper:processFlightData(data, interrupted)
101 local npc = self:getFlightInstructor(data.dest)
102 if not npc then
103 self:TextOut(QHText("TALK_TO_FLIGHT_MASTER"))
104 return false
107 local npc_obj = self:GetObjective("monster", npc)
108 npc_obj:PrepareRouting()
110 local pos = npc_obj:Position()
111 if not pos then
112 -- Don't know te location of the flight instructor.
113 self:TextOut(QHText("TALK_TO_FLIGHT_MASTER"))
114 npc_obj:DoneRouting()
115 return false
118 local correct = true
120 if pos[1].c ~= self.c then
121 correct = false
122 else
123 local x, y = self.Astrolabe:TranslateWorldMapPosition(self.c, self.z, self.x, self.y, self.c, 0)
124 x = x * self.continent_scales_x[self.c]
125 y = y * self.continent_scales_y[self.c]
126 local t = (x-pos[3])*(x-pos[3])+(y-pos[4])*(y-pos[4])
128 --self:TextOut(string.format("(%f,%f) vs (%f,%f) is %f", x, y, pos[3], pos[4], t))
130 if t > 5*5 then
131 correct = false
135 npc_obj:DoneRouting()
137 if not correct then
138 return true
141 if data.start_time and data.end_time and data.end_time > data.start_time then
142 local routes = QuestHelper_FlightRoutes_Local[self.faction]
143 if not routes then
144 routes = {}
145 QuestHelper_FlightRoutes_Local[self.faction] = routes
148 local origin = routes[data.origin]
149 if not origin then
150 origin = {}
151 routes[data.origin] = origin
154 local dest = origin[data.dest]
155 if not dest then
156 dest = {}
157 origin[data.dest] = dest
160 dest[data.hash] = data.end_time - data.start_time
162 if interrupted then -- I'm assuming this doesn't depend on the hash, since I really doubt the routing system would let a player go through zone boundaries if it wasn't mandatory
163 dest.interrupt_count = (dest.interrupt_count or 0) + 1
164 else
165 dest.no_interrupt_count = (dest.no_interrupt_count or 0) + 1
169 return true
172 function QuestHelper:getFlightInstructor(area)
173 local fi_table = QuestHelper_FlightInstructors_Local[self.faction]
174 if fi_table then
175 local npc = fi_table[area]
176 if npc then
177 return npc
181 local static = QuestHelper_StaticData[QuestHelper_Locale]
183 if static then
184 fi_table = static.flight_instructors and static.flight_instructors[self.faction]
185 if fi_table then
186 return fi_table[area]
191 local function getTime(tbl, orig, dest, hash)
192 tbl = tbl and tbl[orig]
193 tbl = tbl and tbl[dest]
194 return tbl and tbl[hash] ~= true and tbl[hash]
197 local function getWalkToFlight(tbl, fi1, fi2)
198 local f, w = 0, 0
200 if tbl then
201 for origin, list in pairs(tbl) do
202 for dest, hashlist in pairs(list) do
203 if type(hashlist[0]) == "number" then
204 local npc1, npc2 = (fi1 and fi1[origin]) or (fi2 and fi2[origin]), (fi1 and fi1[dest]) or (fi2 and fi2[dest])
205 if npc1 and npc2 then
206 local obj1, obj2 = QuestHelper:GetObjective("monster", npc1), QuestHelper:GetObjective("monster", npc2)
207 obj1:PrepareRouting({failable = true})
208 obj2:PrepareRouting({failable = true})
210 local pos1, pos2 = obj1:Position(), obj2:Position()
212 if pos1 and pos2 then
213 local x, y = pos1[3]-pos2[3], pos1[4]-pos2[4]
214 w = w + math.sqrt(x*x+y*y)
215 f = f + hashlist[0]
218 obj2:DoneRouting()
219 obj1:DoneRouting()
226 return f, w
229 function QuestHelper:computeWalkToFlightMult()
230 local l = QuestHelper_FlightRoutes_Local[self.faction]
231 local s = QuestHelper_StaticData[self.locale]
232 s = s and s.flight_routes
233 s = s and s[self.faction]
235 local fi1 = QuestHelper_FlightInstructors_Local[self.faction]
236 local fi2 = QuestHelper_StaticData[self.locale]
237 fi2 = fi2 and fi2.flight_instructors
238 fi2 = fi2 and fi2[self.faction]
240 local f1, w1 = getWalkToFlight(l, fi1, fi2)
241 local f2, w2 = getWalkToFlight(s, fi1, fi2)
242 return (f1+f2+0.032876)/(w1+w2+0.1)
245 function QuestHelper:computeLinkTime(origin, dest, hash, fallback)
246 -- Only works for directly connected flight points.
248 if origin == dest then
249 return 0
252 local l = QuestHelper_FlightRoutes_Local[self.faction]
253 local s = QuestHelper_StaticData[self.locale]
254 s = s and s.flight_routes
255 s = s and s[self.faction]
257 hash = hash or 0
259 -- Will try to lookup flight time there, failing that, will use the time from there to here.
260 local t = getTime(l, origin, dest, hash) or getTime(s, origin, dest, hash) or
261 getTime(l, dest, origin, hash) or getTime(s, dest, origin, hash) or fallback
263 if t == nil then -- Don't have any recored information on this flight time, will estimate based on distances.
264 l = QuestHelper_FlightInstructors_Local[self.faction]
265 s = QuestHelper_StaticData[self.locale]
266 s = s and s.flight_instructors
267 s = s and s[self.faction]
269 local npc1, npc2 = (l and l[origin]) or (s and s[origin]),
270 (l and l[dest]) or (s and s[dest])
272 if npc1 and npc2 then
273 local obj1, obj2 = self:GetObjective("monster", npc1), self:GetObjective("monster", npc2)
274 obj1:PrepareRouting()
275 obj2:PrepareRouting()
277 local pos1, pos2 = obj1:Position(), obj2:Position()
279 if pos1 and pos2 then
280 local x, y = pos1[3]-pos2[3], pos1[4]-pos2[4]
282 t = math.sqrt(x*x+y*y)*self.flight_scalar
285 obj2:DoneRouting()
286 obj1:DoneRouting()
290 return t
293 local moonglade_fp = nil
295 function QuestHelper:addLinkInfo(data, flight_times)
296 if data then
297 local ignored_fp = nil
299 if select(2, UnitClass("player")) ~= "DRUID" then
300 -- As only druids can use the flight point in moonglade, we need to figure out
301 -- where it is so we can ignore it.
303 if not moonglade_fp then
305 local fi_table = QuestHelper_FlightInstructors_Local[self.faction]
307 if fi_table then for area, npc in pairs(fi_table) do
308 local npc_obj = self:GetObjective("monster", npc)
309 npc_obj:PrepareRouting({failable = true})
310 local pos = npc_obj:Position()
311 if pos and QuestHelper_IndexLookup[pos[1].c][pos[1].z] == 20 then
312 moonglade_fp = area
313 npc_obj:DoneRouting()
314 break
316 npc_obj:DoneRouting()
317 end end
319 if not moonglade_fp then
320 fi_table = QuestHelper_StaticData[QuestHelper_Locale]
321 fi_table = fi_table and fi_table.flight_instructors and fi_table.flight_instructors[self.faction]
323 if fi_table then for area, npc in pairs(fi_table) do
324 local npc_obj = self:GetObjective("monster", npc)
325 npc_obj:PrepareRouting({failable = true})
326 local pos = npc_obj:Position()
327 if pos and QuestHelper_IndexLookup[pos[1].c][pos[1].z] == 20 then
328 moonglade_fp = area
329 npc_obj:DoneRouting()
330 break
332 npc_obj:DoneRouting()
333 end end
336 if not moonglade_fp then
337 -- This will always be unknown for the session, even if you call buildFlightTimes again
338 -- but if it's unknown then you won't be able to
339 -- get the waypoint this session since you're not a druid
340 -- so its all good.
341 moonglade_fp = "unknown"
345 ignored_fp = moonglade_fp
348 for origin, list in pairs(data) do
349 local tbl = flight_times[origin]
350 if not tbl then
351 tbl = self:CreateTable()
352 flight_times[origin] = tbl
355 for dest, hashs in pairs(list) do
356 if origin ~= ignored_fp and QuestHelper_KnownFlightRoutes[dest] and hashs[0] then
357 local tbl2 = tbl[dest]
358 if not tbl2 then
359 local t = self:computeLinkTime(origin, dest)
360 if t then
361 tbl2 = self:CreateTable()
362 tbl[dest] = tbl2
363 tbl2[1] = t
364 tbl2[2] = dest
373 local visited = {}
375 local function getDataTime(ft, origin, dest)
376 local str = nil
377 local data = ft[origin][dest]
378 local t = data[1]
380 for key in pairs(visited) do visited[key] = nil end
382 while true do
383 local n = data[2]
385 -- We might be asked about a route that visits the same point multiple times, and
386 -- since this is effectively a linked list, we need to check for this to avoid
387 -- infinite loops.
388 if visited[n] then return end
389 visited[n] = true
391 local temp = QuestHelper:computeLinkTime(origin, n, str and QuestHelper:HashString(str) or 0, false)
393 if temp then
394 t = temp + (n == dest and 0 or ft[n][dest][1])
397 if n == dest then break end
398 str = string.format("%s/%s", str or "", n)
399 data = ft[n][dest]
402 return t
405 function QuestHelper:buildFlightTimes()
406 self.flight_scalar = self:computeWalkToFlightMult()
408 local flight_times = self.flight_times
409 if not flight_times then
410 flight_times = self:CreateTable()
411 self.flight_times = flight_times
414 for key, list in pairs(flight_times) do
415 self:ReleaseTable(list)
416 flight_times[key] = nil
419 local l = QuestHelper_FlightRoutes_Local[self.faction]
420 local s = QuestHelper_StaticData[self.locale]
421 s = s and s.flight_routes
422 s = s and s[self.faction]
424 self:addLinkInfo(l, flight_times)
425 self:addLinkInfo(s, flight_times)
427 local cont = true
428 while cont do
429 cont = false
430 local origin = nil
431 while true do
432 origin = next(flight_times, origin)
433 if not origin then break end
434 local list = flight_times[origin]
436 for dest, data in pairs(list) do
437 if flight_times[dest] then for dest2, data2 in pairs(flight_times[dest]) do
438 if dest2 ~= origin then
439 local dat = list[dest2]
441 if not dat then
442 dat = self:CreateTable()
443 dat[1], dat[2] = data[1]+data2[1], dest
444 list[dest2] = dat
445 dat[1] = getDataTime(flight_times, origin, dest2)
447 if not dat[1] then
448 self:ReleaseTable(dat)
449 list[dest2] = nil
450 else
451 cont = true
453 else
454 local o1, o2 = dat[1], dat[2] -- Temporarly replace old data for the sake of looking up its time.
455 if o2 ~= dest then
456 dat[1], dat[2] = data[1]+data2[1], dest
457 local t2 = getDataTime(flight_times, origin, dest2)
459 if t2 and t2 < o1 then
460 dat[1] = t2
461 cont = true
462 else
463 dat[1], dat[2] = o1, o2
468 end end
469 self:yieldIfNeeded(.1)
474 -- Replace the tables with simple times.
475 for orig, list in pairs(flight_times) do
476 for dest, data in pairs(list) do
477 local t = data[1]
478 self:ReleaseTable(data)
479 list[dest] = t
484 function QuestHelper:taxiMapOpened()
485 local routes = QuestHelper_FlightRoutes_Local[self.faction]
487 if not routes then
488 routes = {}
489 QuestHelper_FlightRoutes_Local[self.faction] = routes
492 local sroutes = QuestHelper_StaticData[self.locale]
493 sroutes = sroutes and sroutes.flight_routes
494 sroutes = sroutes and sroutes[self.faction]
496 local origin, altered = nil, false
498 for i = 1,NumTaxiNodes() do
499 local name = TaxiNodeName(i)
500 if not QuestHelper_KnownFlightRoutes[name] then
501 QuestHelper_KnownFlightRoutes[name] = true
502 altered = true
505 if GetNumRoutes(i) == 0 then -- Zero hops from this location, must be where we are.
506 origin = name
510 if origin then
511 if not QuestHelper_KnownFlightRoutes[origin] then
512 -- Player didn't previously have this flight point, will need to recalculate pathing data to account for it.
513 QuestHelper_KnownFlightRoutes[origin] = true
514 altered = true
517 local npc = UnitName("npc")
519 if npc then
520 -- Record who the flight instructor for this location is.
521 local fi_table = QuestHelper_FlightInstructors_Local[self.faction]
522 if not fi_table then
523 fi_table = {}
524 QuestHelper_FlightInstructors_Local[self.faction] = fi_table
527 fi_table[origin] = npc
530 if not self.flight_times[origin] then
531 -- If this is true, then we probably either didn't who the flight instructor here was,
532 -- or did know but didn't know where.
533 -- As we should now know, the flight times should be updated.
534 altered = true
537 if self.flight_data and self:processFlightData(self.flight_data) then
538 self:TextOut(QHText("TALK_TO_FLIGHT_MASTER_COMPLETE"))
539 self:ReleaseTable(self.flight_data)
540 self.flight_data = nil
543 for j = 1,NumTaxiNodes() do
544 local node_count = GetNumRoutes(j)
545 if node_count and i ~= j and node_count > 0 and node_count < 100 then
546 for k = 1,node_count do
547 local n1, n2 = LookupName(TaxiGetSrcX(j, k), TaxiGetSrcY(j, k)), LookupName(TaxiGetDestX(j, k), TaxiGetDestY(j, k))
549 assert(n1 and n2 and n1 ~= n2)
551 local dest1, dest2 = routes[n1], routes[n2]
553 if not dest1 then
554 dest1 = {}
555 routes[n1] = dest1
558 if not dest2 then
559 dest2 = {}
560 routes[n2] = dest2
563 local hash1, hash2 = dest1[n2], dest2[n1]
565 if not hash1 then
566 hash1 = {}
567 dest1[n2] = hash1
570 if not hash2 then
571 hash2 = {}
572 dest2[n1] = hash2
575 if not hash1[0] then
576 if not (slinks and slinks[n1] and slinks[n1][n2] and slinks[n1][n2][0]) then
577 -- hadn't been considering this link in pathing.
578 altered = true
580 hash1[0] = true
583 if not hash2[0] then
584 if not (slinks and slinks[n2] and slinks[n2][n1] and slinks[n2][n1][0]) then
585 -- hadn't been considering this link in pathing.
586 altered = true
588 hash2[0] = true
595 if altered then
596 self:TextOut(QHText("ROUTES_CHANGED"))
597 self:TextOut(QHText("WILL_RESET_PATH"))
598 self.defered_graph_reset = true
599 self.defered_flight_times = true
600 --self:buildFlightTimes()
604 local elapsed = 0
605 local function flight_updater(frame, delta)
606 elapsed = elapsed + delta
607 if elapsed > 1 then
608 elapsed = elapsed - 1
609 local data = QuestHelper.flight_data
610 if data then
611 frame:SetText(string.format("%s: %s", QuestHelper:HighlightText(select(3, string.find(data.dest, "^(.-),")) or data.dest),
612 QuestHelper:TimeString(math.max(0, data.end_time_estimate-time()))))
613 else
614 frame:Hide()
615 frame:SetScript("OnUpdate", nil)
620 function QuestHelper:flightBegan()
621 if self.flight_data and not self.flight_data.start_time then
622 self.flight_data.start_time = GetTime()
623 local origin, dest = self.flight_data.origin, self.flight_data.dest
624 local eta = self:computeLinkTime(origin, dest, self.flight_data.hash,
625 self.flight_times[origin] and self.flight_times[origin][dest]) or 0
627 local npc = self:getFlightInstructor(self.flight_data.dest) -- Will inform QuestHelper that we're going to be at this NPC in whenever.
628 if npc then
629 local npc_obj = self:GetObjective("monster", npc)
630 npc_obj:PrepareRouting()
631 local pos = npc_obj:Position()
632 if pos then
633 local c, z = pos[1].c, pos[1].z
634 local x, y = self.Astrolabe:TranslateWorldMapPosition(c, 0,
635 pos[3]/self.continent_scales_x[c],
636 pos[4]/self.continent_scales_y[c], c, z)
638 self:SetTargetLocation(QuestHelper_IndexLookup[c][z], x, y, eta)
641 npc_obj:DoneRouting()
644 if QuestHelper_Pref.flight_time then
645 self.flight_data.end_time_estimate = time()+eta
646 self:PerformCustomSearch(flight_updater) -- Reusing the search status indicator to display ETA for flight.
651 function QuestHelper:flightEnded(interrupted)
652 local flight_data = self.flight_data
653 if flight_data and not flight_data.end_time then
654 flight_data.end_time = GetTime()
656 if self:processFlightData(flight_data, interrupted) then
657 self:ReleaseTable(flight_data)
658 self.flight_data = nil
661 self:UnsetTargetLocation()
662 self:StopCustomSearch()