1 QuestHelper_File
["routing.lua"] = "Development Version"
5 local coroutine_running
= false
7 function QuestHelper
:yieldIfNeeded(work
)
8 if coroutine_running
then
9 -- Under normal circemstances, this will need to be called an average of 10 times
10 -- for every unit of work done.
12 -- I consider a call to TravelTime2 to be worth one unit of work.
13 -- I consider TravelTime to be worth .5 units.
14 -- I consider ComputeTravelTime to be worth .3 units of work.
16 -- That's just as a rough guide, they depend greatly on where the positions are,
17 -- and I'm happy enough to just fudge it.
19 work_done
= work_done
+ work
20 / QuestHelper_Pref
.perf_scale
-- Scale work done by global preference.
21 * ((IsInInstance() or QuestHelper_Pref
.hide
) and 5 or 1) -- If hidden, work is overvalued.
22 * ((route_pass
> 0) and 0.02 or .1) -- average 50 calls per work unit if forced, 10 calls per unit otherwise.
24 -- If lots of work is done, we will yeild multiple times in succession
25 -- to maintain the average.
27 while work_done
>= 1 do
28 work_done
= work_done
- 1
34 local function CalcObjectivePriority(obj
)
35 local priority
= obj
.priority
37 for o
in pairs(obj
.before
) do
39 priority
= math
.min(priority
, CalcObjectivePriority(o
))
43 obj
.real_priority
= priority
50 function Route
:findObjectiveRange(obj
)
55 local l
= math
.floor((smn
+smx
)*0.5)
58 while mn
~= mx
and smn
<= smx
do
71 if obj
.real_priority
> o
.real_priority
or obj
.after
[o
] then
74 l
= math
.floor((smn
+smx
)*0.5)
77 elseif obj
.real_priority
< o
.real_priority
or obj
.before
[o
] then
80 l
= math
.floor((smn
+smx
)*0.5)
91 if obj
.real_priority
> o
.real_priority
or obj
.after
[o
] then
94 l
= math
.floor((smn
+smx
)*0.5)
97 elseif obj
.real_priority
< o
.real_priority
or obj
.before
[o
] then
100 l
= math
.floor((smn
+smx
)*0.5)
113 function Route
:addObjective(obj
)
114 local indexes
= self
.index
116 local info
= QuestHelper
:CreateTable()
117 assert(not indexes
[obj
])
124 info
.pos
= obj
.location
128 local player_pos
= QuestHelper
.pos
129 local pos
= obj
.location
130 local c
, x
, y
= pos
[1].c
, pos
[3], pos
[4]
132 local mn
, mx
= self
:findObjectiveRange(obj
)
135 for i
= mn
, math
.min(mx
, len
) do
136 local p
= self
[i
].pos
138 local u
, v
= p
[3]-x
, p
[4]-y
140 if not index
or d2
< distsqr
then
141 index
, distsqr
= i
, d2
147 -- No nodes with the same continent already.
148 -- If the same continent as the player, add to start of list, otherwise add to end of the list.
149 index
= c
== player_pos
[1].c
and mn
or mx
152 -- The next question, do I insert at that point, or do I insert after it?
153 if index
~= mx
and index
<= len
then
154 local p1
= self
[index
].pos
162 p0
= self
[index
-1].pos
165 local oldstart
, newstart
168 local u
, v
= p0
[3]-x
, p0
[4]-y
169 newstart
= math
.sqrt(u
*u
+v
*v
)
170 u
, v
= p0
[3]-p1
[3], p0
[4]-p1
[4]
171 oldstart
= math
.sqrt(u
*u
+v
*v
)
179 p2
= self
[index
+1].pos
183 if p2
and p2
[1].c
== c
then
184 local u
, v
= p2
[3]-x
, p2
[4]-y
185 newend
= math
.sqrt(u
*u
+v
*v
)
186 u
, v
= p2
[3]-p1
[3], p2
[4]-p1
[4]
187 oldend
= math
.sqrt(u
*u
+v
*v
)
193 if oldstart
+newend
< newstart
+oldend
then
200 QuestHelper
:yieldIfNeeded((mn
-mx
+3)*0.05) -- The above checks don't require much effort.
203 local pos
= obj
.location
205 local previnfo
= self
[index
-1]
208 d
, previnfo
.pos
= previnfo
.obj
:TravelTime(pos
)
210 self
.distance
= self
.distance
+ d
211 QuestHelper
:yieldIfNeeded(0.5)
216 d1
, d2
, info
.pos
= obj
:TravelTime2(QuestHelper
.pos
, self
[index
].pos
, --[[nocache=]] true)
218 self
.distance
= self
.distance
+ d2
220 local previnfo
= self
[index
-1]
221 d1
, d2
, info
.pos
= obj
:TravelTime2(previnfo
.pos
, self
[index
].pos
)
223 self
.distance
= self
.distance
+ (d1
- previnfo
.len
+ d2
)
227 QuestHelper
:yieldIfNeeded(1)
230 -- Finally, insert the objective.
231 table.insert(self
, index
, info
)
234 -- Fix indexes of shifted elements.
235 for i
= index
+1,len
+1 do
236 local obj
= self
[i
].obj
237 assert(indexes
[obj
] == i
-1)
244 function Route
:removeObjective(obj
)
245 local indexes
= self
.index
246 local index
= indexes
[obj
]
249 local info
= self
[index
]
250 assert(info
.obj
== obj
)
252 if index
== #self
then
254 self
.distance
= self
.distance
- self
[index
-1].len
255 self
[index
-1].len
= nil
258 elseif index
== 1 then
259 self
.distance
= self
.distance
- info
.len
261 local info1
= self
[index
-1]
263 d
, info1
.pos
= info1
.obj
:TravelTime(self
[index
+1].pos
)
264 self
.distance
= self
.distance
+ (d
- info1
.len
- info
.len
)
266 QuestHelper
:yieldIfNeeded(.5)
269 QuestHelper
:ReleaseTable(info
)
271 table.remove(self
, index
)
274 local info1
= self
[index
-1]
276 if index
<= #self
then
282 for i
= index
,#self
do
283 -- Fix indexes of shifted elements.
284 local obj
= self
[i
].obj
285 indexes
[obj
] = indexes
[obj
]-1
294 function Route
:breed(route_map
)
295 local indexes
= self
.index
301 local prev_pos
= QuestHelper
.pos
303 for route
in pairs(route_map
) do
304 local fit
= route
.fitness
305 local pos
= route
[1].pos
308 if prev_pos
[1].c
== pos
[1].c
then
309 local u
, v
= prev_pos
[3]-pos
[3], prev_pos
[4]-pos
[4]
310 w
= math
.sqrt(u
*u
+v
*v
)
315 w
= fit
* math
.random() / w
317 if not info
or w
> r
then
318 info
, r
= route
[1], w
322 local obj
= route
[i
].obj
323 local tbl
= links
[obj
]
325 tbl
= QuestHelper
:CreateTable()
330 local info
= route
[i
-1]
331 local obj2
= info
.obj
332 tbl
[info
] = (tbl
[info
] or 0) + fit
336 local info
= route
[i
+1]
337 local obj2
= info
.obj
338 if obj
.real_priority
<= obj2
.real_priority
or obj
.before
[obj2
] then
339 tbl
[info
] = (tbl
[info
] or 0) + fit
344 QuestHelper
:yieldIfNeeded(0.01*len
)
357 for i
, weight
in pairs(last
) do
361 if prev_pos
[1].c
== pos
[1].c
then
362 local u
, v
= prev_pos
[3]-pos
[3], prev_pos
[4]-pos
[4]
363 w
= math
.sqrt(u
*u
+v
*v
)
368 w
= weight
* math
.random() / w
370 if not info
or w
> r
then
378 for obj
in pairs(links
) do
381 if prev_pos
[1].c
== pos
[1].c
then
382 local u
, v
= prev_pos
[3]-pos
[3], prev_pos
[4]-pos
[4]
383 w
= math
.sqrt(u
*u
+v
*v
)
388 w
= math
.random() / w
390 if not info
or w
> r
then
391 local route
= next(route_map
)
392 info
, r
= route
[route
.index
[obj]]
, w
400 local link
= links
[obj
]
404 assert(info
.obj
== obj
)
407 QuestHelper
:ReleaseTable(last
)
410 QuestHelper
:yieldIfNeeded(0.01*c
)
413 QuestHelper
:ReleaseTable(last
)
415 for obj
, i
in pairs(indexes
) do
417 info
.obj
, info
.pos
= obj
, seen
[obj
]
421 --[[for i, info in ipairs(self) do
422 io.write(info.obj.name, " ")
425 if math
.random() > 0.2 then
426 -- If we ended up being an exact clone of our parents, then we'll make some random changes.
427 local i
= math
.random(1, len
-1)
428 local j
= math
.random(i
+1, len
)
430 if math
.random() > 0.9 then
431 -- Reverse a chunk of the route
433 self
[i
+k
], self
[j
-k
] = self
[j
-k
], self
[i
+k
]
437 self
[i
], self
[j
] = self
[j
], self
[i
]
443 -- Make sure all the objectives have valid positions in the list.
445 local mn
, mx
= self
:findObjectiveRange(info
.obj
)
447 table.insert(self
, mn
, info
)
448 table.remove(self
, i
)
450 table.remove(self
, i
)
451 table.insert(self
, mx
, info
)
458 local next_info
= self
[2]
459 local prev_info
= self
[1]
460 local next_pos
= next_info
.pos
461 local prev_pos
= QuestHelper
.pos
463 QuestHelper
:yieldIfNeeded(0.03*len
)
465 prev_info
.len
, prev_pos
= select(2, prev_info
.obj
:TravelTime2(QuestHelper
.pos
, next_pos
, --[[nocache=]] true))
466 QuestHelper
:yieldIfNeeded(1)
468 prev_info
.pos
= prev_pos
470 indexes
[self
[1].obj
] = 1
471 indexes
[self
[len
].obj
] = len
475 local info
= next_info
477 next_info
= self
[i
+1]
478 next_pos
= next_info
.pos
480 indexes
[info
.obj
] = i
482 d1
, d2
, pos
= info
.obj
:TravelTime2(prev_pos
, next_pos
)
483 QuestHelper
:yieldIfNeeded(1)
490 distance
= distance
+ d1
493 self
.distance
= distance
+ prev_info
.len
496 function Route
:pathResetBegin()
497 for i
, info
in ipairs(self
) do
499 info
[1], info
[2], info
[3] = pos
[1].c
, pos
[3], pos
[4]
503 function Route
:pathResetEnd()
504 for i
, info
in ipairs(self
) do
505 -- Try to find a new position for this objective, near where we had it originally.
508 local a
, b
, c
= info
[1], info
[2], info
[3]
510 for z
, pl
in pairs(info
.obj
.p
) do
511 for i
, point
in ipairs(pl
) do
512 if a
== point
[1].c
then
513 local x
, y
= b
-point
[3], c
-point
[4]
515 if not p
or d2
< d
then
522 -- Assuming that there will still be positions on the same continents as before, i.e., locations are only added and not removed.
529 local function RouteUpdateRoutine(self
)
530 local map_walker
= self
:CreateWorldMapWalker()
531 local minimap_dodad
= self
.minimap_dodad
534 local route
= self
.route
535 local to_add
, to_remove
= self
.to_add
, self
.to_remove
539 for i
= 1,15 do -- Create some empty routes to use for our population.
540 routes
[setmetatable({index
={},distance
=0}, Route
)] = true
543 -- All the routes are the same right now, so it doesn't matter which we're considering the best.
544 local best_route
= next(routes
)
546 local recheck_position
= 0
549 local changed
= false
552 recheck_position
= recheck_position
+ 1
553 if recheck_position
> #route
then recheck_position
= 1 end
554 local o
= route
[recheck_position
]
556 o
.filter_zone
= o
.location
[1] ~= self
.pos
[1]
558 if not o
:Known() then
559 -- Objective was probably made to depend on an objective that we don't know about yet.
560 -- We add it to both lists, because although we need to remove it, we need it added again when we can.
561 -- This creates an inconsistancy, but it'll get fixed in the removal loop before anything has a chance to
567 if o
.swap_before
then
568 self
:ReleaseTable(o
.before
)
569 o
.before
= o
.swap_before
574 self
:ReleaseTable(o
.after
)
575 o
.after
= o
.swap_after
579 if o
.is_sharing
~= o
.want_share
then
580 o
.is_sharing
= o
.want_share
583 self
:DoShareObjective(o
)
585 self
:DoUnshareObjective(o
)
589 CalcObjectivePriority(o
)
591 local mn
, mx
= best_route
:findObjectiveRange(o
)
592 local old_index
= best_route
.index
[o
]
593 if old_index
< mn
or old_index
> mx
then
594 -- Make sure the objective in best_route is still in a valid position.
595 -- Won't worry about other routes, they should forced into valid configurations by breeding.
597 best_route
:removeObjective(o
)
598 local new_index
= best_route
:addObjective(o
)
600 if old_index
> new_index
then
601 old_index
, new_index
= new_index
, old_index
604 for i
= old_index
, new_index
do
605 local info
= best_route
[i
]
611 if old_index
== 1 then
612 minimap_dodad
:SetObjective(route
[1])
620 -- Remove any waypoints if needed.
622 local obj
= next(to_remove
)
623 if not obj
then break end
626 if obj
.is_sharing
then
627 obj
.is_sharing
= false
628 self
:DoUnshareObjective(obj
)
631 for r
in pairs(routes
) do
632 if r
== best_route
then
633 local index
= r
:removeObjective(obj
)
634 table.remove(route
, index
)
636 minimap_dodad
:SetObjective(route
[1])
639 r
:removeObjective(obj
)
647 -- Add any waypoints if needed
649 local obj
= next(to_add
)
650 if not obj
then break end
656 obj
.filter_zone
= obj
.location
[1] ~= self
.pos
[1]
658 if obj
.filter_zone
and QuestHelper_Pref
.filter_zone
then
659 -- Not going to add it, wrong zone.
663 if not obj
.is_sharing
and obj
.want_share
then
664 obj
.is_sharing
= true
665 self
:DoShareObjective(obj
)
668 CalcObjectivePriority(obj
)
670 for r
in pairs(routes
) do
671 if r
== best_route
then
672 local index
= r
:addObjective(obj
)
673 obj
.pos
= r
[index
].pos
674 table.insert(route
, index
, obj
)
676 minimap_dodad
:SetObjective(route
[1])
690 for obj
in pairs(add_swap
) do
691 -- If one of the objectives we were considering adding was removed, it would be in both lists.
692 -- That would be bad. We can't remove it because we haven't actually added it yet, so
693 -- handle that special case here.
694 if to_remove
[obj
] then
701 to_add
, add_swap
= add_swap
, to_add
704 if #best_route
> 1 then
705 -- If there is 2 or more objectives, randomly combine routes to (hopefully) create something better than we had before.
708 -- Calculate best_route first, so that if other routes are identical, we don't risk swapping with them and
709 -- updating the map_walker.
710 local best
, max_fitness
= best_route
, 1/(self
:ComputeTravelTime(pos
, best_route
[1].pos
)+best_route
.distance
)
711 best_route
.fitness
= max_fitness
713 self
:yieldIfNeeded(.3)
715 for r
in pairs(routes
) do
716 if r
~= best_route
then
717 local fit
= 1/(self
:ComputeTravelTime(pos
, r
[1].pos
)+r
.distance
)
719 if fit
> max_fitness
then
720 best
, max_fitness
= r
, fit
722 self
:yieldIfNeeded(.3)
726 local to_breed
, score
728 for r
in pairs(routes
) do
730 local s
= math
.random()*r
.fitness
731 if not to_breed
or s
< score
then
732 to_breed
, score
= r
, s
737 to_breed
:breed(routes
)
739 if 1/(self
:ComputeTravelTime(pos
, to_breed
[1].pos
)+to_breed
.distance
) > max_fitness
then
743 self
:yieldIfNeeded(.3)
745 if best
~= best_route
then
748 for i
, info
in ipairs(best
) do
754 minimap_dodad
:SetObjective(route
[1])
760 if self
.defered_flight_times
then
761 self
:buildFlightTimes()
762 self
.defered_flight_times
= false
763 assert(self
.defered_graph_reset
)
766 if self
.defered_graph_reset
then
767 for r
in pairs(routes
) do
771 self
:yieldIfNeeded(10)
772 self
.graph_in_limbo
= true
774 self
.graph_in_limbo
= false
775 self
.defered_graph_reset
= false
777 for r
in pairs(routes
) do
781 for i
, info
in ipairs(best_route
) do
787 minimap_dodad
:SetObjective(route
[1])
789 self
:yieldIfNeeded(9)
793 map_walker
:RouteChanged()
796 assert(#route
== #best_route
)
798 if route_pass
> 0 then
799 route_pass
= route_pass
- 1
802 self
:yieldIfNeeded(1)
808 if coroutine
.coco
then
809 -- coco allows yielding across c boundries, which allows me to use xpcall to get
810 -- stack traces for coroutines without calls to yield resulting in thermal nuclear meltdown.
812 -- This isn't part of WoW, I was using it in my driver program: Development/routetest
814 update_route
= coroutine
.create(
816 local state
, err
= xpcall(
818 RouteUpdateRoutine(QuestHelper
)
822 return tostring(err
).."\n"..debugstack(2)
824 return debug
.traceback(tostring(err
), 2)
833 update_route
= coroutine
.create(RouteUpdateRoutine
)
836 function QuestHelper
:RunCoroutine()
837 if coroutine
.status(update_route
) ~= "dead" then
838 coroutine_running
= true
839 local state
, err
= coroutine
.resume(update_route
, self
)
840 coroutine_running
= false
842 self
:TextOut("|cffff0000The routing co-routine just exploded|r: |cffffff77"..tostring(err
).."|r")
847 function QuestHelper
:ForceRouteUpdate(passes
)
848 route_pass
= math
.max(2, passes
or 1)