disable the flight stuff for now, we'll fix it after thanksgiving
[QuestHelper.git] / pathfinding.lua
blob539455320a54df39d17e1ba9f5d319af87b0017c
1 QuestHelper_File["pathfinding.lua"] = "Development Version"
3 local IRONFORGE_PORTAL = {25,0.255,0.084, "Ironforge portal site"}
4 local STORMWIND_CITY_PORTAL = QuestHelper_ConvertCoordsToWrath({36,0.387,0.802, "Stormwind City portal site"}, true) -- Old pre-Wrath coordinates. I could fix it, but . . . meh.
5 local DARNASSUS_PORTAL = {21,0.397,0.824, "Darnassus portal site"}
6 local EXODAR_PORTAL = {12,0.476,0.598, "Exodar portal site"}
8 local SHATTRATH_CITY_PORTAL = {60,0.530,0.492, "Shattrath City portal site"}
9 local DALARAN_PORTAL = {67,0.500,0.394, "Dalaran portal site"}
10 local MOONGLADE_PORTAL = {20,0.563,0.320, "Moonglade portal site"}
12 local SILVERMOON_CITY_PORTAL = {52,0.583,0.192, "Silvermoon City portal site"}
13 local UNDERCITY_PORTAL = {45,0.846,0.163, "Undercity portal site"}
14 local ORGRIMMAR_PORTAL = {1,0.386,0.859, "Orgrimmar portal site"}
15 local THUNDER_BLUFF_PORTAL = {23,0.222,0.168, "Thunder Bluff portal site"}
17 local static_horde_routes =
19 {{7, 0.505, 0.124}, {38, 0.313, 0.303}, 210}, -- Durotar <--> Grom'gol Base Camp
20 {{38, 0.316, 0.289}, {43, 0.621, 0.591}, 210}, -- Grom'gol Base Camp <--> Tirisfal Glades
21 {{43, 0.605, 0.587}, {7, 0.509, 0.141}, 210}, -- Tirisfal Glades <--> Durotar
22 {{45, 0.549, 0.11}, {52, 0.495, 0.148}, 60}, -- Undercity <--> Silvermoon City
24 {{7, 0.413, 0.178}, {65, 0.414, 0.536}, 210}, -- Durotar <--> Warsong Hold
25 {{43, 0.591, 0.590}, {70, 0.777, 0.283}, 210}, -- Tirisfal Glades <--> Vengeance Landing
27 {{60, 0.592, 0.483}, SILVERMOON_CITY_PORTAL, 60, true, nil, "SILVERMOON_CITY_PORTAL"}, -- Shattrath City --> Silvermoon City
28 {{60, 0.528, 0.531}, THUNDER_BLUFF_PORTAL, 60, true, nil, "THUNDER_BLUFF_PORTAL"}, -- Shattrath City --> Thunder Bluff
29 {{60, 0.522, 0.529}, ORGRIMMAR_PORTAL, 60, true, nil, "ORGRIMMAR_PORTAL"}, -- Shattrath City --> Orgrimmar
30 {{60, 0.517, 0.525}, UNDERCITY_PORTAL, 60, true, nil, "UNDERCITY_PORTAL"}, -- Shattrath City --> Undercity
32 {{67, 0.583, 0.216}, SILVERMOON_CITY_PORTAL, 60, true, nil, "SILVERMOON_CITY_PORTAL"}, -- Dalaran --> Silvermoon City
33 {{67, 0.573, 0.219}, THUNDER_BLUFF_PORTAL, 60, true, nil, "THUNDER_BLUFF_PORTAL"}, -- Dalaran --> Thunder Bluff
34 {{67, 0.553, 0.255}, ORGRIMMAR_PORTAL, 60, true, nil, "ORGRIMMAR_PORTAL"}, -- Dalaran --> Orgrimmar
35 {{67, 0.556, 0.238}, UNDERCITY_PORTAL, 60, true, nil, "UNDERCITY_PORTAL"}, -- Dalaran --> Undercity
36 {{67, 0.563, 0.226}, SHATTRATH_CITY_PORTAL, 60, true, nil, "SHATTRATH_CITY_PORTAL"}, -- Dalaran --> Shatt
40 local static_alliance_routes =
42 {{36, 0.639, 0.083}, {25, 0.764, 0.512}, 120}, -- Deeprun Tram
43 {{10, 0.718, 0.565}, {51, 0.047, 0.636}, 210}, -- Theramore Isle <--> Menethil Harmor
44 {{36, 0.228, 0.560}, {16, 0.323, 0.441}, 210}, -- Stormwind City <--> Auberdine
46 {{36, 0.183, 0.255}, {65, 0.597, 0.694}, 210}, -- Stormwind City <--> Valiance Keep
47 {{51, 0.047, 0.571}, {70, 0.612, 0.626}, 210}, -- Menethil <--> Daggercap Bay
49 {{60, 0.558, 0.366}, STORMWIND_CITY_PORTAL, 60, true, nil, "STORMWIND_CITY_PORTAL"}, -- Shattrath City --> Stormwind City
50 {{60, 0.563, 0.37}, IRONFORGE_PORTAL, 60, true, nil, "IRONFORGE_PORTAL"}, -- Shattrath City --> Ironforge
51 {{60, 0.552, 0.364}, DARNASSUS_PORTAL, 60, true, nil, "DARNASSUS_PORTAL"}, -- Shattrath City --> Darnassus
52 {{60, 0.596, 0.467}, EXODAR_PORTAL, 60, true, nil, "EXODAR_PORTAL"}, -- Shattrath City --> Exodar
54 {{67, 0.401, 0.628}, STORMWIND_CITY_PORTAL, 60, true, nil, "STORMWIND_CITY_PORTAL"}, -- Dalaran --> Stormwind City
55 {{67, 0.395, 0.640}, IRONFORGE_PORTAL, 60, true, nil, "IRONFORGE_PORTAL"}, -- Dalaran --> Ironforge
56 {{67, 0.389, 0.651}, DARNASSUS_PORTAL, 60, true, nil, "DARNASSUS_PORTAL"}, -- Dalaran --> Darnassus
57 {{67, 0.382, 0.664}, EXODAR_PORTAL, 60, true, nil, "EXODAR_PORTAL"}, -- Dalaran --> Exodar
58 {{67, 0.371, 0.667}, SHATTRATH_CITY_PORTAL, 60, true, nil, "SHATTRATH_CITY_PORTAL"}, -- Dalaran --> Shatt
61 local static_shared_routes =
63 {{11, 0.638, 0.387}, {38, 0.257, 0.73}, 210}, -- Ratchet <--> Booty Bay
64 {{40, 0.318, 0.503}, {32, 0.347, 0.84}, 130}, -- Burning Steppes <--> Searing Gorge
66 -- More Alliance routes than anything, but without them theres no valid path to these areas for Horde characters.
67 {{24, 0.559, 0.896}, {21, 0.305, 0.414}, 5}, -- Rut'Theran Village <--> Darnassus
68 {{16, 0.332, 0.398}, {24, 0.548, 0.971}, 210}, -- Auberdine <--> Rut'Theran Village
69 {{16, 0.306, 0.409}, {3, 0.2, 0.546}, 210}, -- Auberdine <--> Azuremyst Isle
71 -- Route to new zone. Not valid, exists only to keep routing from exploding if you don't have the flight routes there.
72 {{41, 0.5, 0.5}, {64, 0.5, 0.5}, 7200}, -- Eversong Woods <--> Sunwell
74 {{70, 0.235, 0.578}, {68, 0.496, 0.784}, 210}, -- Kamagua <--> Moa'ki
75 {{65, 0.789, 0.536}, {68, 0.480, 0.787}, 210}, -- Unu'pe <--> Moa'ki
76 {{67, 0.559, 0.467}, {66, 0.158, 0.428}, 5, true}, -- Dalaran --> Violet Stand
77 {{66, 0.157, 0.425}, {67, 0.559, 0.468}, 5, true}, -- Violent Stand --> Dalaran (slightly different coordinates, may be important once solid walls are in)
79 {{34, 0.817, 0.461}, {78, 0.492, 0.312}, 86400}, -- EPL Ebon Hold <--> Scarlet Enclave Ebon Hold. Exists solely to fix some pathing crashes. 24-hour boat ride :D
82 -- Darkportal is handled specially, depending on whether or not you're level 58+ or not.
83 local dark_portal_route = {{33, 0.587, 0.599}, {56, 0.898, 0.502}, 60}
85 local static_zone_transitions =
87 {2, 11, 0.687, 0.872}, -- Ashenvale <--> The Barrens
88 {2, 6, 0.423, 0.711}, -- Ashenvale <--> Stonetalon Mountains
89 {2, 15, 0.954, 0.484}, -- Ashenvale <--> Azshara
90 {2, 16, 0.289, 0.144}, -- Ashenvale <--> Darkshore
91 {2, 13, 0.557, 0.29}, -- Ashenvale <--> Felwood
92 {21, 24, 0.894, 0.358}, -- Darnassus <--> Teldrassil
93 {22, 11, 0.697, 0.604}, -- Mulgore <--> The Barrens
94 {22, 23, 0.376, 0.33}, -- Mulgore <--> Thunder Bluff
95 {22, 23, 0.403, 0.193}, -- Mulgore <--> Thunder Bluff
96 {3, 12, 0.247, 0.494}, -- Azuremyst Isle <--> The Exodar
97 {3, 12, 0.369, 0.469}, -- Azuremyst Isle <--> The Exodar
98 {3, 12, 0.310, 0.487}, -- Azuremyst Isle <--> The Exodar
99 {3, 12, 0.335, 0.494}, -- Azuremyst Isle <--> The Exodar
100 {3, 9, 0.42, 0.013}, -- Azuremyst Isle <--> Bloodmyst Isle
101 {4, 6, 0.539, 0.032}, -- Desolace <--> Stonetalon Mountains
102 {4, 17, 0.428, 0.976}, -- Desolace <--> Feralas
103 {5, 18, 0.865, 0.115}, -- Silithus <--> Un'Goro Crater
104 {7, 11, 0.341, 0.424}, -- Durotar <--> The Barrens
105 {7, 1, 0.455, 0.121}, -- Durotar <--> Orgrimmar
106 {8, 18, 0.269, 0.516}, -- Tanaris <--> Un'Goro Crater
107 {8, 14, 0.512, 0.21}, -- Tanaris <--> Thousand Needles
108 {10, 11, 0.287, 0.472}, -- Dustwallow Marsh <--> The Barrens
109 {10, 11, 0.563, 0.077}, -- Dustwallow Marsh <--> The Barrens
110 {11, 14, 0.442, 0.915}, -- The Barrens <--> Thousand Needles
111 {13, 19, 0.685, 0.06}, -- Felwood <--> Winterspring
112 {13, 20, 0.669, -0.063}, -- Felwood <--> Moonglade
113 {1, 11, 0.118, 0.69}, -- Orgrimmar <--> The Barrens
114 {17, 14, 0.899, 0.46}, -- Feralas <--> Thousand Needles
115 {6, 11, 0.836, 0.973}, -- Stonetalon Mountains <--> The Barrens
116 {26, 48, 0.521, 0.7}, -- Alterac Mountains <--> Hillsbrad Foothills
117 {26, 35, 0.173, 0.482}, -- Alterac Mountains <--> Silverpine Forest
118 {26, 50, 0.807, 0.347}, -- Alterac Mountains <--> Western Plaguelands
119 {39, 51, 0.454, 0.89}, -- Arathi Highlands <--> Wetlands
120 {39, 48, 0.2, 0.293}, -- Arathi Highlands <--> Hillsbrad Foothills
121 {27, 29, 0.49, 0.071}, -- Badlands <--> Loch Modan
122 -- {27, 32, -0.005, 0.636}, -- Badlands <--> Searing Gorge -- This is the "alliance-only" locked path, I'm disabling it for now entirely
123 {33, 46, 0.519, 0.051}, -- Blasted Lands <--> Swamp of Sorrows
124 {40, 30, 0.79, 0.842}, -- Burning Steppes <--> Redridge Mountains
125 {47, 31, 0.324, 0.363}, -- Deadwind Pass <--> Duskwood
126 {47, 46, 0.605, 0.41}, -- Deadwind Pass <--> Swamp of Sorrows
127 {28, 25, 0.534, 0.349}, -- Dun Morogh <--> Ironforge
128 {28, 29, 0.863, 0.514}, -- Dun Morogh <--> Loch Modan
129 {28, 29, 0.844, 0.31}, -- Dun Morogh <--> Loch Modan
130 {31, 37, 0.801, 0.158}, -- Duskwood <--> Elwynn Forest
131 {31, 37, 0.15, 0.214}, -- Duskwood <--> Elwynn Forest
132 {31, 38, 0.447, 0.884}, -- Duskwood <--> Stranglethorn Vale
133 {31, 38, 0.209, 0.863}, -- Duskwood <--> Stranglethorn Vale
134 {31, 30, 0.941, 0.103}, -- Duskwood <--> Redridge Mountains
135 {31, 49, 0.079, 0.638}, -- Duskwood <--> Westfall
136 {34, 50, 0.077, 0.661}, -- Eastern Plaguelands <--> Western Plaguelands
137 {34, 44, 0.575, 0.000}, -- Eastern Plaguelands <--> Ghostlands
138 {37, 36, 0.321, 0.493}, -- Elwynn Forest <--> Stormwind City -- Don't need to convert because it's in Elwynn coordinates, not Stormwind coordinates
139 {37, 49, 0.202, 0.804}, -- Elwynn Forest <--> Westfall
140 {37, 30, 0.944, 0.724}, -- Elwynn Forest <--> Redridge Mountains
141 {41, 52, 0.567, 0.494}, -- Eversong Woods <--> Silvermoon City
142 {41, 44, 0.486, 0.916}, -- Eversong Woods <--> Ghostlands
143 {35, 43, 0.678, 0.049}, -- Silverpine Forest <--> Tirisfal Glades
144 {42, 50, 0.217, 0.264}, -- The Hinterlands <--> Western Plaguelands
145 {43, 45, 0.619, 0.651}, -- Tirisfal Glades <--> Undercity
146 {43, 50, 0.851, 0.703}, -- Tirisfal Glades <--> Western Plaguelands
147 {38, 49, 0.292, 0.024}, -- Stranglethorn Vale <--> Westfall
148 {48, 35, 0.137, 0.458}, -- Hillsbrad Foothills <--> Silverpine Forest
149 {48, 42, 0.899, 0.253}, -- Hillsbrad Foothills <--> The Hinterlands
150 {29, 51, 0.252, 0}, -- Loch Modan <--> Wetlands
152 -- These are just guesses, since I haven't actually been to these areas.
153 {58, 60, 0.783, 0.545}, -- Nagrand <--> Shattrath City
154 {60, 55, 0.782, 0.492}, -- Shattrath City <--> Terokkar Forest
155 {54, 59, 0.842, 0.284}, -- Blade's Edge Mountains <--> Netherstorm
156 {54, 57, 0.522, 0.996}, -- Blade's Edge Mountains <--> Zangarmarsh
157 {54, 57, 0.312, 0.94}, -- Blade's Edge Mountains <--> Zangarmarsh
158 {56, 55, 0.353, 0.901}, -- Hellfire Peninsula <--> Terokkar Forest
159 {56, 57, 0.093, 0.519}, -- Hellfire Peninsula <--> Zangarmarsh
160 {58, 55, 0.8, 0.817}, -- Nagrand <--> Terokkar Forest
161 {58, 57, 0.343, 0.159}, -- Nagrand <--> Zangarmarsh
162 {58, 57, 0.754, 0.331}, -- Nagrand <--> Zangarmarsh
163 {53, 55, 0.208, 0.271}, -- Shadowmoon Valley <--> Terokkar Forest
164 {55, 57, 0.341, 0.098}, -- Terokkar Forest <--> Zangarmarsh
166 -- Most of these are also guesses :)
167 {65, 72, 0.547, 0.059}, -- Borean Tundra <--> Sholazar Basin
168 {65, 68, 0.967, 0.359}, -- Borean Tundra <--> Dragonblight
169 {74, 72, 0.208, 0.191}, -- Wintergrasp <--> Sholazar
170 {68, 74, 0.250, 0.410}, -- Dragonblight <--> Wintergrasp
171 {68, 71, 0.359, 0.155}, -- Dragonblight <--> Icecrown
172 {68, 66, 0.612, 0.142}, -- Dragonblight <--> Crystalsong
173 {68, 75, 0.900, 0.200}, -- Dragonblight <--> Zul'Drak
174 {68, 69, 0.924, 0.304}, -- Dragonblight <--> Grizzly Hills
175 {68, 69, 0.931, 0.634}, -- Dragonblight <--> Grizzly Hills
176 {70, 69, 0.540, 0.042}, -- Howling Fjord <--> Grizzly Hills
177 {70, 69, 0.233, 0.074}, -- Howling Fjord <--> Grizzly Hills
178 {70, 69, 0.753, 0.060}, -- Howling Fjord <--> Grizzly Hills
179 {69, 75, 0.432, 0.253}, -- Grizzly Hills <--> Zul'Drak
180 {69, 75, 0.583, 0.136}, -- Grizzly Hills <--> Zul'Drak
181 {66, 75, 0.967, 0.599}, -- Crystalsong <--> Zul'Drak
182 {66, 71, 0.156, 0.085}, -- Crystalsong <--> Icecrown
183 {66, 73, 0.706, 0.315}, -- Crystalsong <--> Storm Peaks
184 {66, 73, 0.839, 0.340}, -- Crystalsong <--> Storm Peaks
185 {71, 73, 0.920, 0.767}, -- Icecrown <--> Storm Peaks
188 local walkspeed_multiplier = 1/7 -- Every yard walked takes this many seconds.
190 QuestHelper.prepared_objectives = {}
191 QuestHelper.named_nodes = {}
193 local function cont_dist(a, b)
194 local x, y = a.x-b.x, a.y-b.y
195 return math.sqrt(x*x+y*y)
198 function QuestHelper:ComputeRoute(p1, p2)
199 for i in ipairs(p1[1]) do QuestHelper: Assert(p1[2][i], "p1 nil flightpath error resurgence!") end
200 for i in ipairs(p2[1]) do QuestHelper: Assert(p2[2][i], "p2 nil flightpath error resurgence!") end
202 if not p1 or not p2 then QuestHelper:Error("Boom!") end
203 local graph = self.world_graph
205 graph:PrepareSearch()
207 local l = p2[2]
208 local el = p2[1]
209 for i, n in ipairs(el) do
210 n.e, n.w = l[i], 1
211 n.s = 3
214 l = p1[2]
215 for n in pairs(graph.open) do QuestHelper: Assert(nil, "not empty in preparesearch within computeroute") end
216 for i, n in ipairs(p1[1]) do
217 graph:AddRouteStartNode(n, l[i], el)
220 local e = graph:DoRouteSearch(el)
222 assert(e)
224 local d = e.g+e.e
226 if p1[1] == p2[1] then
227 local x, y = p1[3]-p2[3], p1[4]-p2[4]
228 local d2 = math.sqrt(x*x+y*y)
229 if d2 < d then
230 d = d2
231 e = nil
235 return e, d
238 -- Let's annotate the hell out of this
239 -- ComputeTravelTime finds the shortest path between points p1 and p2. It will cheerfully use route boundaries, ships, etc. It returns the distance of that path, gleefully throwing away the path itself. Thanks, ComputeTravelTime! Thomputetraveltime. (That joke does not work as well in this case.)
240 -- ZORBANOTE: Given that Graph works properly, this does too! Almost - it doesn't actually keep track of the last leg when optimizing, though it does create a valid path with a valid cost. Yaaaaaaaay :(
241 function QuestHelper:ComputeTravelTime(p1, p2)
242 if not p1 or not p2 then QuestHelper:Error("Boom!") end
243 local graph = self.world_graph
245 graph:PrepareSearch()
247 local l = p2[2] -- Distance to zone boundaries in p2
248 local el = p2[1] -- Zone object for the zone that p2 is in
249 for i, n in ipairs(el) do -- i is the zone index, n is the zone data
250 n.e, n.w = l[i], 1 -- n.e is distance from p2 to the current zone boundary, n.w is 1 (weight?)
251 assert(n.e)
252 n.s = 3 -- this is "state", I think it means "visited". TODO: untangle n.s and make it suck less than it currently does
255 l = p1[2] -- Distance to zone boundaries, again
256 for i, n in ipairs(p1[1]) do
257 graph:AddStartNode(n, l[i], el) -- We're adding start nodes - a prebuilt cost of the distance-to-that-zone. Each startnode also contains its own endlist, for reasons unknown yet byzantine. "n" is the zone link itself, l[i] is the cost that we still have stored. Why does this need to be in both n.e and AddStartNode?
260 local e = graph:DoSearch(el)
262 assert(e)
264 local d = e.g+e.e -- e.e is presumably the same n.e we introduced earlier. e.g - graph cost? wait a second - does this mean that the graph system is not taking e.e into account? ha ha no it isn't, oh boy oh boy
266 if p1[1] == p2[1] then -- if they're in the same zone, we allow the user to walk from one point to another
267 local x, y = p1[3]-p2[3], p1[4]-p2[4]
268 d = math.min(d, math.sqrt(x*x+y*y))
271 return d
274 function QuestHelper:CreateGraphNode(c, x, y, n)
275 local node = self.world_graph:CreateNode()
277 if y then
278 node.c = c
279 node.x = x
280 node.y = y
281 node.name = n
282 else
283 QuestHelper: Assert(QuestHelper_ZoneLookup[c[1]], "Zone couldn't be found, and should have been")
284 local cont, zone = unpack(QuestHelper_ZoneLookup[c[1]])
285 node.c = cont
286 node.x, node.y = self.Astrolabe:TranslateWorldMapPosition(cont, zone, c[2], c[3], cont, 0)
287 node.x = node.x * self.continent_scales_x[node.c]
288 node.y = node.y * self.continent_scales_y[node.c]
289 node.name = c[5] or QuestHelper_NameLookup[c[1]]
292 node.w = 1
293 return node
296 function QuestHelper:CreateAndAddZoneNode(z, c, x, y)
297 local node = self:CreateGraphNode(c, x, y)
298 if not node then return end -- exception for Wrath changeover
299 -- Not going to merge nodes.
300 --[[local closest, travel_time = nil, 0
302 for i, n in ipairs(z) do
303 local t = math.sqrt((n.x-node.x)*(n.x-node.x)+(n.y-node.y)*(n.y-node.y))
304 if not closest or t < travel_time then
305 closest, travel_time = n, t
309 if closest and travel_time < 10 then
310 closest.x = (closest.x * closest.w + node.x)/(closest.w+1)
311 closest.y = (closest.y * closest.w + node.y)/(closest.w+1)
312 closest.w = closest.w + 1
313 self.world_graph:DestroyNode(node)
314 return closest
315 else]]
316 table.insert(z, node)
317 return node
318 --end
321 function QuestHelper:CreateAndAddStaticNodePair(data)
322 local node1, node2
324 if data[5] and self.named_nodes[data[5]] then
325 node1 = self.named_nodes[data[5]]
326 else
327 node1 = self:CreateAndAddZoneNode(self.zone_nodes[data[1][1]], data[1])
328 if not node1 then return end -- exception for Wrath changeover
329 if data[5] then self.named_nodes[data[5]] = node1 end
332 if data[6] and self.named_nodes[data[6]] then
333 node2 = self.named_nodes[data[6]]
334 else
335 node2 = self:CreateAndAddZoneNode(self.zone_nodes[data[2][1]], data[2])
336 if not node2 then return end -- exception for Wrath changeover
337 if data[6] then self.named_nodes[data[6]] = node2 end
340 node1.name = node1.name or "route to "..QuestHelper_NameLookup[data[2][1]]
341 node2.name = node2.name or "route to "..QuestHelper_NameLookup[data[1][1]]
343 node1:Link(node2, data[3])
345 if not data[4] then -- If data[4] is true, then this is a one-way trip.
346 node2:Link(node1, data[3])
349 QH_Timeslice_Yield()
350 return node1, node2
353 function QuestHelper:GetNodeByName(name, fallback_data)
354 local node = self.named_nodes[name]
355 if not node and fallback_data then
356 node = self:CreateAndAddZoneNode(self.zone_nodes[fallback_data[1]], fallback_data)
357 self.named_nodes[name] = node
359 return node
362 local function nodeLeavesContinent(node, c)
363 if node.c == c then
364 for n, d in pairs(node.n) do
365 if n.c ~= c then
366 return true
370 return false
373 local function isGoodPath(start_node, end_node, i, j)
374 -- Checks to make sure a path doesn't leave the continent only to reenter it.
375 while true do
376 if end_node.p then
377 if end_node.c == i then
378 return false
380 end_node = end_node.p
381 if end_node.c == j then
382 return false
384 else
385 return end_node == start_node
390 local function shouldLink(a, b)
391 -- TODO: Need to have objectives not create links to unreachable nodes.
392 return a ~= b
393 --[[
394 if a == b then
395 return false
396 else
397 for id in pairs(a.id_from) do
398 if not b.id_to[id] then
399 for id in pairs(b.id_to) do
400 if not a.id_from[id] then
401 return true
407 return false
408 end]]
411 local function getNPCNode(npc)
412 local npc_objective = QuestHelper:GetObjective("monster", npc)
413 if npc_objective:Known() then
414 npc_objective:PrepareRouting()
415 local p = npc_objective:Position()
416 local node = nil
418 if p then
419 node = QuestHelper:CreateAndAddZoneNode(p[1], p[1].c, p[3], p[4])
422 npc_objective:DoneRouting()
423 return node
425 return nil
428 function QuestHelper:CreateAndAddTransitionNode(z1, z2, pos)
429 QuestHelper: Assert(z1 and z2, "Zone couldn't be found, and should have been")
431 local node = self:CreateGraphNode(pos)
433 local closest, travel_time = nil, 0
435 for i, n in ipairs(z1) do
436 local t = math.sqrt((n.x-node.x)*(n.x-node.x)+(n.y-node.y)*(n.y-node.y))
437 if not closest or t < travel_time then
438 closest, travel_time = n, t
442 if z1 ~= z2 then
443 for i, n in ipairs(z2) do
444 local t = math.sqrt((n.x-node.x)*(n.x-node.x)+(n.y-node.y)*(n.y-node.y))
445 if not closest or t < travel_time then
446 closest, travel_time = n, t
451 if closest and travel_time < 10 then
452 --QuestHelper:TextOut("Node already exists at "..closest.x..", "..closest.y..", name="..(closest.name or "nil"))
453 closest.x = (closest.x * closest.w + node.x)/(closest.w+1)
454 closest.y = (closest.y * closest.w + node.y)/(closest.w+1)
455 closest.w = closest.w + 1
456 local z1_has, z2_has = false, false
458 -- Just because the node already exists, doesn't mean its already in both lists!
460 for i, n in ipairs(z1) do
461 if n == closest then
462 z1_has = true
463 break
467 if z1 ~= z2 then
468 for i, n in ipairs(z2) do
469 if n == closest then
470 z2_has = true
471 break
474 else
475 z2_has = true
478 if not z1_has then table.insert(z1, closest) end
479 if not z2_has then table.insert(z2, closest) end
481 self.world_graph:DestroyNode(node)
482 QH_Timeslice_Yield()
483 return closest
484 else
485 table.insert(z1, node)
486 if z1 ~= z2 then table.insert(z2, node) end
487 QH_Timeslice_Yield()
488 return node
492 function QuestHelper:ReleaseObjectivePathingInfo(o)
493 if o.setup then
494 for z, pl in pairs(o.p) do
495 self:ReleaseTable(o.d[z])
497 for i, p in ipairs(pl) do
498 self:ReleaseTable(p[2])
499 self:ReleaseTable(p)
502 self:ReleaseTable(pl)
505 self:ReleaseTable(o.d)
506 self:ReleaseTable(o.p)
507 self:ReleaseTable(o.nm)
508 self:ReleaseTable(o.nm2)
509 self:ReleaseTable(o.nl)
511 local cache = o.distance_cache
512 for k, v in pairs(cache) do
513 self:ReleaseTable(v)
514 cache[k] = nil
516 self:ReleaseTable(cache)
518 o.d, o.p, o.nm, o.nm2, o.nl = nil, nil, nil, nil, nil
519 o.distance_cache = nil
520 o.pos, o.sop = nil, nil -- ResetPathing will preserve these values if needed.
521 o.setup = nil
525 function QuestHelper:SetupTeleportInfo(info, can_create)
526 self:TeleportInfoClear(info)
528 if QuestHelper_Home then
529 local node = self:GetNodeByName("HOME_PORTAL", can_create and QuestHelper_Home)
530 if node then
531 local cooldown = self:ItemCooldown(6948)
532 if cooldown then
533 self:SetTeleportInfoTarget(info, node, GetTime()-60*60+cooldown, 60*60, 10)
535 else
536 self.defered_graph_reset = true
540 -- TODO: Compact this. . . and find a better way to tell if the player has a spell.
542 if GetSpellTexture("Teleport: Darnassus") then
543 local node = self:GetNodeByName("DARNASSUS_PORTAL", can_create and DARNASSUS_PORTAL)
544 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
547 if GetSpellTexture("Teleport: Exodar") then
548 local node = self:GetNodeByName("EXODAR_PORTAL", can_create and EXODAR_PORTAL)
549 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
552 if GetSpellTexture("Teleport: Ironforge") then
553 local node = self:GetNodeByName("IRONFORGE_PORTAL", can_create and IRONFORGE_PORTAL)
554 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
557 if GetSpellTexture("Teleport: Moonglade") then
558 local node = self:GetNodeByName("MOONGLADE_PORTAL", can_create and MOONGLADE_PORTAL)
559 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10) else self.defered_graph_reset = true end
562 if GetSpellTexture("Teleport: Orgrimmar") then
563 local node = self:GetNodeByName("ORGRIMMAR_PORTAL", can_create and ORGRIMMAR_PORTAL)
564 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
567 if GetSpellTexture("Teleport: Shattrath") then
568 local node = self:GetNodeByName("SHATTRATH_CITY_PORTAL", can_create and SHATTRATH_CITY_PORTAL)
569 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
572 if GetSpellTexture("Teleport: Silvermoon") then
573 local node = self:GetNodeByName("SILVERMOON_CITY_PORTAL", can_create and SILVERMOON_CITY_PORTAL)
574 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
577 if GetSpellTexture("Teleport: Stormwind") then
578 local node = self:GetNodeByName("STORMWIND_CITY_PORTAL", can_create and STORMWIND_CITY_PORTAL)
579 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
582 if GetSpellTexture("Teleport: Thunder Bluff") then
583 local node = self:GetNodeByName("THUNDER_BLUFF_PORTAL", can_create and THUNDER_BLUFF_PORTAL)
584 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
587 if GetSpellTexture("Teleport: Undercity") then
588 local node = self:GetNodeByName("UNDERCITY_PORTAL", can_create and UNDERCITY_PORTAL)
589 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
592 self:SetTeleportInfoReagent(info, 17031, self:CountItem(17031))
595 function QuestHelper:ResetPathing()
596 for key in pairs(self.named_nodes) do
597 self.named_nodes[key] = nil
600 -- Objectives may include cached information that depends on the world graph.
601 local i = 1
603 while i <= #self.prepared_objectives do
604 local o = self.prepared_objectives[i]
606 if o.setup_count == 0 then
607 table.remove(self.prepared_objectives, i)
608 self:ReleaseObjectivePathingInfo(o)
609 else
610 -- Routing should reset the positions of objectives in the route after the reset is complete.
611 o.pos = nil
612 self:ReleaseObjectivePathingInfo(o)
613 i = i + 1
617 local to_readd = self.prepared_objectives
618 self.prepared_objectives = self.old_prepared_objectives or {}
619 self.old_prepared_objectives = to_readd
621 local zone_nodes = self.zone_nodes
622 if not zone_nodes then
623 zone_nodes = {}
624 self.zone_nodes = zone_nodes
627 local flight_master_nodes = self.flight_master_nodes
628 if not flight_master_nodes then
629 flight_master_nodes = {}
630 self.flight_master_nodes = flight_master_nodes
631 else
632 for key in pairs(flight_master_nodes) do
633 flight_master_nodes[key] = nil
637 self.world_graph:Reset()
638 QH_Timeslice_Yield()
640 local continent_scales_x, continent_scales_y = self.continent_scales_x, self.continent_scales_y
641 if not continent_scales_x then
642 continent_scales_x = {}
643 continent_scales_y = {}
644 self.continent_scales_x = continent_scales_x
645 self.continent_scales_y = continent_scales_y
648 for c in pairs(self.Astrolabe:GetMapVirtualContinents()) do
649 if not continent_scales_x[c] then
650 local _, x, y = self.Astrolabe:ComputeDistance(c, 0, 0.25, 0.25, c, 0, 0.75, 0.75)
652 continent_scales_x[c] = x*walkspeed_multiplier*2
653 continent_scales_y[c] = y*walkspeed_multiplier*2
657 for i, name in pairs(QuestHelper_NameLookup) do
658 local z = zone_nodes[i]
659 if not z then
660 z = QuestHelper:CreateTable("zone") -- This was originally z = {}. I'm pretty sure that the CreateTable/ReleaseTable system is (largely) immune to leaks, and I'm also pretty sure that zones are only created once, at the beginning of the system. Keep this in mind if leaks start occuring.
661 zone_nodes[i] = z
662 z.i, z.c, z.z = i, unpack(QuestHelper_ZoneLookup[i])
663 else
664 for key in pairs(z) do
665 z[key] = nil
667 z.i, z.c, z.z = i, unpack(QuestHelper_ZoneLookup[i])
671 self:SetupTeleportInfo(self.teleport_info, true)
672 QH_Timeslice_Yield()
674 --[[for node, info in pairs(self.teleport_info.node) do
675 self:TextOut("You can teleport to "..(node.name or "nil").. " in "..self:TimeString(info[1]+info[2]-GetTime()))
676 end]]
678 if self.faction == 1 then
679 for i, data in ipairs(static_alliance_routes) do
680 self:CreateAndAddStaticNodePair(data)
682 elseif self.faction == 2 then
683 for i, data in ipairs(static_horde_routes) do
684 self:CreateAndAddStaticNodePair(data)
688 for i, data in ipairs(static_shared_routes) do
689 self:CreateAndAddStaticNodePair(data)
692 if self.player_level >= 58 then
693 dark_portal_route[3] = 5
694 else
695 -- If you can't take the route yet, we'll still add it and just pretend it will take a really long time.
696 dark_portal_route[3] = 86400
699 self:CreateAndAddStaticNodePair(dark_portal_route)
701 local st = self:CreateTable("ResetPathing local st")
703 for i, data in pairs(static_zone_transitions) do
704 st[1], st[2], st[3] = data[1], data[3], data[4]
706 local transnode = self:CreateAndAddTransitionNode(zone_nodes[data[1]],
707 zone_nodes[data[2]],
709 if transnode then transnode.name = QHFormat("ZONE_BORDER", QuestHelper_NameLookup[data[1]], QuestHelper_NameLookup[data[2]]) end -- if the transition node wasn't creatable, we obviously can't name it
712 self:ReleaseTable(st)
714 -- Create and link the flight route nodes.
715 local flight_times = self.flight_times
716 if not flight_times then
717 self:buildFlightTimes()
718 flight_times = self.flight_times
721 for start, list in pairs(flight_times) do
722 for dest, duration in pairs(list) do
723 local a_npc, b_npc = self:getFlightInstructor(start), self:getFlightInstructor(dest)
725 if a_npc and b_npc then
726 local a, b = flight_master_nodes[start], flight_master_nodes[dest]
728 if not a then
729 a = getNPCNode(a_npc)
730 if a then
731 flight_master_nodes[start] = a
732 a.name = (select(3, string.find(start, "^(.*),")) or start).." flight point"
736 if not b then
737 b = getNPCNode(b_npc)
738 if b then
739 flight_master_nodes[dest] = b
740 b.name = (select(3, string.find(dest, "^(.*),")) or dest).." flight point"
744 if a and b then
745 a:Link(b, duration+5)
749 QH_Timeslice_Yield()
752 -- id_from, id_to, and id_local will be used in determining whether there is a point to linking nodes together.
753 for i, n in ipairs(self.world_graph.nodes) do
754 n.id_from = self:CreateTable("ResetPathing n.id_from")
755 n.id_to = self:CreateTable("ResetPathing n.id_to")
756 n.id_local = self:CreateTable("ResetPathing n.id_local")
759 -- Setup the local ids a node exists in.
760 for i, list in pairs(zone_nodes) do
761 for _, n in ipairs(list) do
762 n.id_local[i] = true
763 n.id_to[i] = true
764 n.id_from[i] = true
768 -- Figure out where each node can come from or go to.
769 for i, list in pairs(zone_nodes) do
770 for _, node in ipairs(list) do
771 for n in pairs(node.n) do
772 for id in pairs(n.id_local) do node.id_to[id] = true end
773 for id in pairs(node.id_local) do n.id_from[id] = true end
778 -- We'll treat 0 as a special id for where ever it is the player happens to be.
779 for node in pairs(self.teleport_info.node) do
780 node.id_from[0] = true
783 -- Will go through each zone and link all the nodes we have so far with every other node.
784 for _, list in pairs(zone_nodes) do
785 for i = 1,#list do
786 for j = 1,#list do
787 if shouldLink(list[i], list[j]) then
788 list[i]:Link(list[j], cont_dist(list[i], list[j]))
794 QH_Timeslice_Yield()
796 -- We don't need to know where the nodes can go or come from now.
797 for i, n in ipairs(self.world_graph.nodes) do
798 self:ReleaseTable(n.id_from)
799 self:ReleaseTable(n.id_to)
800 self:ReleaseTable(n.id_local)
801 n.id_from, n.id_to, n.id_local = nil, nil, nil
804 -- TODO: This is a work around until I fix shouldLink
805 for start, list in pairs(flight_times) do
806 for dest, duration in pairs(list) do
807 local a, b = flight_master_nodes[start], flight_master_nodes[dest]
808 if a and b then
809 a:Link(b, duration+5)
814 QH_Timeslice_Yield()
815 -- self.world_graph:SanityCheck()
817 -- Remove objectives again, since we created some for the flight masters.
818 while true do
819 local o = table.remove(self.prepared_objectives)
820 if not o then break end
822 self:ReleaseObjectivePathingInfo(o)
824 if o.setup_count > 0 then
825 -- There's a chance an objective could end up in the list twice, but we'll deal with that by not actually
826 -- adding locations for it if it's already setup.
827 table.insert(to_readd, o)
831 while true do
832 local obj = table.remove(to_readd)
833 if not obj then break end
835 if not obj.setup then -- In case the objective was added multiple times to the to_readd list.
836 obj.d = QuestHelper:CreateTable("ResetPathing obj.d")
837 obj.p = QuestHelper:CreateTable("ResetPathing obj.p")
838 obj.nm = QuestHelper:CreateTable("ResetPathing obj.nm")
839 obj.nm2 = QuestHelper:CreateTable("ResetPathing obj.nm2")
840 obj.nl = QuestHelper:CreateTable("ResetPathing obj.nl")
841 obj.distance_cache = QuestHelper:CreateTable("ResetPathing obj.distance_cache")
842 obj:AppendPositions(obj, 1, nil)
843 obj:FinishAddLoc()
847 if self.i then
848 self.pos[1] = self.zone_nodes[self.i]
849 for i, n in ipairs(self.pos[1]) do
850 local a, b = n.x-self.pos[3], n.y-self.pos[4]
851 self.pos[2][i] = math.sqrt(a*a+b*b)
855 -- And if all went according to plan, we now have a graph we can follow to get from anywhere to anywhere.
857 if self.graph_walker then
858 self.graph_walker:GraphChanged()
863 function QuestHelper:Disallowed(index)
864 return QuestHelper_RestrictedZones[index] ~= QuestHelper_RestrictedZones[self.i]