remove extra +x
[QuestHelper.git] / routing.lua
blobccf423b11960a09b0ad30c6a6698a1d9d953a55e
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
13 local route_pass = 0
14 local coroutine_running = false
15 local coroutine_stop_time = 0
17 function QuestHelper:yieldIfNeeded()
18 if coroutine_running then
19 -- Check if we've run our alotted time
20 if GetTime() > coroutine_stop_time then
21 -- As a safety, reset stop time to 0. If somehow we fail to set it next time,
22 -- we'll be sure to yield promptly.
23 coroutine_stop_time = 0
24 coroutine.yield()
25 end
26 end
27 end
29 local function CalcObjectivePriority(obj)
30 local priority = obj.priority
32 for o in pairs(obj.before) do
33 if o.watched then
34 priority = math.min(priority, CalcObjectivePriority(o))
35 end
36 end
38 obj.real_priority = priority
39 return priority
40 end
42 local Route = {}
43 Route.__index = Route
44 Routing.Route = Route -- Make it available as a member
46 -- This should pass on all routes. If it does not, *things need to be fixed*. No, commenting tests out is not an adequate response - this *must* pass. Eventually this will get rolled into the unsucky Route class.
47 function Route:sanity()
49 local assert = assert
51 if QuestHelper.Error then
52 assert = function(a, b)
53 if not a then
54 QuestHelper:TextOut("Route:sanity(): id="..self.id.."; best_route="..Routing.best_route.id)
55 QuestHelper:Error(b or "Assertion Failed")
56 end
57 end
58 end
60 local l = 0
62 --QuestHelper:TextOut(QuestHelper:StringizeTable(self))
63 for i = 0,#self-1 do
64 --QuestHelper:TextOut(tostring(i))
65 --QuestHelper:TextOut(QuestHelper:StringizeTable(self[i]))
66 --QuestHelper:TextOut(tostring(self[i].len))
67 assert(self[i].len)
68 l = l + self[i].len
69 end
71 --QuestHelper:TextOut("sd: "..l.." rd: "..self.distance)
72 assert(math.abs(l-self.distance) < 0.0001, string.format("compare %f vs %f", l, self.distance))
74 for i, info in ipairs(self) do
75 assert(self.index[info.obj] == i)
76 assert(info.pos)
77 end
79 for obj, i in pairs(self.index) do
80 assert(self[i].obj == obj)
81 end
83 for i = 1, #self-1 do
84 local l = QuestHelper:ComputeTravelTime(self[i].pos, self[i+1].pos, true)
85 assert(math.abs(l-self[i].len) < 0.0001, "Compare at "..i..": "..l.." vs "..self[i].len)
86 end
88 return true
89 end
91 function Route:findObjectiveRange(obj, passes)
93 --[[
94 lines = {}
95 table.insert(lines, string.format("QuestHelper objectiverange for %s (pri %d)", obj.obj, obj.real_priority))
96 for i = 1, #self do
97 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))
98 end]]
100 local mn = #self
101 while mn >= 1 do
102 if obj.real_priority > self[mn].obj.real_priority or obj.after[self[mn].obj] or self[mn].obj.before[obj] then break end
103 mn = mn - 1
106 mn = mn + 1 -- we went too far, actually
108 local mx = 1
109 while mx < #self + 1 do
110 if obj.real_priority < self[mx].obj.real_priority or obj.before[self[mx].obj] or self[mx].obj.after[obj] then break end
111 mx = mx + 1
114 --table.insert(lines, string.format("temp results is %d %d", mn, mx))
116 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.
117 local mid = math.ceil((mx + mn) / 2)
118 mx = mid
119 mn = mid
122 --table.insert(lines, string.format("overall: %d %d", mn, mx))
124 --[[
125 if passes and passes > 90 then
126 for k, v in pairs(lines) do QuestHelper:TextOut(v) end
127 QuestHelper:TextOut(string.format("overall: %d %d", mn, mx))
131 --[[
132 local omn, omx = self:OldFindObjectiveRange(obj)
134 if mn ~= omn or mx ~= omx then
135 for k, v in pairs(lines) do QuestHelper:TextOut(v) end
136 QuestHelper:TextOut(string.format("overall: %d %d vs %d %d", mn, mx, omn, omx))
137 lolcrash = (lolcrash or 0) + 1
138 end]]
140 return mn, mx, lines
144 function Route:addObjectiveFast(obj)
145 assert(self:sanity())
146 local indexes = self.index
147 local len = #self
148 local info = QuestHelper:CreateTable()
149 assert(not indexes[obj])
151 info.obj = obj
153 if len == 0 then
154 local d
155 self[1] = info
156 indexes[obj] = 1
157 d, info.pos = obj:TravelTime(self[0].pos, true)
158 self[0].len = d
159 self.distance = d
160 return 1
163 local player_pos = QuestHelper.pos
164 local pos = obj.location
165 local c, x, y = pos[1].c, pos[3], pos[4]
167 local mn, mx = self:findObjectiveRange(obj)
168 local index, distsqr
170 for i = mn, math.min(mx, len) do
171 local p = self[i].pos
172 if c == p[1].c then
173 local u, v = p[3]-x, p[4]-y
174 local d2 = u*u+v*v
175 if not index or d2 < distsqr then
176 index, distsqr = i, d2
181 if not index then
182 -- No nodes with the same continent already.
183 -- If the same continent as the player, add to start of list, otherwise add to end of the list.
184 index = c == player_pos[1].c and mx or mx
187 -- The next question, do I insert at that point, or do I insert after it?
188 if index ~= mx and index <= len then
189 local p1 = self[index].pos
191 if p1[1].c == c then
192 local p0
194 if index == 1 then
195 p0 = player_pos
196 else
197 p0 = self[index-1].pos
200 local oldstart, newstart
202 if p0[1].c == c then
203 local u, v = p0[3]-x, p0[4]-y
204 newstart = math.sqrt(u*u+v*v)
205 u, v = p0[3]-p1[3], p0[4]-p1[4]
206 oldstart = math.sqrt(u*u+v*v)
207 else
208 newstart = 0
209 oldstart = 0
212 local p2
213 if index ~= len then
214 p2 = self[index+1].pos
217 local oldend, newend
218 if p2 and p2[1].c == c then
219 local u, v = p2[3]-x, p2[4]-y
220 newend = math.sqrt(u*u+v*v)
221 u, v = p2[3]-p1[3], p2[4]-p1[4]
222 oldend = math.sqrt(u*u+v*v)
223 else
224 newend = 0
225 oldend = 0
228 if oldstart+newend < newstart+oldend then
229 index = index + 1
235 QuestHelper:yieldIfNeeded((mn-mx+3)*0.05) -- The above checks don't require much effort.
237 if index > len then
238 local previnfo = self[index-1]
239 assert(previnfo)
240 local d
241 d, info.pos = obj:TravelTime(previnfo.pos)
242 assert(info.pos)
243 QuestHelper:yieldIfNeeded(0.5)
244 previnfo.len = d
245 self.distance = self.distance + d
246 else
247 local d1, d2
249 local previnfo = self[index-1]
250 d1, d2, info.pos = obj:TravelTime2(previnfo.pos, self[index].pos, previnfo.no_cache)
252 info.len = d2
253 self.distance = self.distance + (d1 - previnfo.len + d2)
254 previnfo.len = d1
256 QuestHelper:yieldIfNeeded(1)
259 -- Finally, insert the objective.
260 table.insert(self, index, info)
261 indexes[obj] = index
263 -- Fix indexes of shifted elements.
264 for i = index+1,len+1 do
265 local obj = self[i].obj
266 assert(indexes[obj] == i-1)
267 indexes[obj] = i
270 assert(self:sanity())
272 return index
275 function Route:addObjectiveBest(obj, old_index, old_distance)
276 assert(self:sanity())
278 local indexes = self.index
279 local len = #self
280 local info = QuestHelper:CreateTable()
281 assert(not indexes[obj])
283 info.obj = obj
285 if len == 0 then
286 indexes[obj] = 1
287 self.distance, info.pos = obj:TravelTime(self[0].pos, true)
288 info.len = 0
289 self[0].len = self.distance
290 self[1] = info
291 return 1
294 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.)
296 local best_index, best_delta, best_d1, best_d2, best_p
297 local no_cache, prev_pos, prev_len
298 local mn, mx = self:findObjectiveRange(obj)
300 if old_index and mn <= old_index and old_index <= mx then
301 -- We're trying to re-evaluate it, and it could remain in the same place.
302 -- So that place is our starting best known place.
303 best_index, best_delta = old_index, old_distance - self.distance
305 if best_delta < 0 then
306 -- Somehow, removing the objective actually made the route worse...
307 -- Just re-figure things from scratch.
308 -- TODO: THIS SHOULD NEVER HAPPEN dear god find out what's causing this and stop it
309 --QuestHelper:TextOut("made route worse wtf")
310 best_index, best_delta = nil, nil
314 local pinfo = self[mn-1]
315 no_cache, prev_pos, prev_len = pinfo.no_cache, pinfo.pos, pinfo.len
317 for i = mn, math.min(mx, len) do
318 assert(prev_pos == self[i-1].pos)
320 local info = self[i]
321 local pos = info.pos
323 local d1, d2, p = obj:TravelTime2(prev_pos, pos, no_cache)
325 QuestHelper:yieldIfNeeded(1)
327 local delta = d1 + d2 - prev_len
329 if not best_index or ((delta + improve_margin) < best_delta) or ((i == best_index) and not best_d1) then
330 -- Best so far is:
331 -- * First item we reach
332 -- * Better than previous best
333 -- * We're looking at our best already. But we just got here; how could this be best?
334 -- If this was our prior location and we didn't find anything better earlier in the route,
335 -- that's how. Save the specifics, 'cause we didn't compute them when setting up.
336 best_index, best_delta, best_d1, best_d2, best_p = i, delta, d1, d2, p
339 prev_pos = pos
340 prev_len = info.len
341 no_cache = false
344 if mx > len then
345 assert(mx == len+1)
346 assert(prev_pos == self[len].pos)
347 local delta, p = obj:TravelTime(prev_pos, no_cache)
349 QuestHelper:yieldIfNeeded(.5)
351 if not best_index or ((delta + improve_margin) < best_delta) or ((mx == best_index) and not best_d1) then
352 info.pos = p
353 info.len = 0
354 self[len].len = delta
355 self.distance = self.distance + delta
356 table.insert(self, info)
357 indexes[obj] = mx
359 assert(self:sanity())
361 return mx
365 info.pos = best_p
366 info.len = best_d2
368 local pinfo = self[best_index-1]
369 self.distance = self.distance + (best_d1 - pinfo.len + best_d2)
370 pinfo.len = best_d1
372 table.insert(self, best_index, info)
374 -- QuestHelper:Assert(math.abs(QuestHelper:ComputeTravelTime(self[best_index-1].pos, self[best_index].pos) - self[best_index-1].len) < 0.0001, "aaaaargh")
375 --[[ -- I don't think this is necessary now that TravelTime2 explicitly does this internally, but I'm keeping it anyway.
376 self.distance = self.distance - self[best_index-1].len
377 self[best_index-1].len = QuestHelper:ComputeTravelTime(self[best_index-1].pos, self[best_index].pos, true)
378 self.distance = self.distance + self[best_index-1].len
381 indexes[obj] = best_index
383 for i = best_index+1,len+1 do
384 assert(indexes[self[i].obj] == i-1)
385 indexes[self[i].obj] = i
388 if not old_index or (mn > old_index or old_index > mx) and mn <= best_index and best_index <= mx then
389 -- 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
390 sanityfixed = 1
393 assert(self:sanity())
395 return best_index, sanityfixed
398 function Route:removeObjective(obj)
399 assert(self:sanity())
401 local indexes = self.index
402 local index = indexes[obj]
403 local old_distance = self.distance
405 assert(index)
406 local info = self[index]
407 assert(info.obj == obj)
409 --[[
410 Removing end item: subtract last distance, nothing to recalculate
411 Removing other item: recalculate location of next objective, between prior position and objective after next
412 Special case: if there is no location after next, just recalc location of next objective
413 --]]
414 if index == #self then
415 self.distance = self.distance - self[index-1].len
416 self[index-1].len = 0
417 else
418 local pinfo = self[index-1]
419 local info1 = self[index+1]
420 local info2 = self[index+2]
421 local no_cache = (index == 1)
423 local d1, d2
425 if info2 then
426 d1, d2, info1.pos = info1.obj:TravelTime2(pinfo.pos, info2.pos, no_cache)
427 QuestHelper:yieldIfNeeded(1)
428 self.distance = self.distance - pinfo.len - info.len - info1.len + d1 + d2
429 info1.len = d2
430 else
431 d1, info1.pos = info1.obj:TravelTime(pinfo.pos, no_cache)
432 QuestHelper:yieldIfNeeded(0.5)
433 self.distance = self.distance - pinfo.len - info.len + d1
436 pinfo.len = d1
439 QuestHelper:ReleaseTable(info)
440 indexes[obj] = nil
441 table.remove(self, index)
443 for i = index,#self do
444 -- Fix indexes of shifted elements.
445 local obj = self[i].obj
446 assert(indexes[obj] == i+1)
447 indexes[obj] = i
450 assert(self:sanity())
451 -- assert(self.distance <= old_distance)
453 return index
456 local links = {}
457 local seen = {}
459 function Route:breed(route_map)
460 local indexes = self.index
461 local len = #self
463 local info
464 local r
466 local prev_pos = QuestHelper.pos
467 assert(self[0].pos == prev_pos)
469 -- Pick which objective goes first, selecting from first objective of each route,
470 -- and scaling by the route's fitness and distance from player, with a random adjustment factor.
471 -- While we're at it, record some data about the fitness of adjacent objectives
472 for route in pairs(route_map) do
473 assert(route:sanity())
474 local fit = route.fitness
475 local pos = route[1].pos
476 local w
478 if prev_pos[1].c == pos[1].c then
479 local u, v = prev_pos[3]-pos[3], prev_pos[4]-pos[4]
480 w = math.sqrt(u*u+v*v)
481 else
482 w = 500
485 w = fit * math.random() / w
487 if not info or w > r then
488 info, r = route[1], w
491 for i = 1,len do
492 local obj = route[i].obj
493 local tbl = links[obj]
494 if not tbl then
495 tbl = QuestHelper:CreateTable()
496 links[obj] = tbl
499 if i ~= 1 then
500 local info = route[i-1]
501 local obj2 = info.obj
502 tbl[info] = (tbl[info] or 0) + fit
505 if i ~= len then
506 local info = route[i+1]
507 local obj2 = info.obj
508 if obj.real_priority <= obj2.real_priority or obj.before[obj2] then
509 tbl[info] = (tbl[info] or 0) + fit
514 QuestHelper:yieldIfNeeded(0.01*len)
517 -- Record info for the 'Player Position' objective, so we don't mess it up later
518 seen[self[0].obj] = self[0].pos
520 -- Record the objective that we chose to put first
521 local obj = info.obj
522 indexes[obj] = 1
523 seen[obj] = info.pos -- Save its position, because we don't want to clobber any of the info objects yet
524 prev_pos = info.pos
526 last = links[obj]
527 links[obj] = nil
529 -- Scan the rest of the places in the route, and pick objectives to go there
530 for index = 2,len do
531 info = nil
532 local c = 1
534 -- Scan the list of scores from the prior objective
535 for i, weight in pairs(last) do
536 if links[i.obj] then
537 -- Only consider an item if we have scores for that item
538 local w
539 local pos = i.pos
540 if prev_pos[1].c == pos[1].c then
541 local u, v = prev_pos[3]-pos[3], prev_pos[4]-pos[4]
542 w = math.sqrt(u*u+v*v)
543 else
544 w = 500
547 w = weight * math.random() / w
549 if not info or w > r then
550 info, r = i, w
553 c = c + 1
556 -- In case we had no valid scores, scan the remaining objectives and score by distance
557 if not info then
558 for obj in pairs(links) do
559 local pos = obj.pos
560 local w
561 if prev_pos[1] == pos[1] then
562 -- Same zone
563 local u, v = prev_pos[3]-pos[3], prev_pos[4]-pos[4]
564 w = math.sqrt(u*u+v*v)
565 elseif prev_pos[1].c == pos[1].c then
566 -- Same continent. -- Assume twices as long.
567 local u, v = prev_pos[3]-pos[3], prev_pos[4]-pos[4]
568 w = 2*math.sqrt(u*u+v*v)
569 else
570 -- Different continent. Assume fixed value of 5 minutes.
571 w = 300
574 w = math.random() / w
576 if not info or w > r then
577 local route = next(route_map)
578 info, r = route[route.index[obj]], w
582 assert(info)
585 -- Add the selected item to the route
586 obj = info.obj
587 indexes[obj] = index
588 prev_pos = info.pos
589 seen[obj] = prev_pos
590 assert(info.obj == obj)
592 -- 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
593 local link = links[obj]
594 links[obj] = nil
595 QuestHelper:ReleaseTable(last)
596 last = link
598 QuestHelper:yieldIfNeeded(0.01*c)
601 -- Clean up the last table
602 QuestHelper:ReleaseTable(last)
604 -- Now that we've got our objectives lined up, fill in the info objects with the positions we saved
605 for obj, i in pairs(indexes) do
606 assert(seen[obj])
607 local info = self[i]
608 info.obj, info.pos = obj, seen[obj]
609 seen[obj] = nil
612 -- Now randomly randomize some of the route (aka mutation)
613 while math.random() > 0.3 do
614 local l = math.floor(math.random()^1.6*(len-1))+1
615 local i = math.random(1, len-l)
616 local j = i+l
618 -- Reverse a chunk of the route
619 for k = 0, j-i-1 do
620 self[i+k], self[j-k] = self[j-k], self[i+k]
624 -- But wait, after all that some objectives might violate the rules. Make sure the route follows
625 -- the rules.
627 -- 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.
628 -- Since the entire thing is internally inconsistent anyway, we're just gonna try to consistentize it.
630 local valid_items = {}
631 for k, v in ipairs(self) do
632 valid_items[v.obj] = true
634 for k, v in ipairs(self) do
635 for b in pairs(v.obj.before) do
636 if valid_items[b] then
637 b.after[v.obj] = true
640 for a in pairs(v.obj.after) do
641 if valid_items[a] then
642 a.before[v.obj] = true
647 -- Because priorities might have been changed in here, we next make absolutely sure we have up-to-date priorities.
648 for k, v in ipairs(self) do
649 CalcObjectivePriority(v.obj)
652 -- Have I mentioned I hate this codebase yet?
653 -- Because I do.
654 -- Just, you know.
655 -- FYI.
657 local invalid = true
658 local invalid_passes = 0
659 --local output_strings = {}
660 while invalid do
662 invalid_passes = invalid_passes + 1
664 --[[if invalid_passes >= 100 then
665 for k, v in pairs(output_strings) do
666 QuestHelper:TextOut(v)
668 end]]
670 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
672 invalid = false
673 local i = 1
674 --[[for i = 1, #self do
675 local mn, mx = self:findObjectiveRange(self[i].obj, invalid_passes)
676 table.insert(output_strings, string.format("%d is mn mx %d %d (%s)", i, mn, mx, self[i].obj.obj))
677 end]]
678 while i <= #self do
679 -- Make sure all the objectives have valid positions in the list.
680 local info = self[i]
681 local mn, mx, tabi = self:findObjectiveRange(info.obj, invalid_passes)
682 --if invalid_passes > 90 then for k, v in pairs(tabi) do table.insert(output_strings, v) end end
683 if i < mn then
684 -- In theory, 'i' shouldn't be increased here, as the next
685 -- element will be shifted down into the current position.
687 -- However, it is possible for an infinite loop to be created
688 -- by this, with a small range of objectives constantly
689 -- being shifted.
691 -- So, I mark the route as invalid and go through it another time.
692 -- It's probably still possible to get into an infinite loop,
693 -- but it seems much less likely.
695 table.insert(self, mn, info)
696 table.remove(self, i)
697 invalid = true
698 --table.insert(output_strings, string.format("shifting %d into %d", i, mn))
699 elseif i > mx then
700 table.remove(self, i)
701 table.insert(self, mx, info)
702 invalid = true
703 --table.insert(output_strings, string.format("shifting %d into %d", i, mx))
705 i = i + 1
707 --table.insert(output_strings, "pass done")
710 -- Now that we've chosen a route, re-calculate the cost of each leg of the route
711 local distance = 0
712 local prev_info = self[0]
713 local next_info = self[1]
714 local prev_pos = prev_info.pos
715 local next_pos = next_info.pos
716 assert(prev_pos)
717 assert(next_pos)
719 QuestHelper:yieldIfNeeded(0.03*len)
721 for i = 1, len-1 do
722 local d1, d2
723 local pos
724 local info = next_info
725 next_info = self[i+1]
726 next_pos = next_info.pos
728 indexes[info.obj] = i
730 d1, d2, pos = info.obj:TravelTime2(prev_pos, next_pos, prev_info.no_cache)
731 assert(pos)
732 QuestHelper:yieldIfNeeded(1)
734 prev_info.len = d1
735 info.len = d2
736 info.pos = pos
737 distance = distance + d1
739 prev_info = info
740 prev_pos = pos
743 self.distance = distance + prev_info.len
745 indexes[self[len].obj] = len
746 self[len].len = 0
748 assert(self:sanity())
751 function Route:pathResetBegin()
752 for i, info in ipairs(self) do
753 local pos = info.pos
754 info[1], info[2], info[3] = pos[1].c, pos[3], pos[4]
758 function Route:pathResetEnd()
759 for i, info in ipairs(self) do
760 -- Try to find a new position for this objective, near where we had it originally.
761 local p, d = nil, 0
763 local a, b, c = info[1], info[2], info[3]
765 for z, pl in pairs(info.obj.p) do
766 for i, point in ipairs(pl) do
767 if a == point[1].c then
768 local x, y = b-point[3], c-point[4]
769 local d2 = x*x+y*y
770 if not p or d2 < d then
771 p, d = point, d2
777 -- Assuming that there will still be positions on the same continents as before, i.e., locations are only added and not removed.
778 assert(p)
780 info.pos = p
783 self:recalculateDistances()
786 function Route:recalculateDistances()
788 self.distance = 0
789 for i = 0, #self-1 do
790 self[i].len = QuestHelper:ComputeTravelTime(self[i].pos, self[i+1].pos)
791 self.distance = self.distance + self[i].len
795 function Routing:RoutingSetup()
796 Routing.map_walker = self.qh:CreateWorldMapWalker()
797 Routing.add_swap = {}
798 Routing.routes = {}
800 local routes = Routing.routes
801 local pos = QuestHelper.pos
802 local PlayerObjective = self.qh:NewObjectiveObject() -- Pseudo-objective which reflects player's current position. Always at index 0 of each route.
803 PlayerObjective.pos = pos
804 PlayerObjective.cat = "loc" -- A special case of a location
805 PlayerObjective.obj = "Player's current position" -- Player shouldn't see this, so no need to localize
806 PlayerObjective.icon_id = 6 -- Don't think we'll need these; just filling them in for completeness
807 PlayerObjective.o = {pos=pos}
808 PlayerObjective.fb = {}
810 for i = 1,15 do -- Create some empty routes to use for our population.
811 local new_rt = { index={ [PlayerObjective]=0 },
812 distance=0,
813 [0]={ obj=PlayerObjective, pos=pos, len=0, no_cache=true }, -- Player's current position is always objective #0
814 id=i -- So I can keep track of which route is which; only for debugging.
816 setmetatable(new_rt, Route)
817 routes[new_rt] = true
820 -- All the routes are the same right now, so it doesn't matter which we're considering the best.
821 self.best_route = next(routes)
822 self.recheck_position = 1
826 function Routing:RouteUpdateRoutine()
827 local qh = QuestHelper
828 local map_walker = Routing.map_walker
829 local minimap_dodad = qh.minimap_dodad
831 local route = qh.route
832 local to_add, to_remove, add_swap = qh.to_add, qh.to_remove, self.add_swap
834 local routes = self.routes
835 local pos = qh.pos
837 local best_route = self.best_route
839 local last_cache_clear = GetTime()
841 ------ EVIL HACK OF DEBUG
843 if false then
844 while GetTime() < last_cache_clear + 5 do
845 coroutine.yield()
848 if qh.target then
849 -- We know the player will be at the target location at target_time, so fudge the numbers
850 -- to pretend we're traveling there.
852 pos[1], pos[3], pos[4] = qh.target[1], qh.target[3], qh.target[4]
853 local extra_time = math.max(0, qh.target_time-time())
854 for i, t in ipairs(qh.target[2]) do
855 pos[2][i] = t+extra_time
857 else
858 if not pos[1] -- Need a valid position, in case the player was dead when they loaded the game.
859 or not UnitIsDeadOrGhost("player") then
860 -- Don't update the player's position if they're dead, assume they'll be returning to their corpse.
861 pos[3], pos[4] = qh.Astrolabe:TranslateWorldMapPosition(qh.c, qh.z, qh.x, qh.y, qh.c, 0)
862 assert(pos[3])
863 assert(pos[4])
864 pos[1] = qh.zone_nodes[qh.i]
865 pos[3], pos[4] = pos[3] * qh.continent_scales_x[qh.c], pos[4] * qh.continent_scales_y[qh.c]
867 for i, n in ipairs(pos[1]) do
868 if not n.x then
869 for i, j in pairs(n) do qh:TextOut("[%q]=%s %s", i, type(j), tostring(j) or "???") end
870 assert(false)
873 local a, b = n.x-pos[3], n.y-pos[4]
874 pos[2][i] = math.sqrt(a*a+b*b)
879 local obj = next(to_add)
881 QuestHelper:TextOut("dbghack")
882 QuestHelper:TextOut(QuestHelper:StringizeTable(to_add))
883 obj.filter_zone = false
884 obj.filter_watched = false
885 QuestHelper:TextOut(QuestHelper:StringizeTable(obj))
886 QuestHelper:TextOut(tostring(obj:Known()))
887 obj:PrepareRouting()
888 QuestHelper:TextOut(QuestHelper:StringizeTable(obj))
890 QuestHelper:TextOut("o")
891 QuestHelper:TextOut(QuestHelper:StringizeTable(obj.o))
893 QuestHelper:TextOut("pp")
894 QuestHelper:TextOut(QuestHelper:StringizeTable(obj.pos))
895 QuestHelper:TextOut(QuestHelper:StringizeTable(obj.p))
896 QuestHelper:TextOut(tostring(obj:Known()))
898 local index = best_route:addObjectiveBest(obj)
899 obj.pos = best_route[index].pos
901 QuestHelper:TextOut(QuestHelper:StringizeTable(obj.pos))
903 QuestHelper:TextOut(qh:ComputeTravelTime(pos, obj.pos))
905 Error()
908 ------ EVIL HACK OF DEBUG
910 while true do
911 -- Clear caches out a bit
912 if GetTime() + 15 >= last_cache_clear then
913 qh:CacheCleanup()
914 last_cache_clear = GetTime()
917 -- Update the player's position data.
918 if qh.target then
919 -- We know the player will be at the target location at target_time, so fudge the numbers
920 -- to pretend we're traveling there.
922 pos[1], pos[3], pos[4] = qh.target[1], qh.target[3], qh.target[4]
923 local extra_time = math.max(0, qh.target_time-time())
924 for i, t in ipairs(qh.target[2]) do
925 pos[2][i] = t+extra_time
927 else
928 if not pos[1] -- Need a valid position, in case the player was dead when they loaded the game.
929 or not UnitIsDeadOrGhost("player") then
930 -- Don't update the player's position if they're dead, assume they'll be returning to their corpse.
931 pos[3], pos[4] = qh.Astrolabe:TranslateWorldMapPosition(qh.c, qh.z, qh.x, qh.y, qh.c, 0)
932 assert(pos[3])
933 assert(pos[4])
934 pos[1] = qh.zone_nodes[qh.i]
935 pos[3], pos[4] = pos[3] * qh.continent_scales_x[qh.c], pos[4] * qh.continent_scales_y[qh.c]
937 for i, n in ipairs(pos[1]) do
938 if not n.x then
939 for i, j in pairs(n) do qh:TextOut("[%q]=%s %s", i, type(j), tostring(j) or "???") end
940 assert(false)
943 local a, b = n.x-pos[3], n.y-pos[4]
944 pos[2][i] = math.sqrt(a*a+b*b)
949 local changed = false
951 if #route > 0 then
952 if self.recheck_position > #route then self.recheck_position = 1 end
953 local o = route[self.recheck_position]
955 assert(o.zones)
956 o.filter_zone = o.zones[pos[1].i] == nil
957 o.filter_watched = not o:IsWatched()
959 if not o:Known() then
960 -- Objective was probably made to depend on an objective that we don't know about yet.
961 -- We add it to both lists, because although we need to remove it, we need it added again when we can.
962 -- This creates an inconsistancy, but it'll get fixed in the removal loop before anything has a chance to
963 -- explode from it.
965 to_remove[o] = true
966 to_add[o] = true
967 else
968 if o.swap_before then
969 qh:ReleaseTable(o.before)
970 o.before = o.swap_before
971 o.swap_before = nil
974 if o.swap_after then
975 qh:ReleaseTable(o.after)
976 o.after = o.swap_after
977 o.swap_after = nil
980 if o.is_sharing ~= o.want_share then
981 o.is_sharing = o.want_share
983 if o.want_share then
984 qh:DoShareObjective(o)
985 else
986 qh:DoUnshareObjective(o)
990 CalcObjectivePriority(o)
992 -- Make sure the objective in best_route is still in a valid position.
993 -- Won't worry about other routes, they should forced into valid configurations by breeding.
995 -- 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.
996 local aobt = {}
997 setmetatable(aobt, getmetatable(best_route))
998 for k, v in pairs(best_route) do
999 aobt[k] = v
1001 aobt.index = {}
1002 for k, v in ipairs(best_route) do
1003 -- arglbargl
1004 aobt[k] = QuestHelper:CreateTable("AOBT idiocy")
1005 for t, q in pairs(v) do
1006 aobt[k][t] = q
1008 aobt.index[aobt[k].obj] = k
1010 if aobt[0] then
1011 aobt[0] = QuestHelper:CreateTable("AOBT idiocy")
1012 for t, q in pairs(best_route[0]) do
1013 aobt[0][t] = q
1016 -- Actually duplicating a route is irritatingly hard (this is another thing which will be fixed eventually dammit)
1018 assert(aobt:sanity())
1019 assert(best_route:sanity())
1021 local old_distance, old_index = best_route.distance, best_route:removeObjective(o)
1022 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
1023 local new_index, sanityfixed = best_route:addObjectiveBest(o, old_index, old_distance)
1024 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
1025 -- 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
1027 if new_real_distance < old_real_distance or sanityfixed then -- More of the temporary hack
1028 -- If we're using the new path . . .
1030 if old_index > new_index then
1031 old_index, new_index = new_index, old_index
1034 for i = math.max(1, old_index-1), new_index do
1035 local info = best_route[i]
1036 local obj = info.obj
1037 obj.pos = info.pos
1038 route[i] = obj
1042 if old_index ~= new_index then
1043 if old_index == 1 then
1044 minimap_dodad:SetObjective(route[1])
1047 changed = true
1050 -- . . . release our backup path
1051 for k, v in ipairs(aobt) do QuestHelper:ReleaseTable(v) end
1052 QuestHelper:ReleaseTable(aobt[0])
1053 else -- hack (everything in this conditional besides the above chunk is a horrible hack)
1054 -- If we're using the old path . . .
1055 -- . . . release the *internals* of the new path, then copy everything over. Eugh.
1056 for k, v in ipairs(best_route) do QuestHelper:ReleaseTable(v) end
1057 QuestHelper:ReleaseTable(best_route[0])
1058 while #best_route > 0 do table.remove(best_route) end
1059 for k, v in pairs(aobt) do best_route[k] = v end -- hack
1060 setmetatable(aobt, Route)
1061 assert(best_route:sanity())
1062 end -- hack
1064 -- 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
1065 -- 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*
1066 --if old_index == new_index then
1067 -- We don't advance recheck_position unless the node doesn't get moved.
1068 -- TODO: As the this code is apparently bugged, it's gotten into an infinite loop of constantly swapping
1069 -- and hence never advancing. As a work around for now, we'll always advance.
1070 --else
1071 self.recheck_position = self.recheck_position + 1
1076 -- Remove any waypoints if needed.
1077 while true do
1078 local obj = next(to_remove)
1079 if not obj then break end
1080 to_remove[obj] = nil
1082 if obj.is_sharing then
1083 obj.is_sharing = false
1084 qh:DoUnshareObjective(obj)
1087 for r in pairs(routes) do
1088 if r == best_route then
1089 local index = r:removeObjective(obj)
1090 table.remove(route, index)
1091 if index == 1 then
1092 minimap_dodad:SetObjective(route[1])
1094 else
1095 r:removeObjective(obj)
1099 obj:DoneRouting()
1101 changed = true
1104 -- Add any waypoints if needed
1105 while true do
1106 local obj = next(to_add)
1107 if not obj then break end
1108 to_add[obj] = nil
1110 obj.filter_zone = obj.zones and obj.zones[pos[1].i] == nil
1111 obj.filter_watched = not obj:IsWatched()
1113 if obj:Known() then
1114 obj:PrepareRouting()
1116 obj.filter_zone = obj.zones[pos[1].i] == nil
1118 if obj.filter_zone and QuestHelper_Pref.filter_zone then
1119 -- Not going to add it, wrong zone.
1120 obj:DoneRouting()
1121 add_swap[obj] = true
1122 else
1123 if not obj.is_sharing and obj.want_share then
1124 obj.is_sharing = true
1125 qh:DoShareObjective(obj)
1128 CalcObjectivePriority(obj)
1130 for r in pairs(routes) do
1131 if r == best_route then
1132 local index = r:addObjectiveBest(obj)
1133 obj.pos = r[index].pos
1134 table.insert(route, index, obj)
1135 if index == 1 then
1136 minimap_dodad:SetObjective(route[1])
1138 else
1139 r:addObjectiveFast(obj)
1143 changed = true
1145 else
1146 add_swap[obj] = true
1150 for obj in pairs(add_swap) do
1151 -- If one of the objectives we were considering adding was removed, it would be in both lists.
1152 -- That would be bad. We can't remove it because we haven't actually added it yet, so
1153 -- handle that special case here.
1154 if to_remove[obj] then
1155 to_remove[obj] = nil
1156 to_add[obj] = nil
1157 add_swap[obj] = nil
1161 to_add, add_swap = add_swap, to_add
1162 qh.to_add = to_add
1163 self.add_swap = add_swap
1165 if #best_route > 1 then
1166 -- If there is 2 or more objectives, randomly combine routes to (hopefully) create something better than we had before.
1168 -- Calculate best_route first, so that if other routes are identical, we don't risk swapping with them and
1169 -- updating the map_walker.
1170 local best, max_fitness = best_route, 1/(qh:ComputeTravelTime(pos, best_route[1].pos) + best_route.distance)
1171 best_route.fitness = max_fitness
1173 qh:yieldIfNeeded(.3)
1175 for r in pairs(routes) do
1176 if r ~= best_route then
1177 local fit = 1/(qh:ComputeTravelTime(pos, r[1].pos)+r.distance)
1178 r.fitness = fit
1179 if fit > max_fitness then
1180 best, max_fitness = r, fit
1182 qh:yieldIfNeeded(.3)
1186 local to_breed, score
1188 for r in pairs(routes) do
1189 if r ~= best then
1190 local s = math.random()*r.fitness
1191 if not to_breed or s < score then
1192 to_breed, score = r, s
1197 to_breed:breed(routes)
1199 if 1/(qh:ComputeTravelTime(pos, to_breed[1].pos)+to_breed.distance+improve_margin) > max_fitness then
1200 best = to_breed
1203 qh:yieldIfNeeded(.3)
1205 if best ~= best_route then
1207 assert(best:sanity())
1208 assert(best_route:sanity())
1210 best_route = best
1211 self.best_route = best_route
1213 for i, info in ipairs(best) do
1214 local obj = info.obj
1215 obj.pos = info.pos
1216 route[i] = obj
1219 minimap_dodad:SetObjective(route[1])
1221 changed = true
1225 if qh.defered_flight_times then
1226 qh:buildFlightTimes()
1227 qh.defered_flight_times = false
1228 assert(qh.defered_graph_reset)
1231 if qh.defered_graph_reset then
1232 qh:yieldIfNeeded(10)
1234 for r in pairs(routes) do
1235 r:pathResetBegin()
1238 qh.graph_in_limbo = true
1239 qh:ResetPathing()
1240 qh.graph_in_limbo = false
1241 qh.defered_graph_reset = false
1243 for r in pairs(routes) do
1244 r:pathResetEnd()
1247 for i, info in ipairs(best_route) do
1248 local obj = info.obj
1249 obj.pos = info.pos
1250 route[i] = obj
1252 best_route:recalculateDistances()
1254 minimap_dodad:SetObjective(route[1])
1256 QuestHelper:SetTargetLocationRecalculate()
1258 for r in pairs(routes) do
1259 assert(r:sanity())
1261 best_route:sanity()
1263 qh:yieldIfNeeded(10)
1266 if changed then
1267 map_walker:RouteChanged()
1270 assert(#route == #best_route)
1272 if route_pass > 0 then
1273 route_pass = route_pass - 1
1276 -- temporary hack to cause more errors
1277 --qh.defered_graph_reset = true
1278 --qh.defered_flight_times = true
1280 qh:yieldIfNeeded(1)
1284 local update_route
1286 function QuestHelper:RunCoroutine()
1287 if coroutine.status(update_route) ~= "dead" then
1288 coroutine_running = true
1289 -- At perf = 100%, we will run 5 ms / frame.
1290 coroutine_stop_time = GetTime() + 4e-3 * QuestHelper_Pref.perf_scale * ((route_pass > 0) and 5 or 1)
1291 local state, err = coroutine.resume(update_route, self)
1292 coroutine_running = false
1293 if not state then
1294 self:TextOut("|cffff0000The routing co-routine just exploded|r: |cffffff77"..tostring(err).."|r")
1295 QuestHelper_ErrorCatcher_ExplicitError(err, "", "(Routing error)\n")
1300 function QuestHelper:ForceRouteUpdate(passes)
1301 route_pass = math.max(2, passes or 1)
1304 function Routing:Initialize()
1305 self:RoutingSetup()
1307 if coroutine.coco then
1308 -- coco allows yielding across c boundries, which allows me to use xpcall to get
1309 -- stack traces for coroutines without calls to yield resulting in thermal nuclear meltdown.
1311 -- This isn't part of WoW, I was using it in my driver program: Development/routetest
1313 update_route = coroutine.create(
1314 function()
1315 local state, err = xpcall(
1316 function()
1317 Routing:RouteUpdateRoutine()
1318 end,
1319 function (err)
1320 if debugstack then
1321 return tostring(err).."\n"..debugstack(2)
1322 else
1323 return debug.traceback(tostring(err), 2)
1325 end)
1327 if not state then
1328 error(err, 0)
1330 end)
1331 else
1332 update_route = coroutine.create(function() Routing:RouteUpdateRoutine() end)