update changes
[QuestHelper.git] / flightpath.lua
blob613817d18b52f2b42eafabed86b98790e8ccc9cf
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 QuestHelper: Assert(btn)
50 real_TaxiNodeOnButtonEnter(btn)
52 if QuestHelper_Pref.flight_time then
53 local index = btn:GetID()
54 if TaxiNodeGetType(index) == "REACHABLE" then
55 local origin, dest, hash = getRoute(index)
56 local eta, estimate = nil, false
57 if origin then
58 eta = QuestHelper:computeLinkTime(origin, dest, hash, false)
59 if not eta then
60 eta = QuestHelper.flight_times[origin] and QuestHelper.flight_times[origin][dest]
61 estimate = true
62 end
63 end
65 if eta then -- Going to replace the tooltip.
66 GameTooltip:SetOwner(btn, "ANCHOR_RIGHT")
67 GameTooltip:ClearLines()
68 GameTooltip:AddLine(dest, "", 1.0, 1.0, 1.0)
69 GameTooltip:AddDoubleLine(QHText("TRAVEL_ESTIMATE"), (estimate and "|cffffffff≈|r " or "")..QHFormat("TRAVEL_ESTIMATE_VALUE", eta))
70 local cost = TaxiNodeCost(index)
71 if cost > 0 then
72 SetTooltipMoney(GameTooltip, cost)
73 end
74 GameTooltip:Show()
75 end
76 end
77 end
78 end
80 TakeTaxiNode = function(id)
81 local origin, dest, hash = getRoute(id)
83 if origin then
84 local flight_data = QuestHelper.flight_data
85 if not flight_data then
86 flight_data = QuestHelper:CreateTable()
87 QuestHelper.flight_data = flight_data
88 end
90 flight_data.origin = origin
91 flight_data.dest = dest
92 flight_data.hash = hash
93 flight_data.start_time = nil
94 flight_data.end_time = nil
95 flight_data.end_time_estimate = nil
96 end
98 real_TakeTaxiNode(id)
99 end
101 function QuestHelper:processFlightData(data, interrupted)
102 local npc = self:getFlightInstructor(data.dest)
103 if not npc then
104 self:TextOut(QHText("TALK_TO_FLIGHT_MASTER"))
105 return false
108 local npc_obj = self:GetObjective("monster", npc)
109 npc_obj:PrepareRouting(true)
111 local pos = npc_obj:Position()
112 if not pos then
113 -- Don't know te location of the flight instructor.
114 self:TextOut(QHText("TALK_TO_FLIGHT_MASTER"))
115 npc_obj:DoneRouting()
116 return false
119 local correct = true
121 if pos[1].c ~= self.c then
122 correct = false
123 else
124 local x, y = self.Astrolabe:TranslateWorldMapPosition(self.c, self.z, self.x, self.y, self.c, 0)
125 x = x * self.continent_scales_x[self.c]
126 y = y * self.continent_scales_y[self.c]
127 local t = (x-pos[3])*(x-pos[3])+(y-pos[4])*(y-pos[4])
129 --self:TextOut(string.format("(%f,%f) vs (%f,%f) is %f", x, y, pos[3], pos[4], t))
131 if t > 5*5 then
132 correct = false
136 npc_obj:DoneRouting()
138 if not correct then
139 return true
142 if data.start_time and data.end_time and data.end_time > data.start_time then
143 local routes = QuestHelper_FlightRoutes_Local[self.faction]
144 if not routes then
145 routes = {}
146 QuestHelper_FlightRoutes_Local[self.faction] = routes
149 local origin = routes[data.origin]
150 if not origin then
151 origin = {}
152 routes[data.origin] = origin
155 local dest = origin[data.dest]
156 if not dest then
157 dest = {}
158 origin[data.dest] = dest
161 dest[data.hash] = data.end_time - data.start_time
163 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
164 dest.interrupt_count = (dest.interrupt_count or 0) + 1
165 else
166 dest.no_interrupt_count = (dest.no_interrupt_count or 0) + 1
170 return true
173 function QuestHelper:getFlightInstructor(area)
174 local fi_table = QuestHelper_FlightInstructors_Local[self.faction]
175 if fi_table then
176 local npc = fi_table[area]
177 if npc then
178 return npc
182 local static = QuestHelper_StaticData[QuestHelper_Locale]
184 if static then
185 fi_table = static.flight_instructors and static.flight_instructors[self.faction]
186 if fi_table then
187 return fi_table[area]
192 local function getTime(tbl, orig, dest, hash)
193 tbl = tbl and tbl[orig]
194 tbl = tbl and tbl[dest]
195 return tbl and tbl[hash] ~= true and tbl[hash]
198 -- Okay, I think I've figured out what this is. Given fi1 and fi2, the standard horrifying "canonical/fallback" stuff that all this code does . . .
199 -- For each pair of "origin/dest" in tbl, determine if there is a direct path. (If there is, the hash will be 0.)
200 -- If so, find the flightpath distance and the "walking" distance. Add up walking and flightpath separately, and return the sums.
201 local function getWalkToFlight(tbl, fi1, fi2)
202 local f, w = 0, 0
204 if tbl then
205 for origin, list in pairs(tbl) do
206 for dest, hashlist in pairs(list) do
207 if type(hashlist[0]) == "number" then
208 local npc1, npc2 = (fi1 and fi1[origin]) or (fi2 and fi2[origin]), (fi1 and fi1[dest]) or (fi2 and fi2[dest])
209 if npc1 and npc2 then
210 local obj1, obj2 = QuestHelper:GetObjective("monster", npc1), QuestHelper:GetObjective("monster", npc2)
211 obj1:PrepareRouting(true, {failable = true})
212 obj2:PrepareRouting(true, {failable = true})
214 local pos1, pos2 = obj1:Position(), obj2:Position()
216 if pos1 and pos2 then
217 local x, y = pos1[3]-pos2[3], pos1[4]-pos2[4]
218 w = w + math.sqrt(x*x+y*y)
219 f = f + hashlist[0]
222 obj2:DoneRouting()
223 obj1:DoneRouting()
230 return f, w
233 -- Determines the general multiple faster than flying is than walking.
234 function QuestHelper:computeWalkToFlightMult()
235 local l = QuestHelper_FlightRoutes_Local[self.faction]
236 local s = QuestHelper_StaticData[self.locale]
237 s = s and s.flight_routes
238 s = s and s[self.faction]
240 local fi1 = QuestHelper_FlightInstructors_Local[self.faction]
241 local fi2 = QuestHelper_StaticData[self.locale]
242 fi2 = fi2 and fi2.flight_instructors
243 fi2 = fi2 and fi2[self.faction]
245 local f1, w1 = getWalkToFlight(l, fi1, fi2)
246 local f2, w2 = getWalkToFlight(s, fi1, fi2)
247 return (f1+f2+0.032876)/(w1+w2+0.1)
250 function QuestHelper:computeLinkTime(origin, dest, hash, fallback)
251 -- Only works for directly connected flight points.
252 if origin == dest then
253 return 0
256 local l = QuestHelper_FlightRoutes_Local[self.faction]
257 local s = QuestHelper_StaticData[self.locale]
258 s = s and s.flight_routes
259 s = s and s[self.faction]
261 hash = hash or 0
263 -- Will try to lookup flight time there, failing that, will use the time from there to here.
264 local t = getTime(l, origin, dest, hash) or getTime(s, origin, dest, hash) or
265 getTime(l, dest, origin, hash) or getTime(s, dest, origin, hash) or fallback
267 if t == nil then -- Don't have any recored information on this flight time, will estimate based on distances.
268 l = QuestHelper_FlightInstructors_Local[self.faction]
269 s = QuestHelper_StaticData[self.locale]
270 s = s and s.flight_instructors
271 s = s and s[self.faction]
273 local npc1, npc2 = (l and l[origin]) or (s and s[origin]),
274 (l and l[dest]) or (s and s[dest])
276 if npc1 and npc2 then
277 local obj1, obj2 = self:GetObjective("monster", npc1), self:GetObjective("monster", npc2)
278 obj1:PrepareRouting(true)
279 obj2:PrepareRouting(true)
281 local pos1, pos2 = obj1:Position(), obj2:Position()
283 if pos1 and pos2 then
284 local x, y = pos1[3]-pos2[3], pos1[4]-pos2[4]
286 t = math.sqrt(x*x+y*y)*self.flight_scalar
289 obj2:DoneRouting()
290 obj1:DoneRouting()
294 if t and type(t) ~= "number" then
295 QuestHelper:AppendNotificationError("2008-10-11 computelinktime is not a number", string.format("%s %s", type(t), fallback and type(fallback) or "(nil)"))
296 return nil
298 return t
301 local moonglade_fp = nil
303 function QuestHelper:addLinkInfo(data, flight_times)
304 if data then
305 if select(2, UnitClass("player")) ~= "DRUID" then
306 -- As only druids can use the flight point in moonglade, we need to figure out
307 -- where it is so we can ignore it.
309 if not moonglade_fp then
311 local fi_table = QuestHelper_FlightInstructors_Local[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(true, {failable = true})
316 local pos = npc_obj:Position()
317 if pos and QuestHelper_IndexLookup[pos[1].c][pos[1].z] == 20 and string.find(area, ",") then -- I'm kind of guessing here
318 moonglade_fp = area
319 npc_obj:DoneRouting()
320 break
322 npc_obj:DoneRouting()
323 end end
325 if not moonglade_fp then
326 fi_table = QuestHelper_StaticData[QuestHelper_Locale]
327 fi_table = fi_table and fi_table.flight_instructors and fi_table.flight_instructors[self.faction]
329 if fi_table then for area, npc in pairs(fi_table) do
330 local npc_obj = self:GetObjective("monster", npc)
331 npc_obj:PrepareRouting(true, {failable = true})
332 local pos = npc_obj:Position()
333 if pos and QuestHelper_IndexLookup[pos[1].c][pos[1].z] == 20 and string.find(area, ",") then
334 moonglade_fp = area
335 npc_obj:DoneRouting()
336 break
338 npc_obj:DoneRouting()
339 end end
342 if not moonglade_fp then
343 -- This will always be unknown for the session, even if you call buildFlightTimes again
344 -- but if it's unknown then you won't be able to
345 -- get the waypoint this session since you're not a druid
346 -- so its all good.
347 moonglade_fp = "unknown"
352 for origin, list in pairs(data) do
353 local tbl = flight_times[origin]
354 if not tbl then
355 tbl = self:CreateTable("Flightpath AddLinkInfo origin table")
356 flight_times[origin] = tbl
359 for dest, hashs in pairs(list) do
360 if origin ~= moonglade_fp and QuestHelper_KnownFlightRoutes[dest] and hashs[0] then
361 local tbl2 = tbl[dest]
362 if not tbl2 then
363 local t = self:computeLinkTime(origin, dest)
364 if t then
365 tbl2 = self:CreateTable("Flightpath AddLinkInfo origin->dest data table")
366 tbl[dest] = tbl2
367 tbl2[1] = t
368 tbl2[2] = dest
377 local visited = {}
379 local function getDataTime(ft, origin, dest)
380 local str = nil
381 local data = ft[origin][dest]
382 local t = data[1]
384 for key in pairs(visited) do visited[key] = nil end
386 while true do
387 local n = data[2]
389 -- We might be asked about a route that visits the same point multiple times, and
390 -- since this is effectively a linked list, we need to check for this to avoid
391 -- infinite loops.
392 if visited[n] then return end
393 visited[n] = true
395 local temp = QuestHelper:computeLinkTime(origin, n, str and QuestHelper:HashString(str) or 0, false)
397 if temp then
398 t = temp + (n == dest and 0 or ft[n][dest][1])
401 if n == dest then break end
402 str = string.format("%s/%s", str or "", n)
403 data = ft[n][dest]
406 return t
409 function QuestHelper:buildFlightTimes()
410 self.flight_scalar = self:computeWalkToFlightMult()
412 local flight_times = self.flight_times
413 if not flight_times then
414 flight_times = self:CreateTable()
415 self.flight_times = flight_times
418 for key, list in pairs(flight_times) do
419 self:ReleaseTable(list)
420 flight_times[key] = nil
423 local l = QuestHelper_FlightRoutes_Local[self.faction]
424 local s = QuestHelper_StaticData[self.locale]
425 s = s and s.flight_routes
426 s = s and s[self.faction]
428 self:addLinkInfo(l, flight_times)
429 self:addLinkInfo(s, flight_times)
431 -- This appears to set up flight_times so it gives directions from any node to any other node. I'm not sure what the getDataTime() call is all about, and I'm also not sure what dat[2] is for. In any case, I don't see anything immediately suspicious about this, just dubious.
432 local cont = true
433 while cont do
434 cont = false
435 local origin = nil
436 while true do
437 origin = next(flight_times, origin)
438 if not origin then break end
439 local list = flight_times[origin]
441 for dest, data in pairs(list) do
442 if flight_times[dest] then for dest2, data2 in pairs(flight_times[dest]) do
443 if dest2 ~= origin then
444 local dat = list[dest2]
446 if not dat then
447 dat = self:CreateTable()
448 dat[1], dat[2] = data[1]+data2[1], dest
449 list[dest2] = dat
450 dat[1] = getDataTime(flight_times, origin, dest2)
452 if not dat[1] then
453 self:ReleaseTable(dat)
454 list[dest2] = nil
455 else
456 cont = true
458 else
459 local o1, o2 = dat[1], dat[2] -- Temporarly replace old data for the sake of looking up its time.
460 if o2 ~= dest then
461 dat[1], dat[2] = data[1]+data2[1], dest
462 local t2 = getDataTime(flight_times, origin, dest2)
464 if t2 and t2 < o1 then
465 dat[1] = t2
466 cont = true
467 else
468 dat[1], dat[2] = o1, o2
473 end end
474 QH_Timeslice_Yield()
479 -- Replace the tables with simple times.
480 for orig, list in pairs(flight_times) do
481 for dest, data in pairs(list) do
482 local t = data[1]
483 self:ReleaseTable(data)
484 list[dest] = t
489 function QuestHelper:taxiMapOpened()
490 local routes = QuestHelper_FlightRoutes_Local[self.faction]
492 if not routes then
493 routes = {}
494 QuestHelper_FlightRoutes_Local[self.faction] = routes
497 local sroutes = QuestHelper_StaticData[self.locale]
498 sroutes = sroutes and sroutes.flight_routes
499 sroutes = sroutes and sroutes[self.faction]
501 local origin, altered = nil, false
503 for i = 1,NumTaxiNodes() do
504 local name = TaxiNodeName(i)
505 if not QuestHelper_KnownFlightRoutes[name] then
506 QuestHelper_KnownFlightRoutes[name] = true
507 altered = true
508 self:TextOut("New flight master: " .. name)
511 if GetNumRoutes(i) == 0 then -- Zero hops from this location, must be where we are.
512 origin = name
516 if origin then
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 -- let's make this a bit easier and faster
550 if not QuestHelper_KnownFlightRoutes[n2] then
551 QuestHelper_KnownFlightRoutes[n2] = true
552 altered = true
553 self:TextOut("New flight master implied: " .. n2)
556 --QuestHelper:TextOut(string.format("taxi %d: %d is %s/%s", j, k, n1, n2))
558 assert(n1 and n2 and n1 ~= n2)
560 local dest1, dest2 = routes[n1], routes[n2]
562 if not dest1 then
563 dest1 = {}
564 routes[n1] = dest1
567 if not dest2 then
568 dest2 = {}
569 routes[n2] = dest2
572 local hash1, hash2 = dest1[n2], dest2[n1]
574 if not hash1 then
575 hash1 = {}
576 dest1[n2] = hash1
579 if not hash2 then
580 hash2 = {}
581 dest2[n1] = hash2
584 if not hash1[0] then
585 if not (sroutes and sroutes[n1] and sroutes[n1][n2] and sroutes[n1][n2][0]) then
586 -- hadn't been considering this link in pathing.
587 self:TextOut(string.format("Found new link between %s and %s", n1, n2))
588 altered = true
590 hash1[0] = true
593 if not hash2[0] then
594 if not (sroutes and sroutes[n2] and sroutes[n2][n1] and sroutes[n2][n1][0]) then
595 -- hadn't been considering this link in pathing.
596 self:TextOut(string.format("Found new link between %s and %s", n2, n1))
597 altered = true
599 hash2[0] = true
606 if altered then
607 self:TextOut(QHText("ROUTES_CHANGED"))
608 self:TextOut(QHText("WILL_RESET_PATH"))
609 self.defered_graph_reset = true
610 self.defered_flight_times = true
611 --self:buildFlightTimes()
615 local elapsed = 0
616 local function flight_updater(frame, delta)
617 elapsed = elapsed + delta
618 if elapsed > 1 then
619 elapsed = elapsed - 1
620 local data = QuestHelper.flight_data
621 if data then
622 frame:SetText(string.format("%s: %s", QuestHelper:HighlightText(select(3, string.find(data.dest, "^(.-),")) or data.dest),
623 QuestHelper:TimeString(math.max(0, data.end_time_estimate-time()))))
624 else
625 frame:Hide()
626 frame:SetScript("OnUpdate", nil)
631 function QuestHelper:flightBegan()
632 if self.flight_data and not self.flight_data.start_time then
633 self.flight_data.start_time = GetTime()
634 local origin, dest = self.flight_data.origin, self.flight_data.dest
635 local eta = self:computeLinkTime(origin, dest, self.flight_data.hash,
636 self.flight_times[origin] and self.flight_times[origin][dest]) or 0
638 local npc = self:getFlightInstructor(self.flight_data.dest) -- Will inform QuestHelper that we're going to be at this NPC in whenever.
639 if npc then
640 local npc_obj = self:GetObjective("monster", npc)
641 npc_obj:PrepareRouting(true)
642 local pos = npc_obj:Position()
643 if pos then
644 local c, z = pos[1].c, pos[1].z
645 local x, y = self.Astrolabe:TranslateWorldMapPosition(c, 0,
646 pos[3]/self.continent_scales_x[c],
647 pos[4]/self.continent_scales_y[c], c, z)
649 self:SetTargetLocation(QuestHelper_IndexLookup[c][z], x, y, eta)
652 npc_obj:DoneRouting()
655 if QuestHelper_Pref.flight_time then
656 self.flight_data.end_time_estimate = time()+eta
657 self:PerformCustomSearch(flight_updater) -- Reusing the search status indicator to display ETA for flight.
662 function QuestHelper:flightEnded(interrupted)
663 local flight_data = self.flight_data
664 if flight_data and not flight_data.end_time then
665 flight_data.end_time = GetTime()
667 if self:processFlightData(flight_data, interrupted) then
668 self:ReleaseTable(flight_data)
669 self.flight_data = nil
672 self:UnsetTargetLocation()
673 self:StopCustomSearch()