Merge branch 'master' into achievements
[QuestHelper.git] / flightpath.lua
blob12ec4e911630ccaf55f5dffe39adda45d1b62890
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 local function getSrcDest(id)
50 local snode
51 for i = 1, NumTaxiNodes() do
52 if GetNumRoutes(i) == 0 then
53 snode = TaxiNodeName(i)
54 break
55 end
56 end
57 local dnode = TaxiNodeName(id)
58 return snode, dnode
59 end
61 local function getEtaEstimate(snode, dnode)
62 local eta, estimate = nil, false
63 if QH_Flight_Distances[snode] and QH_Flight_Distances[snode][dnode] then
64 eta, estimate = unpack(QH_Flight_Distances[snode][dnode])
65 end
66 return eta, estimate
67 end
69 TaxiNodeOnButtonEnter = function(btn, ...)
70 QuestHelper: Assert(btn)
71 local rv = real_TaxiNodeOnButtonEnter(btn, ...)
73 if QuestHelper_Pref.flight_time then
74 local index = btn:GetID()
75 if TaxiNodeGetType(index) == "REACHABLE" then
77 local snode, dnode = getSrcDest(index)
79 local eta, estimate = getEtaEstimate(snode, dnode)
81 if eta then -- Going to replace the tooltip.
82 GameTooltip:SetOwner(btn, "ANCHOR_RIGHT")
83 GameTooltip:ClearLines()
84 GameTooltip:AddLine(TaxiNodeName(index), "", 1.0, 1.0, 1.0)
85 GameTooltip:AddDoubleLine(QHText("TRAVEL_ESTIMATE"), (estimate and "|cffffffff≈|r " or "")..QHFormat("TRAVEL_ESTIMATE_VALUE", eta))
86 local cost = TaxiNodeCost(index)
87 if cost > 0 then
88 SetTooltipMoney(GameTooltip, cost)
89 end
90 GameTooltip:Show()
91 end
92 end
93 end
95 return rv
96 end
98 TakeTaxiNode = function(id)
99 local src, dest = getSrcDest(id)
101 if src then
102 local flight_data = QuestHelper.flight_data
103 if not flight_data then
104 flight_data = QuestHelper:CreateTable()
105 QuestHelper.flight_data = flight_data
108 flight_data.src = src
109 flight_data.dest = dest
110 flight_data.start_time = nil
111 flight_data.end_time = nil
112 flight_data.end_time_estimate = nil
115 real_TakeTaxiNode(id)
118 function QuestHelper:getFlightInstructor(area)
119 local fi_table = QuestHelper_FlightInstructors_Local[self.faction]
120 if fi_table then
121 local npc = fi_table[area]
122 if npc then
123 return npc
127 local static = QuestHelper_StaticData[QuestHelper_Locale]
129 if static then
130 fi_table = static.flight_instructors and static.flight_instructors[self.faction]
131 if fi_table then
132 return fi_table[area]
137 local function getTime(tbl, orig, dest, hash)
138 tbl = tbl and tbl[orig]
139 tbl = tbl and tbl[dest]
140 return tbl and tbl[hash] ~= true and tbl[hash]
143 -- 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 . . .
144 -- For each pair of "origin/dest" in tbl, determine if there is a direct path. (If there is, the hash will be 0.)
145 -- If so, find the flightpath distance and the "walking" distance. Add up walking and flightpath separately, and return the sums.
146 local function getWalkToFlight(tbl, fi1, fi2)
147 local f, w = 0, 0
149 if tbl then
150 for origin, list in pairs(tbl) do
151 for dest, hashlist in pairs(list) do
152 if type(hashlist[0]) == "number" then
153 local npc1, npc2 = (fi1 and fi1[origin]) or (fi2 and fi2[origin]), (fi1 and fi1[dest]) or (fi2 and fi2[dest])
154 if npc1 and npc2 then
155 local obj1, obj2 = QuestHelper:GetObjective("monster", npc1), QuestHelper:GetObjective("monster", npc2)
156 obj1:PrepareRouting(true, {failable = true})
157 obj2:PrepareRouting(true, {failable = true})
159 local pos1, pos2 = obj1:Position(), obj2:Position()
161 if pos1 and pos2 then
162 local x, y = pos1[3]-pos2[3], pos1[4]-pos2[4]
163 w = w + math.sqrt(x*x+y*y)
164 f = f + hashlist[0]
167 obj2:DoneRouting()
168 obj1:DoneRouting()
175 return f, w
178 -- Determines the general multiple faster than flying is than walking.
179 function QuestHelper:computeWalkToFlightMult()
180 local l = QuestHelper_FlightRoutes_Local[self.faction]
181 local s = QuestHelper_StaticData[self.locale]
182 s = s and s.flight_routes
183 s = s and s[self.faction]
185 local fi1 = QuestHelper_FlightInstructors_Local[self.faction]
186 local fi2 = QuestHelper_StaticData[self.locale]
187 fi2 = fi2 and fi2.flight_instructors
188 fi2 = fi2 and fi2[self.faction]
190 local f1, w1 = getWalkToFlight(l, fi1, fi2)
191 local f2, w2 = getWalkToFlight(s, fi1, fi2)
192 return (f1+f2+0.032876)/(w1+w2+0.1)
195 function QuestHelper:computeLinkTime(origin, dest, hash, fallback)
196 -- Only works for directly connected flight points.
197 if origin == dest then
198 return 0
201 local l = QuestHelper_FlightRoutes_Local[self.faction]
202 local s = QuestHelper_StaticData[self.locale]
203 s = s and s.flight_routes
204 s = s and s[self.faction]
206 hash = hash or 0
208 -- Will try to lookup flight time there, failing that, will use the time from there to here.
209 local t = getTime(l, origin, dest, hash) or getTime(s, origin, dest, hash) or
210 getTime(l, dest, origin, hash) or getTime(s, dest, origin, hash) or fallback
212 if t == nil then -- Don't have any recored information on this flight time, will estimate based on distances.
213 l = QuestHelper_FlightInstructors_Local[self.faction]
214 s = QuestHelper_StaticData[self.locale]
215 s = s and s.flight_instructors
216 s = s and s[self.faction]
218 local npc1, npc2 = (l and l[origin]) or (s and s[origin]),
219 (l and l[dest]) or (s and s[dest])
221 if npc1 and npc2 then
222 local obj1, obj2 = self:GetObjective("monster", npc1), self:GetObjective("monster", npc2)
223 obj1:PrepareRouting(true)
224 obj2:PrepareRouting(true)
226 local pos1, pos2 = obj1:Position(), obj2:Position()
228 if pos1 and pos2 then
229 local x, y = pos1[3]-pos2[3], pos1[4]-pos2[4]
231 t = math.sqrt(x*x+y*y)*self.flight_scalar
234 obj2:DoneRouting()
235 obj1:DoneRouting()
239 if t and type(t) ~= "number" then
240 QuestHelper:AppendNotificationError("2008-10-11 computelinktime is not a number", string.format("%s %s", type(t), fallback and type(fallback) or "(nil)"))
241 return nil
243 return t
246 local moonglade_fp = nil
248 function QuestHelper:addLinkInfo(data, flight_times)
249 if data then
250 if select(2, UnitClass("player")) ~= "DRUID" then
251 -- As only druids can use the flight point in moonglade, we need to figure out
252 -- where it is so we can ignore it.
254 if not moonglade_fp then
256 local fi_table = QuestHelper_FlightInstructors_Local[self.faction]
258 if fi_table then for area, npc in pairs(fi_table) do
259 local npc_obj = self:GetObjective("monster", npc)
260 npc_obj:PrepareRouting(true, {failable = true})
261 local pos = npc_obj:Position()
262 if pos and QuestHelper_IndexLookup[pos[1].c][pos[1].z] == 20 and string.find(area, ",") then -- I'm kind of guessing here
263 moonglade_fp = area
264 npc_obj:DoneRouting()
265 break
267 npc_obj:DoneRouting()
268 end end
270 if not moonglade_fp then
271 fi_table = QuestHelper_StaticData[QuestHelper_Locale]
272 fi_table = fi_table and fi_table.flight_instructors and fi_table.flight_instructors[self.faction]
274 if fi_table then for area, npc in pairs(fi_table) do
275 local npc_obj = self:GetObjective("monster", npc)
276 npc_obj:PrepareRouting(true, {failable = true})
277 local pos = npc_obj:Position()
278 if pos and QuestHelper_IndexLookup[pos[1].c][pos[1].z] == 20 and string.find(area, ",") then
279 moonglade_fp = area
280 npc_obj:DoneRouting()
281 break
283 npc_obj:DoneRouting()
284 end end
287 if not moonglade_fp then
288 -- This will always be unknown for the session, even if you call buildFlightTimes again
289 -- but if it's unknown then you won't be able to
290 -- get the waypoint this session since you're not a druid
291 -- so its all good.
292 moonglade_fp = "unknown"
297 for origin, list in pairs(data) do
298 local tbl = flight_times[origin]
299 if not tbl then
300 tbl = self:CreateTable("Flightpath AddLinkInfo origin table")
301 flight_times[origin] = tbl
304 for dest, hashs in pairs(list) do
305 if origin ~= moonglade_fp and QuestHelper_KnownFlightRoutes[dest] and hashs[0] then
306 local tbl2 = tbl[dest]
307 if not tbl2 then
308 local t = self:computeLinkTime(origin, dest)
309 if t then
310 tbl2 = self:CreateTable("Flightpath AddLinkInfo origin->dest data table")
311 tbl[dest] = tbl2
312 tbl2[1] = t
313 tbl2[2] = dest
322 local visited = {}
324 local function getDataTime(ft, origin, dest)
325 local str = nil
326 local data = ft[origin][dest]
327 local t = data[1]
329 for key in pairs(visited) do visited[key] = nil end
331 while true do
332 local n = data[2]
334 -- We might be asked about a route that visits the same point multiple times, and
335 -- since this is effectively a linked list, we need to check for this to avoid
336 -- infinite loops.
337 if visited[n] then return end
338 visited[n] = true
340 local temp = QuestHelper:computeLinkTime(origin, n, str and QuestHelper:HashString(str) or 0, false)
342 if temp then
343 t = temp + (n == dest and 0 or ft[n][dest][1])
346 if n == dest then break end
347 str = string.format("%s/%s", str or "", n)
348 data = ft[n][dest]
351 return t
354 -- Used for loading status results. This is a messy solution.
355 QuestHelper_Flight_Updates = 0
356 QuestHelper_Flight_Updates_Current = 0
358 function QuestHelper:buildFlightTimes()
359 self.flight_scalar = self:computeWalkToFlightMult()
361 local flight_times = self.flight_times
362 if not flight_times then
363 flight_times = self:CreateTable()
364 self.flight_times = flight_times
367 for key, list in pairs(flight_times) do
368 self:ReleaseTable(list)
369 flight_times[key] = nil
372 local l = QuestHelper_FlightRoutes_Local[self.faction]
373 local s = QuestHelper_StaticData[self.locale]
374 s = s and s.flight_routes
375 s = s and s[self.faction]
377 self:addLinkInfo(l, flight_times)
378 self:addLinkInfo(s, flight_times)
380 QuestHelper_Flight_Updates_Current = 0
382 -- 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.
383 local cont = true
384 while cont do
385 cont = false
386 local origin = nil
387 while true do
388 origin = next(flight_times, origin)
389 if not origin then break end
390 local list = flight_times[origin]
392 for dest, data in pairs(list) do
393 QuestHelper_Flight_Updates_Current = QuestHelper_Flight_Updates_Current + 1
394 if flight_times[dest] then for dest2, data2 in pairs(flight_times[dest]) do
395 if dest2 ~= origin then
396 local dat = list[dest2]
398 if not dat then
399 dat = self:CreateTable()
400 dat[1], dat[2] = data[1]+data2[1], dest
401 list[dest2] = dat
402 dat[1] = getDataTime(flight_times, origin, dest2)
404 if not dat[1] then
405 self:ReleaseTable(dat)
406 list[dest2] = nil
407 else
408 cont = true
410 else
411 local o1, o2 = dat[1], dat[2] -- Temporarly replace old data for the sake of looking up its time.
412 if o2 ~= dest then
413 dat[1], dat[2] = data[1]+data2[1], dest
414 local t2 = getDataTime(flight_times, origin, dest2)
416 if t2 and t2 < o1 then
417 dat[1] = t2
418 cont = true
419 else
420 dat[1], dat[2] = o1, o2
425 end end
426 QH_Timeslice_Yield()
431 QuestHelper_Flight_Updates = QuestHelper_Flight_Updates_Current
433 -- Replace the tables with simple times.
434 for orig, list in pairs(flight_times) do
435 for dest, data in pairs(list) do
436 local t = data[1]
437 self:ReleaseTable(data)
438 list[dest] = t
443 function QuestHelper:taxiMapOpened()
444 for i = 1,NumTaxiNodes() do
445 local name = TaxiNodeName(i)
446 if not QuestHelper_KnownFlightRoutes[name] then
447 QuestHelper_KnownFlightRoutes[name] = true
448 self:TextOut("New flight master: " .. name)
449 QH_Route_FlightPathRecalc()
454 local elapsed = 0
455 local function flight_updater(frame, delta)
456 elapsed = elapsed + delta
457 if elapsed > 1 then
458 elapsed = elapsed - 1
459 local data = QuestHelper.flight_data
460 if data then
461 frame:SetText(string.format("%s: %s", QuestHelper:HighlightText(select(3, string.find(data.dest, "^(.-),")) or data.dest),
462 QuestHelper:TimeString(math.max(0, data.end_time_estimate-time()))))
463 else
464 frame:Hide()
465 QH_Hook(frame, "OnUpdate", nil)
470 function QuestHelper:flightBegan()
471 if self.flight_data and not self.flight_data.start_time then
472 self.flight_data.start_time = GetTime()
473 local src, dest = self.flight_data.src, self.flight_data.dest
476 local eta, estimate = getEtaEstimate(src, dest)
478 --[[
479 local npc = self:getFlightInstructor(self.flight_data.dest) -- Will inform QuestHelper that we're going to be at this NPC in whenever.
480 if npc then
481 local npc_obj = self:GetObjective("monster", npc)
482 npc_obj:PrepareRouting(true)
483 local pos = npc_obj:Position()
484 if pos then
485 local c, z = pos[1].c, pos[1].z
486 local x, y = self.Astrolabe:TranslateWorldMapPosition(c, 0,
487 pos[3]/self.continent_scales_x[c],
488 pos[4]/self.continent_scales_y[c], c, z)
490 self:SetTargetLocation(QuestHelper_IndexLookup[c][z], x, y, eta)
493 npc_obj:DoneRouting()
494 end]]
497 local loc = QH_Flight_Destinations[dest]
498 if loc then -- sometimes we just don't have a loc, I think due to flightpath recalculations going on right then
499 QuestHelper.routing_ac, QuestHelper.routing_ax, QuestHelper.routing_ay, QuestHelper.routing_c, QuestHelper.routing_z = QuestHelper_ParentLookup[loc.p], loc.x, loc.y, QuestHelper_ZoneLookup[loc.p][1], QuestHelper_ZoneLookup[loc.p][2]
503 if eta and QuestHelper_Pref.flight_time then
504 self.flight_data.end_time_estimate = time() + eta
505 self:PerformCustomSearch(flight_updater) -- Reusing the search status indicator to display ETA for flight.
510 function QuestHelper:flightEnded(interrupted)
511 local flight_data = self.flight_data
512 if flight_data and not flight_data.end_time then
513 flight_data.end_time = GetTime()
515 self:UnsetTargetLocation()
516 self:StopCustomSearch()