local GetTime = QuestHelper_GetTime QuestHelper_File["routing.lua"] = "4.0.1.$svnversion$" QuestHelper_Loadtime["routing.lua"] = GetTime() -- Create module QuestHelper.Routing = {} local Routing = QuestHelper.Routing Routing.qh = QuestHelper -- Constants: local improve_margin = 1e-8 -- Module Status: local work_done = 0 local function CalcObjectivePriority(obj) local priority = obj.priority for o in pairs(obj.before) do if o.watched then priority = math.min(priority, CalcObjectivePriority(o)) end end obj.real_priority = priority return priority end local Route = {} Route.__index = Route Routing.Route = Route -- Make it available as a member -- 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. function Route:sanity() local assert = assert if QuestHelper.Error then assert = function(a, b) if not a then QuestHelper:TextOut("Route:sanity(): id="..self.id.."; best_route="..Routing.best_route.id) QuestHelper:Error(b or "Assertion Failed") end end end local l = 0 --QuestHelper:TextOut(QuestHelper:StringizeTable(self)) for i = 0,#self-1 do --QuestHelper:TextOut(tostring(i)) --QuestHelper:TextOut(QuestHelper:StringizeTable(self[i])) --QuestHelper:TextOut(tostring(self[i].len)) --[[ assert(self[i].len) ]] l = l + self[i].len end --QuestHelper:TextOut("sd: "..l.." rd: "..self.distance) --[[ assert(math.abs(l-self.distance) < 0.0001, string.format("compare %f vs %f", l, self.distance)) ]] for i, info in ipairs(self) do --[[ assert(self.index[info.obj] == i) ]] --[[ assert(info.pos) ]] end for obj, i in pairs(self.index) do --[[ assert(self[i].obj == obj) ]] end for i = 1, #self-1 do local l = QuestHelper:ComputeTravelTime(self[i].pos, self[i+1].pos, true) --[[ assert(math.abs(l-self[i].len) < 0.0001, "Compare at "..i..": "..l.." vs "..self[i].len) ]] end return true end function Route:findObjectiveRange(obj, passes) --[[ lines = {} table.insert(lines, string.format("QuestHelper objectiverange for %s (pri %d)", obj.obj, obj.real_priority)) for i = 1, #self do 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)) end]] local mn = #self while mn >= 1 do if obj.real_priority > self[mn].obj.real_priority or obj.after[self[mn].obj] or self[mn].obj.before[obj] then break end mn = mn - 1 end mn = mn + 1 -- we went too far, actually local mx = 1 while mx < #self + 1 do if obj.real_priority < self[mx].obj.real_priority or obj.before[self[mx].obj] or self[mx].obj.after[obj] then break end mx = mx + 1 end --table.insert(lines, string.format("temp results is %d %d", mn, mx)) 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. local mid = math.ceil((mx + mn) / 2) mx = mid mn = mid end --table.insert(lines, string.format("overall: %d %d", mn, mx)) --[[ if passes and passes > 90 then for k, v in pairs(lines) do QuestHelper:TextOut(v) end QuestHelper:TextOut(string.format("overall: %d %d", mn, mx)) end ]] --[[ local omn, omx = self:OldFindObjectiveRange(obj) if mn ~= omn or mx ~= omx then for k, v in pairs(lines) do QuestHelper:TextOut(v) end QuestHelper:TextOut(string.format("overall: %d %d vs %d %d", mn, mx, omn, omx)) lolcrash = (lolcrash or 0) + 1 end]] return mn, mx, lines end function Route:addObjectiveFast(obj) --[[ assert(self:sanity()) ]] local indexes = self.index local len = #self local info = QuestHelper:CreateTable() --[[ assert(not indexes[obj]) ]] info.obj = obj if len == 0 then local d self[1] = info indexes[obj] = 1 d, info.pos = obj:TravelTime(self[0].pos, true) self[0].len = d self.distance = d return 1 end local player_pos = QuestHelper.pos local pos = obj.location local c, x, y = pos[1].c, pos[3], pos[4] local mn, mx = self:findObjectiveRange(obj) local index, distsqr for i = mn, math.min(mx, len) do local p = self[i].pos if c == p[1].c then local u, v = p[3]-x, p[4]-y local d2 = u*u+v*v if not index or d2 < distsqr then index, distsqr = i, d2 end end end if not index then -- No nodes with the same continent already. -- If the same continent as the player, add to start of list, otherwise add to end of the list. index = c == player_pos[1].c and mx or mx end -- The next question, do I insert at that point, or do I insert after it? if index ~= mx and index <= len then local p1 = self[index].pos if p1[1].c == c then local p0 if index == 1 then p0 = player_pos else p0 = self[index-1].pos end local oldstart, newstart if p0[1].c == c then local u, v = p0[3]-x, p0[4]-y newstart = math.sqrt(u*u+v*v) u, v = p0[3]-p1[3], p0[4]-p1[4] oldstart = math.sqrt(u*u+v*v) else newstart = 0 oldstart = 0 end local p2 if index ~= len then p2 = self[index+1].pos end local oldend, newend if p2 and p2[1].c == c then local u, v = p2[3]-x, p2[4]-y newend = math.sqrt(u*u+v*v) u, v = p2[3]-p1[3], p2[4]-p1[4] oldend = math.sqrt(u*u+v*v) else newend = 0 oldend = 0 end if oldstart+newend < newstart+oldend then index = index + 1 end end end QH_Timeslice_Yield() -- The above checks don't require much effort. if index > len then local previnfo = self[index-1] --[[ assert(previnfo) ]] local d d, info.pos = obj:TravelTime(previnfo.pos) --[[ assert(info.pos) ]] QH_Timeslice_Yield() previnfo.len = d self.distance = self.distance + d else local d1, d2 local previnfo = self[index-1] d1, d2, info.pos = obj:TravelTime2(previnfo.pos, self[index].pos, previnfo.no_cache) info.len = d2 self.distance = self.distance + (d1 - previnfo.len + d2) previnfo.len = d1 QH_Timeslice_Yield() end -- Finally, insert the objective. table.insert(self, index, info) indexes[obj] = index -- Fix indexes of shifted elements. for i = index+1,len+1 do local obj = self[i].obj --[[ assert(indexes[obj] == i-1) ]] indexes[obj] = i end --[[ assert(self:sanity()) ]] return index end function Route:addObjectiveBest(obj, old_index, old_distance) --[[ assert(self:sanity()) ]] local indexes = self.index local len = #self local info = QuestHelper:CreateTable() --[[ assert(not indexes[obj]) ]] info.obj = obj if len == 0 then indexes[obj] = 1 self.distance, info.pos = obj:TravelTime(self[0].pos, true) info.len = 0 self[0].len = self.distance self[1] = info return 1 end 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.) local best_index, best_delta, best_d1, best_d2, best_p local no_cache, prev_pos, prev_len local mn, mx = self:findObjectiveRange(obj) if old_index and mn <= old_index and old_index <= mx then -- We're trying to re-evaluate it, and it could remain in the same place. -- So that place is our starting best known place. best_index, best_delta = old_index, old_distance - self.distance if best_delta < 0 then -- Somehow, removing the objective actually made the route worse... -- Just re-figure things from scratch. -- TODO: THIS SHOULD NEVER HAPPEN dear god find out what's causing this and stop it --QuestHelper:TextOut("made route worse wtf") best_index, best_delta = nil, nil end end local pinfo = self[mn-1] no_cache, prev_pos, prev_len = pinfo.no_cache, pinfo.pos, pinfo.len for i = mn, math.min(mx, len) do --[[ assert(prev_pos == self[i-1].pos) ]] local info = self[i] local pos = info.pos local d1, d2, p = obj:TravelTime2(prev_pos, pos, no_cache) QH_Timeslice_Yield() local delta = d1 + d2 - prev_len if not best_index or ((delta + improve_margin) < best_delta) or ((i == best_index) and not best_d1) then -- Best so far is: -- * First item we reach -- * Better than previous best -- * We're looking at our best already. But we just got here; how could this be best? -- If this was our prior location and we didn't find anything better earlier in the route, -- that's how. Save the specifics, 'cause we didn't compute them when setting up. best_index, best_delta, best_d1, best_d2, best_p = i, delta, d1, d2, p end prev_pos = pos prev_len = info.len no_cache = false end if mx > len then --[[ assert(mx == len+1) ]] --[[ assert(prev_pos == self[len].pos) ]] local delta, p = obj:TravelTime(prev_pos, no_cache) QH_Timeslice_Yield() if not best_index or ((delta + improve_margin) < best_delta) or ((mx == best_index) and not best_d1) then info.pos = p info.len = 0 self[len].len = delta self.distance = self.distance + delta table.insert(self, info) indexes[obj] = mx --[[ assert(self:sanity()) ]] return mx end end info.pos = best_p info.len = best_d2 local pinfo = self[best_index-1] self.distance = self.distance + (best_d1 - pinfo.len + best_d2) pinfo.len = best_d1 table.insert(self, best_index, info) -- --[[ QuestHelper:Assert(math.abs(QuestHelper:ComputeTravelTime(self[best_index-1].pos, self[best_index].pos) - self[best_index-1].len) < 0.0001, "aaaaargh") ]] --[[ -- I don't think this is necessary now that TravelTime2 explicitly does this internally, but I'm keeping it anyway. self.distance = self.distance - self[best_index-1].len self[best_index-1].len = QuestHelper:ComputeTravelTime(self[best_index-1].pos, self[best_index].pos, true) self.distance = self.distance + self[best_index-1].len ]] indexes[obj] = best_index for i = best_index+1,len+1 do --[[ assert(indexes[self[i].obj] == i-1) ]] indexes[self[i].obj] = i end if not old_index or (mn > old_index or old_index > mx) and mn <= best_index and best_index <= mx then -- 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 sanityfixed = 1 end --[[ assert(self:sanity()) ]] return best_index, sanityfixed end function Route:removeObjective(obj) --[[ assert(self:sanity()) ]] local indexes = self.index local index = indexes[obj] local old_distance = self.distance --[[ assert(index) ]] local info = self[index] --[[ assert(info.obj == obj) ]] --[[ Removing end item: subtract last distance, nothing to recalculate Removing other item: recalculate location of next objective, between prior position and objective after next Special case: if there is no location after next, just recalc location of next objective --]] if index == #self then self.distance = self.distance - self[index-1].len self[index-1].len = 0 else local pinfo = self[index-1] local info1 = self[index+1] local info2 = self[index+2] local no_cache = (index == 1) local d1, d2 if info2 then d1, d2, info1.pos = info1.obj:TravelTime2(pinfo.pos, info2.pos, no_cache) QH_Timeslice_Yield() self.distance = self.distance - pinfo.len - info.len - info1.len + d1 + d2 info1.len = d2 else d1, info1.pos = info1.obj:TravelTime(pinfo.pos, no_cache) QH_Timeslice_Yield() self.distance = self.distance - pinfo.len - info.len + d1 end pinfo.len = d1 end QuestHelper:ReleaseTable(info) indexes[obj] = nil table.remove(self, index) for i = index,#self do -- Fix indexes of shifted elements. local obj = self[i].obj --[[ assert(indexes[obj] == i+1) ]] indexes[obj] = i end --[[ assert(self:sanity()) ]] -- --[[ assert(self.distance <= old_distance) ]] return index end local links = {} local seen = {} function Route:breed(route_map) local indexes = self.index local len = #self local info local r local prev_pos = QuestHelper.pos --[[ assert(self[0].pos == prev_pos) ]] -- Pick which objective goes first, selecting from first objective of each route, -- and scaling by the route's fitness and distance from player, with a random adjustment factor. -- While we're at it, record some data about the fitness of adjacent objectives for route in pairs(route_map) do --[[ assert(route:sanity()) ]] local fit = route.fitness local pos = route[1].pos local w if prev_pos[1].c == pos[1].c then local u, v = prev_pos[3]-pos[3], prev_pos[4]-pos[4] w = math.sqrt(u*u+v*v) else w = 500 end w = fit * math.random() / w if not info or w > r then info, r = route[1], w end for i = 1,len do local obj = route[i].obj local tbl = links[obj] if not tbl then tbl = QuestHelper:CreateTable() links[obj] = tbl end if i ~= 1 then local info = route[i-1] local obj2 = info.obj tbl[info] = (tbl[info] or 0) + fit end if i ~= len then local info = route[i+1] local obj2 = info.obj if obj.real_priority <= obj2.real_priority or obj.before[obj2] then tbl[info] = (tbl[info] or 0) + fit end end end QH_Timeslice_Yield() end -- Record info for the 'Player Position' objective, so we don't mess it up later seen[self[0].obj] = self[0].pos -- Record the objective that we chose to put first local obj = info.obj indexes[obj] = 1 seen[obj] = info.pos -- Save its position, because we don't want to clobber any of the info objects yet prev_pos = info.pos last = links[obj] links[obj] = nil -- Scan the rest of the places in the route, and pick objectives to go there for index = 2,len do info = nil local c = 1 -- Scan the list of scores from the prior objective for i, weight in pairs(last) do if links[i.obj] then -- Only consider an item if we have scores for that item local w local pos = i.pos if prev_pos[1].c == pos[1].c then local u, v = prev_pos[3]-pos[3], prev_pos[4]-pos[4] w = math.sqrt(u*u+v*v) else w = 500 end w = weight * math.random() / w if not info or w > r then info, r = i, w end end c = c + 1 end -- In case we had no valid scores, scan the remaining objectives and score by distance if not info then for obj in pairs(links) do local pos = obj.pos local w if prev_pos[1] == pos[1] then -- Same zone local u, v = prev_pos[3]-pos[3], prev_pos[4]-pos[4] w = math.sqrt(u*u+v*v) elseif prev_pos[1].c == pos[1].c then -- Same continent. -- Assume twices as long. local u, v = prev_pos[3]-pos[3], prev_pos[4]-pos[4] w = 2*math.sqrt(u*u+v*v) else -- Different continent. Assume fixed value of 5 minutes. w = 300 end w = math.random() / w if not info or w > r then local route = next(route_map) info, r = route[route.index[obj]], w end end --[[ assert(info) ]] end -- Add the selected item to the route obj = info.obj indexes[obj] = index prev_pos = info.pos seen[obj] = prev_pos --[[ assert(info.obj == obj) ]] -- 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 local link = links[obj] links[obj] = nil QuestHelper:ReleaseTable(last) last = link QH_Timeslice_Yield() end -- Clean up the last table QuestHelper:ReleaseTable(last) -- Now that we've got our objectives lined up, fill in the info objects with the positions we saved for obj, i in pairs(indexes) do --[[ assert(seen[obj]) ]] local info = self[i] info.obj, info.pos = obj, seen[obj] seen[obj] = nil end -- Now randomly randomize some of the route (aka mutation) while math.random() > 0.3 do local l = math.floor(math.random()^1.6*(len-1))+1 local i = math.random(1, len-l) local j = i+l -- Reverse a chunk of the route for k = 0, j-i-1 do self[i+k], self[j-k] = self[j-k], self[i+k] end end -- But wait, after all that some objectives might violate the rules. Make sure the route follows -- the rules. -- 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. -- Since the entire thing is internally inconsistent anyway, we're just gonna try to consistentize it. local valid_items = {} for k, v in ipairs(self) do valid_items[v.obj] = true end for k, v in ipairs(self) do for b in pairs(v.obj.before) do if valid_items[b] then b.after[v.obj] = true end end for a in pairs(v.obj.after) do if valid_items[a] then a.before[v.obj] = true end end end -- Because priorities might have been changed in here, we next make absolutely sure we have up-to-date priorities. for k, v in ipairs(self) do CalcObjectivePriority(v.obj) end -- Have I mentioned I hate this codebase yet? -- Because I do. -- Just, you know. -- FYI. local invalid = true local invalid_passes = 0 --local output_strings = {} while invalid do invalid_passes = invalid_passes + 1 --[[if invalid_passes >= 100 then for k, v in pairs(output_strings) do QuestHelper:TextOut(v) end end]] if invalid_passes >= 100 then -- ugh QuestHelper.mutation_passes_exceeded = true break end 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 invalid = false local i = 1 --[[for i = 1, #self do local mn, mx = self:findObjectiveRange(self[i].obj, invalid_passes) table.insert(output_strings, string.format("%d is mn mx %d %d (%s)", i, mn, mx, self[i].obj.obj)) end]] while i <= #self do -- Make sure all the objectives have valid positions in the list. local info = self[i] local mn, mx, tabi = self:findObjectiveRange(info.obj, invalid_passes) --if invalid_passes > 90 then for k, v in pairs(tabi) do table.insert(output_strings, v) end end if i < mn then -- In theory, 'i' shouldn't be increased here, as the next -- element will be shifted down into the current position. -- However, it is possible for an infinite loop to be created -- by this, with a small range of objectives constantly -- being shifted. -- So, I mark the route as invalid and go through it another time. -- It's probably still possible to get into an infinite loop, -- but it seems much less likely. table.insert(self, mn, info) table.remove(self, i) invalid = true --table.insert(output_strings, string.format("shifting %d into %d", i, mn)) elseif i > mx then table.remove(self, i) table.insert(self, mx, info) invalid = true --table.insert(output_strings, string.format("shifting %d into %d", i, mx)) end i = i + 1 end --table.insert(output_strings, "pass done") end -- Now that we've chosen a route, re-calculate the cost of each leg of the route local distance = 0 local prev_info = self[0] local next_info = self[1] local prev_pos = prev_info.pos local next_pos = next_info.pos --[[ assert(prev_pos) ]] --[[ assert(next_pos) ]] QH_Timeslice_Yield() for i = 1, len-1 do local d1, d2 local pos local info = next_info next_info = self[i+1] next_pos = next_info.pos indexes[info.obj] = i d1, d2, pos = info.obj:TravelTime2(prev_pos, next_pos, prev_info.no_cache) --[[ assert(pos) ]] QH_Timeslice_Yield() prev_info.len = d1 info.len = d2 info.pos = pos distance = distance + d1 prev_info = info prev_pos = pos end self.distance = distance + prev_info.len indexes[self[len].obj] = len self[len].len = 0 --[[ assert(self:sanity()) ]] end function Route:pathResetBegin() for i, info in ipairs(self) do local pos = info.pos info[1], info[2], info[3] = pos[1].c, pos[3], pos[4] end end function Route:pathResetEnd() for i, info in ipairs(self) do -- Try to find a new position for this objective, near where we had it originally. local p, d = nil, 0 local a, b, c = info[1], info[2], info[3] for z, pl in pairs(info.obj.p) do for i, point in ipairs(pl) do if a == point[1].c then local x, y = b-point[3], c-point[4] local d2 = x*x+y*y if not p or d2 < d then p, d = point, d2 end end end end -- Assuming that there will still be positions on the same continents as before, i.e., locations are only added and not removed. --[[ assert(p) ]] info.pos = p end self:recalculateDistances() end function Route:recalculateDistances() self.distance = 0 for i = 0, #self-1 do self[i].len = QuestHelper:ComputeTravelTime(self[i].pos, self[i+1].pos) self.distance = self.distance + self[i].len end end function Routing:RoutingSetup() Routing.map_walker = self.qh:CreateWorldMapWalker() Routing.add_swap = {} Routing.routes = {} local routes = Routing.routes local pos = QuestHelper.pos local PlayerObjective = self.qh:NewObjectiveObject() -- Pseudo-objective which reflects player's current position. Always at index 0 of each route. PlayerObjective.pos = pos PlayerObjective.cat = "loc" -- A special case of a location PlayerObjective.obj = "Player's current position" -- Player shouldn't see this, so no need to localize PlayerObjective.icon_id = 6 -- Don't think we'll need these; just filling them in for completeness PlayerObjective.o = {pos=pos} PlayerObjective.fb = {} for i = 1,15 do -- Create some empty routes to use for our population. local new_rt = { index={ [PlayerObjective]=0 }, distance=0, [0]={ obj=PlayerObjective, pos=pos, len=0, no_cache=true }, -- Player's current position is always objective #0 id=i -- So I can keep track of which route is which; only for debugging. } setmetatable(new_rt, Route) routes[new_rt] = true end -- All the routes are the same right now, so it doesn't matter which we're considering the best. self.best_route = next(routes) self.recheck_position = 1 end function Routing:RouteUpdateRoutine() local qh = QuestHelper local map_walker = Routing.map_walker local minimap_dodad = qh.minimap_dodad local route = qh.route local to_add, to_remove, add_swap = qh.to_add, qh.to_remove, self.add_swap local routes = self.routes local pos = qh.pos local best_route = self.best_route local last_cache_clear = GetTime() ------ EVIL HACK OF DEBUG ------ EVIL HACK OF DEBUG while true do -- Clear caches out a bit if GetTime() + 15 >= last_cache_clear then qh:CacheCleanup() last_cache_clear = GetTime() end -- Update the player's position data. if qh.target then -- We know the player will be at the target location at target_time, so fudge the numbers -- to pretend we're traveling there. pos[1], pos[3], pos[4] = qh.target[1], qh.target[3], qh.target[4] local extra_time = math.max(0, qh.target_time-time()) for i, t in ipairs(qh.target[2]) do pos[2][i] = t+extra_time end else if not pos[1] -- Need a valid position, in case the player was dead when they loaded the game. or not UnitIsDeadOrGhost("player") then -- Don't update the player's position if they're dead, assume they'll be returning to their corpse. pos[3], pos[4] = qh.Astrolabe:TranslateWorldMapPosition(qh.m, qh.f, qh.x, qh.y, qh.m, 0) --[[ assert(pos[3]) ]] --[[ assert(pos[4]) ]] pos[1] = qh.zone_nodes[qh.i] pos[3], pos[4] = pos[3] * qh.continent_scales_x[qh.c], pos[4] * qh.continent_scales_y[qh.c] for i, n in ipairs(pos[1]) do if not n.x then for i, j in pairs(n) do qh:TextOut("[%q]=%s %s", i, type(j), tostring(j) or "???") end --[[ assert(false) ]] end local a, b = n.x-pos[3], n.y-pos[4] pos[2][i] = math.sqrt(a*a+b*b) end end end local changed = false if #route > 0 then if self.recheck_position > #route then self.recheck_position = 1 end local o = route[self.recheck_position] --[[ assert(o.zones) ]] o.filter_zone = o.zones[pos[1].i] == nil o.filter_watched = not o:IsWatched() if not o:Known() then -- Objective was probably made to depend on an objective that we don't know about yet. -- We add it to both lists, because although we need to remove it, we need it added again when we can. -- This creates an inconsistancy, but it'll get fixed in the removal loop before anything has a chance to -- explode from it. to_remove[o] = true to_add[o] = true else if o.swap_before then qh:ReleaseTable(o.before) o.before = o.swap_before o.swap_before = nil end if o.swap_after then qh:ReleaseTable(o.after) o.after = o.swap_after o.swap_after = nil end if o.is_sharing ~= o.want_share then o.is_sharing = o.want_share if o.want_share then qh:DoShareObjective(o) else qh:DoUnshareObjective(o) end end CalcObjectivePriority(o) -- Make sure the objective in best_route is still in a valid position. -- Won't worry about other routes, they should forced into valid configurations by breeding. -- 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. local aobt = {} setmetatable(aobt, getmetatable(best_route)) for k, v in pairs(best_route) do aobt[k] = v end aobt.index = {} for k, v in ipairs(best_route) do -- arglbargl aobt[k] = QuestHelper:CreateTable("AOBT idiocy") for t, q in pairs(v) do aobt[k][t] = q end aobt.index[aobt[k].obj] = k end if aobt[0] then aobt[0] = QuestHelper:CreateTable("AOBT idiocy") for t, q in pairs(best_route[0]) do aobt[0][t] = q end end -- Actually duplicating a route is irritatingly hard (this is another thing which will be fixed eventually dammit) --[[ assert(aobt:sanity()) ]] --[[ assert(best_route:sanity()) ]] local old_distance, old_index = best_route.distance, best_route:removeObjective(o) 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 local new_index, sanityfixed = best_route:addObjectiveBest(o, old_index, old_distance) 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 -- 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 if new_real_distance < old_real_distance or sanityfixed then -- More of the temporary hack -- If we're using the new path . . . if old_index > new_index then old_index, new_index = new_index, old_index end for i = math.max(1, old_index-1), new_index do local info = best_route[i] local obj = info.obj obj.pos = info.pos route[i] = obj end if old_index ~= new_index then if old_index == 1 then minimap_dodad:SetObjective(route[1]) end changed = true end -- . . . release our backup path for k, v in ipairs(aobt) do QuestHelper:ReleaseTable(v) end QuestHelper:ReleaseTable(aobt[0]) else -- hack (everything in this conditional besides the above chunk is a horrible hack) -- If we're using the old path . . . -- . . . release the *internals* of the new path, then copy everything over. Eugh. for k, v in ipairs(best_route) do QuestHelper:ReleaseTable(v) end QuestHelper:ReleaseTable(best_route[0]) while #best_route > 0 do table.remove(best_route) end for k, v in pairs(aobt) do best_route[k] = v end -- hack setmetatable(aobt, Route) --[[ assert(best_route:sanity()) ]] end -- hack -- 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 -- 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* --if old_index == new_index then -- We don't advance recheck_position unless the node doesn't get moved. -- TODO: As the this code is apparently bugged, it's gotten into an infinite loop of constantly swapping -- and hence never advancing. As a work around for now, we'll always advance. -- THIS IS A GREAT IDEA --else self.recheck_position = self.recheck_position + 1 end end -- Remove any waypoints if needed. while true do local obj = next(to_remove) if not obj then break end to_remove[obj] = nil if obj.is_sharing then obj.is_sharing = false qh:DoUnshareObjective(obj) end for r in pairs(routes) do if r == best_route then local index = r:removeObjective(obj) table.remove(route, index) if index == 1 then minimap_dodad:SetObjective(route[1]) end else r:removeObjective(obj) end end obj:DoneRouting() changed = true end -- Add any waypoints if needed while true do local obj = next(to_add) if not obj then break end to_add[obj] = nil obj.filter_zone = obj.zones and obj.zones[pos[1].i] == nil obj.filter_watched = not obj:IsWatched() if obj:Known() then obj:PrepareRouting() obj.filter_zone = obj.zones[pos[1].i] == nil if obj.filter_zone and QuestHelper_Pref.filter_zone then -- Not going to add it, wrong zone. obj:DoneRouting() add_swap[obj] = true else if not obj.is_sharing and obj.want_share then obj.is_sharing = true qh:DoShareObjective(obj) end CalcObjectivePriority(obj) for r in pairs(routes) do if r == best_route then local index = r:addObjectiveBest(obj) obj.pos = r[index].pos table.insert(route, index, obj) if index == 1 then minimap_dodad:SetObjective(route[1]) end else r:addObjectiveFast(obj) end end changed = true end else add_swap[obj] = true end end for obj in pairs(add_swap) do -- If one of the objectives we were considering adding was removed, it would be in both lists. -- That would be bad. We can't remove it because we haven't actually added it yet, so -- handle that special case here. if to_remove[obj] then to_remove[obj] = nil to_add[obj] = nil add_swap[obj] = nil end end to_add, add_swap = add_swap, to_add qh.to_add = to_add self.add_swap = add_swap if #best_route > 1 then -- If there is 2 or more objectives, randomly combine routes to (hopefully) create something better than we had before. -- Calculate best_route first, so that if other routes are identical, we don't risk swapping with them and -- updating the map_walker. local best, max_fitness = best_route, 1/(qh:ComputeTravelTime(pos, best_route[1].pos) + best_route.distance) best_route.fitness = max_fitness QH_Timeslice_Yield() for r in pairs(routes) do if r ~= best_route then local fit = 1/(qh:ComputeTravelTime(pos, r[1].pos)+r.distance) r.fitness = fit if fit > max_fitness then best, max_fitness = r, fit end QH_Timeslice_Yield() end end local to_breed, score for r in pairs(routes) do if r ~= best then local s = math.random()*r.fitness if not to_breed or s < score then to_breed, score = r, s end end end to_breed:breed(routes) if 1/(qh:ComputeTravelTime(pos, to_breed[1].pos)+to_breed.distance+improve_margin) > max_fitness then best = to_breed end QH_Timeslice_Yield() if best ~= best_route then --[[ assert(best:sanity()) ]] --[[ assert(best_route:sanity()) ]] best_route = best self.best_route = best_route for i, info in ipairs(best) do local obj = info.obj obj.pos = info.pos route[i] = obj end minimap_dodad:SetObjective(route[1]) changed = true end end if qh.defered_flight_times then qh:buildFlightTimes() qh.defered_flight_times = false --[[ assert(qh.defered_graph_reset) ]] end if qh.defered_graph_reset then QH_Timeslice_Yield() for r in pairs(routes) do r:pathResetBegin() end qh.graph_in_limbo = true qh:ResetPathing() qh.graph_in_limbo = false qh.defered_graph_reset = false for r in pairs(routes) do r:pathResetEnd() end for i, info in ipairs(best_route) do local obj = info.obj obj.pos = info.pos route[i] = obj end best_route:recalculateDistances() minimap_dodad:SetObjective(route[1]) QuestHelper:SetTargetLocationRecalculate() for r in pairs(routes) do --[[ assert(r:sanity()) ]] end best_route:sanity() QH_Timeslice_Yield() end if changed then map_walker:RouteChanged() end --[[ assert(#route == #best_route) ]] -- temporary hack to cause more errors --qh.defered_graph_reset = true --qh.defered_flight_times = true QH_Timeslice_Yield() end end function Routing:Initialize() self:RoutingSetup() QH_Timeslice_Add(function() Routing:RouteUpdateRoutine() end, "routing") QH_Timeslice_Toggle("routing", false) --[[ if coroutine.coco then -- coco allows yielding across c boundries, which allows me to use xpcall to get -- stack traces for coroutines without calls to yield resulting in thermal nuclear meltdown. -- This isn't part of WoW, I was using it in my driver program: Development/routetest update_route = coroutine.create( function() local state, err = xpcall( function() Routing:RouteUpdateRoutine() end, function (err) if debugstack then return tostring(err).."\n"..debugstack(2) else return debug.traceback(tostring(err), 2) end end) if not state then error(err, 0) end end) else update_route = coroutine.create(function() Routing:RouteUpdateRoutine() end) end ]] end