1 QuestHelper_File
["routing.lua"] = "Development Version"
4 QuestHelper
.Routing
= {}
5 local Routing
= QuestHelper
.Routing
6 Routing
.qh
= QuestHelper
9 local improve_margin
= 1e-8
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
29 local function CalcObjectivePriority(obj
)
30 local priority
= obj
.priority
32 for o
in pairs(obj
.before
) do
34 priority
= math
.min(priority
, CalcObjectivePriority(o
))
38 obj
.real_priority
= priority
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()
51 if QuestHelper
.Error
then
52 assert = function(a
, b
)
54 QuestHelper
:TextOut("Route:sanity(): id="..self
.id
.."; best_route="..Routing
.best_route
.id
)
55 QuestHelper
:Error(b
or "Assertion Failed")
62 --QuestHelper:TextOut(QuestHelper:StringizeTable(self))
64 --QuestHelper:TextOut(tostring(i))
65 --QuestHelper:TextOut(QuestHelper:StringizeTable(self[i]))
66 --QuestHelper:TextOut(tostring(self[i].len))
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
)
79 for obj
, i
in pairs(self
.index
) do
80 assert(self
[i
].obj
== obj
)
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
)
91 function Route
:findObjectiveRange(obj
)
96 local l
= math
.floor((smn
+smx
)*0.5)
105 local o
= self
[l
].obj
107 if obj
.real_priority
> o
.real_priority
or obj
.after
[o
] then
110 l
= math
.floor((smn
+smx
)*0.5)
113 elseif obj
.real_priority
< o
.real_priority
or obj
.before
[o
] then
116 l
= math
.floor((smn
+smx
)*0.5)
127 if obj
.real_priority
> o
.real_priority
or obj
.after
[o
] then
130 l
= math
.floor((smn
+smx
)*0.5)
133 elseif obj
.real_priority
< o
.real_priority
or obj
.before
[o
] then
136 l
= math
.floor((smn
+smx
)*0.5)
147 function Route
:addObjectiveFast(obj
)
148 assert(self
:sanity())
149 local indexes
= self
.index
151 local info
= QuestHelper
:CreateTable()
152 assert(not indexes
[obj
])
160 d
, info
.pos
= obj
:TravelTime(self
[0].pos
, true)
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
)
173 for i
= mn
, math
.min(mx
, len
) do
174 local p
= self
[i
].pos
176 local u
, v
= p
[3]-x
, p
[4]-y
178 if not index
or d2
< distsqr
then
179 index
, distsqr
= i
, d2
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
200 p0
= self
[index
-1].pos
203 local oldstart
, newstart
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
)
217 p2
= self
[index
+1].pos
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
)
231 if oldstart
+newend
< newstart
+oldend
then
238 QuestHelper
:yieldIfNeeded((mn
-mx
+3)*0.05) -- The above checks don't require much effort.
241 local previnfo
= self
[index
-1]
244 d
, info
.pos
= obj
:TravelTime(previnfo
.pos
)
246 QuestHelper
:yieldIfNeeded(0.5)
248 self
.distance
= self
.distance
+ d
252 local previnfo
= self
[index
-1]
253 d1
, d2
, info
.pos
= obj
:TravelTime2(previnfo
.pos
, self
[index
].pos
, previnfo
.no_cache
)
256 self
.distance
= self
.distance
+ (d1
- previnfo
.len
+ d2
)
259 QuestHelper
:yieldIfNeeded(1)
262 -- Finally, insert the objective.
263 table.insert(self
, index
, info
)
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)
273 assert(self
:sanity())
278 function Route
:addObjectiveBest(obj
, old_index
, old_distance
)
279 assert(self
:sanity())
281 local indexes
= self
.index
283 local info
= QuestHelper
:CreateTable()
284 assert(not indexes
[obj
])
290 self
.distance
, info
.pos
= obj
:TravelTime(self
[0].pos
, true)
292 self
[0].len
= self
.distance
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
)
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
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
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
357 self
[len
].len
= delta
358 self
.distance
= self
.distance
+ delta
359 table.insert(self
, info
)
362 assert(self
:sanity())
371 local pinfo
= self
[best_index
-1]
372 self
.distance
= self
.distance
+ (best_d1
- pinfo
.len
+ best_d2
)
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
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
409 local info
= self
[index
]
410 assert(info
.obj
== obj
)
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
417 if index
== #self
then
418 self
.distance
= self
.distance
- self
[index
-1].len
419 self
[index
-1].len
= 0
421 local pinfo
= self
[index
-1]
422 local info1
= self
[index
+1]
423 local info2
= self
[index
+2]
424 local no_cache
= (index
== 1)
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
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
442 QuestHelper
:ReleaseTable(info
)
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)
453 assert(self
:sanity())
454 -- assert(self.distance <= old_distance)
462 function Route
:breed(route_map
)
463 local indexes
= self
.index
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
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
)
488 w
= fit
* math
.random() / w
490 if not info
or w
> r
then
491 info
, r
= route
[1], w
495 local obj
= route
[i
].obj
496 local tbl
= links
[obj
]
498 tbl
= QuestHelper
:CreateTable()
503 local info
= route
[i
-1]
504 local obj2
= info
.obj
505 tbl
[info
] = (tbl
[info
] or 0) + fit
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
526 seen
[obj
] = info
.pos
-- Save its position, because we don't want to clobber any of the info objects yet
532 -- Scan the rest of the places in the route, and pick objectives to go there
537 -- Scan the list of scores from the prior objective
538 for i
, weight
in pairs(last
) do
540 -- Only consider an item if we have scores for that item
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
)
550 w
= weight
* math
.random() / w
552 if not info
or w
> r
then
559 -- In case we had no valid scores, scan the remaining objectives and score by distance
561 for obj
in pairs(links
) do
564 if prev_pos
[1] == pos
[1] then
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
)
573 -- Different continent. Assume fixed value of 5 minutes.
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
588 -- Add the selected item to the route
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
]
598 QuestHelper
:ReleaseTable(last
)
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
611 info
.obj
, info
.pos
= obj
, seen
[obj
]
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
)
621 -- Reverse a chunk of the route
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
630 local invalid_passes
= 0
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
637 -- Make sure all the objectives have valid positions in the list.
639 local mn
, mx
= self
:findObjectiveRange(info
.obj
)
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
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
)
656 table.remove(self
, i
)
657 table.insert(self
, mx
, info
)
664 -- Now that we've chosen a route, re-calculate the cost of each leg of the route
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
673 QuestHelper
:yieldIfNeeded(0.03*len
)
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
)
686 QuestHelper
:yieldIfNeeded(1)
691 distance
= distance
+ d1
697 self
.distance
= distance
+ prev_info
.len
699 indexes
[self
[len
].obj
] = len
702 assert(self
:sanity())
705 function Route
:pathResetBegin()
706 for i
, info
in ipairs(self
) do
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.
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]
724 if not p
or d2
< d
then
731 -- Assuming that there will still be positions on the same continents as before, i.e., locations are only added and not removed.
738 function Routing
:RoutingSetup()
739 Routing
.map_walker
= self
.qh
:CreateWorldMapWalker()
740 Routing
.add_swap
= {}
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 },
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
780 local best_route
= self
.best_route
782 local last_cache_clear
= GetTime()
784 ------ EVIL HACK OF DEBUG
788 while GetTime() < last_cache_clear + 5 do
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
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)
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
813 for i, j in pairs(n) do qh:TextOut("[%q]=%s %s", i, type(j), tostring(j) or "???") end
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()))
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))
852 ------ EVIL HACK OF DEBUG
855 -- Clear caches out a bit
856 if GetTime() + 15 >= last_cache_clear
then
858 last_cache_clear
= GetTime()
861 -- Update the player's position data.
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
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)
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
883 for i
, j
in pairs(n
) do qh
:TextOut("[%q]=%s %s", i
, type(j
), tostring(j
) or "???") end
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
896 if self
.recheck_position
> #route
then self
.recheck_position
= 1 end
897 local o
= route
[self
.recheck_position
]
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
912 if o
.swap_before
then
913 qh
:ReleaseTable(o
.before
)
914 o
.before
= o
.swap_before
919 qh
:ReleaseTable(o
.after
)
920 o
.after
= o
.swap_after
924 if o
.is_sharing
~= o
.want_share
then
925 o
.is_sharing
= o
.want_share
928 qh
:DoShareObjective(o
)
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.
941 setmetatable(aobt
, getmetatable(best_route
))
942 for k
, v
in pairs(best_route
) do
946 for k
, v
in ipairs(best_route
) do
948 aobt
[k
] = QuestHelper
:CreateTable("AOBT idiocy")
949 for t
, q
in pairs(v
) do
952 aobt
.index
[aobt
[k
].obj
] = k
955 aobt
[0] = QuestHelper
:CreateTable("AOBT idiocy")
956 for t
, q
in pairs(best_route
[0]) do
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
]
986 if old_index
~= new_index
then
987 if old_index
== 1 then
988 minimap_dodad
:SetObjective(route
[1])
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())
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.
1015 self
.recheck_position
= self
.recheck_position
+ 1
1020 -- Remove any waypoints if needed.
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
)
1036 minimap_dodad
:SetObjective(route
[1])
1039 r
:removeObjective(obj
)
1048 -- Add any waypoints if needed
1050 local obj
= next(to_add
)
1051 if not obj
then break end
1054 obj
.filter_zone
= obj
.zones
and obj
.zones
[pos
[1].i
] == nil
1055 obj
.filter_watched
= not obj
:IsWatched()
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.
1065 add_swap
[obj
] = true
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
)
1080 minimap_dodad
:SetObjective(route
[1])
1083 r
:addObjectiveFast(obj
)
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
1105 to_add
, add_swap
= add_swap
, 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
)
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
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
1147 qh
:yieldIfNeeded(.3)
1149 if best
~= best_route
then
1151 assert(best
:sanity())
1152 assert(best_route
:sanity())
1155 self
.best_route
= best_route
1157 for i
, info
in ipairs(best
) do
1158 local obj
= info
.obj
1163 minimap_dodad
:SetObjective(route
[1])
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
1180 qh
:yieldIfNeeded(10)
1181 qh
.graph_in_limbo
= true
1183 qh
.graph_in_limbo
= false
1184 qh
.defered_graph_reset
= false
1186 for r
in pairs(routes
) do
1190 for i
, info
in ipairs(best_route
) do
1191 local obj
= info
.obj
1196 minimap_dodad
:SetObjective(route
[1])
1202 map_walker
:RouteChanged()
1205 assert(#route
== #best_route
)
1207 if route_pass
> 0 then
1208 route_pass
= route_pass
- 1
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
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()
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(
1245 local state
, err
= xpcall(
1247 Routing
:RouteUpdateRoutine()
1251 return tostring(err
).."\n"..debugstack(2)
1253 return debug
.traceback(tostring(err
), 2)
1262 update_route
= coroutine
.create(function() Routing
:RouteUpdateRoutine() end)