update changes
[QuestHelper.git] / routing.lua
blob5b3fd793621898fe84864c776f34fc6ba2909e84
1 QuestHelper_File["routing.lua"] = "Development Version"
3 -- Create module
4 QuestHelper.Routing = {}
5 local Routing = QuestHelper.Routing
6 Routing.qh = QuestHelper
8 -- Constants:
9 local improve_margin = 1e-8
11 -- Module Status:
12 local work_done = 0
13 local route_pass = 0
14 local coroutine_running = false
15 local coroutine_stop_time = 0
17 function QuestHelper:yieldIfNeeded()
18 if coroutine_running then
19 -- Check if we've run our alotted time
20 if GetTime() > coroutine_stop_time then
21 -- As a safety, reset stop time to 0. If somehow we fail to set it next time,
22 -- we'll be sure to yield promptly.
23 coroutine_stop_time = 0
24 coroutine.yield()
25 end
26 end
27 end
29 local function CalcObjectivePriority(obj)
30 local priority = obj.priority
32 for o in pairs(obj.before) do
33 if o.watched then
34 priority = math.min(priority, CalcObjectivePriority(o))
35 end
36 end
38 obj.real_priority = priority
39 return priority
40 end
42 local Route = {}
43 Route.__index = Route
44 Routing.Route = Route -- Make it available as a member
46 -- This should pass on all routes. If it does not, *things need to be fixed*. No, commenting tests out is not an adequate response - this *must* pass. Eventually this will get rolled into the unsucky Route class.
47 function Route:sanity()
49 local assert = assert
51 if QuestHelper.Error then
52 assert = function(a, b)
53 if not a then
54 QuestHelper:TextOut("Route:sanity(): id="..self.id.."; best_route="..Routing.best_route.id)
55 QuestHelper:Error(b or "Assertion Failed")
56 end
57 end
58 end
60 local l = 0
62 --QuestHelper:TextOut(QuestHelper:StringizeTable(self))
63 for i = 0,#self-1 do
64 --QuestHelper:TextOut(tostring(i))
65 --QuestHelper:TextOut(QuestHelper:StringizeTable(self[i]))
66 --QuestHelper:TextOut(tostring(self[i].len))
67 assert(self[i].len)
68 l = l + self[i].len
69 end
71 --QuestHelper:TextOut("sd: "..l.." rd: "..self.distance)
72 assert(math.abs(l-self.distance) < 0.0001)
74 for i, info in ipairs(self) do
75 assert(self.index[info.obj] == i)
76 assert(info.pos)
77 end
79 for obj, i in pairs(self.index) do
80 assert(self[i].obj == obj)
81 end
83 for i = 1, #self-1 do
84 local l = QuestHelper:ComputeTravelTime(self[i].pos, self[i+1].pos, true)
85 assert(math.abs(l-self[i].len) < 0.0001, "Compare at "..i..": "..l.." vs "..self[i].len)
86 end
88 return true
89 end
91 function Route:findObjectiveRange(obj)
92 local mn, smn = 1, 1
93 local smx = #self
94 local mx = smx+1
96 local l = math.floor((smn+smx)*0.5)
97 local r = l+1
99 while true do
100 while true do
101 if l < smn then
102 return mn, mx
105 local o = self[l].obj
107 if obj.real_priority > o.real_priority or obj.after[o] then
108 mn = l+1
109 smn = r
110 l = math.floor((smn+smx)*0.5)
111 r = l+1
112 break
113 elseif obj.real_priority < o.real_priority or obj.before[o] then
114 mx = l
115 smx = l-1
116 l = math.floor((smn+smx)*0.5)
117 r = l+1
118 break
121 if r > smx then
122 return mn, mx
125 o = self[r].obj
127 if obj.real_priority > o.real_priority or obj.after[o] then
128 mn = r+1
129 smn = r+1
130 l = math.floor((smn+smx)*0.5)
131 r = l+1
132 break
133 elseif obj.real_priority < o.real_priority or obj.before[o] then
134 mx = r
135 smx = l-1
136 l = math.floor((smn+smx)*0.5)
137 r = l+1
138 break
141 l = l - 1
142 r = r + 1
147 function Route:addObjectiveFast(obj)
148 assert(self:sanity())
149 local indexes = self.index
150 local len = #self
151 local info = QuestHelper:CreateTable()
152 assert(not indexes[obj])
154 info.obj = obj
156 if len == 0 then
157 local d
158 self[1] = info
159 indexes[obj] = 1
160 d, info.pos = obj:TravelTime(self[0].pos, true)
161 self[0].len = d
162 self.distance = d
163 return 1
166 local player_pos = QuestHelper.pos
167 local pos = obj.location
168 local c, x, y = pos[1].c, pos[3], pos[4]
170 local mn, mx = self:findObjectiveRange(obj)
171 local index, distsqr
173 for i = mn, math.min(mx, len) do
174 local p = self[i].pos
175 if c == p[1].c then
176 local u, v = p[3]-x, p[4]-y
177 local d2 = u*u+v*v
178 if not index or d2 < distsqr then
179 index, distsqr = i, d2
184 if not index then
185 -- No nodes with the same continent already.
186 -- If the same continent as the player, add to start of list, otherwise add to end of the list.
187 index = c == player_pos[1].c and mn or mx
190 -- The next question, do I insert at that point, or do I insert after it?
191 if index ~= mx and index <= len then
192 local p1 = self[index].pos
194 if p1[1].c == c then
195 local p0
197 if index == 1 then
198 p0 = player_pos
199 else
200 p0 = self[index-1].pos
203 local oldstart, newstart
205 if p0[1].c == c then
206 local u, v = p0[3]-x, p0[4]-y
207 newstart = math.sqrt(u*u+v*v)
208 u, v = p0[3]-p1[3], p0[4]-p1[4]
209 oldstart = math.sqrt(u*u+v*v)
210 else
211 newstart = 0
212 oldstart = 0
215 local p2
216 if index ~= len then
217 p2 = self[index+1].pos
220 local oldend, newend
221 if p2 and p2[1].c == c then
222 local u, v = p2[3]-x, p2[4]-y
223 newend = math.sqrt(u*u+v*v)
224 u, v = p2[3]-p1[3], p2[4]-p1[4]
225 oldend = math.sqrt(u*u+v*v)
226 else
227 newend = 0
228 oldend = 0
231 if oldstart+newend < newstart+oldend then
232 index = index + 1
238 QuestHelper:yieldIfNeeded((mn-mx+3)*0.05) -- The above checks don't require much effort.
240 if index > len then
241 local previnfo = self[index-1]
242 assert(previnfo)
243 local d
244 d, info.pos = obj:TravelTime(previnfo.pos)
245 assert(info.pos)
246 QuestHelper:yieldIfNeeded(0.5)
247 previnfo.len = d
248 self.distance = self.distance + d
249 else
250 local d1, d2
252 local previnfo = self[index-1]
253 d1, d2, info.pos = obj:TravelTime2(previnfo.pos, self[index].pos, previnfo.no_cache)
255 info.len = d2
256 self.distance = self.distance + (d1 - previnfo.len + d2)
257 previnfo.len = d1
259 QuestHelper:yieldIfNeeded(1)
262 -- Finally, insert the objective.
263 table.insert(self, index, info)
264 indexes[obj] = index
266 -- Fix indexes of shifted elements.
267 for i = index+1,len+1 do
268 local obj = self[i].obj
269 assert(indexes[obj] == i-1)
270 indexes[obj] = i
273 assert(self:sanity())
275 return index
278 function Route:addObjectiveBest(obj, old_index, old_distance)
279 assert(self:sanity())
281 local indexes = self.index
282 local len = #self
283 local info = QuestHelper:CreateTable()
284 assert(not indexes[obj])
286 info.obj = obj
288 if len == 0 then
289 indexes[obj] = 1
290 self.distance, info.pos = obj:TravelTime(self[0].pos, true)
291 info.len = 0
292 self[0].len = self.distance
293 self[1] = info
294 return 1
297 local sanityfixed = nil -- If we've done something to improve the sanity of the overall path, i.e. force the path into a situation where it's no longer trying to turn in quests before the quest has been completed, then we definitely want to accept this path overall. Before, this wasn't a problem, since this function was so unstable that it would randomly change the path anyway, and it doesn't *break* things once they're fixed. Now that we have a check that this function actually *improves* the path, we need a slightly more complicated definition of "improve". Ideally, that shouldn't be necessary, but for now it is (until, at least, this function actually puts things in the best location, and that will have to wait until the get-optimal-path functions actually get optimal paths.)
299 local best_index, best_delta, best_d1, best_d2, best_p
300 local no_cache, prev_pos, prev_len
301 local mn, mx = self:findObjectiveRange(obj)
303 if old_index and mn <= old_index and old_index <= mx then
304 -- We're trying to re-evaluate it, and it could remain in the same place.
305 -- So that place is our starting best known place.
306 best_index, best_delta = old_index, old_distance - self.distance
308 if best_delta < 0 then
309 -- Somehow, removing the objective actually made the route worse...
310 -- Just re-figure things from scratch.
311 -- TODO: THIS SHOULD NEVER HAPPEN dear god find out what's causing this and stop it
312 --QuestHelper:TextOut("made route worse wtf")
313 best_index, best_delta = nil, nil
317 local pinfo = self[mn-1]
318 no_cache, prev_pos, prev_len = pinfo.no_cache, pinfo.pos, pinfo.len
320 for i = mn, math.min(mx, len) do
321 assert(prev_pos == self[i-1].pos)
323 local info = self[i]
324 local pos = info.pos
326 local d1, d2, p = obj:TravelTime2(prev_pos, pos, no_cache)
328 QuestHelper:yieldIfNeeded(1)
330 local delta = d1 + d2 - prev_len
332 if not best_index or ((delta + improve_margin) < best_delta) or ((i == best_index) and not best_d1) then
333 -- Best so far is:
334 -- * First item we reach
335 -- * Better than previous best
336 -- * We're looking at our best already. But we just got here; how could this be best?
337 -- If this was our prior location and we didn't find anything better earlier in the route,
338 -- that's how. Save the specifics, 'cause we didn't compute them when setting up.
339 best_index, best_delta, best_d1, best_d2, best_p = i, delta, d1, d2, p
342 prev_pos = pos
343 prev_len = info.len
344 no_cache = false
347 if mx > len then
348 assert(mx == len+1)
349 assert(prev_pos == self[len].pos)
350 local delta, p = obj:TravelTime(prev_pos, no_cache)
352 QuestHelper:yieldIfNeeded(.5)
354 if not best_index or ((delta + improve_margin) < best_delta) or ((mx == best_index) and not best_d1) then
355 info.pos = p
356 info.len = 0
357 self[len].len = delta
358 self.distance = self.distance + delta
359 table.insert(self, info)
360 indexes[obj] = mx
362 assert(self:sanity())
364 return mx
368 info.pos = best_p
369 info.len = best_d2
371 local pinfo = self[best_index-1]
372 self.distance = self.distance + (best_d1 - pinfo.len + best_d2)
373 pinfo.len = best_d1
375 table.insert(self, best_index, info)
377 -- QuestHelper:Assert(math.abs(QuestHelper:ComputeTravelTime(self[best_index-1].pos, self[best_index].pos) - self[best_index-1].len) < 0.0001, "aaaaargh")
378 --[[ -- I don't think this is necessary now that TravelTime2 explicitly does this internally, but I'm keeping it anyway.
379 self.distance = self.distance - self[best_index-1].len
380 self[best_index-1].len = QuestHelper:ComputeTravelTime(self[best_index-1].pos, self[best_index].pos, true)
381 self.distance = self.distance + self[best_index-1].len
384 indexes[obj] = best_index
386 for i = best_index+1,len+1 do
387 assert(indexes[self[i].obj] == i-1)
388 indexes[self[i].obj] = i
391 if not old_index or (mn > old_index or old_index > mx) and mn <= best_index and best_index <= mx then
392 -- if we didn't have an old index, or our old index was out of bounds and our best index is in bounds, then we've done something Majorly Good and we should be using this path even if the old one was faster
393 sanityfixed = 1
396 assert(self:sanity())
398 return best_index, sanityfixed
401 function Route:removeObjective(obj)
402 assert(self:sanity())
404 local indexes = self.index
405 local index = indexes[obj]
406 local old_distance = self.distance
408 assert(index)
409 local info = self[index]
410 assert(info.obj == obj)
412 --[[
413 Removing end item: subtract last distance, nothing to recalculate
414 Removing other item: recalculate location of next objective, between prior position and objective after next
415 Special case: if there is no location after next, just recalc location of next objective
416 --]]
417 if index == #self then
418 self.distance = self.distance - self[index-1].len
419 self[index-1].len = 0
420 else
421 local pinfo = self[index-1]
422 local info1 = self[index+1]
423 local info2 = self[index+2]
424 local no_cache = (index == 1)
426 local d1, d2
428 if info2 then
429 d1, d2, info1.pos = info1.obj:TravelTime2(pinfo.pos, info2.pos, no_cache)
430 QuestHelper:yieldIfNeeded(1)
431 self.distance = self.distance - pinfo.len - info.len - info1.len + d1 + d2
432 info1.len = d2
433 else
434 d1, info1.pos = info1.obj:TravelTime(pinfo.pos, no_cache)
435 QuestHelper:yieldIfNeeded(0.5)
436 self.distance = self.distance - pinfo.len - info.len + d1
439 pinfo.len = d1
442 QuestHelper:ReleaseTable(info)
443 indexes[obj] = nil
444 table.remove(self, index)
446 for i = index,#self do
447 -- Fix indexes of shifted elements.
448 local obj = self[i].obj
449 assert(indexes[obj] == i+1)
450 indexes[obj] = i
453 assert(self:sanity())
454 -- assert(self.distance <= old_distance)
456 return index
459 local links = {}
460 local seen = {}
462 function Route:breed(route_map)
463 local indexes = self.index
464 local len = #self
466 local info
467 local r
469 local prev_pos = QuestHelper.pos
470 assert(self[0].pos == prev_pos)
472 -- Pick which objective goes first, selecting from first objective of each route,
473 -- and scaling by the route's fitness and distance from player, with a random adjustment factor.
474 -- While we're at it, record some data about the fitness of adjacent objectives
475 for route in pairs(route_map) do
476 assert(route:sanity())
477 local fit = route.fitness
478 local pos = route[1].pos
479 local w
481 if prev_pos[1].c == pos[1].c then
482 local u, v = prev_pos[3]-pos[3], prev_pos[4]-pos[4]
483 w = math.sqrt(u*u+v*v)
484 else
485 w = 500
488 w = fit * math.random() / w
490 if not info or w > r then
491 info, r = route[1], w
494 for i = 1,len do
495 local obj = route[i].obj
496 local tbl = links[obj]
497 if not tbl then
498 tbl = QuestHelper:CreateTable()
499 links[obj] = tbl
502 if i ~= 1 then
503 local info = route[i-1]
504 local obj2 = info.obj
505 tbl[info] = (tbl[info] or 0) + fit
508 if i ~= len then
509 local info = route[i+1]
510 local obj2 = info.obj
511 if obj.real_priority <= obj2.real_priority or obj.before[obj2] then
512 tbl[info] = (tbl[info] or 0) + fit
517 QuestHelper:yieldIfNeeded(0.01*len)
520 -- Record info for the 'Player Position' objective, so we don't mess it up later
521 seen[self[0].obj] = self[0].pos
523 -- Record the objective that we chose to put first
524 local obj = info.obj
525 indexes[obj] = 1
526 seen[obj] = info.pos -- Save its position, because we don't want to clobber any of the info objects yet
527 prev_pos = info.pos
529 last = links[obj]
530 links[obj] = nil
532 -- Scan the rest of the places in the route, and pick objectives to go there
533 for index = 2,len do
534 info = nil
535 local c = 1
537 -- Scan the list of scores from the prior objective
538 for i, weight in pairs(last) do
539 if links[i.obj] then
540 -- Only consider an item if we have scores for that item
541 local w
542 local pos = i.pos
543 if prev_pos[1].c == pos[1].c then
544 local u, v = prev_pos[3]-pos[3], prev_pos[4]-pos[4]
545 w = math.sqrt(u*u+v*v)
546 else
547 w = 500
550 w = weight * math.random() / w
552 if not info or w > r then
553 info, r = i, w
556 c = c + 1
559 -- In case we had no valid scores, scan the remaining objectives and score by distance
560 if not info then
561 for obj in pairs(links) do
562 local pos = obj.pos
563 local w
564 if prev_pos[1] == pos[1] then
565 -- Same zone
566 local u, v = prev_pos[3]-pos[3], prev_pos[4]-pos[4]
567 w = math.sqrt(u*u+v*v)
568 elseif prev_pos[1].c == pos[1].c then
569 -- Same continent. -- Assume twices as long.
570 local u, v = prev_pos[3]-pos[3], prev_pos[4]-pos[4]
571 w = 2*math.sqrt(u*u+v*v)
572 else
573 -- Different continent. Assume fixed value of 5 minutes.
574 w = 300
577 w = math.random() / w
579 if not info or w > r then
580 local route = next(route_map)
581 info, r = route[route.index[obj]], w
585 assert(info)
588 -- Add the selected item to the route
589 obj = info.obj
590 indexes[obj] = index
591 prev_pos = info.pos
592 seen[obj] = prev_pos
593 assert(info.obj == obj)
595 -- Get the scores table for this objective, clear it out, discard the scores from the prior objective, and save these scores for next time around
596 local link = links[obj]
597 links[obj] = nil
598 QuestHelper:ReleaseTable(last)
599 last = link
601 QuestHelper:yieldIfNeeded(0.01*c)
604 -- Clean up the last table
605 QuestHelper:ReleaseTable(last)
607 -- Now that we've got our objectives lined up, fill in the info objects with the positions we saved
608 for obj, i in pairs(indexes) do
609 assert(seen[obj])
610 local info = self[i]
611 info.obj, info.pos = obj, seen[obj]
612 seen[obj] = nil
615 -- Now randomly randomize some of the route (aka mutation)
616 while math.random() > 0.3 do
617 local l = math.floor(math.random()^1.6*(len-1))+1
618 local i = math.random(1, len-l)
619 local j = i+l
621 -- Reverse a chunk of the route
622 for k = 0, j-i-1 do
623 self[i+k], self[j-k] = self[j-k], self[i+k]
627 -- But wait, after all that some objectives might violate the rules. Make sure the route follows
628 -- the rules.
629 local invalid = true
630 local invalid_passes = 0
631 while invalid do
632 invalid_passes = invalid_passes + 1
633 QuestHelper: Assert(invalid_passes <= 100, "Too many mutation passes needed to preserve sanity, something has gone Horribly Wrong, please report this as a bug (you will probably need to restart WoW, sorry about that)") -- space is so it works in the real code
634 invalid = false
635 local i = 1
636 while i <= #self do
637 -- Make sure all the objectives have valid positions in the list.
638 local info = self[i]
639 local mn, mx = self:findObjectiveRange(info.obj)
640 if i < mn then
641 -- In theory, 'i' shouldn't be increased here, as the next
642 -- element will be shifted down into the current position.
644 -- However, it is possible for an infinite loop to be created
645 -- by this, with a small range of objectives constantly
646 -- being shifted.
648 -- So, I mark the route as invalid and go through it another time.
649 -- It's probably still possible to get into an infinite loop,
650 -- but it seems much less likely.
652 table.insert(self, mn, info)
653 table.remove(self, i)
654 invalid = true
655 elseif i > mx then
656 table.remove(self, i)
657 table.insert(self, mx, info)
658 invalid = true
660 i = i + 1
664 -- Now that we've chosen a route, re-calculate the cost of each leg of the route
665 local distance = 0
666 local prev_info = self[0]
667 local next_info = self[1]
668 local prev_pos = prev_info.pos
669 local next_pos = next_info.pos
670 assert(prev_pos)
671 assert(next_pos)
673 QuestHelper:yieldIfNeeded(0.03*len)
675 for i = 1, len-1 do
676 local d1, d2
677 local pos
678 local info = next_info
679 next_info = self[i+1]
680 next_pos = next_info.pos
682 indexes[info.obj] = i
684 d1, d2, pos = info.obj:TravelTime2(prev_pos, next_pos, prev_info.no_cache)
685 assert(pos)
686 QuestHelper:yieldIfNeeded(1)
688 prev_info.len = d1
689 info.len = d2
690 info.pos = pos
691 distance = distance + d1
693 prev_info = info
694 prev_pos = pos
697 self.distance = distance + prev_info.len
699 indexes[self[len].obj] = len
700 self[len].len = 0
702 assert(self:sanity())
705 function Route:pathResetBegin()
706 for i, info in ipairs(self) do
707 local pos = info.pos
708 info[1], info[2], info[3] = pos[1].c, pos[3], pos[4]
712 function Route:pathResetEnd()
713 for i, info in ipairs(self) do
714 -- Try to find a new position for this objective, near where we had it originally.
715 local p, d = nil, 0
717 local a, b, c = info[1], info[2], info[3]
719 for z, pl in pairs(info.obj.p) do
720 for i, point in ipairs(pl) do
721 if a == point[1].c then
722 local x, y = b-point[3], c-point[4]
723 local d2 = x*x+y*y
724 if not p or d2 < d then
725 p, d = point, d2
731 -- Assuming that there will still be positions on the same continents as before, i.e., locations are only added and not removed.
732 assert(p)
734 info.pos = p
738 function Routing:RoutingSetup()
739 Routing.map_walker = self.qh:CreateWorldMapWalker()
740 Routing.add_swap = {}
741 Routing.routes = {}
743 local routes = Routing.routes
744 local pos = QuestHelper.pos
745 local PlayerObjective = self.qh:NewObjectiveObject() -- Pseudo-objective which reflects player's current position. Always at index 0 of each route.
746 PlayerObjective.pos = pos
747 PlayerObjective.cat = "loc" -- A special case of a location
748 PlayerObjective.obj = "Player's current position" -- Player shouldn't see this, so no need to localize
749 PlayerObjective.icon_id = 6 -- Don't think we'll need these; just filling them in for completeness
750 PlayerObjective.o = {pos=pos}
751 PlayerObjective.fb = {}
753 for i = 1,15 do -- Create some empty routes to use for our population.
754 local new_rt = { index={ [PlayerObjective]=0 },
755 distance=0,
756 [0]={ obj=PlayerObjective, pos=pos, len=0, no_cache=true }, -- Player's current position is always objective #0
757 id=i -- So I can keep track of which route is which; only for debugging.
759 setmetatable(new_rt, Route)
760 routes[new_rt] = true
763 -- All the routes are the same right now, so it doesn't matter which we're considering the best.
764 self.best_route = next(routes)
765 self.recheck_position = 1
769 function Routing:RouteUpdateRoutine()
770 local qh = QuestHelper
771 local map_walker = Routing.map_walker
772 local minimap_dodad = qh.minimap_dodad
774 local route = qh.route
775 local to_add, to_remove, add_swap = qh.to_add, qh.to_remove, self.add_swap
777 local routes = self.routes
778 local pos = qh.pos
780 local best_route = self.best_route
782 local last_cache_clear = GetTime()
784 ------ EVIL HACK OF DEBUG
786 --[[
787 if false then
788 while GetTime() < last_cache_clear + 5 do
789 coroutine.yield()
792 if qh.target then
793 -- We know the player will be at the target location at target_time, so fudge the numbers
794 -- to pretend we're traveling there.
796 pos[1], pos[3], pos[4] = qh.target[1], qh.target[3], qh.target[4]
797 local extra_time = math.max(0, qh.target_time-time())
798 for i, t in ipairs(qh.target[2]) do
799 pos[2][i] = t+extra_time
801 else
802 if not pos[1] -- Need a valid position, in case the player was dead when they loaded the game.
803 or not UnitIsDeadOrGhost("player") then
804 -- Don't update the player's position if they're dead, assume they'll be returning to their corpse.
805 pos[3], pos[4] = qh.Astrolabe:TranslateWorldMapPosition(qh.c, qh.z, qh.x, qh.y, qh.c, 0)
806 assert(pos[3])
807 assert(pos[4])
808 pos[1] = qh.zone_nodes[qh.i]
809 pos[3], pos[4] = pos[3] * qh.continent_scales_x[qh.c], pos[4] * qh.continent_scales_y[qh.c]
811 for i, n in ipairs(pos[1]) do
812 if not n.x then
813 for i, j in pairs(n) do qh:TextOut("[%q]=%s %s", i, type(j), tostring(j) or "???") end
814 assert(false)
817 local a, b = n.x-pos[3], n.y-pos[4]
818 pos[2][i] = math.sqrt(a*a+b*b)
823 local obj = next(to_add)
825 QuestHelper:TextOut("dbghack")
826 QuestHelper:TextOut(QuestHelper:StringizeTable(to_add))
827 obj.filter_zone = false
828 obj.filter_watched = false
829 QuestHelper:TextOut(QuestHelper:StringizeTable(obj))
830 QuestHelper:TextOut(tostring(obj:Known()))
831 obj:PrepareRouting()
832 QuestHelper:TextOut(QuestHelper:StringizeTable(obj))
834 QuestHelper:TextOut("o")
835 QuestHelper:TextOut(QuestHelper:StringizeTable(obj.o))
837 QuestHelper:TextOut("pp")
838 QuestHelper:TextOut(QuestHelper:StringizeTable(obj.pos))
839 QuestHelper:TextOut(QuestHelper:StringizeTable(obj.p))
840 QuestHelper:TextOut(tostring(obj:Known()))
842 local index = best_route:addObjectiveBest(obj)
843 obj.pos = best_route[index].pos
845 QuestHelper:TextOut(QuestHelper:StringizeTable(obj.pos))
847 QuestHelper:TextOut(qh:ComputeTravelTime(pos, obj.pos))
849 Error()
850 end]]
852 ------ EVIL HACK OF DEBUG
854 while true do
855 -- Clear caches out a bit
856 if GetTime() + 15 >= last_cache_clear then
857 qh:CacheCleanup()
858 last_cache_clear = GetTime()
861 -- Update the player's position data.
862 if qh.target then
863 -- We know the player will be at the target location at target_time, so fudge the numbers
864 -- to pretend we're traveling there.
866 pos[1], pos[3], pos[4] = qh.target[1], qh.target[3], qh.target[4]
867 local extra_time = math.max(0, qh.target_time-time())
868 for i, t in ipairs(qh.target[2]) do
869 pos[2][i] = t+extra_time
871 else
872 if not pos[1] -- Need a valid position, in case the player was dead when they loaded the game.
873 or not UnitIsDeadOrGhost("player") then
874 -- Don't update the player's position if they're dead, assume they'll be returning to their corpse.
875 pos[3], pos[4] = qh.Astrolabe:TranslateWorldMapPosition(qh.c, qh.z, qh.x, qh.y, qh.c, 0)
876 assert(pos[3])
877 assert(pos[4])
878 pos[1] = qh.zone_nodes[qh.i]
879 pos[3], pos[4] = pos[3] * qh.continent_scales_x[qh.c], pos[4] * qh.continent_scales_y[qh.c]
881 for i, n in ipairs(pos[1]) do
882 if not n.x then
883 for i, j in pairs(n) do qh:TextOut("[%q]=%s %s", i, type(j), tostring(j) or "???") end
884 assert(false)
887 local a, b = n.x-pos[3], n.y-pos[4]
888 pos[2][i] = math.sqrt(a*a+b*b)
893 local changed = false
895 if #route > 0 then
896 if self.recheck_position > #route then self.recheck_position = 1 end
897 local o = route[self.recheck_position]
899 assert(o.zones)
900 o.filter_zone = o.zones[pos[1].i] == nil
901 o.filter_watched = not o:IsWatched()
903 if not o:Known() then
904 -- Objective was probably made to depend on an objective that we don't know about yet.
905 -- We add it to both lists, because although we need to remove it, we need it added again when we can.
906 -- This creates an inconsistancy, but it'll get fixed in the removal loop before anything has a chance to
907 -- explode from it.
909 to_remove[o] = true
910 to_add[o] = true
911 else
912 if o.swap_before then
913 qh:ReleaseTable(o.before)
914 o.before = o.swap_before
915 o.swap_before = nil
918 if o.swap_after then
919 qh:ReleaseTable(o.after)
920 o.after = o.swap_after
921 o.swap_after = nil
924 if o.is_sharing ~= o.want_share then
925 o.is_sharing = o.want_share
927 if o.want_share then
928 qh:DoShareObjective(o)
929 else
930 qh:DoUnshareObjective(o)
934 CalcObjectivePriority(o)
936 -- Make sure the objective in best_route is still in a valid position.
937 -- Won't worry about other routes, they should forced into valid configurations by breeding.
939 -- This is a temporary, horrible hack - we want to do a "best" test without actually clobbering our old route, so we're making a new temporary one to jam our route in for now. In theory, AddObjectiveBest should, I don't know, add it in the *best place*, but at the moment it does not necessarily, thus this nastiness.
940 local aobt = {}
941 setmetatable(aobt, getmetatable(best_route))
942 for k, v in pairs(best_route) do
943 aobt[k] = v
945 aobt.index = {}
946 for k, v in ipairs(best_route) do
947 -- arglbargl
948 aobt[k] = QuestHelper:CreateTable("AOBT idiocy")
949 for t, q in pairs(v) do
950 aobt[k][t] = q
952 aobt.index[aobt[k].obj] = k
954 if aobt[0] then
955 aobt[0] = QuestHelper:CreateTable("AOBT idiocy")
956 for t, q in pairs(best_route[0]) do
957 aobt[0][t] = q
960 -- Actually duplicating a route is irritatingly hard (this is another thing which will be fixed eventually dammit)
962 assert(aobt:sanity())
963 assert(best_route:sanity())
965 local old_distance, old_index = best_route.distance, best_route:removeObjective(o)
966 local old_real_distance = (best_route.distance or 0) + (best_route[1] and qh:ComputeTravelTime(pos, best_route[1].pos) or 0) -- part of hack
967 local new_index, sanityfixed = best_route:addObjectiveBest(o, old_index, old_distance)
968 local new_real_distance = (best_route.distance or 0) + (best_route[1] and qh:ComputeTravelTime(pos, best_route[1].pos) or 0) -- part of hack
969 -- not sure if best_route.distance can ever be nil or not, I was just getting errors I couldn't find for a while and ended up with that test included when I fixed the real error
971 if new_real_distance < old_real_distance or sanityfixed then -- More of the temporary hack
972 -- If we're using the new path . . .
974 if old_index > new_index then
975 old_index, new_index = new_index, old_index
978 for i = math.max(1, old_index-1), new_index do
979 local info = best_route[i]
980 local obj = info.obj
981 obj.pos = info.pos
982 route[i] = obj
986 if old_index ~= new_index then
987 if old_index == 1 then
988 minimap_dodad:SetObjective(route[1])
991 changed = true
994 -- . . . release our backup path
995 for k, v in ipairs(aobt) do QuestHelper:ReleaseTable(v) end
996 QuestHelper:ReleaseTable(aobt[0])
997 else -- hack (everything in this conditional besides the above chunk is a horrible hack)
998 -- If we're using the old path . . .
999 -- . . . release the *internals* of the new path, then copy everything over. Eugh.
1000 for k, v in ipairs(best_route) do QuestHelper:ReleaseTable(v) end
1001 QuestHelper:ReleaseTable(best_route[0])
1002 while #best_route > 0 do table.remove(best_route) end
1003 for k, v in pairs(aobt) do best_route[k] = v end -- hack
1004 setmetatable(aobt, Route)
1005 assert(best_route:sanity())
1006 end -- hack
1008 -- this chunk of code used to live up by old_index ~= new_index, but it obviously no longer does. should probably be moved back once Best works again
1009 -- Maybe the bug he's referring to is the one I'm fighting with in this chunk of code? Hey dude, if you find a bug, *fix the damn bug don't work around it*
1010 --if old_index == new_index then
1011 -- We don't advance recheck_position unless the node doesn't get moved.
1012 -- TODO: As the this code is apparently bugged, it's gotten into an infinite loop of constantly swapping
1013 -- and hence never advancing. As a work around for now, we'll always advance.
1014 --else
1015 self.recheck_position = self.recheck_position + 1
1020 -- Remove any waypoints if needed.
1021 while true do
1022 local obj = next(to_remove)
1023 if not obj then break end
1024 to_remove[obj] = nil
1026 if obj.is_sharing then
1027 obj.is_sharing = false
1028 qh:DoUnshareObjective(obj)
1031 for r in pairs(routes) do
1032 if r == best_route then
1033 local index = r:removeObjective(obj)
1034 table.remove(route, index)
1035 if index == 1 then
1036 minimap_dodad:SetObjective(route[1])
1038 else
1039 r:removeObjective(obj)
1043 obj:DoneRouting()
1045 changed = true
1048 -- Add any waypoints if needed
1049 while true do
1050 local obj = next(to_add)
1051 if not obj then break end
1052 to_add[obj] = nil
1054 obj.filter_zone = obj.zones and obj.zones[pos[1].i] == nil
1055 obj.filter_watched = not obj:IsWatched()
1057 if obj:Known() then
1058 obj:PrepareRouting()
1060 obj.filter_zone = obj.zones[pos[1].i] == nil
1062 if obj.filter_zone and QuestHelper_Pref.filter_zone then
1063 -- Not going to add it, wrong zone.
1064 obj:DoneRouting()
1065 add_swap[obj] = true
1066 else
1067 if not obj.is_sharing and obj.want_share then
1068 obj.is_sharing = true
1069 qh:DoShareObjective(obj)
1072 CalcObjectivePriority(obj)
1074 for r in pairs(routes) do
1075 if r == best_route then
1076 local index = r:addObjectiveBest(obj)
1077 obj.pos = r[index].pos
1078 table.insert(route, index, obj)
1079 if index == 1 then
1080 minimap_dodad:SetObjective(route[1])
1082 else
1083 r:addObjectiveFast(obj)
1087 changed = true
1089 else
1090 add_swap[obj] = true
1094 for obj in pairs(add_swap) do
1095 -- If one of the objectives we were considering adding was removed, it would be in both lists.
1096 -- That would be bad. We can't remove it because we haven't actually added it yet, so
1097 -- handle that special case here.
1098 if to_remove[obj] then
1099 to_remove[obj] = nil
1100 to_add[obj] = nil
1101 add_swap[obj] = nil
1105 to_add, add_swap = add_swap, to_add
1106 qh.to_add = to_add
1107 self.add_swap = add_swap
1109 if #best_route > 1 then
1110 -- If there is 2 or more objectives, randomly combine routes to (hopefully) create something better than we had before.
1112 -- Calculate best_route first, so that if other routes are identical, we don't risk swapping with them and
1113 -- updating the map_walker.
1114 local best, max_fitness = best_route, 1/(qh:ComputeTravelTime(pos, best_route[1].pos) + best_route.distance)
1115 best_route.fitness = max_fitness
1117 qh:yieldIfNeeded(.3)
1119 for r in pairs(routes) do
1120 if r ~= best_route then
1121 local fit = 1/(qh:ComputeTravelTime(pos, r[1].pos)+r.distance)
1122 r.fitness = fit
1123 if fit > max_fitness then
1124 best, max_fitness = r, fit
1126 qh:yieldIfNeeded(.3)
1130 local to_breed, score
1132 for r in pairs(routes) do
1133 if r ~= best then
1134 local s = math.random()*r.fitness
1135 if not to_breed or s < score then
1136 to_breed, score = r, s
1141 to_breed:breed(routes)
1143 if 1/(qh:ComputeTravelTime(pos, to_breed[1].pos)+to_breed.distance+improve_margin) > max_fitness then
1144 best = to_breed
1147 qh:yieldIfNeeded(.3)
1149 if best ~= best_route then
1151 assert(best:sanity())
1152 assert(best_route:sanity())
1154 best_route = best
1155 self.best_route = best_route
1157 for i, info in ipairs(best) do
1158 local obj = info.obj
1159 obj.pos = info.pos
1160 route[i] = obj
1163 minimap_dodad:SetObjective(route[1])
1165 changed = true
1169 if qh.defered_flight_times then
1170 qh:buildFlightTimes()
1171 qh.defered_flight_times = false
1172 assert(qh.defered_graph_reset)
1175 if qh.defered_graph_reset then
1176 for r in pairs(routes) do
1177 r:pathResetBegin()
1180 qh:yieldIfNeeded(10)
1181 qh.graph_in_limbo = true
1182 qh:ResetPathing()
1183 qh.graph_in_limbo = false
1184 qh.defered_graph_reset = false
1186 for r in pairs(routes) do
1187 r:pathResetEnd()
1190 for i, info in ipairs(best_route) do
1191 local obj = info.obj
1192 obj.pos = info.pos
1193 route[i] = obj
1196 minimap_dodad:SetObjective(route[1])
1198 qh:yieldIfNeeded(9)
1201 if changed then
1202 map_walker:RouteChanged()
1205 assert(#route == #best_route)
1207 if route_pass > 0 then
1208 route_pass = route_pass - 1
1211 qh:yieldIfNeeded(1)
1215 local update_route
1217 function QuestHelper:RunCoroutine()
1218 if coroutine.status(update_route) ~= "dead" then
1219 coroutine_running = true
1220 -- At perf = 100%, we will run 5 ms / frame.
1221 coroutine_stop_time = GetTime() + 4e-3 * QuestHelper_Pref.perf_scale * ((route_pass > 0) and 5 or 1)
1222 local state, err = coroutine.resume(update_route, self)
1223 coroutine_running = false
1224 if not state then
1225 self:TextOut("|cffff0000The routing co-routine just exploded|r: |cffffff77"..tostring(err).."|r")
1230 function QuestHelper:ForceRouteUpdate(passes)
1231 route_pass = math.max(2, passes or 1)
1234 function Routing:Initialize()
1235 self:RoutingSetup()
1237 if coroutine.coco then
1238 -- coco allows yielding across c boundries, which allows me to use xpcall to get
1239 -- stack traces for coroutines without calls to yield resulting in thermal nuclear meltdown.
1241 -- This isn't part of WoW, I was using it in my driver program: Development/routetest
1243 update_route = coroutine.create(
1244 function()
1245 local state, err = xpcall(
1246 function()
1247 Routing:RouteUpdateRoutine()
1248 end,
1249 function (err)
1250 if debugstack then
1251 return tostring(err).."\n"..debugstack(2)
1252 else
1253 return debug.traceback(tostring(err), 2)
1255 end)
1257 if not state then
1258 error(err, 0)
1260 end)
1261 else
1262 update_route = coroutine.create(function() Routing:RouteUpdateRoutine() end)