1
0
Bifurcation 0
Ce dépôt a été archivé le 2020-03-15. Vous pouvez voir ses fichiers ou le cloner, mais pas ouvrir de ticket ou de demandes d'ajout, ni soumettre de changements.
questhelperredux/QuestHelper/routing.lua

1246 lignes
37 KiB
Lua
Brut Vue normale Historique

local GetTime = QuestHelper_GetTime
QuestHelper_File["routing.lua"] = "4.0.1.$svnversion$"
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
for o in pairs(obj.before) do
if o.watched then
priority = math.min(priority, CalcObjectivePriority(o))
end
end
2010-10-24 23:17:33 +02:00
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()
2010-10-24 23:17:33 +02:00
local assert = assert
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
local l = 0
2010-10-24 23:17:33 +02:00
--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
2010-10-24 23:17:33 +02:00
--QuestHelper:TextOut("sd: "..l.." rd: "..self.distance)
--[[ assert(math.abs(l-self.distance) < 0.0001, string.format("compare %f vs %f", l, self.distance)) ]]
2010-10-24 23:17:33 +02:00
for i, info in ipairs(self) do
--[[ assert(self.index[info.obj] == i) ]]
--[[ assert(info.pos) ]]
end
2010-10-24 23:17:33 +02:00
for obj, i in pairs(self.index) do
--[[ assert(self[i].obj == obj) ]]
end
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
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]]
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
mn = mn + 1 -- we went too far, actually
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
--table.insert(lines, string.format("temp results is %d %d", mn, mx))
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
--table.insert(lines, string.format("overall: %d %d", mn, mx))
2010-10-24 23:17:33 +02:00
--[[
if passes and passes > 90 then
2010-10-24 23:17:33 +02:00
for k, v in pairs(lines) do QuestHelper:TextOut(v) end
QuestHelper:TextOut(string.format("overall: %d %d", mn, mx))
end
]]
2010-10-24 23:17:33 +02:00
--[[
local omn, omx = self:OldFindObjectiveRange(obj)
2010-10-24 23:17:33 +02:00
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]]
2010-10-24 23:17:33 +02:00
return mn, mx, lines
2010-10-24 23:17:33 +02:00
end
function Route:addObjectiveFast(obj)
--[[ assert(self:sanity()) ]]
local indexes = self.index
local len = #self
local info = QuestHelper:CreateTable()
--[[ assert(not indexes[obj]) ]]
2010-10-24 23:17:33 +02:00
info.obj = obj
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
local player_pos = QuestHelper.pos
local pos = obj.location
local c, x, y = pos[1].c, pos[3], pos[4]
2010-10-24 23:17:33 +02:00
local mn, mx = self:findObjectiveRange(obj)
local index, distsqr
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
-- 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
2010-10-24 23:17:33 +02:00
if p1[1].c == c then
local p0
2010-10-24 23:17:33 +02:00
if index == 1 then
p0 = player_pos
else
p0 = self[index-1].pos
end
2010-10-24 23:17:33 +02:00
local oldstart, newstart
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
local p2
if index ~= len then
p2 = self[index+1].pos
end
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
if oldstart+newend < newstart+oldend then
index = index + 1
end
2010-10-24 23:17:33 +02:00
end
end
2010-10-24 23:17:33 +02:00
QH_Timeslice_Yield() -- The above checks don't require much effort.
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
local previnfo = self[index-1]
d1, d2, info.pos = obj:TravelTime2(previnfo.pos, self[index].pos, previnfo.no_cache)
2010-10-24 23:17:33 +02:00
info.len = d2
self.distance = self.distance + (d1 - previnfo.len + d2)
previnfo.len = d1
2010-10-24 23:17:33 +02:00
QH_Timeslice_Yield()
end
2010-10-24 23:17:33 +02:00
-- Finally, insert the objective.
table.insert(self, index, info)
indexes[obj] = index
2010-10-24 23:17:33 +02:00
-- 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
2010-10-24 23:17:33 +02:00
--[[ assert(self:sanity()) ]]
2010-10-24 23:17:33 +02:00
return index
end
function Route:addObjectiveBest(obj, old_index, old_distance)
--[[ assert(self:sanity()) ]]
2010-10-24 23:17:33 +02:00
local indexes = self.index
local len = #self
local info = QuestHelper:CreateTable()
--[[ assert(not indexes[obj]) ]]
2010-10-24 23:17:33 +02:00
info.obj = obj
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
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.)
2010-10-24 23:17:33 +02:00
local best_index, best_delta, best_d1, best_d2, best_p
local no_cache, prev_pos, prev_len
local mn, mx = self:findObjectiveRange(obj)
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
local pinfo = self[mn-1]
no_cache, prev_pos, prev_len = pinfo.no_cache, pinfo.pos, pinfo.len
2010-10-24 23:17:33 +02:00
for i = mn, math.min(mx, len) do
--[[ assert(prev_pos == self[i-1].pos) ]]
2010-10-24 23:17:33 +02:00
local info = self[i]
local pos = info.pos
2010-10-24 23:17:33 +02:00
local d1, d2, p = obj:TravelTime2(prev_pos, pos, no_cache)
2010-10-24 23:17:33 +02:00
QH_Timeslice_Yield()
2010-10-24 23:17:33 +02:00
local delta = d1 + d2 - prev_len
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
prev_pos = pos
prev_len = info.len
no_cache = false
end
2010-10-24 23:17:33 +02:00
if mx > len then
--[[ assert(mx == len+1) ]]
--[[ assert(prev_pos == self[len].pos) ]]
local delta, p = obj:TravelTime(prev_pos, no_cache)
2010-10-24 23:17:33 +02:00
QH_Timeslice_Yield()
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
--[[ assert(self:sanity()) ]]
2010-10-24 23:17:33 +02:00
return mx
end
end
info.pos = best_p
info.len = best_d2
2010-10-24 23:17:33 +02:00
local pinfo = self[best_index-1]
self.distance = self.distance + (best_d1 - pinfo.len + best_d2)
pinfo.len = best_d1
2010-10-24 23:17:33 +02:00
table.insert(self, best_index, info)
2010-10-24 23:17:33 +02:00
-- --[[ 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
]]
2010-10-24 23:17:33 +02:00
indexes[obj] = best_index
2010-10-24 23:17:33 +02:00
for i = best_index+1,len+1 do
--[[ assert(indexes[self[i].obj] == i-1) ]]
indexes[self[i].obj] = i
end
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
--[[ assert(self:sanity()) ]]
2010-10-24 23:17:33 +02:00
return best_index, sanityfixed
end
function Route:removeObjective(obj)
--[[ assert(self:sanity()) ]]
2010-10-24 23:17:33 +02:00
local indexes = self.index
local index = indexes[obj]
local old_distance = self.distance
2010-10-24 23:17:33 +02:00
--[[ assert(index) ]]
local info = self[index]
--[[ assert(info.obj == obj) ]]
2010-10-24 23:17:33 +02:00
--[[
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)
2010-10-24 23:17:33 +02:00
local d1, d2
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
pinfo.len = d1
end
2010-10-24 23:17:33 +02:00
QuestHelper:ReleaseTable(info)
indexes[obj] = nil
table.remove(self, index)
2010-10-24 23:17:33 +02:00
for i = index,#self do
-- Fix indexes of shifted elements.
local obj = self[i].obj
--[[ assert(indexes[obj] == i+1) ]]
indexes[obj] = i
end
2010-10-24 23:17:33 +02:00
--[[ assert(self:sanity()) ]]
-- --[[ assert(self.distance <= old_distance) ]]
2010-10-24 23:17:33 +02:00
return index
end
local links = {}
local seen = {}
function Route:breed(route_map)
local indexes = self.index
local len = #self
2010-10-24 23:17:33 +02:00
local info
local r
2010-10-24 23:17:33 +02:00
local prev_pos = QuestHelper.pos
--[[ assert(self[0].pos == prev_pos) ]]
2010-10-24 23:17:33 +02:00
-- 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
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
w = fit * math.random() / w
2010-10-24 23:17:33 +02:00
if not info or w > r then
info, r = route[1], w
end
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
if i ~= 1 then
local info = route[i-1]
local obj2 = info.obj
tbl[info] = (tbl[info] or 0) + fit
end
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
QH_Timeslice_Yield()
end
2010-10-24 23:17:33 +02:00
-- Record info for the 'Player Position' objective, so we don't mess it up later
seen[self[0].obj] = self[0].pos
2010-10-24 23:17:33 +02:00
-- 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
2010-10-24 23:17:33 +02:00
last = links[obj]
links[obj] = nil
2010-10-24 23:17:33 +02:00
-- 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
2010-10-24 23:17:33 +02:00
-- 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
2010-10-24 23:17:33 +02:00
w = weight * math.random() / w
2010-10-24 23:17:33 +02:00
if not info or w > r then
info, r = i, w
end
end
c = c + 1
end
2010-10-24 23:17:33 +02:00
-- 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
2010-10-24 23:17:33 +02:00
w = math.random() / w
2010-10-24 23:17:33 +02:00
if not info or w > r then
local route = next(route_map)
info, r = route[route.index[obj]], w
end
end
2010-10-24 23:17:33 +02:00
--[[ assert(info) ]]
end
2010-10-24 23:17:33 +02:00
-- 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) ]]
2010-10-24 23:17:33 +02:00
-- 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
2010-10-24 23:17:33 +02:00
QH_Timeslice_Yield()
end
2010-10-24 23:17:33 +02:00
-- Clean up the last table
QuestHelper:ReleaseTable(last)
2010-10-24 23:17:33 +02:00
-- 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
2010-10-24 23:17:33 +02:00
-- 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
2010-10-24 23:17:33 +02:00
-- 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
2010-10-24 23:17:33 +02:00
-- 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.
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
-- 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
2010-10-24 23:17:33 +02:00
-- 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
2010-10-24 23:17:33 +02:00
invalid_passes = invalid_passes + 1
2010-10-24 23:17:33 +02:00
--[[if invalid_passes >= 100 then
for k, v in pairs(output_strings) do
QuestHelper:TextOut(v)
end
end]]
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
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.
2010-10-24 23:17:33 +02:00
-- However, it is possible for an infinite loop to be created
-- by this, with a small range of objectives constantly
-- being shifted.
2010-10-24 23:17:33 +02:00
-- 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.
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
-- 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) ]]
2010-10-24 23:17:33 +02:00
QH_Timeslice_Yield()
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
indexes[info.obj] = i
2010-10-24 23:17:33 +02:00
d1, d2, pos = info.obj:TravelTime2(prev_pos, next_pos, prev_info.no_cache)
--[[ assert(pos) ]]
QH_Timeslice_Yield()
2010-10-24 23:17:33 +02:00
prev_info.len = d1
info.len = d2
info.pos = pos
distance = distance + d1
2010-10-24 23:17:33 +02:00
prev_info = info
prev_pos = pos
end
2010-10-24 23:17:33 +02:00
self.distance = distance + prev_info.len
2010-10-24 23:17:33 +02:00
indexes[self[len].obj] = len
self[len].len = 0
2010-10-24 23:17:33 +02:00
--[[ 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
2010-10-24 23:17:33 +02:00
local a, b, c = info[1], info[2], info[3]
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
-- Assuming that there will still be positions on the same continents as before, i.e., locations are only added and not removed.
--[[ assert(p) ]]
2010-10-24 23:17:33 +02:00
info.pos = p
end
2010-10-24 23:17:33 +02:00
self:recalculateDistances()
end
function Route:recalculateDistances()
2010-10-24 23:17:33 +02:00
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 = {}
2010-10-24 23:17:33 +02:00
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 = {}
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
-- 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
2010-10-24 23:17:33 +02:00
end
function Routing:RouteUpdateRoutine()
local qh = QuestHelper
local map_walker = Routing.map_walker
local minimap_dodad = qh.minimap_dodad
2010-10-24 23:17:33 +02:00
local route = qh.route
local to_add, to_remove, add_swap = qh.to_add, qh.to_remove, self.add_swap
2010-10-24 23:17:33 +02:00
local routes = self.routes
local pos = qh.pos
2010-10-24 23:17:33 +02:00
local best_route = self.best_route
2012-01-01 03:22:56 +01:00
local last_cache_clear = GetTime()
2010-10-24 23:17:33 +02:00
------ EVIL HACK OF DEBUG
2010-10-24 23:17:33 +02:00
------ EVIL HACK OF DEBUG
2010-10-24 23:17:33 +02:00
while true do
-- Clear caches out a bit
2012-01-01 03:22:56 +01:00
if GetTime() + 15 >= last_cache_clear then
2010-10-24 23:17:33 +02:00
qh:CacheCleanup()
2012-01-01 03:22:56 +01:00
last_cache_clear = GetTime()
2010-10-24 23:17:33 +02:00
end
2010-10-24 23:17:33 +02:00
-- 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.
2010-10-24 23:17:33 +02:00
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)
2010-10-24 23:17:33 +02:00
--[[ 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]
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
local a, b = n.x-pos[3], n.y-pos[4]
pos[2][i] = math.sqrt(a*a+b*b)
end
end
end
2010-10-24 23:17:33 +02:00
local changed = false
2010-10-24 23:17:33 +02:00
if #route > 0 then
if self.recheck_position > #route then self.recheck_position = 1 end
local o = route[self.recheck_position]
2010-10-24 23:17:33 +02:00
--[[ assert(o.zones) ]]
o.filter_zone = o.zones[pos[1].i] == nil
o.filter_watched = not o:IsWatched()
2010-10-24 23:17:33 +02:00
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.
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
if o.swap_after then
qh:ReleaseTable(o.after)
o.after = o.swap_after
o.swap_after = nil
end
2010-10-24 23:17:33 +02:00
if o.is_sharing ~= o.want_share then
o.is_sharing = o.want_share
2010-10-24 23:17:33 +02:00
if o.want_share then
qh:DoShareObjective(o)
else
qh:DoUnshareObjective(o)
end
end
2010-10-24 23:17:33 +02:00
CalcObjectivePriority(o)
2010-10-24 23:17:33 +02:00
-- 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.
2010-10-24 23:17:33 +02:00
-- 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)
2010-10-24 23:17:33 +02:00
--[[ assert(aobt:sanity()) ]]
--[[ assert(best_route:sanity()) ]]
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
if new_real_distance < old_real_distance or sanityfixed then -- More of the temporary hack
-- If we're using the new path . . .
2010-10-24 23:17:33 +02:00
if old_index > new_index then
old_index, new_index = new_index, old_index
end
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
if old_index ~= new_index then
if old_index == 1 then
minimap_dodad:SetObjective(route[1])
end
2010-10-24 23:17:33 +02:00
changed = true
end
2010-10-24 23:17:33 +02:00
-- . . . 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
2010-10-24 23:17:33 +02:00
-- 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
2010-10-24 23:17:33 +02:00
end
end
2010-10-24 23:17:33 +02:00
-- Remove any waypoints if needed.
while true do
local obj = next(to_remove)
if not obj then break end
to_remove[obj] = nil
2010-10-24 23:17:33 +02:00
if obj.is_sharing then
obj.is_sharing = false
qh:DoUnshareObjective(obj)
end
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
obj:DoneRouting()
2010-10-24 23:17:33 +02:00
changed = true
end
2010-10-24 23:17:33 +02:00
-- Add any waypoints if needed
while true do
local obj = next(to_add)
if not obj then break end
to_add[obj] = nil
2010-10-24 23:17:33 +02:00
obj.filter_zone = obj.zones and obj.zones[pos[1].i] == nil
obj.filter_watched = not obj:IsWatched()
2010-10-24 23:17:33 +02:00
if obj:Known() then
obj:PrepareRouting()
2010-10-24 23:17:33 +02:00
obj.filter_zone = obj.zones[pos[1].i] == nil
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
CalcObjectivePriority(obj)
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
changed = true
end
else
add_swap[obj] = true
end
end
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
to_add, add_swap = add_swap, to_add
qh.to_add = to_add
self.add_swap = add_swap
2010-10-24 23:17:33 +02:00
if #best_route > 1 then
-- If there is 2 or more objectives, randomly combine routes to (hopefully) create something better than we had before.
2010-10-24 23:17:33 +02:00
-- 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
2010-10-24 23:17:33 +02:00
QH_Timeslice_Yield()
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
local to_breed, score
2010-10-24 23:17:33 +02:00
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
2010-10-24 23:17:33 +02:00
to_breed:breed(routes)
2010-10-24 23:17:33 +02:00
if 1/(qh:ComputeTravelTime(pos, to_breed[1].pos)+to_breed.distance+improve_margin) > max_fitness then
best = to_breed
end
2010-10-24 23:17:33 +02:00
QH_Timeslice_Yield()
2010-10-24 23:17:33 +02:00
if best ~= best_route then
2010-10-24 23:17:33 +02:00
--[[ assert(best:sanity()) ]]
--[[ assert(best_route:sanity()) ]]
2010-10-24 23:17:33 +02:00
best_route = best
self.best_route = best_route
2010-10-24 23:17:33 +02:00
for i, info in ipairs(best) do
local obj = info.obj
obj.pos = info.pos
route[i] = obj
end
2010-10-24 23:17:33 +02:00
minimap_dodad:SetObjective(route[1])
2010-10-24 23:17:33 +02:00
changed = true
end
end
2010-10-24 23:17:33 +02:00
if qh.defered_flight_times then
qh:buildFlightTimes()
qh.defered_flight_times = false
--[[ assert(qh.defered_graph_reset) ]]
end
2010-10-24 23:17:33 +02:00
if qh.defered_graph_reset then
QH_Timeslice_Yield()
2010-10-24 23:17:33 +02:00
for r in pairs(routes) do
r:pathResetBegin()
end
2010-10-24 23:17:33 +02:00
qh.graph_in_limbo = true
qh:ResetPathing()
qh.graph_in_limbo = false
qh.defered_graph_reset = false
2010-10-24 23:17:33 +02:00
for r in pairs(routes) do
r:pathResetEnd()
end
2010-10-24 23:17:33 +02:00
for i, info in ipairs(best_route) do
local obj = info.obj
obj.pos = info.pos
route[i] = obj
end
best_route:recalculateDistances()
2010-10-24 23:17:33 +02:00
minimap_dodad:SetObjective(route[1])
2010-10-24 23:17:33 +02:00
QuestHelper:SetTargetLocationRecalculate()
2010-10-24 23:17:33 +02:00
for r in pairs(routes) do
--[[ assert(r:sanity()) ]]
end
best_route:sanity()
2010-10-24 23:17:33 +02:00
QH_Timeslice_Yield()
end
2010-10-24 23:17:33 +02:00
if changed then
map_walker:RouteChanged()
end
2010-10-24 23:17:33 +02:00
--[[ assert(#route == #best_route) ]]
2010-10-24 23:17:33 +02:00
-- temporary hack to cause more errors
--qh.defered_graph_reset = true
--qh.defered_flight_times = true
2010-10-24 23:17:33 +02:00
QH_Timeslice_Yield()
end
end
function Routing:Initialize()
self:RoutingSetup()
2010-10-24 23:17:33 +02:00
QH_Timeslice_Add(function() Routing:RouteUpdateRoutine() end, "routing")
QH_Timeslice_Toggle("routing", false)
2010-10-24 23:17:33 +02:00
--[[
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.
2010-10-24 23:17:33 +02:00
-- This isn't part of WoW, I was using it in my driver program: Development/routetest
2010-10-24 23:17:33 +02:00
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)
2010-10-24 23:17:33 +02:00
if not state then
error(err, 0)
end
end)
else
update_route = coroutine.create(function() Routing:RouteUpdateRoutine() end)
end
]]
end