update changes
[QuestHelper.git] / routing.lua
blobe4e0bc823118b04377cd79f3f2bbc9df52597cef
1 QuestHelper_File["routing.lua"] = "Development Version"
3 -- Create module
4 QuestHelper.Routing = {}
5 local Routing = QuestHelper.Routing
6 Routing.qh = QuestHelper
8 -- Constants:
9 local improve_margin = 1e-8
11 -- Module Status:
12 local work_done = 0
14 local function CalcObjectivePriority(obj)
15 local priority = obj.priority
17 for o in pairs(obj.before) do
18 if o.watched then
19 priority = math.min(priority, CalcObjectivePriority(o))
20 end
21 end
23 obj.real_priority = priority
24 return priority
25 end
27 local Route = {}
28 Route.__index = Route
29 Routing.Route = Route -- Make it available as a member
31 -- 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.
32 function Route:sanity()
34 local assert = assert
36 if QuestHelper.Error then
37 assert = function(a, b)
38 if not a then
39 QuestHelper:TextOut("Route:sanity(): id="..self.id.."; best_route="..Routing.best_route.id)
40 QuestHelper:Error(b or "Assertion Failed")
41 end
42 end
43 end
45 local l = 0
47 --QuestHelper:TextOut(QuestHelper:StringizeTable(self))
48 for i = 0,#self-1 do
49 --QuestHelper:TextOut(tostring(i))
50 --QuestHelper:TextOut(QuestHelper:StringizeTable(self[i]))
51 --QuestHelper:TextOut(tostring(self[i].len))
52 assert(self[i].len)
53 l = l + self[i].len
54 end
56 --QuestHelper:TextOut("sd: "..l.." rd: "..self.distance)
57 assert(math.abs(l-self.distance) < 0.0001, string.format("compare %f vs %f", l, self.distance))
59 for i, info in ipairs(self) do
60 assert(self.index[info.obj] == i)
61 assert(info.pos)
62 end
64 for obj, i in pairs(self.index) do
65 assert(self[i].obj == obj)
66 end
68 for i = 1, #self-1 do
69 local l = QuestHelper:ComputeTravelTime(self[i].pos, self[i+1].pos, true)
70 assert(math.abs(l-self[i].len) < 0.0001, "Compare at "..i..": "..l.." vs "..self[i].len)
71 end
73 return true
74 end
76 function Route:findObjectiveRange(obj, passes)
78 --[[
79 lines = {}
80 table.insert(lines, string.format("QuestHelper objectiverange for %s (pri %d)", obj.obj, obj.real_priority))
81 for i = 1, #self do
82 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))
83 end]]
85 local mn = #self
86 while mn >= 1 do
87 if obj.real_priority > self[mn].obj.real_priority or obj.after[self[mn].obj] or self[mn].obj.before[obj] then break end
88 mn = mn - 1
89 end
91 mn = mn + 1 -- we went too far, actually
93 local mx = 1
94 while mx < #self + 1 do
95 if obj.real_priority < self[mx].obj.real_priority or obj.before[self[mx].obj] or self[mx].obj.after[obj] then break end
96 mx = mx + 1
97 end
99 --table.insert(lines, string.format("temp results is %d %d", mn, mx))
101 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.
102 local mid = math.ceil((mx + mn) / 2)
103 mx = mid
104 mn = mid
107 --table.insert(lines, string.format("overall: %d %d", mn, mx))
109 --[[
110 if passes and passes > 90 then
111 for k, v in pairs(lines) do QuestHelper:TextOut(v) end
112 QuestHelper:TextOut(string.format("overall: %d %d", mn, mx))
116 --[[
117 local omn, omx = self:OldFindObjectiveRange(obj)
119 if mn ~= omn or mx ~= omx then
120 for k, v in pairs(lines) do QuestHelper:TextOut(v) end
121 QuestHelper:TextOut(string.format("overall: %d %d vs %d %d", mn, mx, omn, omx))
122 lolcrash = (lolcrash or 0) + 1
123 end]]
125 return mn, mx, lines
129 function Route:addObjectiveFast(obj)
130 assert(self:sanity())
131 local indexes = self.index
132 local len = #self
133 local info = QuestHelper:CreateTable()
134 assert(not indexes[obj])
136 info.obj = obj
138 if len == 0 then
139 local d
140 self[1] = info
141 indexes[obj] = 1
142 d, info.pos = obj:TravelTime(self[0].pos, true)
143 self[0].len = d
144 self.distance = d
145 return 1
148 local player_pos = QuestHelper.pos
149 local pos = obj.location
150 local c, x, y = pos[1].c, pos[3], pos[4]
152 local mn, mx = self:findObjectiveRange(obj)
153 local index, distsqr
155 for i = mn, math.min(mx, len) do
156 local p = self[i].pos
157 if c == p[1].c then
158 local u, v = p[3]-x, p[4]-y
159 local d2 = u*u+v*v
160 if not index or d2 < distsqr then
161 index, distsqr = i, d2
166 if not index then
167 -- No nodes with the same continent already.
168 -- If the same continent as the player, add to start of list, otherwise add to end of the list.
169 index = c == player_pos[1].c and mx or mx
172 -- The next question, do I insert at that point, or do I insert after it?
173 if index ~= mx and index <= len then
174 local p1 = self[index].pos
176 if p1[1].c == c then
177 local p0
179 if index == 1 then
180 p0 = player_pos
181 else
182 p0 = self[index-1].pos
185 local oldstart, newstart
187 if p0[1].c == c then
188 local u, v = p0[3]-x, p0[4]-y
189 newstart = math.sqrt(u*u+v*v)
190 u, v = p0[3]-p1[3], p0[4]-p1[4]
191 oldstart = math.sqrt(u*u+v*v)
192 else
193 newstart = 0
194 oldstart = 0
197 local p2
198 if index ~= len then
199 p2 = self[index+1].pos
202 local oldend, newend
203 if p2 and p2[1].c == c then
204 local u, v = p2[3]-x, p2[4]-y
205 newend = math.sqrt(u*u+v*v)
206 u, v = p2[3]-p1[3], p2[4]-p1[4]
207 oldend = math.sqrt(u*u+v*v)
208 else
209 newend = 0
210 oldend = 0
213 if oldstart+newend < newstart+oldend then
214 index = index + 1
220 QH_Timeslice_Yield() -- The above checks don't require much effort.
222 if index > len then
223 local previnfo = self[index-1]
224 assert(previnfo)
225 local d
226 d, info.pos = obj:TravelTime(previnfo.pos)
227 assert(info.pos)
228 QH_Timeslice_Yield()
229 previnfo.len = d
230 self.distance = self.distance + d
231 else
232 local d1, d2
234 local previnfo = self[index-1]
235 d1, d2, info.pos = obj:TravelTime2(previnfo.pos, self[index].pos, previnfo.no_cache)
237 info.len = d2
238 self.distance = self.distance + (d1 - previnfo.len + d2)
239 previnfo.len = d1
241 QH_Timeslice_Yield()
244 -- Finally, insert the objective.
245 table.insert(self, index, info)
246 indexes[obj] = index
248 -- Fix indexes of shifted elements.
249 for i = index+1,len+1 do
250 local obj = self[i].obj
251 assert(indexes[obj] == i-1)
252 indexes[obj] = i
255 assert(self:sanity())
257 return index
260 function Route:addObjectiveBest(obj, old_index, old_distance)
261 assert(self:sanity())
263 local indexes = self.index
264 local len = #self
265 local info = QuestHelper:CreateTable()
266 assert(not indexes[obj])
268 info.obj = obj
270 if len == 0 then
271 indexes[obj] = 1
272 self.distance, info.pos = obj:TravelTime(self[0].pos, true)
273 info.len = 0
274 self[0].len = self.distance
275 self[1] = info
276 return 1
279 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.)
281 local best_index, best_delta, best_d1, best_d2, best_p
282 local no_cache, prev_pos, prev_len
283 local mn, mx = self:findObjectiveRange(obj)
285 if old_index and mn <= old_index and old_index <= mx then
286 -- We're trying to re-evaluate it, and it could remain in the same place.
287 -- So that place is our starting best known place.
288 best_index, best_delta = old_index, old_distance - self.distance
290 if best_delta < 0 then
291 -- Somehow, removing the objective actually made the route worse...
292 -- Just re-figure things from scratch.
293 -- TODO: THIS SHOULD NEVER HAPPEN dear god find out what's causing this and stop it
294 --QuestHelper:TextOut("made route worse wtf")
295 best_index, best_delta = nil, nil
299 local pinfo = self[mn-1]
300 no_cache, prev_pos, prev_len = pinfo.no_cache, pinfo.pos, pinfo.len
302 for i = mn, math.min(mx, len) do
303 assert(prev_pos == self[i-1].pos)
305 local info = self[i]
306 local pos = info.pos
308 local d1, d2, p = obj:TravelTime2(prev_pos, pos, no_cache)
310 QH_Timeslice_Yield()
312 local delta = d1 + d2 - prev_len
314 if not best_index or ((delta + improve_margin) < best_delta) or ((i == best_index) and not best_d1) then
315 -- Best so far is:
316 -- * First item we reach
317 -- * Better than previous best
318 -- * We're looking at our best already. But we just got here; how could this be best?
319 -- If this was our prior location and we didn't find anything better earlier in the route,
320 -- that's how. Save the specifics, 'cause we didn't compute them when setting up.
321 best_index, best_delta, best_d1, best_d2, best_p = i, delta, d1, d2, p
324 prev_pos = pos
325 prev_len = info.len
326 no_cache = false
329 if mx > len then
330 assert(mx == len+1)
331 assert(prev_pos == self[len].pos)
332 local delta, p = obj:TravelTime(prev_pos, no_cache)
334 QH_Timeslice_Yield()
336 if not best_index or ((delta + improve_margin) < best_delta) or ((mx == best_index) and not best_d1) then
337 info.pos = p
338 info.len = 0
339 self[len].len = delta
340 self.distance = self.distance + delta
341 table.insert(self, info)
342 indexes[obj] = mx
344 assert(self:sanity())
346 return mx
350 info.pos = best_p
351 info.len = best_d2
353 local pinfo = self[best_index-1]
354 self.distance = self.distance + (best_d1 - pinfo.len + best_d2)
355 pinfo.len = best_d1
357 table.insert(self, best_index, info)
359 -- QuestHelper:Assert(math.abs(QuestHelper:ComputeTravelTime(self[best_index-1].pos, self[best_index].pos) - self[best_index-1].len) < 0.0001, "aaaaargh")
360 --[[ -- I don't think this is necessary now that TravelTime2 explicitly does this internally, but I'm keeping it anyway.
361 self.distance = self.distance - self[best_index-1].len
362 self[best_index-1].len = QuestHelper:ComputeTravelTime(self[best_index-1].pos, self[best_index].pos, true)
363 self.distance = self.distance + self[best_index-1].len
366 indexes[obj] = best_index
368 for i = best_index+1,len+1 do
369 assert(indexes[self[i].obj] == i-1)
370 indexes[self[i].obj] = i
373 if not old_index or (mn > old_index or old_index > mx) and mn <= best_index and best_index <= mx then
374 -- 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
375 sanityfixed = 1
378 assert(self:sanity())
380 return best_index, sanityfixed
383 function Route:removeObjective(obj)
384 assert(self:sanity())
386 local indexes = self.index
387 local index = indexes[obj]
388 local old_distance = self.distance
390 assert(index)
391 local info = self[index]
392 assert(info.obj == obj)
394 --[[
395 Removing end item: subtract last distance, nothing to recalculate
396 Removing other item: recalculate location of next objective, between prior position and objective after next
397 Special case: if there is no location after next, just recalc location of next objective
398 --]]
399 if index == #self then
400 self.distance = self.distance - self[index-1].len
401 self[index-1].len = 0
402 else
403 local pinfo = self[index-1]
404 local info1 = self[index+1]
405 local info2 = self[index+2]
406 local no_cache = (index == 1)
408 local d1, d2
410 if info2 then
411 d1, d2, info1.pos = info1.obj:TravelTime2(pinfo.pos, info2.pos, no_cache)
412 QH_Timeslice_Yield()
413 self.distance = self.distance - pinfo.len - info.len - info1.len + d1 + d2
414 info1.len = d2
415 else
416 d1, info1.pos = info1.obj:TravelTime(pinfo.pos, no_cache)
417 QH_Timeslice_Yield()
418 self.distance = self.distance - pinfo.len - info.len + d1
421 pinfo.len = d1
424 QuestHelper:ReleaseTable(info)
425 indexes[obj] = nil
426 table.remove(self, index)
428 for i = index,#self do
429 -- Fix indexes of shifted elements.
430 local obj = self[i].obj
431 assert(indexes[obj] == i+1)
432 indexes[obj] = i
435 assert(self:sanity())
436 -- assert(self.distance <= old_distance)
438 return index
441 local links = {}
442 local seen = {}
444 function Route:breed(route_map)
445 local indexes = self.index
446 local len = #self
448 local info
449 local r
451 local prev_pos = QuestHelper.pos
452 assert(self[0].pos == prev_pos)
454 -- Pick which objective goes first, selecting from first objective of each route,
455 -- and scaling by the route's fitness and distance from player, with a random adjustment factor.
456 -- While we're at it, record some data about the fitness of adjacent objectives
457 for route in pairs(route_map) do
458 assert(route:sanity())
459 local fit = route.fitness
460 local pos = route[1].pos
461 local w
463 if prev_pos[1].c == pos[1].c then
464 local u, v = prev_pos[3]-pos[3], prev_pos[4]-pos[4]
465 w = math.sqrt(u*u+v*v)
466 else
467 w = 500
470 w = fit * math.random() / w
472 if not info or w > r then
473 info, r = route[1], w
476 for i = 1,len do
477 local obj = route[i].obj
478 local tbl = links[obj]
479 if not tbl then
480 tbl = QuestHelper:CreateTable()
481 links[obj] = tbl
484 if i ~= 1 then
485 local info = route[i-1]
486 local obj2 = info.obj
487 tbl[info] = (tbl[info] or 0) + fit
490 if i ~= len then
491 local info = route[i+1]
492 local obj2 = info.obj
493 if obj.real_priority <= obj2.real_priority or obj.before[obj2] then
494 tbl[info] = (tbl[info] or 0) + fit
499 QH_Timeslice_Yield()
502 -- Record info for the 'Player Position' objective, so we don't mess it up later
503 seen[self[0].obj] = self[0].pos
505 -- Record the objective that we chose to put first
506 local obj = info.obj
507 indexes[obj] = 1
508 seen[obj] = info.pos -- Save its position, because we don't want to clobber any of the info objects yet
509 prev_pos = info.pos
511 last = links[obj]
512 links[obj] = nil
514 -- Scan the rest of the places in the route, and pick objectives to go there
515 for index = 2,len do
516 info = nil
517 local c = 1
519 -- Scan the list of scores from the prior objective
520 for i, weight in pairs(last) do
521 if links[i.obj] then
522 -- Only consider an item if we have scores for that item
523 local w
524 local pos = i.pos
525 if prev_pos[1].c == pos[1].c then
526 local u, v = prev_pos[3]-pos[3], prev_pos[4]-pos[4]
527 w = math.sqrt(u*u+v*v)
528 else
529 w = 500
532 w = weight * math.random() / w
534 if not info or w > r then
535 info, r = i, w
538 c = c + 1
541 -- In case we had no valid scores, scan the remaining objectives and score by distance
542 if not info then
543 for obj in pairs(links) do
544 local pos = obj.pos
545 local w
546 if prev_pos[1] == pos[1] then
547 -- Same zone
548 local u, v = prev_pos[3]-pos[3], prev_pos[4]-pos[4]
549 w = math.sqrt(u*u+v*v)
550 elseif prev_pos[1].c == pos[1].c then
551 -- Same continent. -- Assume twices as long.
552 local u, v = prev_pos[3]-pos[3], prev_pos[4]-pos[4]
553 w = 2*math.sqrt(u*u+v*v)
554 else
555 -- Different continent. Assume fixed value of 5 minutes.
556 w = 300
559 w = math.random() / w
561 if not info or w > r then
562 local route = next(route_map)
563 info, r = route[route.index[obj]], w
567 assert(info)
570 -- Add the selected item to the route
571 obj = info.obj
572 indexes[obj] = index
573 prev_pos = info.pos
574 seen[obj] = prev_pos
575 assert(info.obj == obj)
577 -- 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
578 local link = links[obj]
579 links[obj] = nil
580 QuestHelper:ReleaseTable(last)
581 last = link
583 QH_Timeslice_Yield()
586 -- Clean up the last table
587 QuestHelper:ReleaseTable(last)
589 -- Now that we've got our objectives lined up, fill in the info objects with the positions we saved
590 for obj, i in pairs(indexes) do
591 assert(seen[obj])
592 local info = self[i]
593 info.obj, info.pos = obj, seen[obj]
594 seen[obj] = nil
597 -- Now randomly randomize some of the route (aka mutation)
598 while math.random() > 0.3 do
599 local l = math.floor(math.random()^1.6*(len-1))+1
600 local i = math.random(1, len-l)
601 local j = i+l
603 -- Reverse a chunk of the route
604 for k = 0, j-i-1 do
605 self[i+k], self[j-k] = self[j-k], self[i+k]
609 -- But wait, after all that some objectives might violate the rules. Make sure the route follows
610 -- the rules.
612 -- 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.
613 -- Since the entire thing is internally inconsistent anyway, we're just gonna try to consistentize it.
615 local valid_items = {}
616 for k, v in ipairs(self) do
617 valid_items[v.obj] = true
619 for k, v in ipairs(self) do
620 for b in pairs(v.obj.before) do
621 if valid_items[b] then
622 b.after[v.obj] = true
625 for a in pairs(v.obj.after) do
626 if valid_items[a] then
627 a.before[v.obj] = true
632 -- Because priorities might have been changed in here, we next make absolutely sure we have up-to-date priorities.
633 for k, v in ipairs(self) do
634 CalcObjectivePriority(v.obj)
637 -- Have I mentioned I hate this codebase yet?
638 -- Because I do.
639 -- Just, you know.
640 -- FYI.
642 local invalid = true
643 local invalid_passes = 0
644 --local output_strings = {}
645 while invalid do
647 invalid_passes = invalid_passes + 1
649 --[[if invalid_passes >= 100 then
650 for k, v in pairs(output_strings) do
651 QuestHelper:TextOut(v)
653 end]]
655 if invalid_passes >= 100 then
656 -- ugh
657 QuestHelper.mutation_passes_exceeded = true
658 break
660 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
662 invalid = false
663 local i = 1
664 --[[for i = 1, #self do
665 local mn, mx = self:findObjectiveRange(self[i].obj, invalid_passes)
666 table.insert(output_strings, string.format("%d is mn mx %d %d (%s)", i, mn, mx, self[i].obj.obj))
667 end]]
668 while i <= #self do
669 -- Make sure all the objectives have valid positions in the list.
670 local info = self[i]
671 local mn, mx, tabi = self:findObjectiveRange(info.obj, invalid_passes)
672 --if invalid_passes > 90 then for k, v in pairs(tabi) do table.insert(output_strings, v) end end
673 if i < mn then
674 -- In theory, 'i' shouldn't be increased here, as the next
675 -- element will be shifted down into the current position.
677 -- However, it is possible for an infinite loop to be created
678 -- by this, with a small range of objectives constantly
679 -- being shifted.
681 -- So, I mark the route as invalid and go through it another time.
682 -- It's probably still possible to get into an infinite loop,
683 -- but it seems much less likely.
685 table.insert(self, mn, info)
686 table.remove(self, i)
687 invalid = true
688 --table.insert(output_strings, string.format("shifting %d into %d", i, mn))
689 elseif i > mx then
690 table.remove(self, i)
691 table.insert(self, mx, info)
692 invalid = true
693 --table.insert(output_strings, string.format("shifting %d into %d", i, mx))
695 i = i + 1
697 --table.insert(output_strings, "pass done")
700 -- Now that we've chosen a route, re-calculate the cost of each leg of the route
701 local distance = 0
702 local prev_info = self[0]
703 local next_info = self[1]
704 local prev_pos = prev_info.pos
705 local next_pos = next_info.pos
706 assert(prev_pos)
707 assert(next_pos)
709 QH_Timeslice_Yield()
711 for i = 1, len-1 do
712 local d1, d2
713 local pos
714 local info = next_info
715 next_info = self[i+1]
716 next_pos = next_info.pos
718 indexes[info.obj] = i
720 d1, d2, pos = info.obj:TravelTime2(prev_pos, next_pos, prev_info.no_cache)
721 assert(pos)
722 QH_Timeslice_Yield()
724 prev_info.len = d1
725 info.len = d2
726 info.pos = pos
727 distance = distance + d1
729 prev_info = info
730 prev_pos = pos
733 self.distance = distance + prev_info.len
735 indexes[self[len].obj] = len
736 self[len].len = 0
738 assert(self:sanity())
741 function Route:pathResetBegin()
742 for i, info in ipairs(self) do
743 local pos = info.pos
744 info[1], info[2], info[3] = pos[1].c, pos[3], pos[4]
748 function Route:pathResetEnd()
749 for i, info in ipairs(self) do
750 -- Try to find a new position for this objective, near where we had it originally.
751 local p, d = nil, 0
753 local a, b, c = info[1], info[2], info[3]
755 for z, pl in pairs(info.obj.p) do
756 for i, point in ipairs(pl) do
757 if a == point[1].c then
758 local x, y = b-point[3], c-point[4]
759 local d2 = x*x+y*y
760 if not p or d2 < d then
761 p, d = point, d2
767 -- Assuming that there will still be positions on the same continents as before, i.e., locations are only added and not removed.
768 assert(p)
770 info.pos = p
773 self:recalculateDistances()
776 function Route:recalculateDistances()
778 self.distance = 0
779 for i = 0, #self-1 do
780 self[i].len = QuestHelper:ComputeTravelTime(self[i].pos, self[i+1].pos)
781 self.distance = self.distance + self[i].len
785 function Routing:RoutingSetup()
786 Routing.map_walker = self.qh:CreateWorldMapWalker()
787 Routing.add_swap = {}
788 Routing.routes = {}
790 local routes = Routing.routes
791 local pos = QuestHelper.pos
792 local PlayerObjective = self.qh:NewObjectiveObject() -- Pseudo-objective which reflects player's current position. Always at index 0 of each route.
793 PlayerObjective.pos = pos
794 PlayerObjective.cat = "loc" -- A special case of a location
795 PlayerObjective.obj = "Player's current position" -- Player shouldn't see this, so no need to localize
796 PlayerObjective.icon_id = 6 -- Don't think we'll need these; just filling them in for completeness
797 PlayerObjective.o = {pos=pos}
798 PlayerObjective.fb = {}
800 for i = 1,15 do -- Create some empty routes to use for our population.
801 local new_rt = { index={ [PlayerObjective]=0 },
802 distance=0,
803 [0]={ obj=PlayerObjective, pos=pos, len=0, no_cache=true }, -- Player's current position is always objective #0
804 id=i -- So I can keep track of which route is which; only for debugging.
806 setmetatable(new_rt, Route)
807 routes[new_rt] = true
810 -- All the routes are the same right now, so it doesn't matter which we're considering the best.
811 self.best_route = next(routes)
812 self.recheck_position = 1
816 function Routing:RouteUpdateRoutine()
817 local qh = QuestHelper
818 local map_walker = Routing.map_walker
819 local minimap_dodad = qh.minimap_dodad
821 local route = qh.route
822 local to_add, to_remove, add_swap = qh.to_add, qh.to_remove, self.add_swap
824 local routes = self.routes
825 local pos = qh.pos
827 local best_route = self.best_route
829 local last_cache_clear = GetTime()
831 ------ EVIL HACK OF DEBUG
833 if false then
834 while GetTime() < last_cache_clear + 5 do
835 coroutine.yield()
838 if qh.target then
839 -- We know the player will be at the target location at target_time, so fudge the numbers
840 -- to pretend we're traveling there.
842 pos[1], pos[3], pos[4] = qh.target[1], qh.target[3], qh.target[4]
843 local extra_time = math.max(0, qh.target_time-time())
844 for i, t in ipairs(qh.target[2]) do
845 pos[2][i] = t+extra_time
847 else
848 if not pos[1] -- Need a valid position, in case the player was dead when they loaded the game.
849 or not UnitIsDeadOrGhost("player") then
850 -- Don't update the player's position if they're dead, assume they'll be returning to their corpse.
851 pos[3], pos[4] = qh.Astrolabe:TranslateWorldMapPosition(qh.c, qh.z, qh.x, qh.y, qh.c, 0)
852 assert(pos[3])
853 assert(pos[4])
854 pos[1] = qh.zone_nodes[qh.i]
855 pos[3], pos[4] = pos[3] * qh.continent_scales_x[qh.c], pos[4] * qh.continent_scales_y[qh.c]
857 for i, n in ipairs(pos[1]) do
858 if not n.x then
859 for i, j in pairs(n) do qh:TextOut("[%q]=%s %s", i, type(j), tostring(j) or "???") end
860 assert(false)
863 local a, b = n.x-pos[3], n.y-pos[4]
864 pos[2][i] = math.sqrt(a*a+b*b)
869 local obj = next(to_add)
871 QuestHelper:TextOut("dbghack")
872 QuestHelper:TextOut(QuestHelper:StringizeTable(to_add))
873 obj.filter_zone = false
874 obj.filter_watched = false
875 QuestHelper:TextOut(QuestHelper:StringizeTable(obj))
876 QuestHelper:TextOut(tostring(obj:Known()))
877 obj:PrepareRouting()
878 QuestHelper:TextOut(QuestHelper:StringizeTable(obj))
880 QuestHelper:TextOut("o")
881 QuestHelper:TextOut(QuestHelper:StringizeTable(obj.o))
883 QuestHelper:TextOut("pp")
884 QuestHelper:TextOut(QuestHelper:StringizeTable(obj.pos))
885 QuestHelper:TextOut(QuestHelper:StringizeTable(obj.p))
886 QuestHelper:TextOut(tostring(obj:Known()))
888 local index = best_route:addObjectiveBest(obj)
889 obj.pos = best_route[index].pos
891 QuestHelper:TextOut(QuestHelper:StringizeTable(obj.pos))
893 QuestHelper:TextOut(qh:ComputeTravelTime(pos, obj.pos))
895 Error()
898 ------ EVIL HACK OF DEBUG
900 while true do
901 -- Clear caches out a bit
902 if GetTime() + 15 >= last_cache_clear then
903 qh:CacheCleanup()
904 last_cache_clear = GetTime()
907 -- Update the player's position data.
908 if qh.target then
909 -- We know the player will be at the target location at target_time, so fudge the numbers
910 -- to pretend we're traveling there.
912 pos[1], pos[3], pos[4] = qh.target[1], qh.target[3], qh.target[4]
913 local extra_time = math.max(0, qh.target_time-time())
914 for i, t in ipairs(qh.target[2]) do
915 pos[2][i] = t+extra_time
917 else
918 if not pos[1] -- Need a valid position, in case the player was dead when they loaded the game.
919 or not UnitIsDeadOrGhost("player") then
920 -- Don't update the player's position if they're dead, assume they'll be returning to their corpse.
921 pos[3], pos[4] = qh.Astrolabe:TranslateWorldMapPosition(qh.c, qh.z, qh.x, qh.y, qh.c, 0)
922 assert(pos[3])
923 assert(pos[4])
924 pos[1] = qh.zone_nodes[qh.i]
925 pos[3], pos[4] = pos[3] * qh.continent_scales_x[qh.c], pos[4] * qh.continent_scales_y[qh.c]
927 for i, n in ipairs(pos[1]) do
928 if not n.x then
929 for i, j in pairs(n) do qh:TextOut("[%q]=%s %s", i, type(j), tostring(j) or "???") end
930 assert(false)
933 local a, b = n.x-pos[3], n.y-pos[4]
934 pos[2][i] = math.sqrt(a*a+b*b)
939 local changed = false
941 if #route > 0 then
942 if self.recheck_position > #route then self.recheck_position = 1 end
943 local o = route[self.recheck_position]
945 assert(o.zones)
946 o.filter_zone = o.zones[pos[1].i] == nil
947 o.filter_watched = not o:IsWatched()
949 if not o:Known() then
950 -- Objective was probably made to depend on an objective that we don't know about yet.
951 -- We add it to both lists, because although we need to remove it, we need it added again when we can.
952 -- This creates an inconsistancy, but it'll get fixed in the removal loop before anything has a chance to
953 -- explode from it.
955 to_remove[o] = true
956 to_add[o] = true
957 else
958 if o.swap_before then
959 qh:ReleaseTable(o.before)
960 o.before = o.swap_before
961 o.swap_before = nil
964 if o.swap_after then
965 qh:ReleaseTable(o.after)
966 o.after = o.swap_after
967 o.swap_after = nil
970 if o.is_sharing ~= o.want_share then
971 o.is_sharing = o.want_share
973 if o.want_share then
974 qh:DoShareObjective(o)
975 else
976 qh:DoUnshareObjective(o)
980 CalcObjectivePriority(o)
982 -- Make sure the objective in best_route is still in a valid position.
983 -- Won't worry about other routes, they should forced into valid configurations by breeding.
985 -- 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.
986 local aobt = {}
987 setmetatable(aobt, getmetatable(best_route))
988 for k, v in pairs(best_route) do
989 aobt[k] = v
991 aobt.index = {}
992 for k, v in ipairs(best_route) do
993 -- arglbargl
994 aobt[k] = QuestHelper:CreateTable("AOBT idiocy")
995 for t, q in pairs(v) do
996 aobt[k][t] = q
998 aobt.index[aobt[k].obj] = k
1000 if aobt[0] then
1001 aobt[0] = QuestHelper:CreateTable("AOBT idiocy")
1002 for t, q in pairs(best_route[0]) do
1003 aobt[0][t] = q
1006 -- Actually duplicating a route is irritatingly hard (this is another thing which will be fixed eventually dammit)
1008 assert(aobt:sanity())
1009 assert(best_route:sanity())
1011 local old_distance, old_index = best_route.distance, best_route:removeObjective(o)
1012 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
1013 local new_index, sanityfixed = best_route:addObjectiveBest(o, old_index, old_distance)
1014 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
1015 -- 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
1017 if new_real_distance < old_real_distance or sanityfixed then -- More of the temporary hack
1018 -- If we're using the new path . . .
1020 if old_index > new_index then
1021 old_index, new_index = new_index, old_index
1024 for i = math.max(1, old_index-1), new_index do
1025 local info = best_route[i]
1026 local obj = info.obj
1027 obj.pos = info.pos
1028 route[i] = obj
1032 if old_index ~= new_index then
1033 if old_index == 1 then
1034 minimap_dodad:SetObjective(route[1])
1037 changed = true
1040 -- . . . release our backup path
1041 for k, v in ipairs(aobt) do QuestHelper:ReleaseTable(v) end
1042 QuestHelper:ReleaseTable(aobt[0])
1043 else -- hack (everything in this conditional besides the above chunk is a horrible hack)
1044 -- If we're using the old path . . .
1045 -- . . . release the *internals* of the new path, then copy everything over. Eugh.
1046 for k, v in ipairs(best_route) do QuestHelper:ReleaseTable(v) end
1047 QuestHelper:ReleaseTable(best_route[0])
1048 while #best_route > 0 do table.remove(best_route) end
1049 for k, v in pairs(aobt) do best_route[k] = v end -- hack
1050 setmetatable(aobt, Route)
1051 assert(best_route:sanity())
1052 end -- hack
1054 -- 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
1055 -- 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*
1056 --if old_index == new_index then
1057 -- We don't advance recheck_position unless the node doesn't get moved.
1058 -- TODO: As the this code is apparently bugged, it's gotten into an infinite loop of constantly swapping
1059 -- and hence never advancing. As a work around for now, we'll always advance.
1060 --else
1061 self.recheck_position = self.recheck_position + 1
1066 -- Remove any waypoints if needed.
1067 while true do
1068 local obj = next(to_remove)
1069 if not obj then break end
1070 to_remove[obj] = nil
1072 if obj.is_sharing then
1073 obj.is_sharing = false
1074 qh:DoUnshareObjective(obj)
1077 for r in pairs(routes) do
1078 if r == best_route then
1079 local index = r:removeObjective(obj)
1080 table.remove(route, index)
1081 if index == 1 then
1082 minimap_dodad:SetObjective(route[1])
1084 else
1085 r:removeObjective(obj)
1089 obj:DoneRouting()
1091 changed = true
1094 -- Add any waypoints if needed
1095 while true do
1096 local obj = next(to_add)
1097 if not obj then break end
1098 to_add[obj] = nil
1100 obj.filter_zone = obj.zones and obj.zones[pos[1].i] == nil
1101 obj.filter_watched = not obj:IsWatched()
1103 if obj:Known() then
1104 obj:PrepareRouting()
1106 obj.filter_zone = obj.zones[pos[1].i] == nil
1108 if obj.filter_zone and QuestHelper_Pref.filter_zone then
1109 -- Not going to add it, wrong zone.
1110 obj:DoneRouting()
1111 add_swap[obj] = true
1112 else
1113 if not obj.is_sharing and obj.want_share then
1114 obj.is_sharing = true
1115 qh:DoShareObjective(obj)
1118 CalcObjectivePriority(obj)
1120 for r in pairs(routes) do
1121 if r == best_route then
1122 local index = r:addObjectiveBest(obj)
1123 obj.pos = r[index].pos
1124 table.insert(route, index, obj)
1125 if index == 1 then
1126 minimap_dodad:SetObjective(route[1])
1128 else
1129 r:addObjectiveFast(obj)
1133 changed = true
1135 else
1136 add_swap[obj] = true
1140 for obj in pairs(add_swap) do
1141 -- If one of the objectives we were considering adding was removed, it would be in both lists.
1142 -- That would be bad. We can't remove it because we haven't actually added it yet, so
1143 -- handle that special case here.
1144 if to_remove[obj] then
1145 to_remove[obj] = nil
1146 to_add[obj] = nil
1147 add_swap[obj] = nil
1151 to_add, add_swap = add_swap, to_add
1152 qh.to_add = to_add
1153 self.add_swap = add_swap
1155 if #best_route > 1 then
1156 -- If there is 2 or more objectives, randomly combine routes to (hopefully) create something better than we had before.
1158 -- Calculate best_route first, so that if other routes are identical, we don't risk swapping with them and
1159 -- updating the map_walker.
1160 local best, max_fitness = best_route, 1/(qh:ComputeTravelTime(pos, best_route[1].pos) + best_route.distance)
1161 best_route.fitness = max_fitness
1163 QH_Timeslice_Yield()
1165 for r in pairs(routes) do
1166 if r ~= best_route then
1167 local fit = 1/(qh:ComputeTravelTime(pos, r[1].pos)+r.distance)
1168 r.fitness = fit
1169 if fit > max_fitness then
1170 best, max_fitness = r, fit
1172 QH_Timeslice_Yield()
1176 local to_breed, score
1178 for r in pairs(routes) do
1179 if r ~= best then
1180 local s = math.random()*r.fitness
1181 if not to_breed or s < score then
1182 to_breed, score = r, s
1187 to_breed:breed(routes)
1189 if 1/(qh:ComputeTravelTime(pos, to_breed[1].pos)+to_breed.distance+improve_margin) > max_fitness then
1190 best = to_breed
1193 QH_Timeslice_Yield()
1195 if best ~= best_route then
1197 assert(best:sanity())
1198 assert(best_route:sanity())
1200 best_route = best
1201 self.best_route = best_route
1203 for i, info in ipairs(best) do
1204 local obj = info.obj
1205 obj.pos = info.pos
1206 route[i] = obj
1209 minimap_dodad:SetObjective(route[1])
1211 changed = true
1215 if qh.defered_flight_times then
1216 qh:buildFlightTimes()
1217 qh.defered_flight_times = false
1218 assert(qh.defered_graph_reset)
1221 if qh.defered_graph_reset then
1222 QH_Timeslice_Yield()
1224 for r in pairs(routes) do
1225 r:pathResetBegin()
1228 qh.graph_in_limbo = true
1229 qh:ResetPathing()
1230 qh.graph_in_limbo = false
1231 qh.defered_graph_reset = false
1233 for r in pairs(routes) do
1234 r:pathResetEnd()
1237 for i, info in ipairs(best_route) do
1238 local obj = info.obj
1239 obj.pos = info.pos
1240 route[i] = obj
1242 best_route:recalculateDistances()
1244 minimap_dodad:SetObjective(route[1])
1246 QuestHelper:SetTargetLocationRecalculate()
1248 for r in pairs(routes) do
1249 assert(r:sanity())
1251 best_route:sanity()
1253 QH_Timeslice_Yield()
1256 if changed then
1257 map_walker:RouteChanged()
1260 assert(#route == #best_route)
1262 -- temporary hack to cause more errors
1263 --qh.defered_graph_reset = true
1264 --qh.defered_flight_times = true
1266 QH_Timeslice_Yield()
1270 function Routing:Initialize()
1271 self:RoutingSetup()
1273 QH_Timeslice_Add(function() Routing:RouteUpdateRoutine() end, 0, "routing")
1274 QH_Timeslice_Toggle("routing", false)
1276 --[[
1277 if coroutine.coco then
1278 -- coco allows yielding across c boundries, which allows me to use xpcall to get
1279 -- stack traces for coroutines without calls to yield resulting in thermal nuclear meltdown.
1281 -- This isn't part of WoW, I was using it in my driver program: Development/routetest
1283 update_route = coroutine.create(
1284 function()
1285 local state, err = xpcall(
1286 function()
1287 Routing:RouteUpdateRoutine()
1288 end,
1289 function (err)
1290 if debugstack then
1291 return tostring(err).."\n"..debugstack(2)
1292 else
1293 return debug.traceback(tostring(err), 2)
1295 end)
1297 if not state then
1298 error(err, 0)
1300 end)
1301 else
1302 update_route = coroutine.create(function() Routing:RouteUpdateRoutine() end)