Merge branch 'master' of git://cams.pavlovian.net/questhelper
[QuestHelper.git] / flightpath.lua
blob77174701fa0923a851c0c14c53feefe9b8778959
1 QuestHelper_File["flightpath.lua"] = "Development Version"
2 QuestHelper_Loadtime["flightpath.lua"] = GetTime()
4 local real_TakeTaxiNode = TakeTaxiNode
5 local real_TaxiNodeOnButtonEnter= TaxiNodeOnButtonEnter
7 assert(type(real_TakeTaxiNode) == "function")
8 assert(type(real_TaxiNodeOnButtonEnter) == "function")
10 local function LookupName(x, y)
11 local best, d2
12 for i = 1,NumTaxiNodes() do
13 local u, v = TaxiNodePosition(i)
14 u = u - x
15 v = v - y
16 u = u*u+v*v
17 if not best or u < d2 then
18 best, d2 = TaxiNodeName(i), u
19 end
20 end
22 return best
23 end
25 local function getRoute(id)
26 for i = 1,NumTaxiNodes() do
27 if GetNumRoutes(i) == 0 then
28 local routes = GetNumRoutes(id)
29 if routes and routes > 0 and routes < 100 then
30 local origin, dest = TaxiNodeName(i), TaxiNodeName(id)
31 local path_hash = 0
33 if routes > 1 then
34 local path_str = ""
36 for j = 1,routes-1 do
37 path_str = string.format("%s/%s", path_str, LookupName(TaxiGetDestX(id, j), TaxiGetDestY(id, j)))
38 end
40 path_hash = QuestHelper:HashString(path_str)
41 end
43 return origin, dest, path_hash
44 end
45 end
46 end
47 end
49 TaxiNodeOnButtonEnter = function(btn, ...)
50 QuestHelper: Assert(btn)
51 local rv = real_TaxiNodeOnButtonEnter(btn, ...)
53 if QuestHelper_Pref.flight_time then
54 local index = btn:GetID()
55 if TaxiNodeGetType(index) == "REACHABLE" then
56 local origin, dest, hash = getRoute(index)
57 local eta, estimate = nil, false
58 if origin then
59 eta = QuestHelper:computeLinkTime(origin, dest, hash, false)
60 if not eta then
61 eta = QuestHelper.flight_times[origin] and QuestHelper.flight_times[origin][dest]
62 estimate = true
63 end
64 end
66 if eta then -- Going to replace the tooltip.
67 GameTooltip:SetOwner(btn, "ANCHOR_RIGHT")
68 GameTooltip:ClearLines()
69 GameTooltip:AddLine(dest, "", 1.0, 1.0, 1.0)
70 GameTooltip:AddDoubleLine(QHText("TRAVEL_ESTIMATE"), (estimate and "|cffffffff≈|r " or "")..QHFormat("TRAVEL_ESTIMATE_VALUE", eta))
71 local cost = TaxiNodeCost(index)
72 if cost > 0 then
73 SetTooltipMoney(GameTooltip, cost)
74 end
75 GameTooltip:Show()
76 end
77 end
78 end
80 return rv
81 end
83 TakeTaxiNode = function(id)
84 local origin, dest, hash = getRoute(id)
86 if origin then
87 local flight_data = QuestHelper.flight_data
88 if not flight_data then
89 flight_data = QuestHelper:CreateTable()
90 QuestHelper.flight_data = flight_data
91 end
93 flight_data.origin = origin
94 flight_data.dest = dest
95 flight_data.hash = hash
96 flight_data.start_time = nil
97 flight_data.end_time = nil
98 flight_data.end_time_estimate = nil
99 end
101 real_TakeTaxiNode(id)
104 function QuestHelper:processFlightData(data, interrupted)
105 local npc = self:getFlightInstructor(data.dest)
106 if not npc then
107 self:TextOut(QHText("TALK_TO_FLIGHT_MASTER"))
108 return false
111 local npc_obj = self:GetObjective("monster", npc)
112 npc_obj:PrepareRouting(true)
114 local pos = npc_obj:Position()
115 if not pos then
116 -- Don't know te location of the flight instructor.
117 self:TextOut(QHText("TALK_TO_FLIGHT_MASTER"))
118 npc_obj:DoneRouting()
119 return false
122 local correct = true
124 if pos[1].c ~= self.c then
125 correct = false
126 else
127 local x, y = self.Astrolabe:TranslateWorldMapPosition(self.c, self.z, self.x, self.y, self.c, 0)
128 x = x * self.continent_scales_x[self.c]
129 y = y * self.continent_scales_y[self.c]
130 local t = (x-pos[3])*(x-pos[3])+(y-pos[4])*(y-pos[4])
132 --self:TextOut(string.format("(%f,%f) vs (%f,%f) is %f", x, y, pos[3], pos[4], t))
134 if t > 5*5 then
135 correct = false
139 npc_obj:DoneRouting()
141 if not correct then
142 return true
145 if data.start_time and data.end_time and data.end_time > data.start_time then
146 local routes = QuestHelper_FlightRoutes_Local[self.faction]
147 if not routes then
148 routes = {}
149 QuestHelper_FlightRoutes_Local[self.faction] = routes
152 local origin = routes[data.origin]
153 if not origin then
154 origin = {}
155 routes[data.origin] = origin
158 local dest = origin[data.dest]
159 if not dest then
160 dest = {}
161 origin[data.dest] = dest
164 dest[data.hash] = data.end_time - data.start_time
166 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
167 dest.interrupt_count = (dest.interrupt_count or 0) + 1
168 else
169 dest.no_interrupt_count = (dest.no_interrupt_count or 0) + 1
173 return true
176 function QuestHelper:getFlightInstructor(area)
177 local fi_table = QuestHelper_FlightInstructors_Local[self.faction]
178 if fi_table then
179 local npc = fi_table[area]
180 if npc then
181 return npc
185 local static = QuestHelper_StaticData[QuestHelper_Locale]
187 if static then
188 fi_table = static.flight_instructors and static.flight_instructors[self.faction]
189 if fi_table then
190 return fi_table[area]
195 local function getTime(tbl, orig, dest, hash)
196 tbl = tbl and tbl[orig]
197 tbl = tbl and tbl[dest]
198 return tbl and tbl[hash] ~= true and tbl[hash]
201 -- 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 . . .
202 -- For each pair of "origin/dest" in tbl, determine if there is a direct path. (If there is, the hash will be 0.)
203 -- If so, find the flightpath distance and the "walking" distance. Add up walking and flightpath separately, and return the sums.
204 local function getWalkToFlight(tbl, fi1, fi2)
205 local f, w = 0, 0
207 if tbl then
208 for origin, list in pairs(tbl) do
209 for dest, hashlist in pairs(list) do
210 if type(hashlist[0]) == "number" then
211 local npc1, npc2 = (fi1 and fi1[origin]) or (fi2 and fi2[origin]), (fi1 and fi1[dest]) or (fi2 and fi2[dest])
212 if npc1 and npc2 then
213 local obj1, obj2 = QuestHelper:GetObjective("monster", npc1), QuestHelper:GetObjective("monster", npc2)
214 obj1:PrepareRouting(true, {failable = true})
215 obj2:PrepareRouting(true, {failable = true})
217 local pos1, pos2 = obj1:Position(), obj2:Position()
219 if pos1 and pos2 then
220 local x, y = pos1[3]-pos2[3], pos1[4]-pos2[4]
221 w = w + math.sqrt(x*x+y*y)
222 f = f + hashlist[0]
225 obj2:DoneRouting()
226 obj1:DoneRouting()
233 return f, w
236 -- Determines the general multiple faster than flying is than walking.
237 function QuestHelper:computeWalkToFlightMult()
238 local l = QuestHelper_FlightRoutes_Local[self.faction]
239 local s = QuestHelper_StaticData[self.locale]
240 s = s and s.flight_routes
241 s = s and s[self.faction]
243 local fi1 = QuestHelper_FlightInstructors_Local[self.faction]
244 local fi2 = QuestHelper_StaticData[self.locale]
245 fi2 = fi2 and fi2.flight_instructors
246 fi2 = fi2 and fi2[self.faction]
248 local f1, w1 = getWalkToFlight(l, fi1, fi2)
249 local f2, w2 = getWalkToFlight(s, fi1, fi2)
250 return (f1+f2+0.032876)/(w1+w2+0.1)
253 function QuestHelper:computeLinkTime(origin, dest, hash, fallback)
254 -- Only works for directly connected flight points.
255 if origin == dest then
256 return 0
259 local l = QuestHelper_FlightRoutes_Local[self.faction]
260 local s = QuestHelper_StaticData[self.locale]
261 s = s and s.flight_routes
262 s = s and s[self.faction]
264 hash = hash or 0
266 -- Will try to lookup flight time there, failing that, will use the time from there to here.
267 local t = getTime(l, origin, dest, hash) or getTime(s, origin, dest, hash) or
268 getTime(l, dest, origin, hash) or getTime(s, dest, origin, hash) or fallback
270 if t == nil then -- Don't have any recored information on this flight time, will estimate based on distances.
271 l = QuestHelper_FlightInstructors_Local[self.faction]
272 s = QuestHelper_StaticData[self.locale]
273 s = s and s.flight_instructors
274 s = s and s[self.faction]
276 local npc1, npc2 = (l and l[origin]) or (s and s[origin]),
277 (l and l[dest]) or (s and s[dest])
279 if npc1 and npc2 then
280 local obj1, obj2 = self:GetObjective("monster", npc1), self:GetObjective("monster", npc2)
281 obj1:PrepareRouting(true)
282 obj2:PrepareRouting(true)
284 local pos1, pos2 = obj1:Position(), obj2:Position()
286 if pos1 and pos2 then
287 local x, y = pos1[3]-pos2[3], pos1[4]-pos2[4]
289 t = math.sqrt(x*x+y*y)*self.flight_scalar
292 obj2:DoneRouting()
293 obj1:DoneRouting()
297 if t and type(t) ~= "number" then
298 QuestHelper:AppendNotificationError("2008-10-11 computelinktime is not a number", string.format("%s %s", type(t), fallback and type(fallback) or "(nil)"))
299 return nil
301 return t
304 local moonglade_fp = nil
306 function QuestHelper:addLinkInfo(data, flight_times)
307 if data then
308 if select(2, UnitClass("player")) ~= "DRUID" then
309 -- As only druids can use the flight point in moonglade, we need to figure out
310 -- where it is so we can ignore it.
312 if not moonglade_fp then
314 local fi_table = QuestHelper_FlightInstructors_Local[self.faction]
316 if fi_table then for area, npc in pairs(fi_table) do
317 local npc_obj = self:GetObjective("monster", npc)
318 npc_obj:PrepareRouting(true, {failable = true})
319 local pos = npc_obj:Position()
320 if pos and QuestHelper_IndexLookup[pos[1].c][pos[1].z] == 20 and string.find(area, ",") then -- I'm kind of guessing here
321 moonglade_fp = area
322 npc_obj:DoneRouting()
323 break
325 npc_obj:DoneRouting()
326 end end
328 if not moonglade_fp then
329 fi_table = QuestHelper_StaticData[QuestHelper_Locale]
330 fi_table = fi_table and fi_table.flight_instructors and fi_table.flight_instructors[self.faction]
332 if fi_table then for area, npc in pairs(fi_table) do
333 local npc_obj = self:GetObjective("monster", npc)
334 npc_obj:PrepareRouting(true, {failable = true})
335 local pos = npc_obj:Position()
336 if pos and QuestHelper_IndexLookup[pos[1].c][pos[1].z] == 20 and string.find(area, ",") then
337 moonglade_fp = area
338 npc_obj:DoneRouting()
339 break
341 npc_obj:DoneRouting()
342 end end
345 if not moonglade_fp then
346 -- This will always be unknown for the session, even if you call buildFlightTimes again
347 -- but if it's unknown then you won't be able to
348 -- get the waypoint this session since you're not a druid
349 -- so its all good.
350 moonglade_fp = "unknown"
355 for origin, list in pairs(data) do
356 local tbl = flight_times[origin]
357 if not tbl then
358 tbl = self:CreateTable("Flightpath AddLinkInfo origin table")
359 flight_times[origin] = tbl
362 for dest, hashs in pairs(list) do
363 if origin ~= moonglade_fp and QuestHelper_KnownFlightRoutes[dest] and hashs[0] then
364 local tbl2 = tbl[dest]
365 if not tbl2 then
366 local t = self:computeLinkTime(origin, dest)
367 if t then
368 tbl2 = self:CreateTable("Flightpath AddLinkInfo origin->dest data table")
369 tbl[dest] = tbl2
370 tbl2[1] = t
371 tbl2[2] = dest
380 local visited = {}
382 local function getDataTime(ft, origin, dest)
383 local str = nil
384 local data = ft[origin][dest]
385 local t = data[1]
387 for key in pairs(visited) do visited[key] = nil end
389 while true do
390 local n = data[2]
392 -- We might be asked about a route that visits the same point multiple times, and
393 -- since this is effectively a linked list, we need to check for this to avoid
394 -- infinite loops.
395 if visited[n] then return end
396 visited[n] = true
398 local temp = QuestHelper:computeLinkTime(origin, n, str and QuestHelper:HashString(str) or 0, false)
400 if temp then
401 t = temp + (n == dest and 0 or ft[n][dest][1])
404 if n == dest then break end
405 str = string.format("%s/%s", str or "", n)
406 data = ft[n][dest]
409 return t
412 -- Used for loading status results. This is a messy solution.
413 QuestHelper_Flight_Updates = 0
414 QuestHelper_Flight_Updates_Current = 0
416 function QuestHelper:buildFlightTimes()
417 self.flight_scalar = self:computeWalkToFlightMult()
419 local flight_times = self.flight_times
420 if not flight_times then
421 flight_times = self:CreateTable()
422 self.flight_times = flight_times
425 for key, list in pairs(flight_times) do
426 self:ReleaseTable(list)
427 flight_times[key] = nil
430 local l = QuestHelper_FlightRoutes_Local[self.faction]
431 local s = QuestHelper_StaticData[self.locale]
432 s = s and s.flight_routes
433 s = s and s[self.faction]
435 self:addLinkInfo(l, flight_times)
436 self:addLinkInfo(s, flight_times)
438 QuestHelper_Flight_Updates_Current = 0
440 -- 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.
441 local cont = true
442 while cont do
443 cont = false
444 local origin = nil
445 while true do
446 origin = next(flight_times, origin)
447 if not origin then break end
448 local list = flight_times[origin]
450 for dest, data in pairs(list) do
451 QuestHelper_Flight_Updates_Current = QuestHelper_Flight_Updates_Current + 1
452 if flight_times[dest] then for dest2, data2 in pairs(flight_times[dest]) do
453 if dest2 ~= origin then
454 local dat = list[dest2]
456 if not dat then
457 dat = self:CreateTable()
458 dat[1], dat[2] = data[1]+data2[1], dest
459 list[dest2] = dat
460 dat[1] = getDataTime(flight_times, origin, dest2)
462 if not dat[1] then
463 self:ReleaseTable(dat)
464 list[dest2] = nil
465 else
466 cont = true
468 else
469 local o1, o2 = dat[1], dat[2] -- Temporarly replace old data for the sake of looking up its time.
470 if o2 ~= dest then
471 dat[1], dat[2] = data[1]+data2[1], dest
472 local t2 = getDataTime(flight_times, origin, dest2)
474 if t2 and t2 < o1 then
475 dat[1] = t2
476 cont = true
477 else
478 dat[1], dat[2] = o1, o2
483 end end
484 QH_Timeslice_Yield()
489 QuestHelper_Flight_Updates = QuestHelper_Flight_Updates_Current
491 -- Replace the tables with simple times.
492 for orig, list in pairs(flight_times) do
493 for dest, data in pairs(list) do
494 local t = data[1]
495 self:ReleaseTable(data)
496 list[dest] = t
501 function QuestHelper:taxiMapOpened()
502 local routes = QuestHelper_FlightRoutes_Local[self.faction]
504 if not routes then
505 routes = {}
506 QuestHelper_FlightRoutes_Local[self.faction] = routes
509 local sroutes = QuestHelper_StaticData[self.locale]
510 sroutes = sroutes and sroutes.flight_routes
511 sroutes = sroutes and sroutes[self.faction]
513 local origin, altered = nil, false
515 for i = 1,NumTaxiNodes() do
516 local name = TaxiNodeName(i)
517 if not QuestHelper_KnownFlightRoutes[name] then
518 QuestHelper_KnownFlightRoutes[name] = true
519 altered = true
520 self:TextOut("New flight master: " .. name)
523 if GetNumRoutes(i) == 0 then -- Zero hops from this location, must be where we are.
524 origin = name
528 if origin then
529 local npc = UnitName("npc")
531 if npc then
532 -- Record who the flight instructor for this location is.
533 local fi_table = QuestHelper_FlightInstructors_Local[self.faction]
534 if not fi_table then
535 fi_table = {}
536 QuestHelper_FlightInstructors_Local[self.faction] = fi_table
539 fi_table[origin] = npc
542 if not self.flight_times[origin] then
543 -- If this is true, then we probably either didn't who the flight instructor here was,
544 -- or did know but didn't know where.
545 -- As we should now know, the flight times should be updated.
546 altered = true
549 if self.flight_data and self:processFlightData(self.flight_data) then
550 self:TextOut(QHText("TALK_TO_FLIGHT_MASTER_COMPLETE"))
551 self:ReleaseTable(self.flight_data)
552 self.flight_data = nil
555 for j = 1,NumTaxiNodes() do
556 local node_count = GetNumRoutes(j)
557 if node_count and i ~= j and node_count > 0 and node_count < 100 then
558 for k = 1,node_count do
559 local n1, n2 = LookupName(TaxiGetSrcX(j, k), TaxiGetSrcY(j, k)), LookupName(TaxiGetDestX(j, k), TaxiGetDestY(j, k))
561 -- let's make this a bit easier and faster
562 if not QuestHelper_KnownFlightRoutes[n2] then
563 QuestHelper_KnownFlightRoutes[n2] = true
564 altered = true
565 self:TextOut("New flight master implied: " .. n2)
568 --QuestHelper:TextOut(string.format("taxi %d: %d is %s/%s", j, k, n1, n2))
570 assert(n1 and n2 and n1 ~= n2)
572 local dest1, dest2 = routes[n1], routes[n2]
574 if not dest1 then
575 dest1 = {}
576 routes[n1] = dest1
579 if not dest2 then
580 dest2 = {}
581 routes[n2] = dest2
584 local hash1, hash2 = dest1[n2], dest2[n1]
586 if not hash1 then
587 hash1 = {}
588 dest1[n2] = hash1
591 if not hash2 then
592 hash2 = {}
593 dest2[n1] = hash2
596 if not hash1[0] then
597 if not (sroutes and sroutes[n1] and sroutes[n1][n2] and sroutes[n1][n2][0]) then
598 -- hadn't been considering this link in pathing.
599 self:TextOut(string.format("Found new link between %s and %s", n1, n2))
600 altered = true
602 hash1[0] = true
605 if not hash2[0] then
606 if not (sroutes and sroutes[n2] and sroutes[n2][n1] and sroutes[n2][n1][0]) then
607 -- hadn't been considering this link in pathing.
608 self:TextOut(string.format("Found new link between %s and %s", n2, n1))
609 altered = true
611 hash2[0] = true
618 if altered then
619 self:TextOut(QHText("ROUTES_CHANGED"))
620 self:TextOut(QHText("WILL_RESET_PATH"))
621 self.defered_graph_reset = true
622 self.defered_flight_times = true
623 --self:buildFlightTimes()
627 local elapsed = 0
628 local function flight_updater(frame, delta)
629 elapsed = elapsed + delta
630 if elapsed > 1 then
631 elapsed = elapsed - 1
632 local data = QuestHelper.flight_data
633 if data then
634 frame:SetText(string.format("%s: %s", QuestHelper:HighlightText(select(3, string.find(data.dest, "^(.-),")) or data.dest),
635 QuestHelper:TimeString(math.max(0, data.end_time_estimate-time()))))
636 else
637 frame:Hide()
638 frame:SetScript("OnUpdate", nil)
643 function QuestHelper:flightBegan()
644 if self.flight_data and not self.flight_data.start_time then
645 self.flight_data.start_time = GetTime()
646 local origin, dest = self.flight_data.origin, self.flight_data.dest
647 local eta = self:computeLinkTime(origin, dest, self.flight_data.hash,
648 self.flight_times[origin] and self.flight_times[origin][dest]) or 0
650 local npc = self:getFlightInstructor(self.flight_data.dest) -- Will inform QuestHelper that we're going to be at this NPC in whenever.
651 if npc then
652 local npc_obj = self:GetObjective("monster", npc)
653 npc_obj:PrepareRouting(true)
654 local pos = npc_obj:Position()
655 if pos then
656 local c, z = pos[1].c, pos[1].z
657 local x, y = self.Astrolabe:TranslateWorldMapPosition(c, 0,
658 pos[3]/self.continent_scales_x[c],
659 pos[4]/self.continent_scales_y[c], c, z)
661 self:SetTargetLocation(QuestHelper_IndexLookup[c][z], x, y, eta)
664 npc_obj:DoneRouting()
667 if QuestHelper_Pref.flight_time then
668 self.flight_data.end_time_estimate = time()+eta
669 self:PerformCustomSearch(flight_updater) -- Reusing the search status indicator to display ETA for flight.
674 function QuestHelper:flightEnded(interrupted)
675 local flight_data = self.flight_data
676 if flight_data and not flight_data.end_time then
677 flight_data.end_time = GetTime()
679 if self:processFlightData(flight_data, interrupted) then
680 self:ReleaseTable(flight_data)
681 self.flight_data = nil
684 self:UnsetTargetLocation()
685 self:StopCustomSearch()