make right-click on minimap work better
[QuestHelper.git] / routing.lua
blob95a7f7a21b6a33420f55eab9e0bfc07f064f87cf
1 QuestHelper_File["routing.lua"] = "Development Version"
2 QuestHelper_Loadtime["routing.lua"] = GetTime()
4 -- Create module
5 QuestHelper.Routing = {}
6 local Routing = QuestHelper.Routing
7 Routing.qh = QuestHelper
9 -- Constants:
10 local improve_margin = 1e-8
12 -- Module Status:
13 local work_done = 0
15 local function CalcObjectivePriority(obj)
16 local priority = obj.priority
18 for o in pairs(obj.before) do
19 if o.watched then
20 priority = math.min(priority, CalcObjectivePriority(o))
21 end
22 end
24 obj.real_priority = priority
25 return priority
26 end
28 local Route = {}
29 Route.__index = Route
30 Routing.Route = Route -- Make it available as a member
32 -- 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.
33 function Route:sanity()
35 local assert = assert
37 if QuestHelper.Error then
38 assert = function(a, b)
39 if not a then
40 QuestHelper:TextOut("Route:sanity(): id="..self.id.."; best_route="..Routing.best_route.id)
41 QuestHelper:Error(b or "Assertion Failed")
42 end
43 end
44 end
46 local l = 0
48 --QuestHelper:TextOut(QuestHelper:StringizeTable(self))
49 for i = 0,#self-1 do
50 --QuestHelper:TextOut(tostring(i))
51 --QuestHelper:TextOut(QuestHelper:StringizeTable(self[i]))
52 --QuestHelper:TextOut(tostring(self[i].len))
53 assert(self[i].len)
54 l = l + self[i].len
55 end
57 --QuestHelper:TextOut("sd: "..l.." rd: "..self.distance)
58 assert(math.abs(l-self.distance) < 0.0001, string.format("compare %f vs %f", l, self.distance))
60 for i, info in ipairs(self) do
61 assert(self.index[info.obj] == i)
62 assert(info.pos)
63 end
65 for obj, i in pairs(self.index) do
66 assert(self[i].obj == obj)
67 end
69 for i = 1, #self-1 do
70 local l = QuestHelper:ComputeTravelTime(self[i].pos, self[i+1].pos, true)
71 assert(math.abs(l-self[i].len) < 0.0001, "Compare at "..i..": "..l.." vs "..self[i].len)
72 end
74 return true
75 end
77 function Route:findObjectiveRange(obj, passes)
79 --[[
80 lines = {}
81 table.insert(lines, string.format("QuestHelper objectiverange for %s (pri %d)", obj.obj, obj.real_priority))
82 for i = 1, #self do
83 table.insert(lines, string.format("%d %d %d --- %d %d %d (vs %s, %d)", obj.real_priority > self[i].obj.real_priority and 1 or 0, obj.after[self[i].obj] and 1 or 0, self[i].obj.before[obj] and 1 or 0, obj.real_priority < self[i].obj.real_priority and 1 or 0, obj.before[self[i].obj] and 1 or 0, self[i].obj.after[obj] and 1 or 0, self[i].obj.obj, self[i].obj.real_priority))
84 end]]
86 local mn = #self
87 while mn >= 1 do
88 if obj.real_priority > self[mn].obj.real_priority or obj.after[self[mn].obj] or self[mn].obj.before[obj] then break end
89 mn = mn - 1
90 end
92 mn = mn + 1 -- we went too far, actually
94 local mx = 1
95 while mx < #self + 1 do
96 if obj.real_priority < self[mx].obj.real_priority or obj.before[self[mx].obj] or self[mx].obj.after[obj] then break end
97 mx = mx + 1
98 end
100 --table.insert(lines, string.format("temp results is %d %d", mn, mx))
102 if mx < mn then -- well, technically, there's no place we can put this. So we guess wildly. Eventually it'll sanify itself. We hope.
103 local mid = math.ceil((mx + mn) / 2)
104 mx = mid
105 mn = mid
108 --table.insert(lines, string.format("overall: %d %d", mn, mx))
110 --[[
111 if passes and passes > 90 then
112 for k, v in pairs(lines) do QuestHelper:TextOut(v) end
113 QuestHelper:TextOut(string.format("overall: %d %d", mn, mx))
117 --[[
118 local omn, omx = self:OldFindObjectiveRange(obj)
120 if mn ~= omn or mx ~= omx then
121 for k, v in pairs(lines) do QuestHelper:TextOut(v) end
122 QuestHelper:TextOut(string.format("overall: %d %d vs %d %d", mn, mx, omn, omx))
123 lolcrash = (lolcrash or 0) + 1
124 end]]
126 return mn, mx, lines
130 function Route:addObjectiveFast(obj)
131 assert(self:sanity())
132 local indexes = self.index
133 local len = #self
134 local info = QuestHelper:CreateTable()
135 assert(not indexes[obj])
137 info.obj = obj
139 if len == 0 then
140 local d
141 self[1] = info
142 indexes[obj] = 1
143 d, info.pos = obj:TravelTime(self[0].pos, true)
144 self[0].len = d
145 self.distance = d
146 return 1
149 local player_pos = QuestHelper.pos
150 local pos = obj.location
151 local c, x, y = pos[1].c, pos[3], pos[4]
153 local mn, mx = self:findObjectiveRange(obj)
154 local index, distsqr
156 for i = mn, math.min(mx, len) do
157 local p = self[i].pos
158 if c == p[1].c then
159 local u, v = p[3]-x, p[4]-y
160 local d2 = u*u+v*v
161 if not index or d2 < distsqr then
162 index, distsqr = i, d2
167 if not index then
168 -- No nodes with the same continent already.
169 -- If the same continent as the player, add to start of list, otherwise add to end of the list.
170 index = c == player_pos[1].c and mx or mx
173 -- The next question, do I insert at that point, or do I insert after it?
174 if index ~= mx and index <= len then
175 local p1 = self[index].pos
177 if p1[1].c == c then
178 local p0
180 if index == 1 then
181 p0 = player_pos
182 else
183 p0 = self[index-1].pos
186 local oldstart, newstart
188 if p0[1].c == c then
189 local u, v = p0[3]-x, p0[4]-y
190 newstart = math.sqrt(u*u+v*v)
191 u, v = p0[3]-p1[3], p0[4]-p1[4]
192 oldstart = math.sqrt(u*u+v*v)
193 else
194 newstart = 0
195 oldstart = 0
198 local p2
199 if index ~= len then
200 p2 = self[index+1].pos
203 local oldend, newend
204 if p2 and p2[1].c == c then
205 local u, v = p2[3]-x, p2[4]-y
206 newend = math.sqrt(u*u+v*v)
207 u, v = p2[3]-p1[3], p2[4]-p1[4]
208 oldend = math.sqrt(u*u+v*v)
209 else
210 newend = 0
211 oldend = 0
214 if oldstart+newend < newstart+oldend then
215 index = index + 1
221 QH_Timeslice_Yield() -- The above checks don't require much effort.
223 if index > len then
224 local previnfo = self[index-1]
225 assert(previnfo)
226 local d
227 d, info.pos = obj:TravelTime(previnfo.pos)
228 assert(info.pos)
229 QH_Timeslice_Yield()
230 previnfo.len = d
231 self.distance = self.distance + d
232 else
233 local d1, d2
235 local previnfo = self[index-1]
236 d1, d2, info.pos = obj:TravelTime2(previnfo.pos, self[index].pos, previnfo.no_cache)
238 info.len = d2
239 self.distance = self.distance + (d1 - previnfo.len + d2)
240 previnfo.len = d1
242 QH_Timeslice_Yield()
245 -- Finally, insert the objective.
246 table.insert(self, index, info)
247 indexes[obj] = index
249 -- Fix indexes of shifted elements.
250 for i = index+1,len+1 do
251 local obj = self[i].obj
252 assert(indexes[obj] == i-1)
253 indexes[obj] = i
256 assert(self:sanity())
258 return index
261 function Route:addObjectiveBest(obj, old_index, old_distance)
262 assert(self:sanity())
264 local indexes = self.index
265 local len = #self
266 local info = QuestHelper:CreateTable()
267 assert(not indexes[obj])
269 info.obj = obj
271 if len == 0 then
272 indexes[obj] = 1
273 self.distance, info.pos = obj:TravelTime(self[0].pos, true)
274 info.len = 0
275 self[0].len = self.distance
276 self[1] = info
277 return 1
280 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.)
282 local best_index, best_delta, best_d1, best_d2, best_p
283 local no_cache, prev_pos, prev_len
284 local mn, mx = self:findObjectiveRange(obj)
286 if old_index and mn <= old_index and old_index <= mx then
287 -- We're trying to re-evaluate it, and it could remain in the same place.
288 -- So that place is our starting best known place.
289 best_index, best_delta = old_index, old_distance - self.distance
291 if best_delta < 0 then
292 -- Somehow, removing the objective actually made the route worse...
293 -- Just re-figure things from scratch.
294 -- TODO: THIS SHOULD NEVER HAPPEN dear god find out what's causing this and stop it
295 --QuestHelper:TextOut("made route worse wtf")
296 best_index, best_delta = nil, nil
300 local pinfo = self[mn-1]
301 no_cache, prev_pos, prev_len = pinfo.no_cache, pinfo.pos, pinfo.len
303 for i = mn, math.min(mx, len) do
304 assert(prev_pos == self[i-1].pos)
306 local info = self[i]
307 local pos = info.pos
309 local d1, d2, p = obj:TravelTime2(prev_pos, pos, no_cache)
311 QH_Timeslice_Yield()
313 local delta = d1 + d2 - prev_len
315 if not best_index or ((delta + improve_margin) < best_delta) or ((i == best_index) and not best_d1) then
316 -- Best so far is:
317 -- * First item we reach
318 -- * Better than previous best
319 -- * We're looking at our best already. But we just got here; how could this be best?
320 -- If this was our prior location and we didn't find anything better earlier in the route,
321 -- that's how. Save the specifics, 'cause we didn't compute them when setting up.
322 best_index, best_delta, best_d1, best_d2, best_p = i, delta, d1, d2, p
325 prev_pos = pos
326 prev_len = info.len
327 no_cache = false
330 if mx > len then
331 assert(mx == len+1)
332 assert(prev_pos == self[len].pos)
333 local delta, p = obj:TravelTime(prev_pos, no_cache)
335 QH_Timeslice_Yield()
337 if not best_index or ((delta + improve_margin) < best_delta) or ((mx == best_index) and not best_d1) then
338 info.pos = p
339 info.len = 0
340 self[len].len = delta
341 self.distance = self.distance + delta
342 table.insert(self, info)
343 indexes[obj] = mx
345 assert(self:sanity())
347 return mx
351 info.pos = best_p
352 info.len = best_d2
354 local pinfo = self[best_index-1]
355 self.distance = self.distance + (best_d1 - pinfo.len + best_d2)
356 pinfo.len = best_d1
358 table.insert(self, best_index, info)
360 -- QuestHelper:Assert(math.abs(QuestHelper:ComputeTravelTime(self[best_index-1].pos, self[best_index].pos) - self[best_index-1].len) < 0.0001, "aaaaargh")
361 --[[ -- I don't think this is necessary now that TravelTime2 explicitly does this internally, but I'm keeping it anyway.
362 self.distance = self.distance - self[best_index-1].len
363 self[best_index-1].len = QuestHelper:ComputeTravelTime(self[best_index-1].pos, self[best_index].pos, true)
364 self.distance = self.distance + self[best_index-1].len
367 indexes[obj] = best_index
369 for i = best_index+1,len+1 do
370 assert(indexes[self[i].obj] == i-1)
371 indexes[self[i].obj] = i
374 if not old_index or (mn > old_index or old_index > mx) and mn <= best_index and best_index <= mx then
375 -- 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
376 sanityfixed = 1
379 assert(self:sanity())
381 return best_index, sanityfixed
384 function Route:removeObjective(obj)
385 assert(self:sanity())
387 local indexes = self.index
388 local index = indexes[obj]
389 local old_distance = self.distance
391 assert(index)
392 local info = self[index]
393 assert(info.obj == obj)
395 --[[
396 Removing end item: subtract last distance, nothing to recalculate
397 Removing other item: recalculate location of next objective, between prior position and objective after next
398 Special case: if there is no location after next, just recalc location of next objective
399 --]]
400 if index == #self then
401 self.distance = self.distance - self[index-1].len
402 self[index-1].len = 0
403 else
404 local pinfo = self[index-1]
405 local info1 = self[index+1]
406 local info2 = self[index+2]
407 local no_cache = (index == 1)
409 local d1, d2
411 if info2 then
412 d1, d2, info1.pos = info1.obj:TravelTime2(pinfo.pos, info2.pos, no_cache)
413 QH_Timeslice_Yield()
414 self.distance = self.distance - pinfo.len - info.len - info1.len + d1 + d2
415 info1.len = d2
416 else
417 d1, info1.pos = info1.obj:TravelTime(pinfo.pos, no_cache)
418 QH_Timeslice_Yield()
419 self.distance = self.distance - pinfo.len - info.len + d1
422 pinfo.len = d1
425 QuestHelper:ReleaseTable(info)
426 indexes[obj] = nil
427 table.remove(self, index)
429 for i = index,#self do
430 -- Fix indexes of shifted elements.
431 local obj = self[i].obj
432 assert(indexes[obj] == i+1)
433 indexes[obj] = i
436 assert(self:sanity())
437 -- assert(self.distance <= old_distance)
439 return index
442 local links = {}
443 local seen = {}
445 function Route:breed(route_map)
446 local indexes = self.index
447 local len = #self
449 local info
450 local r
452 local prev_pos = QuestHelper.pos
453 assert(self[0].pos == prev_pos)
455 -- Pick which objective goes first, selecting from first objective of each route,
456 -- and scaling by the route's fitness and distance from player, with a random adjustment factor.
457 -- While we're at it, record some data about the fitness of adjacent objectives
458 for route in pairs(route_map) do
459 assert(route:sanity())
460 local fit = route.fitness
461 local pos = route[1].pos
462 local w
464 if prev_pos[1].c == pos[1].c then
465 local u, v = prev_pos[3]-pos[3], prev_pos[4]-pos[4]
466 w = math.sqrt(u*u+v*v)
467 else
468 w = 500
471 w = fit * math.random() / w
473 if not info or w > r then
474 info, r = route[1], w
477 for i = 1,len do
478 local obj = route[i].obj
479 local tbl = links[obj]
480 if not tbl then
481 tbl = QuestHelper:CreateTable()
482 links[obj] = tbl
485 if i ~= 1 then
486 local info = route[i-1]
487 local obj2 = info.obj
488 tbl[info] = (tbl[info] or 0) + fit
491 if i ~= len then
492 local info = route[i+1]
493 local obj2 = info.obj
494 if obj.real_priority <= obj2.real_priority or obj.before[obj2] then
495 tbl[info] = (tbl[info] or 0) + fit
500 QH_Timeslice_Yield()
503 -- Record info for the 'Player Position' objective, so we don't mess it up later
504 seen[self[0].obj] = self[0].pos
506 -- Record the objective that we chose to put first
507 local obj = info.obj
508 indexes[obj] = 1
509 seen[obj] = info.pos -- Save its position, because we don't want to clobber any of the info objects yet
510 prev_pos = info.pos
512 last = links[obj]
513 links[obj] = nil
515 -- Scan the rest of the places in the route, and pick objectives to go there
516 for index = 2,len do
517 info = nil
518 local c = 1
520 -- Scan the list of scores from the prior objective
521 for i, weight in pairs(last) do
522 if links[i.obj] then
523 -- Only consider an item if we have scores for that item
524 local w
525 local pos = i.pos
526 if prev_pos[1].c == pos[1].c then
527 local u, v = prev_pos[3]-pos[3], prev_pos[4]-pos[4]
528 w = math.sqrt(u*u+v*v)
529 else
530 w = 500
533 w = weight * math.random() / w
535 if not info or w > r then
536 info, r = i, w
539 c = c + 1
542 -- In case we had no valid scores, scan the remaining objectives and score by distance
543 if not info then
544 for obj in pairs(links) do
545 local pos = obj.pos
546 local w
547 if prev_pos[1] == pos[1] then
548 -- Same zone
549 local u, v = prev_pos[3]-pos[3], prev_pos[4]-pos[4]
550 w = math.sqrt(u*u+v*v)
551 elseif prev_pos[1].c == pos[1].c then
552 -- Same continent. -- Assume twices as long.
553 local u, v = prev_pos[3]-pos[3], prev_pos[4]-pos[4]
554 w = 2*math.sqrt(u*u+v*v)
555 else
556 -- Different continent. Assume fixed value of 5 minutes.
557 w = 300
560 w = math.random() / w
562 if not info or w > r then
563 local route = next(route_map)
564 info, r = route[route.index[obj]], w
568 assert(info)
571 -- Add the selected item to the route
572 obj = info.obj
573 indexes[obj] = index
574 prev_pos = info.pos
575 seen[obj] = prev_pos
576 assert(info.obj == obj)
578 -- 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
579 local link = links[obj]
580 links[obj] = nil
581 QuestHelper:ReleaseTable(last)
582 last = link
584 QH_Timeslice_Yield()
587 -- Clean up the last table
588 QuestHelper:ReleaseTable(last)
590 -- Now that we've got our objectives lined up, fill in the info objects with the positions we saved
591 for obj, i in pairs(indexes) do
592 assert(seen[obj])
593 local info = self[i]
594 info.obj, info.pos = obj, seen[obj]
595 seen[obj] = nil
598 -- Now randomly randomize some of the route (aka mutation)
599 while math.random() > 0.3 do
600 local l = math.floor(math.random()^1.6*(len-1))+1
601 local i = math.random(1, len-l)
602 local j = i+l
604 -- Reverse a chunk of the route
605 for k = 0, j-i-1 do
606 self[i+k], self[j-k] = self[j-k], self[i+k]
610 -- But wait, after all that some objectives might violate the rules. Make sure the route follows
611 -- the rules.
613 -- There's some horrifying ugly here. The "before" and "after" links are not properly updated simultaneously. This means that X can be flagged as after Y without Y being flagged as before X. Making things worse (because, oh man, things had to be made worse!) this means that X might have a lower priority than Y despite needing to happen before it. Urgh.
614 -- Since the entire thing is internally inconsistent anyway, we're just gonna try to consistentize it.
616 local valid_items = {}
617 for k, v in ipairs(self) do
618 valid_items[v.obj] = true
620 for k, v in ipairs(self) do
621 for b in pairs(v.obj.before) do
622 if valid_items[b] then
623 b.after[v.obj] = true
626 for a in pairs(v.obj.after) do
627 if valid_items[a] then
628 a.before[v.obj] = true
633 -- Because priorities might have been changed in here, we next make absolutely sure we have up-to-date priorities.
634 for k, v in ipairs(self) do
635 CalcObjectivePriority(v.obj)
638 -- Have I mentioned I hate this codebase yet?
639 -- Because I do.
640 -- Just, you know.
641 -- FYI.
643 local invalid = true
644 local invalid_passes = 0
645 --local output_strings = {}
646 while invalid do
648 invalid_passes = invalid_passes + 1
650 --[[if invalid_passes >= 100 then
651 for k, v in pairs(output_strings) do
652 QuestHelper:TextOut(v)
654 end]]
656 if invalid_passes >= 100 then
657 -- ugh
658 QuestHelper.mutation_passes_exceeded = true
659 break
661 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 for QH to continue working, sorry about that)") -- space is so it works in the real code
663 invalid = false
664 local i = 1
665 --[[for i = 1, #self do
666 local mn, mx = self:findObjectiveRange(self[i].obj, invalid_passes)
667 table.insert(output_strings, string.format("%d is mn mx %d %d (%s)", i, mn, mx, self[i].obj.obj))
668 end]]
669 while i <= #self do
670 -- Make sure all the objectives have valid positions in the list.
671 local info = self[i]
672 local mn, mx, tabi = self:findObjectiveRange(info.obj, invalid_passes)
673 --if invalid_passes > 90 then for k, v in pairs(tabi) do table.insert(output_strings, v) end end
674 if i < mn then
675 -- In theory, 'i' shouldn't be increased here, as the next
676 -- element will be shifted down into the current position.
678 -- However, it is possible for an infinite loop to be created
679 -- by this, with a small range of objectives constantly
680 -- being shifted.
682 -- So, I mark the route as invalid and go through it another time.
683 -- It's probably still possible to get into an infinite loop,
684 -- but it seems much less likely.
686 table.insert(self, mn, info)
687 table.remove(self, i)
688 invalid = true
689 --table.insert(output_strings, string.format("shifting %d into %d", i, mn))
690 elseif i > mx then
691 table.remove(self, i)
692 table.insert(self, mx, info)
693 invalid = true
694 --table.insert(output_strings, string.format("shifting %d into %d", i, mx))
696 i = i + 1
698 --table.insert(output_strings, "pass done")
701 -- Now that we've chosen a route, re-calculate the cost of each leg of the route
702 local distance = 0
703 local prev_info = self[0]
704 local next_info = self[1]
705 local prev_pos = prev_info.pos
706 local next_pos = next_info.pos
707 assert(prev_pos)
708 assert(next_pos)
710 QH_Timeslice_Yield()
712 for i = 1, len-1 do
713 local d1, d2
714 local pos
715 local info = next_info
716 next_info = self[i+1]
717 next_pos = next_info.pos
719 indexes[info.obj] = i
721 d1, d2, pos = info.obj:TravelTime2(prev_pos, next_pos, prev_info.no_cache)
722 assert(pos)
723 QH_Timeslice_Yield()
725 prev_info.len = d1
726 info.len = d2
727 info.pos = pos
728 distance = distance + d1
730 prev_info = info
731 prev_pos = pos
734 self.distance = distance + prev_info.len
736 indexes[self[len].obj] = len
737 self[len].len = 0
739 assert(self:sanity())
742 function Route:pathResetBegin()
743 for i, info in ipairs(self) do
744 local pos = info.pos
745 info[1], info[2], info[3] = pos[1].c, pos[3], pos[4]
749 function Route:pathResetEnd()
750 for i, info in ipairs(self) do
751 -- Try to find a new position for this objective, near where we had it originally.
752 local p, d = nil, 0
754 local a, b, c = info[1], info[2], info[3]
756 for z, pl in pairs(info.obj.p) do
757 for i, point in ipairs(pl) do
758 if a == point[1].c then
759 local x, y = b-point[3], c-point[4]
760 local d2 = x*x+y*y
761 if not p or d2 < d then
762 p, d = point, d2
768 -- Assuming that there will still be positions on the same continents as before, i.e., locations are only added and not removed.
769 assert(p)
771 info.pos = p
774 self:recalculateDistances()
777 function Route:recalculateDistances()
779 self.distance = 0
780 for i = 0, #self-1 do
781 self[i].len = QuestHelper:ComputeTravelTime(self[i].pos, self[i+1].pos)
782 self.distance = self.distance + self[i].len
786 function Routing:RoutingSetup()
787 Routing.map_walker = self.qh:CreateWorldMapWalker()
788 Routing.add_swap = {}
789 Routing.routes = {}
791 local routes = Routing.routes
792 local pos = QuestHelper.pos
793 local PlayerObjective = self.qh:NewObjectiveObject() -- Pseudo-objective which reflects player's current position. Always at index 0 of each route.
794 PlayerObjective.pos = pos
795 PlayerObjective.cat = "loc" -- A special case of a location
796 PlayerObjective.obj = "Player's current position" -- Player shouldn't see this, so no need to localize
797 PlayerObjective.icon_id = 6 -- Don't think we'll need these; just filling them in for completeness
798 PlayerObjective.o = {pos=pos}
799 PlayerObjective.fb = {}
801 for i = 1,15 do -- Create some empty routes to use for our population.
802 local new_rt = { index={ [PlayerObjective]=0 },
803 distance=0,
804 [0]={ obj=PlayerObjective, pos=pos, len=0, no_cache=true }, -- Player's current position is always objective #0
805 id=i -- So I can keep track of which route is which; only for debugging.
807 setmetatable(new_rt, Route)
808 routes[new_rt] = true
811 -- All the routes are the same right now, so it doesn't matter which we're considering the best.
812 self.best_route = next(routes)
813 self.recheck_position = 1
817 function Routing:RouteUpdateRoutine()
818 local qh = QuestHelper
819 local map_walker = Routing.map_walker
820 local minimap_dodad = qh.minimap_dodad
822 local route = qh.route
823 local to_add, to_remove, add_swap = qh.to_add, qh.to_remove, self.add_swap
825 local routes = self.routes
826 local pos = qh.pos
828 local best_route = self.best_route
830 local last_cache_clear = GetTime()
832 ------ EVIL HACK OF DEBUG
834 if false then
835 while GetTime() < last_cache_clear + 5 do
836 coroutine.yield()
839 if qh.target then
840 -- We know the player will be at the target location at target_time, so fudge the numbers
841 -- to pretend we're traveling there.
843 pos[1], pos[3], pos[4] = qh.target[1], qh.target[3], qh.target[4]
844 local extra_time = math.max(0, qh.target_time-time())
845 for i, t in ipairs(qh.target[2]) do
846 pos[2][i] = t+extra_time
848 else
849 if not pos[1] -- Need a valid position, in case the player was dead when they loaded the game.
850 or not UnitIsDeadOrGhost("player") then
851 -- Don't update the player's position if they're dead, assume they'll be returning to their corpse.
852 pos[3], pos[4] = qh.Astrolabe:TranslateWorldMapPosition(qh.c, qh.z, qh.x, qh.y, qh.c, 0)
853 assert(pos[3])
854 assert(pos[4])
855 pos[1] = qh.zone_nodes[qh.i]
856 pos[3], pos[4] = pos[3] * qh.continent_scales_x[qh.c], pos[4] * qh.continent_scales_y[qh.c]
858 for i, n in ipairs(pos[1]) do
859 if not n.x then
860 for i, j in pairs(n) do qh:TextOut("[%q]=%s %s", i, type(j), tostring(j) or "???") end
861 assert(false)
864 local a, b = n.x-pos[3], n.y-pos[4]
865 pos[2][i] = math.sqrt(a*a+b*b)
870 local obj = next(to_add)
872 QuestHelper:TextOut("dbghack")
873 QuestHelper:TextOut(QuestHelper:StringizeTable(to_add))
874 obj.filter_zone = false
875 obj.filter_watched = false
876 QuestHelper:TextOut(QuestHelper:StringizeTable(obj))
877 QuestHelper:TextOut(tostring(obj:Known()))
878 obj:PrepareRouting()
879 QuestHelper:TextOut(QuestHelper:StringizeTable(obj))
881 QuestHelper:TextOut("o")
882 QuestHelper:TextOut(QuestHelper:StringizeTable(obj.o))
884 QuestHelper:TextOut("pp")
885 QuestHelper:TextOut(QuestHelper:StringizeTable(obj.pos))
886 QuestHelper:TextOut(QuestHelper:StringizeTable(obj.p))
887 QuestHelper:TextOut(tostring(obj:Known()))
889 local index = best_route:addObjectiveBest(obj)
890 obj.pos = best_route[index].pos
892 QuestHelper:TextOut(QuestHelper:StringizeTable(obj.pos))
894 QuestHelper:TextOut(qh:ComputeTravelTime(pos, obj.pos))
896 Error()
899 ------ EVIL HACK OF DEBUG
901 while true do
902 -- Clear caches out a bit
903 if GetTime() + 15 >= last_cache_clear then
904 qh:CacheCleanup()
905 last_cache_clear = GetTime()
908 -- Update the player's position data.
909 if qh.target then
910 -- We know the player will be at the target location at target_time, so fudge the numbers
911 -- to pretend we're traveling there.
913 pos[1], pos[3], pos[4] = qh.target[1], qh.target[3], qh.target[4]
914 local extra_time = math.max(0, qh.target_time-time())
915 for i, t in ipairs(qh.target[2]) do
916 pos[2][i] = t+extra_time
918 else
919 if not pos[1] -- Need a valid position, in case the player was dead when they loaded the game.
920 or not UnitIsDeadOrGhost("player") then
921 -- Don't update the player's position if they're dead, assume they'll be returning to their corpse.
922 pos[3], pos[4] = qh.Astrolabe:TranslateWorldMapPosition(qh.c, qh.z, qh.x, qh.y, qh.c, 0)
923 assert(pos[3])
924 assert(pos[4])
925 pos[1] = qh.zone_nodes[qh.i]
926 pos[3], pos[4] = pos[3] * qh.continent_scales_x[qh.c], pos[4] * qh.continent_scales_y[qh.c]
928 for i, n in ipairs(pos[1]) do
929 if not n.x then
930 for i, j in pairs(n) do qh:TextOut("[%q]=%s %s", i, type(j), tostring(j) or "???") end
931 assert(false)
934 local a, b = n.x-pos[3], n.y-pos[4]
935 pos[2][i] = math.sqrt(a*a+b*b)
940 local changed = false
942 if #route > 0 then
943 if self.recheck_position > #route then self.recheck_position = 1 end
944 local o = route[self.recheck_position]
946 assert(o.zones)
947 o.filter_zone = o.zones[pos[1].i] == nil
948 o.filter_watched = not o:IsWatched()
950 if not o:Known() then
951 -- Objective was probably made to depend on an objective that we don't know about yet.
952 -- We add it to both lists, because although we need to remove it, we need it added again when we can.
953 -- This creates an inconsistancy, but it'll get fixed in the removal loop before anything has a chance to
954 -- explode from it.
956 to_remove[o] = true
957 to_add[o] = true
958 else
959 if o.swap_before then
960 qh:ReleaseTable(o.before)
961 o.before = o.swap_before
962 o.swap_before = nil
965 if o.swap_after then
966 qh:ReleaseTable(o.after)
967 o.after = o.swap_after
968 o.swap_after = nil
971 if o.is_sharing ~= o.want_share then
972 o.is_sharing = o.want_share
974 if o.want_share then
975 qh:DoShareObjective(o)
976 else
977 qh:DoUnshareObjective(o)
981 CalcObjectivePriority(o)
983 -- Make sure the objective in best_route is still in a valid position.
984 -- Won't worry about other routes, they should forced into valid configurations by breeding.
986 -- 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.
987 local aobt = {}
988 setmetatable(aobt, getmetatable(best_route))
989 for k, v in pairs(best_route) do
990 aobt[k] = v
992 aobt.index = {}
993 for k, v in ipairs(best_route) do
994 -- arglbargl
995 aobt[k] = QuestHelper:CreateTable("AOBT idiocy")
996 for t, q in pairs(v) do
997 aobt[k][t] = q
999 aobt.index[aobt[k].obj] = k
1001 if aobt[0] then
1002 aobt[0] = QuestHelper:CreateTable("AOBT idiocy")
1003 for t, q in pairs(best_route[0]) do
1004 aobt[0][t] = q
1007 -- Actually duplicating a route is irritatingly hard (this is another thing which will be fixed eventually dammit)
1009 assert(aobt:sanity())
1010 assert(best_route:sanity())
1012 local old_distance, old_index = best_route.distance, best_route:removeObjective(o)
1013 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
1014 local new_index, sanityfixed = best_route:addObjectiveBest(o, old_index, old_distance)
1015 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
1016 -- 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
1018 if new_real_distance < old_real_distance or sanityfixed then -- More of the temporary hack
1019 -- If we're using the new path . . .
1021 if old_index > new_index then
1022 old_index, new_index = new_index, old_index
1025 for i = math.max(1, old_index-1), new_index do
1026 local info = best_route[i]
1027 local obj = info.obj
1028 obj.pos = info.pos
1029 route[i] = obj
1033 if old_index ~= new_index then
1034 if old_index == 1 then
1035 minimap_dodad:SetObjective(route[1])
1038 changed = true
1041 -- . . . release our backup path
1042 for k, v in ipairs(aobt) do QuestHelper:ReleaseTable(v) end
1043 QuestHelper:ReleaseTable(aobt[0])
1044 else -- hack (everything in this conditional besides the above chunk is a horrible hack)
1045 -- If we're using the old path . . .
1046 -- . . . release the *internals* of the new path, then copy everything over. Eugh.
1047 for k, v in ipairs(best_route) do QuestHelper:ReleaseTable(v) end
1048 QuestHelper:ReleaseTable(best_route[0])
1049 while #best_route > 0 do table.remove(best_route) end
1050 for k, v in pairs(aobt) do best_route[k] = v end -- hack
1051 setmetatable(aobt, Route)
1052 assert(best_route:sanity())
1053 end -- hack
1055 -- 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
1056 -- 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*
1057 --if old_index == new_index then
1058 -- We don't advance recheck_position unless the node doesn't get moved.
1059 -- TODO: As the this code is apparently bugged, it's gotten into an infinite loop of constantly swapping
1060 -- and hence never advancing. As a work around for now, we'll always advance.
1061 -- THIS IS A GREAT IDEA
1062 --else
1063 self.recheck_position = self.recheck_position + 1
1068 -- Remove any waypoints if needed.
1069 while true do
1070 local obj = next(to_remove)
1071 if not obj then break end
1072 to_remove[obj] = nil
1074 if obj.is_sharing then
1075 obj.is_sharing = false
1076 qh:DoUnshareObjective(obj)
1079 for r in pairs(routes) do
1080 if r == best_route then
1081 local index = r:removeObjective(obj)
1082 table.remove(route, index)
1083 if index == 1 then
1084 minimap_dodad:SetObjective(route[1])
1086 else
1087 r:removeObjective(obj)
1091 obj:DoneRouting()
1093 changed = true
1096 -- Add any waypoints if needed
1097 while true do
1098 local obj = next(to_add)
1099 if not obj then break end
1100 to_add[obj] = nil
1102 obj.filter_zone = obj.zones and obj.zones[pos[1].i] == nil
1103 obj.filter_watched = not obj:IsWatched()
1105 if obj:Known() then
1106 obj:PrepareRouting()
1108 obj.filter_zone = obj.zones[pos[1].i] == nil
1110 if obj.filter_zone and QuestHelper_Pref.filter_zone then
1111 -- Not going to add it, wrong zone.
1112 obj:DoneRouting()
1113 add_swap[obj] = true
1114 else
1115 if not obj.is_sharing and obj.want_share then
1116 obj.is_sharing = true
1117 qh:DoShareObjective(obj)
1120 CalcObjectivePriority(obj)
1122 for r in pairs(routes) do
1123 if r == best_route then
1124 local index = r:addObjectiveBest(obj)
1125 obj.pos = r[index].pos
1126 table.insert(route, index, obj)
1127 if index == 1 then
1128 minimap_dodad:SetObjective(route[1])
1130 else
1131 r:addObjectiveFast(obj)
1135 changed = true
1137 else
1138 add_swap[obj] = true
1142 for obj in pairs(add_swap) do
1143 -- If one of the objectives we were considering adding was removed, it would be in both lists.
1144 -- That would be bad. We can't remove it because we haven't actually added it yet, so
1145 -- handle that special case here.
1146 if to_remove[obj] then
1147 to_remove[obj] = nil
1148 to_add[obj] = nil
1149 add_swap[obj] = nil
1153 to_add, add_swap = add_swap, to_add
1154 qh.to_add = to_add
1155 self.add_swap = add_swap
1157 if #best_route > 1 then
1158 -- If there is 2 or more objectives, randomly combine routes to (hopefully) create something better than we had before.
1160 -- Calculate best_route first, so that if other routes are identical, we don't risk swapping with them and
1161 -- updating the map_walker.
1162 local best, max_fitness = best_route, 1/(qh:ComputeTravelTime(pos, best_route[1].pos) + best_route.distance)
1163 best_route.fitness = max_fitness
1165 QH_Timeslice_Yield()
1167 for r in pairs(routes) do
1168 if r ~= best_route then
1169 local fit = 1/(qh:ComputeTravelTime(pos, r[1].pos)+r.distance)
1170 r.fitness = fit
1171 if fit > max_fitness then
1172 best, max_fitness = r, fit
1174 QH_Timeslice_Yield()
1178 local to_breed, score
1180 for r in pairs(routes) do
1181 if r ~= best then
1182 local s = math.random()*r.fitness
1183 if not to_breed or s < score then
1184 to_breed, score = r, s
1189 to_breed:breed(routes)
1191 if 1/(qh:ComputeTravelTime(pos, to_breed[1].pos)+to_breed.distance+improve_margin) > max_fitness then
1192 best = to_breed
1195 QH_Timeslice_Yield()
1197 if best ~= best_route then
1199 assert(best:sanity())
1200 assert(best_route:sanity())
1202 best_route = best
1203 self.best_route = best_route
1205 for i, info in ipairs(best) do
1206 local obj = info.obj
1207 obj.pos = info.pos
1208 route[i] = obj
1211 minimap_dodad:SetObjective(route[1])
1213 changed = true
1217 if qh.defered_flight_times then
1218 qh:buildFlightTimes()
1219 qh.defered_flight_times = false
1220 assert(qh.defered_graph_reset)
1223 if qh.defered_graph_reset then
1224 QH_Timeslice_Yield()
1226 for r in pairs(routes) do
1227 r:pathResetBegin()
1230 qh.graph_in_limbo = true
1231 qh:ResetPathing()
1232 qh.graph_in_limbo = false
1233 qh.defered_graph_reset = false
1235 for r in pairs(routes) do
1236 r:pathResetEnd()
1239 for i, info in ipairs(best_route) do
1240 local obj = info.obj
1241 obj.pos = info.pos
1242 route[i] = obj
1244 best_route:recalculateDistances()
1246 minimap_dodad:SetObjective(route[1])
1248 QuestHelper:SetTargetLocationRecalculate()
1250 for r in pairs(routes) do
1251 assert(r:sanity())
1253 best_route:sanity()
1255 QH_Timeslice_Yield()
1258 if changed then
1259 map_walker:RouteChanged()
1262 assert(#route == #best_route)
1264 -- temporary hack to cause more errors
1265 --qh.defered_graph_reset = true
1266 --qh.defered_flight_times = true
1268 QH_Timeslice_Yield()
1272 function Routing:Initialize()
1273 self:RoutingSetup()
1275 QH_Timeslice_Add(function() Routing:RouteUpdateRoutine() end, "routing")
1276 QH_Timeslice_Toggle("routing", false)
1278 --[[
1279 if coroutine.coco then
1280 -- coco allows yielding across c boundries, which allows me to use xpcall to get
1281 -- stack traces for coroutines without calls to yield resulting in thermal nuclear meltdown.
1283 -- This isn't part of WoW, I was using it in my driver program: Development/routetest
1285 update_route = coroutine.create(
1286 function()
1287 local state, err = xpcall(
1288 function()
1289 Routing:RouteUpdateRoutine()
1290 end,
1291 function (err)
1292 if debugstack then
1293 return tostring(err).."\n"..debugstack(2)
1294 else
1295 return debug.traceback(tostring(err), 2)
1297 end)
1299 if not state then
1300 error(err, 0)
1302 end)
1303 else
1304 update_route = coroutine.create(function() Routing:RouteUpdateRoutine() end)