remove extra +x
[QuestHelper.git] / pathfinding.lua
blob88f561910b6e7c322fca922968189d926c92128c
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_ConvertCoordsFromWrath(QuestHelper_ConvertCoordsToWrath({36,0.387,0.802, "Stormwind City portal site"}, true)) -- The annoying "wrath" construction here first forces it into Wrath format, then puts it into Native format.
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}, 5}, -- Undercity <--> Silvermoon City
24 {{7, 0.413, 0.178}, {65, 0.414, 0.536}, 5}, -- Durotar <--> Warsong Hold
25 {{43, 0.591, 0.590}, {70, 0.777, 0.283}, 5}, -- Tirisfal Glades <--> Vengeance Landing
27 {{60, 0.592, 0.483}, SILVERMOON_CITY_PORTAL, 5, true, nil, "SILVERMOON_CITY_PORTAL"}, -- Shattrath City --> Silvermoon City
28 {{60, 0.528, 0.531}, THUNDER_BLUFF_PORTAL, 5, true, nil, "THUNDER_BLUFF_PORTAL"}, -- Shattrath City --> Thunder Bluff
29 {{60, 0.522, 0.529}, ORGRIMMAR_PORTAL, 5, true, nil, "ORGRIMMAR_PORTAL"}, -- Shattrath City --> Orgrimmar
30 {{60, 0.517, 0.525}, UNDERCITY_PORTAL, 5, true, nil, "UNDERCITY_PORTAL"}, -- Shattrath City --> Undercity
32 {{67, 0.583, 0.216}, SILVERMOON_CITY_PORTAL, 5, true, nil, "SILVERMOON_CITY_PORTAL"}, -- Dalaran --> Silvermoon City
33 {{67, 0.573, 0.219}, THUNDER_BLUFF_PORTAL, 5, true, nil, "THUNDER_BLUFF_PORTAL"}, -- Dalaran --> Thunder Bluff
34 {{67, 0.553, 0.255}, ORGRIMMAR_PORTAL, 5, true, nil, "ORGRIMMAR_PORTAL"}, -- Dalaran --> Orgrimmar
35 {{67, 0.556, 0.238}, UNDERCITY_PORTAL, 5, true, nil, "UNDERCITY_PORTAL"}, -- Dalaran --> Undercity
36 {{67, 0.563, 0.226}, SHATTRATH_CITY_PORTAL, 5, 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
45 {QuestHelper_ConvertCoordsFromWrath({36, 0.183, 0.255}), {65, 0.597, 0.694}, 210}, -- Stormwind City <--> Valiance Keep (note: new Stormwind location)
46 {{51, 0.047, 0.571}, {70, 0.612, 0.626}, 210}, -- Menethil <--> Daggercap Bay
48 {{60, 0.558, 0.366}, STORMWIND_CITY_PORTAL, 5, true, nil, "STORMWIND_CITY_PORTAL"}, -- Shattrath City --> Stormwind City
49 {{60, 0.563, 0.37}, IRONFORGE_PORTAL, 5, true, nil, "IRONFORGE_PORTAL"}, -- Shattrath City --> Ironforge
50 {{60, 0.552, 0.364}, DARNASSUS_PORTAL, 5, true, nil, "DARNASSUS_PORTAL"}, -- Shattrath City --> Darnassus
51 {{60, 0.596, 0.467}, EXODAR_PORTAL, 5, true, nil, "EXODAR_PORTAL"}, -- Shattrath City --> Exodar
53 {{67, 0.401, 0.628}, STORMWIND_CITY_PORTAL, 5, true, nil, "STORMWIND_CITY_PORTAL"}, -- Dalaran --> Stormwind City
54 {{67, 0.395, 0.640}, IRONFORGE_PORTAL, 5, true, nil, "IRONFORGE_PORTAL"}, -- Dalaran --> Ironforge
55 {{67, 0.389, 0.651}, DARNASSUS_PORTAL, 5, true, nil, "DARNASSUS_PORTAL"}, -- Dalaran --> Darnassus
56 {{67, 0.382, 0.664}, EXODAR_PORTAL, 5, true, nil, "EXODAR_PORTAL"}, -- Dalaran --> Exodar
57 {{67, 0.371, 0.667}, SHATTRATH_CITY_PORTAL, 5, true, nil, "SHATTRATH_CITY_PORTAL"}, -- Dalaran --> Shatt
60 if QuestHelper:IsWrath() then -- siiiiigh
61 table.insert(static_alliance_routes, {{36, 0.228, 0.560}, {16, 0.323, 0.441}, 210}) -- Stormwind City <--> Auberdine (note: new Stormwind location)
62 else
63 table.insert(static_alliance_routes, {{51, 0.044, 0.569}, {16, 0.323, 0.441}, 210}) -- Menethil Harbor <--> Auberdine
64 end
66 local static_shared_routes =
68 {{11, 0.638, 0.387}, {38, 0.257, 0.73}, 210}, -- Ratchet <--> Booty Bay
69 {{40, 0.318, 0.503}, {32, 0.347, 0.84}, 130}, -- Burning Steppes <--> Searing Gorge
71 -- More Alliance routes than anything, but without them theres no valid path to these areas for Horde characters.
72 {{24, 0.559, 0.896}, {21, 0.305, 0.414}, 5}, -- Rut'Theran Village <--> Darnassus
73 {{16, 0.332, 0.398}, {24, 0.548, 0.971}, 210}, -- Auberdine <--> Rut'Theran Village
74 {{16, 0.306, 0.409}, {3, 0.2, 0.546}, 210}, -- Auberdine <--> Azuremyst Isle
76 -- Route to new zone. Not valid, exists only to keep routing from exploding if you don't have the flight routes there.
77 {{41, 0.5, 0.5}, {64, 0.5, 0.5}, 7200}, -- Eversong Woods <--> Sunwell
79 {{70, 0.235, 0.578}, {68, 0.496, 0.784}, 210}, -- Kamagua <--> Moa'ki
80 {{65, 0.789, 0.536}, {68, 0.480, 0.787}, 210}, -- Unu'pe <--> Moa'ki
81 {{67, 0.559, 0.467}, {66, 0.158, 0.428}, 5, true}, -- Dalaran --> Violet Stand
82 {{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)
84 {{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.
87 -- Darkportal is handled specially, depending on whether or not you're level 58+ or not.
88 local dark_portal_route = {{33, 0.587, 0.599}, {56, 0.898, 0.502}, 5}
90 local static_zone_transitions =
92 {2, 11, 0.687, 0.872}, -- Ashenvale <--> The Barrens
93 {2, 6, 0.423, 0.711}, -- Ashenvale <--> Stonetalon Mountains
94 {2, 15, 0.954, 0.484}, -- Ashenvale <--> Azshara
95 {2, 16, 0.289, 0.144}, -- Ashenvale <--> Darkshore
96 {2, 13, 0.557, 0.29}, -- Ashenvale <--> Felwood
97 {21, 24, 0.894, 0.358}, -- Darnassus <--> Teldrassil
98 {22, 11, 0.697, 0.604}, -- Mulgore <--> The Barrens
99 {22, 23, 0.376, 0.33}, -- Mulgore <--> Thunder Bluff
100 {22, 23, 0.403, 0.193}, -- Mulgore <--> Thunder Bluff
101 {3, 12, 0.247, 0.494}, -- Azuremyst Isle <--> The Exodar
102 {3, 12, 0.369, 0.469}, -- Azuremyst Isle <--> The Exodar
103 {3, 12, 0.310, 0.487}, -- Azuremyst Isle <--> The Exodar
104 {3, 12, 0.335, 0.494}, -- Azuremyst Isle <--> The Exodar
105 {3, 9, 0.42, 0.013}, -- Azuremyst Isle <--> Bloodmyst Isle
106 {4, 6, 0.539, 0.032}, -- Desolace <--> Stonetalon Mountains
107 {4, 17, 0.428, 0.976}, -- Desolace <--> Feralas
108 {5, 18, 0.865, 0.115}, -- Silithus <--> Un'Goro Crater
109 {7, 11, 0.341, 0.424}, -- Durotar <--> The Barrens
110 {7, 1, 0.455, 0.121}, -- Durotar <--> Orgrimmar
111 {8, 18, 0.269, 0.516}, -- Tanaris <--> Un'Goro Crater
112 {8, 14, 0.512, 0.21}, -- Tanaris <--> Thousand Needles
113 {10, 11, 0.287, 0.472}, -- Dustwallow Marsh <--> The Barrens
114 {10, 11, 0.563, 0.077}, -- Dustwallow Marsh <--> The Barrens
115 {11, 14, 0.442, 0.915}, -- The Barrens <--> Thousand Needles
116 {13, 19, 0.685, 0.06}, -- Felwood <--> Winterspring
117 {13, 20, 0.669, -0.063}, -- Felwood <--> Moonglade
118 {1, 11, 0.118, 0.69}, -- Orgrimmar <--> The Barrens
119 {17, 14, 0.899, 0.46}, -- Feralas <--> Thousand Needles
120 {6, 11, 0.836, 0.973}, -- Stonetalon Mountains <--> The Barrens
121 {26, 48, 0.521, 0.7}, -- Alterac Mountains <--> Hillsbrad Foothills
122 {26, 35, 0.173, 0.482}, -- Alterac Mountains <--> Silverpine Forest
123 {26, 50, 0.807, 0.347}, -- Alterac Mountains <--> Western Plaguelands
124 {39, 51, 0.454, 0.89}, -- Arathi Highlands <--> Wetlands
125 {39, 48, 0.2, 0.293}, -- Arathi Highlands <--> Hillsbrad Foothills
126 {27, 29, 0.49, 0.071}, -- Badlands <--> Loch Modan
127 -- {27, 32, -0.005, 0.636}, -- Badlands <--> Searing Gorge -- This is the "alliance-only" locked path, I'm disabling it for now entirely
128 {33, 46, 0.519, 0.051}, -- Blasted Lands <--> Swamp of Sorrows
129 {40, 30, 0.79, 0.842}, -- Burning Steppes <--> Redridge Mountains
130 {47, 31, 0.324, 0.363}, -- Deadwind Pass <--> Duskwood
131 {47, 46, 0.605, 0.41}, -- Deadwind Pass <--> Swamp of Sorrows
132 {28, 25, 0.534, 0.349}, -- Dun Morogh <--> Ironforge
133 {28, 29, 0.863, 0.514}, -- Dun Morogh <--> Loch Modan
134 {28, 29, 0.844, 0.31}, -- Dun Morogh <--> Loch Modan
135 {31, 37, 0.801, 0.158}, -- Duskwood <--> Elwynn Forest
136 {31, 37, 0.15, 0.214}, -- Duskwood <--> Elwynn Forest
137 {31, 38, 0.447, 0.884}, -- Duskwood <--> Stranglethorn Vale
138 {31, 38, 0.209, 0.863}, -- Duskwood <--> Stranglethorn Vale
139 {31, 30, 0.941, 0.103}, -- Duskwood <--> Redridge Mountains
140 {31, 49, 0.079, 0.638}, -- Duskwood <--> Westfall
141 {34, 50, 0.077, 0.661}, -- Eastern Plaguelands <--> Western Plaguelands
142 {34, 44, 0.575, 0.000}, -- Eastern Plaguelands <--> Ghostlands
143 {37, 36, 0.321, 0.493}, -- Elwynn Forest <--> Stormwind City -- Don't need to convert because it's in Elwynn coordinates, not Stormwind coordinates
144 {37, 49, 0.202, 0.804}, -- Elwynn Forest <--> Westfall
145 {37, 30, 0.944, 0.724}, -- Elwynn Forest <--> Redridge Mountains
146 {41, 52, 0.567, 0.494}, -- Eversong Woods <--> Silvermoon City
147 {41, 44, 0.486, 0.916}, -- Eversong Woods <--> Ghostlands
148 {35, 43, 0.678, 0.049}, -- Silverpine Forest <--> Tirisfal Glades
149 {42, 50, 0.217, 0.264}, -- The Hinterlands <--> Western Plaguelands
150 {43, 45, 0.619, 0.651}, -- Tirisfal Glades <--> Undercity
151 {43, 50, 0.851, 0.703}, -- Tirisfal Glades <--> Western Plaguelands
152 {38, 49, 0.292, 0.024}, -- Stranglethorn Vale <--> Westfall
153 {48, 35, 0.137, 0.458}, -- Hillsbrad Foothills <--> Silverpine Forest
154 {48, 42, 0.899, 0.253}, -- Hillsbrad Foothills <--> The Hinterlands
155 {29, 51, 0.252, 0}, -- Loch Modan <--> Wetlands
157 -- These are just guesses, since I haven't actually been to these areas.
158 {58, 60, 0.783, 0.545}, -- Nagrand <--> Shattrath City
159 {60, 55, 0.782, 0.492}, -- Shattrath City <--> Terokkar Forest
160 {54, 59, 0.842, 0.284}, -- Blade's Edge Mountains <--> Netherstorm
161 {54, 57, 0.522, 0.996}, -- Blade's Edge Mountains <--> Zangarmarsh
162 {54, 57, 0.312, 0.94}, -- Blade's Edge Mountains <--> Zangarmarsh
163 {56, 55, 0.353, 0.901}, -- Hellfire Peninsula <--> Terokkar Forest
164 {56, 57, 0.093, 0.519}, -- Hellfire Peninsula <--> Zangarmarsh
165 {58, 55, 0.8, 0.817}, -- Nagrand <--> Terokkar Forest
166 {58, 57, 0.343, 0.159}, -- Nagrand <--> Zangarmarsh
167 {58, 57, 0.754, 0.331}, -- Nagrand <--> Zangarmarsh
168 {53, 55, 0.208, 0.271}, -- Shadowmoon Valley <--> Terokkar Forest
169 {55, 57, 0.341, 0.098}, -- Terokkar Forest <--> Zangarmarsh
171 -- Most of these are also guesses :)
172 {65, 72, 0.547, 0.059}, -- Borean Tundra <--> Sholazar Basin
173 {65, 68, 0.967, 0.359}, -- Borean Tundra <--> Dragonblight
174 {74, 72, 0.208, 0.191}, -- Wintergrasp <--> Sholazar
175 {68, 74, 0.250, 0.410}, -- Dragonblight <--> Wintergrasp
176 {68, 71, 0.359, 0.155}, -- Dragonblight <--> Icecrown
177 {68, 66, 0.612, 0.142}, -- Dragonblight <--> Crystalsong
178 {68, 75, 0.900, 0.200}, -- Dragonblight <--> Zul'Drak
179 {68, 69, 0.924, 0.304}, -- Dragonblight <--> Grizzly Hills
180 {68, 69, 0.931, 0.634}, -- Dragonblight <--> Grizzly Hills
181 {70, 69, 0.540, 0.042}, -- Howling Fjord <--> Grizzly Hills
182 {70, 69, 0.233, 0.074}, -- Howling Fjord <--> Grizzly Hills
183 {70, 69, 0.753, 0.060}, -- Howling Fjord <--> Grizzly Hills
184 {69, 75, 0.432, 0.253}, -- Grizzly Hills <--> Zul'Drak
185 {69, 75, 0.583, 0.136}, -- Grizzly Hills <--> Zul'Drak
186 {66, 75, 0.967, 0.599}, -- Crystalsong <--> Zul'Drak
187 {66, 71, 0.156, 0.085}, -- Crystalsong <--> Icecrown
188 {66, 73, 0.706, 0.315}, -- Crystalsong <--> Storm Peaks
189 {66, 73, 0.839, 0.340}, -- Crystalsong <--> Storm Peaks
190 {71, 73, 0.920, 0.767}, -- Icecrown <--> Storm Peaks
193 local walkspeed_multiplier = 1/7 -- Every yard walked takes this many seconds.
195 QuestHelper.prepared_objectives = {}
196 QuestHelper.named_nodes = {}
198 local function cont_dist(a, b)
199 local x, y = a.x-b.x, a.y-b.y
200 return math.sqrt(x*x+y*y)
203 function QuestHelper:ComputeRoute(p1, p2)
204 for i in ipairs(p1[1]) do QuestHelper: Assert(p1[2][i], "p1 nil flightpath error resurgence!") end
205 for i in ipairs(p2[1]) do QuestHelper: Assert(p2[2][i], "p2 nil flightpath error resurgence!") end
207 if not p1 or not p2 then QuestHelper:Error("Boom!") end
208 local graph = self.world_graph
210 graph:PrepareSearch()
212 local l = p2[2]
213 local el = p2[1]
214 for i, n in ipairs(el) do
215 n.e, n.w = l[i], 1
216 n.s = 3
219 l = p1[2]
220 for n in pairs(graph.open) do QuestHelper: Assert(nil, "not empty in preparesearch within computeroute") end
221 for i, n in ipairs(p1[1]) do
222 graph:AddRouteStartNode(n, l[i], el)
225 local e = graph:DoRouteSearch(el)
227 assert(e)
229 local d = e.g+e.e
231 if p1[1] == p2[1] then
232 local x, y = p1[3]-p2[3], p1[4]-p2[4]
233 local d2 = math.sqrt(x*x+y*y)
234 if d2 < d then
235 d = d2
236 e = nil
240 return e, d
243 -- Let's annotate the hell out of this
244 -- 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.)
245 -- 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 :(
246 function QuestHelper:ComputeTravelTime(p1, p2)
247 if not p1 or not p2 then QuestHelper:Error("Boom!") end
248 local graph = self.world_graph
250 graph:PrepareSearch()
252 local l = p2[2] -- Distance to zone boundaries in p2
253 local el = p2[1] -- Zone object for the zone that p2 is in
254 for i, n in ipairs(el) do -- i is the zone index, n is the zone data
255 n.e, n.w = l[i], 1 -- n.e is distance from p2 to the current zone boundary, n.w is 1 (weight?)
256 assert(n.e)
257 n.s = 3 -- this is "state", I think it means "visited". TODO: untangle n.s and make it suck less than it currently does
260 l = p1[2] -- Distance to zone boundaries, again
261 for i, n in ipairs(p1[1]) do
262 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?
265 local e = graph:DoSearch(el)
267 assert(e)
269 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
271 if p1[1] == p2[1] then -- if they're in the same zone, we allow the user to walk from one point to another
272 local x, y = p1[3]-p2[3], p1[4]-p2[4]
273 d = math.min(d, math.sqrt(x*x+y*y))
276 return d
279 function QuestHelper:CreateGraphNode(c, x, y, n)
280 local node = self.world_graph:CreateNode()
282 if y then
283 node.c = c
284 node.x = x
285 node.y = y
286 node.name = n
287 else
288 if not QuestHelper_ZoneLookup[c[1]] then -- exception for Wrath changeover
289 QuestHelper:Assert(not QuestHelper:IsWrath(), "Zone couldn't be found, and should have been")
290 return
292 local cont, zone = unpack(QuestHelper_ZoneLookup[c[1]])
293 node.c = cont
294 node.x, node.y = self.Astrolabe:TranslateWorldMapPosition(cont, zone, c[2], c[3], cont, 0)
295 node.x = node.x * self.continent_scales_x[node.c]
296 node.y = node.y * self.continent_scales_y[node.c]
297 node.name = c[5] or QuestHelper_NameLookup[c[1]]
300 node.w = 1
301 return node
304 function QuestHelper:CreateAndAddZoneNode(z, c, x, y)
305 local node = self:CreateGraphNode(c, x, y)
306 if not node then return end -- exception for Wrath changeover
307 -- Not going to merge nodes.
308 --[[local closest, travel_time = nil, 0
310 for i, n in ipairs(z) do
311 local t = math.sqrt((n.x-node.x)*(n.x-node.x)+(n.y-node.y)*(n.y-node.y))
312 if not closest or t < travel_time then
313 closest, travel_time = n, t
317 if closest and travel_time < 10 then
318 closest.x = (closest.x * closest.w + node.x)/(closest.w+1)
319 closest.y = (closest.y * closest.w + node.y)/(closest.w+1)
320 closest.w = closest.w + 1
321 self.world_graph:DestroyNode(node)
322 return closest
323 else]]
324 table.insert(z, node)
325 return node
326 --end
329 function QuestHelper:CreateAndAddStaticNodePair(data)
330 local node1, node2
332 if data[5] and self.named_nodes[data[5]] then
333 node1 = self.named_nodes[data[5]]
334 else
335 node1 = self:CreateAndAddZoneNode(self.zone_nodes[data[1][1]], data[1])
336 if not node1 then return end -- exception for Wrath changeover
337 if data[5] then self.named_nodes[data[5]] = node1 end
340 if data[6] and self.named_nodes[data[6]] then
341 node2 = self.named_nodes[data[6]]
342 else
343 node2 = self:CreateAndAddZoneNode(self.zone_nodes[data[2][1]], data[2])
344 if not node2 then return end -- exception for Wrath changeover
345 if data[6] then self.named_nodes[data[6]] = node2 end
348 node1.name = node1.name or "route to "..QuestHelper_NameLookup[data[2][1]]
349 node2.name = node2.name or "route to "..QuestHelper_NameLookup[data[1][1]]
351 node1:Link(node2, data[3])
353 if not data[4] then -- If data[4] is true, then this is a one-way trip.
354 node2:Link(node1, data[3])
357 self:yieldIfNeeded()
358 return node1, node2
361 function QuestHelper:GetNodeByName(name, fallback_data)
362 local node = self.named_nodes[name]
363 if not node and fallback_data then
364 node = self:CreateAndAddZoneNode(self.zone_nodes[fallback_data[1]], fallback_data)
365 self.named_nodes[name] = node
367 return node
370 local function nodeLeavesContinent(node, c)
371 if node.c == c then
372 for n, d in pairs(node.n) do
373 if n.c ~= c then
374 return true
378 return false
381 local function isGoodPath(start_node, end_node, i, j)
382 -- Checks to make sure a path doesn't leave the continent only to reenter it.
383 while true do
384 if end_node.p then
385 if end_node.c == i then
386 return false
388 end_node = end_node.p
389 if end_node.c == j then
390 return false
392 else
393 return end_node == start_node
398 local function shouldLink(a, b)
399 -- TODO: Need to have objectives not create links to unreachable nodes.
400 return a ~= b
401 --[[
402 if a == b then
403 return false
404 else
405 for id in pairs(a.id_from) do
406 if not b.id_to[id] then
407 for id in pairs(b.id_to) do
408 if not a.id_from[id] then
409 return true
415 return false
416 end]]
419 local function getNPCNode(npc)
420 local npc_objective = QuestHelper:GetObjective("monster", npc)
421 if npc_objective:Known() then
422 npc_objective:PrepareRouting()
423 local p = npc_objective:Position()
424 local node = nil
426 if p then
427 node = QuestHelper:CreateAndAddZoneNode(p[1], p[1].c, p[3], p[4])
430 npc_objective:DoneRouting()
431 return node
433 return nil
436 function QuestHelper:CreateAndAddTransitionNode(z1, z2, pos)
437 if not z1 or not z2 then
438 QuestHelper:Assert(not QuestHelper:IsWrath(), "Zone couldn't be found, and should have been")
439 return
442 local node = self:CreateGraphNode(pos)
444 local closest, travel_time = nil, 0
446 for i, n in ipairs(z1) do
447 local t = math.sqrt((n.x-node.x)*(n.x-node.x)+(n.y-node.y)*(n.y-node.y))
448 if not closest or t < travel_time then
449 closest, travel_time = n, t
453 if z1 ~= z2 then
454 for i, n in ipairs(z2) do
455 local t = math.sqrt((n.x-node.x)*(n.x-node.x)+(n.y-node.y)*(n.y-node.y))
456 if not closest or t < travel_time then
457 closest, travel_time = n, t
462 if closest and travel_time < 10 then
463 --QuestHelper:TextOut("Node already exists at "..closest.x..", "..closest.y..", name="..(closest.name or "nil"))
464 closest.x = (closest.x * closest.w + node.x)/(closest.w+1)
465 closest.y = (closest.y * closest.w + node.y)/(closest.w+1)
466 closest.w = closest.w + 1
467 local z1_has, z2_has = false, false
469 -- Just because the node already exists, doesn't mean its already in both lists!
471 for i, n in ipairs(z1) do
472 if n == closest then
473 z1_has = true
474 break
478 if z1 ~= z2 then
479 for i, n in ipairs(z2) do
480 if n == closest then
481 z2_has = true
482 break
485 else
486 z2_has = true
489 if not z1_has then table.insert(z1, closest) end
490 if not z2_has then table.insert(z2, closest) end
492 self.world_graph:DestroyNode(node)
493 self:yieldIfNeeded()
494 return closest
495 else
496 table.insert(z1, node)
497 if z1 ~= z2 then table.insert(z2, node) end
498 self:yieldIfNeeded()
499 return node
503 function QuestHelper:ReleaseObjectivePathingInfo(o)
504 if o.setup then
505 for z, pl in pairs(o.p) do
506 self:ReleaseTable(o.d[z])
508 for i, p in ipairs(pl) do
509 self:ReleaseTable(p[2])
510 self:ReleaseTable(p)
513 self:ReleaseTable(pl)
516 self:ReleaseTable(o.d)
517 self:ReleaseTable(o.p)
518 self:ReleaseTable(o.nm)
519 self:ReleaseTable(o.nm2)
520 self:ReleaseTable(o.nl)
522 local cache = o.distance_cache
523 for k, v in pairs(cache) do
524 self:ReleaseTable(v)
525 cache[k] = nil
527 self:ReleaseTable(cache)
529 o.d, o.p, o.nm, o.nm2, o.nl = nil, nil, nil, nil, nil
530 o.distance_cache = nil
531 o.pos, o.sop = nil, nil -- ResetPathing will preserve these values if needed.
532 o.setup = nil
536 function QuestHelper:SetupTeleportInfo(info, can_create)
537 self:TeleportInfoClear(info)
539 if QuestHelper_Home then
540 local node = self:GetNodeByName("HOME_PORTAL", can_create and QuestHelper_Home)
541 if node then
542 local cooldown = self:ItemCooldown(6948)
543 if cooldown then
544 self:SetTeleportInfoTarget(info, node, GetTime()-60*60+cooldown, 60*60, 10)
546 else
547 self.defered_graph_reset = true
551 -- TODO: Compact this. . . and find a better way to tell if the player has a spell.
553 if GetSpellTexture("Teleport: Darnassus") then
554 local node = self:GetNodeByName("DARNASSUS_PORTAL", can_create and DARNASSUS_PORTAL)
555 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
558 if GetSpellTexture("Teleport: Exodar") then
559 local node = self:GetNodeByName("EXODAR_PORTAL", can_create and EXODAR_PORTAL)
560 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
563 if GetSpellTexture("Teleport: Ironforge") then
564 local node = self:GetNodeByName("IRONFORGE_PORTAL", can_create and IRONFORGE_PORTAL)
565 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
568 if GetSpellTexture("Teleport: Moonglade") then
569 local node = self:GetNodeByName("MOONGLADE_PORTAL", can_create and MOONGLADE_PORTAL)
570 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10) else self.defered_graph_reset = true end
573 if GetSpellTexture("Teleport: Orgrimmar") then
574 local node = self:GetNodeByName("ORGRIMMAR_PORTAL", can_create and ORGRIMMAR_PORTAL)
575 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
578 if GetSpellTexture("Teleport: Shattrath") then
579 local node = self:GetNodeByName("SHATTRATH_CITY_PORTAL", can_create and SHATTRATH_CITY_PORTAL)
580 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
583 if GetSpellTexture("Teleport: Silvermoon") then
584 local node = self:GetNodeByName("SILVERMOON_CITY_PORTAL", can_create and SILVERMOON_CITY_PORTAL)
585 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
588 if GetSpellTexture("Teleport: Stormwind") then
589 local node = self:GetNodeByName("STORMWIND_CITY_PORTAL", can_create and STORMWIND_CITY_PORTAL)
590 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
593 if GetSpellTexture("Teleport: Thunder Bluff") then
594 local node = self:GetNodeByName("THUNDER_BLUFF_PORTAL", can_create and THUNDER_BLUFF_PORTAL)
595 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
598 if GetSpellTexture("Teleport: Undercity") then
599 local node = self:GetNodeByName("UNDERCITY_PORTAL", can_create and UNDERCITY_PORTAL)
600 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
603 self:SetTeleportInfoReagent(info, 17031, self:CountItem(17031))
606 function QuestHelper:ResetPathing()
607 for key in pairs(self.named_nodes) do
608 self.named_nodes[key] = nil
611 -- Objectives may include cached information that depends on the world graph.
612 local i = 1
614 while i <= #self.prepared_objectives do
615 local o = self.prepared_objectives[i]
617 if o.setup_count == 0 then
618 table.remove(self.prepared_objectives, i)
619 self:ReleaseObjectivePathingInfo(o)
620 else
621 -- Routing should reset the positions of objectives in the route after the reset is complete.
622 o.pos = nil
623 self:ReleaseObjectivePathingInfo(o)
624 i = i + 1
628 local to_readd = self.prepared_objectives
629 self.prepared_objectives = self.old_prepared_objectives or {}
630 self.old_prepared_objectives = to_readd
632 local zone_nodes = self.zone_nodes
633 if not zone_nodes then
634 zone_nodes = {}
635 self.zone_nodes = zone_nodes
638 local flight_master_nodes = self.flight_master_nodes
639 if not flight_master_nodes then
640 flight_master_nodes = {}
641 self.flight_master_nodes = flight_master_nodes
642 else
643 for key in pairs(flight_master_nodes) do
644 flight_master_nodes[key] = nil
648 self.world_graph:Reset()
649 self:yieldIfNeeded()
651 local continent_scales_x, continent_scales_y = self.continent_scales_x, self.continent_scales_y
652 if not continent_scales_x then
653 continent_scales_x = {}
654 continent_scales_y = {}
655 self.continent_scales_x = continent_scales_x
656 self.continent_scales_y = continent_scales_y
659 for c in pairs(self.Astrolabe:GetMapVirtualContinents()) do
660 if not continent_scales_x[c] then
661 local _, x, y = self.Astrolabe:ComputeDistance(c, 0, 0.25, 0.25, c, 0, 0.75, 0.75)
663 continent_scales_x[c] = x*walkspeed_multiplier*2
664 continent_scales_y[c] = y*walkspeed_multiplier*2
668 for i, name in pairs(QuestHelper_NameLookup) do
669 local z = zone_nodes[i]
670 if not z then
671 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.
672 zone_nodes[i] = z
673 z.i, z.c, z.z = i, unpack(QuestHelper_ZoneLookup[i])
674 else
675 for key in pairs(z) do
676 z[key] = nil
678 z.i, z.c, z.z = i, unpack(QuestHelper_ZoneLookup[i])
682 self:SetupTeleportInfo(self.teleport_info, true)
683 self:yieldIfNeeded()
685 --[[for node, info in pairs(self.teleport_info.node) do
686 self:TextOut("You can teleport to "..(node.name or "nil").. " in "..self:TimeString(info[1]+info[2]-GetTime()))
687 end]]
689 if self.faction == 1 then
690 for i, data in ipairs(static_alliance_routes) do
691 self:CreateAndAddStaticNodePair(data)
693 elseif self.faction == 2 then
694 for i, data in ipairs(static_horde_routes) do
695 self:CreateAndAddStaticNodePair(data)
699 for i, data in ipairs(static_shared_routes) do
700 self:CreateAndAddStaticNodePair(data)
703 if self.player_level >= 58 then
704 dark_portal_route[3] = 5
705 else
706 -- If you can't take the route yet, we'll still add it and just pretend it will take a really long time.
707 dark_portal_route[3] = 86400
710 self:CreateAndAddStaticNodePair(dark_portal_route)
712 local st = self:CreateTable("ResetPathing local st")
714 for i, data in pairs(static_zone_transitions) do
715 st[1], st[2], st[3] = data[1], data[3], data[4]
717 local transnode = self:CreateAndAddTransitionNode(zone_nodes[data[1]],
718 zone_nodes[data[2]],
720 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
723 self:ReleaseTable(st)
725 -- Create and link the flight route nodes.
726 local flight_times = self.flight_times
727 if not flight_times then
728 self:buildFlightTimes()
729 flight_times = self.flight_times
732 for start, list in pairs(flight_times) do
733 for dest, duration in pairs(list) do
734 local a_npc, b_npc = self:getFlightInstructor(start), self:getFlightInstructor(dest)
736 if a_npc and b_npc then
737 local a, b = flight_master_nodes[start], flight_master_nodes[dest]
739 if not a then
740 a = getNPCNode(a_npc)
741 if a then
742 flight_master_nodes[start] = a
743 a.name = (select(3, string.find(start, "^(.*),")) or start).." flight point"
747 if not b then
748 b = getNPCNode(b_npc)
749 if b then
750 flight_master_nodes[dest] = b
751 b.name = (select(3, string.find(dest, "^(.*),")) or dest).." flight point"
755 if a and b then
756 a:Link(b, duration+5)
760 self:yieldIfNeeded()
763 -- id_from, id_to, and id_local will be used in determining whether there is a point to linking nodes together.
764 for i, n in ipairs(self.world_graph.nodes) do
765 n.id_from = self:CreateTable("ResetPathing n.id_from")
766 n.id_to = self:CreateTable("ResetPathing n.id_to")
767 n.id_local = self:CreateTable("ResetPathing n.id_local")
770 -- Setup the local ids a node exists in.
771 for i, list in pairs(zone_nodes) do
772 for _, n in ipairs(list) do
773 n.id_local[i] = true
774 n.id_to[i] = true
775 n.id_from[i] = true
779 -- Figure out where each node can come from or go to.
780 for i, list in pairs(zone_nodes) do
781 for _, node in ipairs(list) do
782 for n in pairs(node.n) do
783 for id in pairs(n.id_local) do node.id_to[id] = true end
784 for id in pairs(node.id_local) do n.id_from[id] = true end
789 -- We'll treat 0 as a special id for where ever it is the player happens to be.
790 for node in pairs(self.teleport_info.node) do
791 node.id_from[0] = true
794 -- Will go through each zone and link all the nodes we have so far with every other node.
795 for _, list in pairs(zone_nodes) do
796 for i = 1,#list do
797 for j = 1,#list do
798 if shouldLink(list[i], list[j]) then
799 list[i]:Link(list[j], cont_dist(list[i], list[j]))
805 self:yieldIfNeeded()
807 -- We don't need to know where the nodes can go or come from now.
808 for i, n in ipairs(self.world_graph.nodes) do
809 self:ReleaseTable(n.id_from)
810 self:ReleaseTable(n.id_to)
811 self:ReleaseTable(n.id_local)
812 n.id_from, n.id_to, n.id_local = nil, nil, nil
815 -- TODO: This is a work around until I fix shouldLink
816 for start, list in pairs(flight_times) do
817 for dest, duration in pairs(list) do
818 local a, b = flight_master_nodes[start], flight_master_nodes[dest]
819 if a and b then
820 a:Link(b, duration+5)
825 self:yieldIfNeeded()
826 -- self.world_graph:SanityCheck()
828 -- Remove objectives again, since we created some for the flight masters.
829 while true do
830 local o = table.remove(self.prepared_objectives)
831 if not o then break end
833 self:ReleaseObjectivePathingInfo(o)
835 if o.setup_count > 0 then
836 -- There's a chance an objective could end up in the list twice, but we'll deal with that by not actually
837 -- adding locations for it if it's already setup.
838 table.insert(to_readd, o)
842 while true do
843 local obj = table.remove(to_readd)
844 if not obj then break end
846 if not obj.setup then -- In case the objective was added multiple times to the to_readd list.
847 obj.d = QuestHelper:CreateTable("ResetPathing obj.d")
848 obj.p = QuestHelper:CreateTable("ResetPathing obj.p")
849 obj.nm = QuestHelper:CreateTable("ResetPathing obj.nm")
850 obj.nm2 = QuestHelper:CreateTable("ResetPathing obj.nm2")
851 obj.nl = QuestHelper:CreateTable("ResetPathing obj.nl")
852 obj.distance_cache = QuestHelper:CreateTable("ResetPathing obj.distance_cache")
853 obj:AppendPositions(obj, 1, nil)
854 obj:FinishAddLoc()
858 if self.i then
859 self.pos[1] = self.zone_nodes[self.i]
860 for i, n in ipairs(self.pos[1]) do
861 local a, b = n.x-self.pos[3], n.y-self.pos[4]
862 self.pos[2][i] = math.sqrt(a*a+b*b)
866 -- And if all went according to plan, we now have a graph we can follow to get from anywhere to anywhere.
868 if self.graph_walker then
869 self.graph_walker:GraphChanged()
874 function QuestHelper:Disallowed(index)
875 return QuestHelper_RestrictedZones[index] ~= QuestHelper_RestrictedZones[self.i]