Added colour for the progress of objectives in the quest tracker.
[QuestHelper.git] / routing.lua
blobc871c1c8eeb18a1071f146eb93a9eb191a41999e
1 QuestHelper_File["routing.lua"] = "Development Version"
3 local work_done = 0
4 local route_pass = 0
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
29 coroutine.yield()
30 end
31 end
32 end
34 local function CalcObjectivePriority(obj)
35 local priority = obj.priority
37 for o in pairs(obj.before) do
38 if o.watched then
39 priority = math.min(priority, CalcObjectivePriority(o))
40 end
41 end
43 obj.real_priority = priority
44 return priority
45 end
47 local Route = {}
48 Route.__index = Route
50 function Route:findObjectiveRange(obj)
51 local mn, smn = 1, 1
52 local smx = #self
53 local mx = smx+1
55 local l = math.floor((smn+smx)*0.5)
56 local r = l+1
58 while mn ~= mx and smn <= smx do
59 while true do
60 assert(mn <= mx)
61 assert(smn <= smx)
62 assert(smn >= 1)
63 assert(smn <= #self)
65 if l < smn then
66 return mn, mx
67 end
69 local o = self[l].obj
71 if obj.real_priority > o.real_priority or obj.after[o] then
72 mn = l+1
73 smn = r
74 l = math.floor((smn+smx)*0.5)
75 r = l+1
76 break
77 elseif obj.real_priority < o.real_priority or obj.before[o] then
78 mx = l
79 smx = l-1
80 l = math.floor((smn+smx)*0.5)
81 r = l+1
82 break
83 end
85 if r > smx then
86 return mn, mx
87 end
89 o = self[r].obj
91 if obj.real_priority > o.real_priority or obj.after[o] then
92 mn = r+1
93 smn = r+1
94 l = math.floor((smn+smx)*0.5)
95 r = l+1
96 break
97 elseif obj.real_priority < o.real_priority or obj.before[o] then
98 mx = r
99 smx = l-1
100 l = math.floor((smn+smx)*0.5)
101 r = l+1
102 break
105 l = l - 1
106 r = r + 1
110 return mn, mx
113 function Route:addObjective(obj)
114 local indexes = self.index
115 local len = #self
116 local info = QuestHelper:CreateTable()
117 assert(not indexes[obj])
119 info.obj = obj
121 if len == 0 then
122 self[1] = info
123 indexes[obj] = 1
124 info.pos = obj.location
125 return 1
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)
133 local index, distsqr
135 for i = mn, math.min(mx, len) do
136 local p = self[i].pos
137 if c == p[1].c then
138 local u, v = p[3]-x, p[4]-y
139 local d2 = u*u+v*v
140 if not index or d2 < distsqr then
141 index, distsqr = i, d2
146 if not index then
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
156 if p1[1].c == c then
157 local p0
159 if index == 1 then
160 p0 = player_pos
161 else
162 p0 = self[index-1].pos
165 local oldstart, newstart
167 if p0[1].c == c then
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)
172 else
173 newstart = 0
174 oldstart = 0
177 local p2
178 if index ~= len then
179 p2 = self[index+1].pos
182 local oldend, newend
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)
188 else
189 newend = 0
190 oldend = 0
193 if oldstart+newend < newstart+oldend then
194 index = index + 1
200 QuestHelper:yieldIfNeeded((mn-mx+3)*0.05) -- The above checks don't require much effort.
202 if index > len then
203 local pos = obj.location
204 info.pos = pos
205 local previnfo = self[index-1]
206 assert(previnfo)
207 local d
208 d, previnfo.pos = previnfo.obj:TravelTime(pos)
209 previnfo.len = d
210 self.distance = self.distance + d
211 QuestHelper:yieldIfNeeded(0.5)
212 else
213 local d1, d2
215 if index == 1 then
216 d1, d2, info.pos = obj:TravelTime2(QuestHelper.pos, self[index].pos, --[[nocache=]] true)
217 info.len = d2
218 self.distance = self.distance + d2
219 else
220 local previnfo = self[index-1]
221 d1, d2, info.pos = obj:TravelTime2(previnfo.pos, self[index].pos)
222 info.len = d2
223 self.distance = self.distance + (d1 - previnfo.len + d2)
224 previnfo.len = d1
227 QuestHelper:yieldIfNeeded(1)
230 -- Finally, insert the objective.
231 table.insert(self, index, info)
232 indexes[obj] = index
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)
238 indexes[obj] = i
241 return index
244 function Route:removeObjective(obj)
245 local indexes = self.index
246 local index = indexes[obj]
248 assert(index)
249 local info = self[index]
250 assert(info.obj == obj)
252 if index == #self then
253 if index ~= 1 then
254 self.distance = self.distance - self[index-1].len
255 self[index-1].len = nil
256 else
258 elseif index == 1 then
259 self.distance = self.distance - info.len
260 else
261 local info1 = self[index-1]
262 local d
263 d, info1.pos = info1.obj:TravelTime(self[index+1].pos)
264 self.distance = self.distance + (d - info1.len - info.len)
265 info1.len = d
266 QuestHelper:yieldIfNeeded(.5)
269 QuestHelper:ReleaseTable(info)
270 indexes[obj] = nil
271 table.remove(self, index)
273 if index ~= 1 then
274 local info1 = self[index-1]
276 if index <= #self then
277 local len
278 else
282 for i = index,#self do
283 -- Fix indexes of shifted elements.
284 local obj = self[i].obj
285 indexes[obj] = indexes[obj]-1
288 return index
291 local links = {}
292 local seen = {}
294 function Route:breed(route_map)
295 local indexes = self.index
296 local len = #self
298 local info
299 local r
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
306 local w
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)
311 else
312 w = 500
315 w = fit * math.random() / w
317 if not info or w > r then
318 info, r = route[1], w
321 for i = 1,len do
322 local obj = route[i].obj
323 local tbl = links[obj]
324 if not tbl then
325 tbl = QuestHelper:CreateTable()
326 links[obj] = tbl
329 if i ~= 1 then
330 local info = route[i-1]
331 local obj2 = info.obj
332 tbl[info] = (tbl[info] or 0) + fit
335 if i ~= len then
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)
347 local obj = info.obj
348 indexes[obj] = 1
349 seen[obj] = info.pos
350 last = links[obj]
351 links[obj] = nil
353 for index = 2,len do
354 info = nil
355 local c = 1
357 for i, weight in pairs(last) do
358 if links[i.obj] then
359 local w
360 local pos = i.pos
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)
364 else
365 w = 500
368 w = weight * math.random() / w
370 if not info or w > r then
371 info, r = i, w
374 c = c + 1
377 if not info then
378 for obj in pairs(links) do
379 local pos = obj.pos
380 local w
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)
384 else
385 w = 500
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
396 assert(info)
399 obj = info.obj
400 local link = links[obj]
401 indexes[obj] = index
402 prev_pos = info.pos
403 seen[obj] = prev_pos
404 assert(info.obj == obj)
406 links[obj] = nil
407 QuestHelper:ReleaseTable(last)
408 last = link
410 QuestHelper:yieldIfNeeded(0.01*c)
413 QuestHelper:ReleaseTable(last)
415 for obj, i in pairs(indexes) do
416 local info = self[i]
417 info.obj, info.pos = obj, seen[obj]
418 seen[obj] = nil
421 --[[for i, info in ipairs(self) do
422 io.write(info.obj.name, " ")
423 end io.write("\n")]]
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
432 for k = 0, j-i-1 do
433 self[i+k], self[j-k] = self[j-k], self[i+k]
435 else
436 -- Swap two nodes.
437 self[i], self[j] = self[j], self[i]
441 local i = 1
442 while i <= #self do
443 -- Make sure all the objectives have valid positions in the list.
444 local info = self[i]
445 local mn, mx = self:findObjectiveRange(info.obj)
446 if i < mn then
447 table.insert(self, mn, info)
448 table.remove(self, i)
449 elseif i > mx then
450 table.remove(self, i)
451 table.insert(self, mx, info)
452 else
453 i = i + 1
457 local distance = 0
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
473 for i = 2, len-1 do
474 local d1, d2
475 local info = next_info
476 local pos = next_pos
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)
485 prev_info.len = d1
486 info.len = d2
487 prev_info = info
488 prev_pos = pos
489 info.pos = pos
490 distance = distance + d1
493 self.distance = distance + prev_info.len
496 function Route:pathResetBegin()
497 for i, info in ipairs(self) do
498 local pos = info.pos
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.
506 local p, d = nil, 0
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]
514 local d2 = x*x+y*y
515 if not p or d2 < d then
516 p, d = point, d2
522 -- Assuming that there will still be positions on the same continents as before, i.e., locations are only added and not removed.
523 assert(p)
525 info.pos = p
529 local function RouteUpdateRoutine(self)
530 local map_walker = self:CreateWorldMapWalker()
531 local minimap_dodad = self.minimap_dodad
533 local add_swap = {}
534 local route = self.route
535 local to_add, to_remove = self.to_add, self.to_remove
537 local routes = {}
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
548 while true do
549 local changed = false
551 if #route > 0 then
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
562 -- explode from it.
564 to_remove[o] = true
565 to_add[o] = true
566 else
567 if o.swap_before then
568 self:ReleaseTable(o.before)
569 o.before = o.swap_before
570 o.swap_before = nil
573 if o.swap_after then
574 self:ReleaseTable(o.after)
575 o.after = o.swap_after
576 o.swap_after = nil
579 if o.is_sharing ~= o.want_share then
580 o.is_sharing = o.want_share
582 if o.want_share then
583 self:DoShareObjective(o)
584 else
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]
606 local obj = info.obj
607 obj.pos = info.pos
608 route[i] = obj
611 if old_index == 1 then
612 minimap_dodad:SetObjective(route[1])
615 changed = true
620 -- Remove any waypoints if needed.
621 while true do
622 local obj = next(to_remove)
623 if not obj then break end
624 to_remove[obj] = nil
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)
635 if index == 1 then
636 minimap_dodad:SetObjective(route[1])
638 else
639 r:removeObjective(obj)
643 obj:DoneRouting()
644 changed = true
647 -- Add any waypoints if needed
648 while true do
649 local obj = next(to_add)
650 if not obj then break end
651 to_add[obj] = nil
653 if obj:Known() then
654 obj:PrepareRouting()
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.
660 obj:DoneRouting()
661 add_swap[obj] = true
662 else
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)
675 if index == 1 then
676 minimap_dodad:SetObjective(route[1])
678 else
679 r:addObjective(obj)
683 changed = true
685 else
686 add_swap[obj] = true
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
695 to_remove[obj] = nil
696 to_add[obj] = nil
697 add_swap[obj] = nil
701 to_add, add_swap = add_swap, to_add
702 self.to_add = 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.
706 local pos = self.pos
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)
718 r.fitness = fit
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
729 if r ~= best then
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
740 best = to_breed
743 self:yieldIfNeeded(.3)
745 if best ~= best_route then
746 best_route = best
748 for i, info in ipairs(best) do
749 local obj = info.obj
750 obj.pos = info.pos
751 route[i] = obj
754 minimap_dodad:SetObjective(route[1])
756 changed = true
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
768 r:pathResetBegin()
771 self:yieldIfNeeded(10)
772 self.graph_in_limbo = true
773 self:ResetPathing()
774 self.graph_in_limbo = false
775 self.defered_graph_reset = false
777 for r in pairs(routes) do
778 r:pathResetEnd()
781 for i, info in ipairs(best_route) do
782 local obj = info.obj
783 obj.pos = info.pos
784 route[i] = obj
787 minimap_dodad:SetObjective(route[1])
789 self:yieldIfNeeded(9)
792 if changed then
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)
806 local update_route
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(
815 function()
816 local state, err = xpcall(
817 function()
818 RouteUpdateRoutine(QuestHelper)
819 end,
820 function (err)
821 if debugstack then
822 return tostring(err).."\n"..debugstack(2)
823 else
824 return debug.traceback(tostring(err), 2)
826 end)
828 if not state then
829 error(err, 0)
831 end)
832 else
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
841 if not state then
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)