local GetTime = QuestHelper_GetTime QuestHelper_File["routing_core.lua"] = "4.0.1.$svnversion$" QuestHelper_Loadtime["routing_core.lua"] = GetTime() local debug_output = (QuestHelper_File["routing_core.lua"] == "Development Version") --[[ let's think about clustering Easiest way to pass in clusters, as we've already observed, is to just have a "cluster object" to pass in as an addition. This isn't a node, and we don't want to require "clusters" when people just want to add a single node. It isn't an objective either - it's a group of objectives, because when we return a route, we return a series of objectives. So, "add cluster" is intrinsically different. The next question, though, is how we link things. I'm liking the idea of the same ol' "link cluster X to cluster Y" thing. I think it's a good idea to create a list of "start nodes", though. We're going to restrict it so that dependencies can only be done with clusters, just for the sake of my sanity. This will also probably make it easier to ignore single parts of clusters, since we can do so by just tweaking the cluster definitions. I think this works. I think this works tomorrow. ]] --[[ hey hey ignoring is complicated so let's discuss that! Problem: One item can be ignored by multiple modules. Further problem: One item can be "de-ignored" explicitly by the player. Further further problem: either clusters or items can be ignored. Current solution: We provide "ignore" and "unignore" functions that take a cluster and an identifier. Ignoring only stacks if the identifier is unique. If X depends on Y, and Y is ignored, then X is implicitly ignored. Later, we'll provide something similar on items (or just dump items entirely? it's not like they're used for anything) ]] local QH_Timeslice_Yield = QH_Timeslice_Yield -- performance hack :( local mpow = math.pow local pairs, ipairs = pairs, ipairs local random = math.random local OptimizationHackery = false if OptimizationHackery then debug_output = false end -- :ughh: -- Ant colony optimization. Moving from X to Y has the quality (Distance[x,y]^alpha)*(Weight[x,y]^beta). Sum all available qualities, then choose weighted randomly. -- Weight adjustment: Weight[x,y] = Weight[x,y]*weightadj + sum(alltravels)(1/distance_of_travel) (note: this is somewhat out of date) -- Configuration local PheremonePreservation = 0.98 -- must be within 0 and 1 exclusive local AntCount = 20 -- number of ants to run before doing a pheremone pass -- Weighting for the various factors local WeightFactor = 0.61 local DistanceFactor = -2.5 local DistanceDeweight = 1.4 -- Add this to all distances to avoid sqrt(-1) deals -- Small amount to add to all weights to ensure it never hits, and to make sure things can still be chosen after a lot of iterations local UniversalBonus = 0.06 -- End configuration local Notifier local DistBatch -- Node storage and data structures local CurrentNodes = 1 local ActiveNodes = {1} local DeadNodes = {} local NodeIgnored = {[1] = {}} local NodeIgnoredCount = {[1] = 0} -- Clusters local Cluster = {} -- Goes from cluster ID to node IDs local ClusterLookup = {} -- Goes from node ID to cluster ID local ClusterTableLookup = {} -- Goes from the actual cluster table to the cluster ID local ClusterDead = {} -- List of dead clusters that can be reclaimed local ClusterIgnored = {} -- key-to-table-of-reasons-ignored local ClusterIgnoredCount = {} local ClusterIgnoredNodeActive = {} local ClusterPriority = {} local Priorities = {} local PriorityCount = {} local DependencyLinks = {} -- Every cluster that cluster X depends on local DependencyLinksReverse = {} -- Every cluster that is depended on by cluster X local DependencyCounts = {} -- How many different nodes cluster X depends on local StartNode = {ignore = true, loc = {x = 37690, y = 19671, p = 25, c = 0}} -- Ironforge mailbox :) local NodeLookup = {[StartNode] = 1} local NodeList = {[1] = StartNode} local Distance = {{0}} local Weight = {{0}} local DistanceWaiting = {} -- which node indices are waiting for distance data weight_ave = 0.001 -- End node storage and data structures local early_exit = false --[[ ---------------------------------- Here's that wacky storage system. ----------------------------------]] local function unsigned2b(c) if c > 65535 then -- ughh. again. print(c) c = 65535 end if not (c < 65536) then print(c) end QuestHelper: Assert(c < 65536) QuestHelper: Assert(bit.mod(c, 256)) QuestHelper: Assert(bit.rshift(c, 8)) local strix = strchar(bit.mod(c, 256), bit.rshift(c, 8)) QuestHelper: Assert(#strix == 2) return strix end -- L local loopcount = 0 local function Storage_Loop() loopcount = loopcount + 1 end local function Storage_LoopFlush() if loopcount > 0 then QH_Merger_Add(QH_Collect_Routing_Dump, "L" .. unsigned2b(loopcount) .. "L") loopcount = 0 end end -- - local function Storage_Distance_StoreFromIDToAll(id) if not QH_Collect_Routing_Dump then return end Storage_LoopFlush() QH_Merger_Add(QH_Collect_Routing_Dump, "-" .. unsigned2b(id)) for _, v in ipairs(ActiveNodes) do QH_Merger_Add(QH_Collect_Routing_Dump, unsigned2b(Distance[id][v])) end QH_Merger_Add(QH_Collect_Routing_Dump, "-") end -- X local function Storage_Distance_StoreCrossID(id) if not QH_Collect_Routing_Dump then return end Storage_LoopFlush() QH_Merger_Add(QH_Collect_Routing_Dump, "X") for _, v in ipairs(ActiveNodes) do QH_Merger_Add(QH_Collect_Routing_Dump, unsigned2b(Distance[id][v])) if v ~= id then QH_Merger_Add(QH_Collect_Routing_Dump, unsigned2b(Distance[v][id])) end end QH_Merger_Add(QH_Collect_Routing_Dump, "X") end -- # local function Storage_Distance_StoreAll() if not QH_Collect_Routing_Dump then return end Storage_LoopFlush() QH_Merger_Add(QH_Collect_Routing_Dump, "#") for _, v in ipairs(ActiveNodes) do for _, w in ipairs(ActiveNodes) do QH_Merger_Add(QH_Collect_Routing_Dump, unsigned2b(Distance[v][w])) end end QH_Merger_Add(QH_Collect_Routing_Dump, "#") end -- A local function Storage_NodeAdded(id) if not QH_Collect_Routing_Dump then return end Storage_LoopFlush() QH_Merger_Add(QH_Collect_Routing_Dump, "A" .. unsigned2b(id)) Storage_Distance_StoreCrossID(id) QH_Merger_Add(QH_Collect_Routing_Dump, "A") end -- R local function Storage_NodeRemoved(id) if not QH_Collect_Routing_Dump then return end Storage_LoopFlush() QH_Merger_Add(QH_Collect_Routing_Dump, "R" .. unsigned2b(id) .. "R") end -- C local function Storage_ClusterCreated(id) if not QH_Collect_Routing_Dump then return end Storage_LoopFlush() QH_Merger_Add(QH_Collect_Routing_Dump, "C" .. unsigned2b(id) .. unsigned2b(#Cluster[id])) for _, v in ipairs(Cluster[id]) do QH_Merger_Add(QH_Collect_Routing_Dump, unsigned2b(v)) end QH_Merger_Add(QH_Collect_Routing_Dump, "C") end -- D local function Storage_ClusterDestroyed(id) if not QH_Collect_Routing_Dump then return end Storage_LoopFlush() QH_Merger_Add(QH_Collect_Routing_Dump, "D" .. unsigned2b(id) .. "D") end -- > local function Storage_ClusterDependency(from, to) if not QH_Collect_Routing_Dump then return end Storage_LoopFlush() QH_Merger_Add(QH_Collect_Routing_Dump, ">" .. unsigned2b(from) .. unsigned2b(to) .. ">") end --[[ ---------------------------------- and here's the other end of the wacky storage system ----------------------------------]] -- we may need to play with these local QH_Route_Core_NodeAdd_Internal local QH_Route_Core_NodeRemove_Internal if OptimizationHackery then function Unstorage_SetDists(newdists) local tc = 1 QuestHelper: Assert(#newdists == #ActiveNodes * #ActiveNodes) for _, v in ipairs(ActiveNodes) do for _, w in ipairs(ActiveNodes) do Distance[v][w] = newdists[tc] tc = tc + 1 end end QuestHelper: Assert(tc - 1 == #newdists) end function Unstorage_SetDistsX(pivot, newdists) local tc = 1 QuestHelper: Assert(#newdists == #ActiveNodes * 2 - 1) for _, v in ipairs(ActiveNodes) do Distance[pivot][v] = newdists[tc] tc = tc + 1 if v ~= pivot then Distance[v][pivot] = newdists[tc] tc = tc + 1 end end QuestHelper: Assert(tc - 1 == #newdists) end function Unstorage_SetDistsLine(pivot, newdists) local tc = 1 QuestHelper: Assert(#newdists == #ActiveNodes) if pivot == 1 then if last_best and #last_best > 1 then last_best.distance = last_best.distance - Distance[last_best[1]][last_best[2]] end end for _, v in ipairs(ActiveNodes) do Distance[pivot][v] = newdists[tc] tc = tc + 1 end QuestHelper: Assert(tc - 1 == #newdists) if pivot == 1 then if last_best and #last_best > 1 then last_best.distance = last_best.distance + Distance[last_best[1]][last_best[2]] end end end function Unstorage_Add(nod) QH_Route_Core_NodeAdd_Internal({}, nod) end function Unstorage_Remove(nod) QH_Route_Core_NodeRemove_Internal({}, nod) end function Unstorage_ClusterAdd(nod, tab) QH_Route_Core_ClusterAdd({}, nod) for _, v in ipairs(tab) do QuestHelper: Assert(NodeList[v]) ClusterLookup[v] = nod table.insert(Cluster[nod], v) end end function Unstorage_ClusterRemove(nod) QH_Route_Core_ClusterRemove({}, nod) end function Unstorage_Link(a, b) QH_Route_Core_ClusterRequires(a, b, true) end function Unstorage_Nastyscan() for _, v in ipairs(ActiveNodes) do for _, w in ipairs(ActiveNodes) do QuestHelper: Assert(Distance[v][w]) QuestHelper: Assert(Weight[v][w]) end end end function Unstorage_Magic(tab) local touched = {} PheremonePreservation = tab.PheremonePreservation QuestHelper: Assert(PheremonePreservation) touched.PheremonePreservation = true AntCount = tab.AntCount QuestHelper: Assert(AntCount) touched.AntCount = true WeightFactor = tab.WeightFactor QuestHelper: Assert(WeightFactor) touched.WeightFactor = true DistanceFactor = tab.DistanceFactor QuestHelper: Assert(DistanceFactor) touched.DistanceFactor = true DistanceDeweight = tab.DistanceDeweight QuestHelper: Assert(DistanceDeweight) touched.DistanceDeweight = true UniversalBonus = tab.UniversalBonus QuestHelper: Assert(UniversalBonus) touched.UniversalBonus = true for k, v in pairs(tab) do QuestHelper: Assert(touched[k]) end end end --[[ ---------------------------------- here ends the butt of the wacky storage system. yeah, that's right. I said butt. Butt. Hee hee. Butt. ----------------------------------]] function QH_Route_Core_EarlyExit() early_exit = true QH_Timeslice_Bump("new_routing", 50) end function QH_Route_Core_NodeCount() return #ActiveNodes end function QH_Route_Core_TraverseNodes(func) for _, v in ipairs(ActiveNodes) do if v ~= 1 then local blocked = false if ClusterLookup[v] and DependencyLinks[ClusterLookup[v]] and #DependencyLinks[ClusterLookup[v]] > 0 then blocked = true end --print("nlx", NodeList[v], blocked) func(NodeList[v], blocked) end end end function QH_Route_Core_TraverseClusters(func) for k, v in pairs(ClusterTableLookup) do func(k) end end function QH_Route_Core_IgnoredReasons_Cluster(clust, func) for k, _ in pairs(ClusterIgnored[ClusterTableLookup[clust]]) do if type(k) == "table" then func(k) end end end function QH_Route_Core_IgnoredReasons_Node(node, func) for k, _ in pairs(NodeIgnored[NodeLookup[node]]) do if type(k) == "table" then func(k) end end end function QH_Route_Core_Ignored_Cluster(clust) return ClusterIgnoredCount[ClusterTableLookup[clust]] ~= 0 end function QH_Route_Core_Ignored_Cluster_Active(clust) return ClusterIgnoredNodeActive[ClusterTableLookup[clust]] end -- fuck floating-point local function almost(a, b) if a == b then return true end if type(a) ~= "number" or type(b) ~= "number" then return false end if a == 0 or b == 0 then return false end return math.abs(a / b - 1) < 0.0001 end -- Initialization function QH_Route_Core_Init(PathNotifier, DistanceBatch) Notifier = PathNotifier DistBatch = DistanceBatch QuestHelper: Assert(Notifier) QuestHelper: Assert(DistBatch) end -- End initialization local last_best = nil local last_best_tweaked = false local function ValidateNumber(x) QuestHelper: Assert(x == x) QuestHelper: Assert(x ~= math.huge) QuestHelper: Assert(x ~= -math.huge) end local function GetWeight(x, y) if x == y then return 0.00000000001 end -- sigh --local idx = GetIndex(x, y) --local revidx = GetIndex(y, x) if not Weight[x][y] or not Distance[x][y] then RTO(string.format("%d/%d %d", x, y, CurrentNodes)) QuestHelper: Assert(x <= CurrentNodes) QuestHelper: Assert(y <= CurrentNodes) QuestHelper: Assert(false) end local weight = mpow(Weight[x][y], WeightFactor) * mpow(Distance[x][y] + DistanceDeweight, DistanceFactor) --print(Weight[idx], Weight[revidx], bonus, WeightFactor, Distance[idx], DistanceFactor) --ValidateNumber(weight) return weight end -- Realtime splice local function DespliceCN(cluster, node) QuestHelper: Assert(not cluster or not node) QuestHelper: Assert(cluster or node) if not last_best then return end local ct = 0 for i = 2, #last_best do if last_best[i] and (last_best[i] == node or ClusterLookup[last_best[i]] == cluster) then -- Splice it out! last_best.distance = last_best.distance - Distance[last_best[i - 1]][last_best[i]] if i ~= #last_best then last_best.distance = last_best.distance - Distance[last_best[i]][last_best[i + 1]] end table.remove(last_best, i) if i ~= #last_best + 1 then last_best.distance = last_best.distance + Distance[last_best[i - 1]][last_best[i]] end ct = ct + 1 i = i - 1 end end last_best_tweaked = true --QuestHelper:TextOut("Despliced with " .. ct) end local function SpliceIn(index, touched) QuestHelper: Assert(index) QuestHelper: Assert(last_best) if touched[index] then return end QH_Timeslice_Yield() -- First, try to splice everything it depends on if DependencyLinks[index] then for _, v in ipairs(DependencyLinks[index]) do if SpliceIn(v, touched) then return true end end end local dl_lookup = QuestHelper:CreateTable("splice dl lookup") local dlr_lookup = QuestHelper:CreateTable("splice dlr lookup") if DependencyLinks[index] then for _, v in ipairs(DependencyLinks[index]) do dl_lookup[v] = true end end if DependencyLinksReverse[index] then for _, v in ipairs(DependencyLinksReverse[index]) do dlr_lookup[v] = true end end local start_bound = 2 local end_bound -- Next, figure out where it can go for i = 2, #last_best do --print(index, last_best[i], ClusterLookup[last_best[i]], dl_lookup[ClusterLookup[last_best[i]]], dlr_lookup[ClusterLookup[last_best[i]]], ClusterPriority[ClusterLookup[last_best[i]]], ClusterPriority[index]) if dl_lookup[ClusterLookup[last_best[i]]] then start_bound = i + 1 end if dlr_lookup[ClusterLookup[last_best[i]]] and not end_bound then end_bound = i end if ClusterPriority[ClusterLookup[last_best[i]]] < ClusterPriority[index] then start_bound = i + 1 end if ClusterPriority[ClusterLookup[last_best[i]]] > ClusterPriority[index] and not end_bound then end_bound = i end end if not end_bound then end_bound = #last_best + 1 end --QuestHelper: TextOut(string.format("Placed cluster %d between %d and %d", index, start_bound, end_bound)) if end_bound < start_bound then -- arrrrgh -- this should never happen, but I don't want it to show up all the time, sooooo QuestHelper_ErrorCatcher_ExplicitError(false, string.format("Routing paradox: %d and %d, panicking and restarting", start_bound, end_bound)) return true end -- Figure out the best place to put it local best_spot = nil local best_node = nil local best_cost = nil for i = start_bound, end_bound do for _, nindex in ipairs(Cluster[index]) do if NodeIgnoredCount[nindex] == 0 then local tcost = Distance[last_best[i - 1]][nindex] -- Cost of that-node-to-this-one if i <= #last_best then tcost = tcost + Distance[nindex][last_best[i]] - Distance[last_best[i - 1]][last_best[i]] end if not best_cost or tcost < best_cost then best_spot, best_node, best_cost = i, nindex, tcost end end end end QuestHelper: Assert(best_spot) table.insert(last_best, best_spot, best_node) last_best.distance = last_best.distance + best_cost touched[index] = true last_best_tweaked = true QuestHelper:ReleaseTable(dl_lookup) QuestHelper:CreateTable(dlr_lookup) end -- end realtime splice -- Yeah, this function, right here? This is QH's brain. This is the only thing in all of Questhelper that actually generates routes. THIS IS IT. local function RunAnt() local route = NewRoute() route[1] = 1 route.distance = 0 local dependencies = QuestHelper:CreateTable("route_core_dependencies") local needed = QuestHelper:CreateTable("route_core_needed") local needed_count = 0 local needed_ready_count = 0 for k, v in pairs(DependencyCounts) do dependencies[k] = v QuestHelper: Assert(dependencies[k] >= 0) end local curloc = 1 local gwc = QuestHelper:CreateTable("route_core_gwc") TestShit() for k, v in ipairs(Priorities) do if Priorities[k + 1] then QuestHelper: Assert(Priorities[k] < Priorities[k + 1]) end end for _, current_pri in ipairs(Priorities) do -- Here is we add the new batch of nodes for _, v in ipairs(ActiveNodes) do QH_Timeslice_Yield() if v ~= 1 then -- if it's ignored, then we just plain don't do anything local clustid = ClusterLookup[v] QuestHelper: Assert(clustid) if ClusterPriority[clustid] < current_pri then QuestHelper: Assert(dependencies[clustid] == -1 or NodeIgnoredCount[v] > 0 or ClusterIgnoredCount[clustid] >= 0) elseif ClusterPriority[clustid] == current_pri then if NodeIgnoredCount[v] == 0 and ClusterIgnoredCount[clustid] == 0 then local need = false QuestHelper: Assert(dependencies[clustid]) if dependencies[clustid] == 0 then needed[v] = true needed_ready_count = needed_ready_count + 1 end needed_count = needed_count + 1 end else QuestHelper: Assert(dependencies[clustid] ~= -1, clustid) end end end if not (needed_ready_count > 0 or needed_count == 0) then QuestHelper: Assert(needed_ready_count > 0 or needed_count == 0, string.format("%d %d", needed_ready_count, needed_count)) -- I should really rig this to output stuff of this sort more easily end while needed_count > 0 do QH_Timeslice_Yield() if early_exit then if debug_output then QuestHelper:TextOut("early exit") end return end QuestHelper: Assert(needed_ready_count > 0) local accumulated_weight = 0 local tweight = 0 for k, _ in pairs(needed) do local tw = GetWeight(curloc, k) gwc[k] = tw accumulated_weight = accumulated_weight + tw end tweight = accumulated_weight accumulated_weight = accumulated_weight * random() QH_Timeslice_Yield() if early_exit then if debug_output then QuestHelper:TextOut("early exit") end return end local nod = nil for k, _ in pairs(needed) do accumulated_weight = accumulated_weight - gwc[k] if accumulated_weight < 0 then nod = k break end end if not nod then RTO(string.format("no nod :( %f/%f", accumulated_weight, tweight)) for k, _ in pairs(needed) do nod = k break end end -- Now we've chosen stuff. Bookkeeping. local clust = ClusterLookup[nod] QuestHelper: Assert(clust) -- Obliterate other cluster items. Guaranteed to be at the same priority level. for _, v in pairs(Cluster[clust]) do if NodeIgnoredCount[v] == 0 then needed[v] = nil needed_count = needed_count - 1 needed_ready_count = needed_ready_count - 1 end end -- Dependency links. if DependencyLinksReverse[clust] then for _, v in ipairs(DependencyLinksReverse[clust]) do dependencies[v] = dependencies[v] - 1 QuestHelper: Assert(dependencies[v] >= 0) if dependencies[v] == 0 and ClusterIgnoredCount[v] == 0 and ClusterPriority[v] == current_pri then for _, v in pairs(Cluster[v]) do if NodeIgnoredCount[v] == 0 then needed[v] = true needed_ready_count = needed_ready_count + 1 end end end end end QuestHelper: Assert(dependencies[clust] == 0) QuestHelper: Assert(ClusterPriority[clust] == current_pri) dependencies[clust] = -1 --print(needed_count, needed_ready_count) route.distance = route.distance + Distance[curloc][nod] table.insert(route, nod) curloc = nod end QuestHelper: Assert(needed_ready_count == 0 and needed_count == 0) end QuestHelper:ReleaseTable(gwc) QuestHelper:ReleaseTable(dependencies) QuestHelper:ReleaseTable(needed) return route end -- Lots of doublechecks to make sure our route is both sane and complete local function CheckRoute(route) --print("starting check") QuestHelper: Assert(route[1] == 1) -- starting at the beginning local td = 0 for i = 1, #route - 1 do td = td + Distance[route[i]][route[i + 1]] end --print(td, route.distance) QuestHelper: Assert(abs(td - route.distance) < 5 or abs(td / route.distance - 1) < 0.0001) local seen = QuestHelper:CreateTable("check seen") local cpri = nil for i = 1, #route do QuestHelper: Assert(NodeIgnoredCount[route[i]] == 0) local clust = ClusterLookup[route[i]] if clust then --print("seeing cluster ", clust, cpri, ClusterPriority[clust]) QuestHelper: Assert(ClusterIgnoredCount[clust] == 0) QuestHelper: Assert(not seen[clust]) seen[clust] = true QuestHelper: Assert(not cpri or cpri <= ClusterPriority[clust]) cpri = ClusterPriority[clust] if DependencyLinks[clust] then for _, v in ipairs(DependencyLinks[clust]) do QuestHelper: Assert(seen[v]) end end if DependencyLinksReverse[clust] then for _, v in ipairs(DependencyLinksReverse[clust]) do QuestHelper: Assert(not seen[v]) end end end end for k, v in pairs(ClusterIgnoredCount) do QuestHelper: Assert(not seen[k] == (ClusterIgnoredCount[k] > 0)) end QuestHelper:ReleaseTable(seen) --print("ending check") end local function BetterRoute(route) CheckRoute(route) local rt = {} for k, v in ipairs(route) do rt[k] = NodeList[v] end rt.distance = route.distance -- this is probably temporary Notifier(rt) end local QH_Route_Core_DistanceClear_Local -- sigh -- Core process function function QH_Route_Core_Process() early_exit = false --QuestHelper:TextOut("Startprocess") Storage_Loop() --QuestHelper:TextOut("Pathing") -- First we check to see if we need to add more distances, and if so, we do so local route_tweak_progress local better_route_progress do local refreshcount = 0 for k, v in pairs(DistanceWaiting) do refreshcount = refreshcount + 1 end --[[ assert(not QuestHelper.route_change_progress) ]] if refreshcount > 0 then QuestHelper.route_change_progress = QuestHelper.CreateLoadingCounter() route_tweak_progress = QuestHelper.route_change_progress:MakeSubcategory(0.1) better_route_progress = QuestHelper.route_change_progress:MakeSubcategory(0.2) if debug_output then QuestHelper:TextOut(string.format("Refreshing %d", refreshcount)) end if refreshcount >= #ActiveNodes / 2 then -- Refresh everything! QH_Route_Core_DistanceClear_Local(QuestHelper.route_change_progress:MakeSubcategory(0.7)) else local resynch_progress = QuestHelper.route_change_progress:MakeSubcategory(0.7) local tlnod = QuestHelper:CreateTable("routecore distance tlnod") for _, v in ipairs(ActiveNodes) do table.insert(tlnod, NodeList[v]) end local ct = 0 for idx, _ in pairs(DistanceWaiting) do ct = ct + 1 end local ctd = 0 for idx, _ in pairs(DistanceWaiting) do -- Refresh a subset of things. local forward = DistBatch(NodeList[idx], tlnod) local backward = DistBatch(NodeList[idx], tlnod, true) for k, v in ipairs(ActiveNodes) do Distance[idx][v] = forward[k] Distance[v][idx] = backward[k] end QuestHelper:ReleaseTable(forward) QuestHelper:ReleaseTable(backward) ctd = ctd + 1 resynch_progress:SetPercentage(ctd / ct) end QuestHelper:ReleaseTable(tlnod) end QuestHelper:ReleaseTable(DistanceWaiting) DistanceWaiting = QuestHelper:CreateTable("routecore distance waiting") end end --QuestHelper:TextOut("Inserting/removing") -- Next we see if last_best needs tweaking if last_best then local touched_clusts = QuestHelper:CreateTable("routing touched") for i = 2, #last_best do local clust = ClusterLookup[last_best[i]] QuestHelper: Assert(clust) QuestHelper: Assert(not touched_clusts[clust]) touched_clusts[clust] = true end if not route_tweak_progress then --[[ assert(not QuestHelper.route_change_progress) ]] QuestHelper.route_change_progress = QuestHelper.CreateLoadingCounter() route_tweak_progress = QuestHelper.route_change_progress:MakeSubcategory(0.1) better_route_progress = QuestHelper.route_change_progress:MakeSubcategory(0.9) end local ct = 0 for k, _ in pairs(Cluster) do ct = ct + 1 end local ctd = 0 for k, _ in pairs(Cluster) do local exists = touched_clusts[k] local ignored = (ClusterIgnoredCount[k] ~= 0) QuestHelper: Assert(not (ignored and exists)) -- something went wrong, item should have been removed if not ignored and not exists then -- here we go if SpliceIn(k, touched_clusts) then last_best = nil break end last_best_tweaked = true end ctd = ctd + 1 route_tweak_progress:SetPercentage(ctd / ct) end QuestHelper:ReleaseTable(touched_clusts) end --QuestHelper:TextOut("Posting") if last_best_tweaked and last_best then --QuestHelper:TextOut("Pushing tweaked") BetterRoute(last_best, better_route_progress) last_best_tweaked = false end QH_Timeslice_Bump_Reset("new_routing") QuestHelper.route_change_progress = nil local worst = 0 local best_is_local = false --QuestHelper:TextOut("Anting") local trouts = QuestHelper:CreateTable("routing_core_trouts") -- Run the ants through the system building routes. for x = 1, AntCount do if early_exit then if debug_output then QuestHelper:TextOut("early exit") end return end -- get money fuck routing local ant = RunAnt() if ant then table.insert(trouts, ant) end if early_exit then if debug_output then QuestHelper:TextOut("early exit") end return end --if last_best then RTO(string.format("Path generated: %s vs %s", PathToString(trouts[#trouts]), PathToString(last_best))) end if not last_best or last_best.distance > trouts[#trouts].distance then if last_best and not best_is_local then QuestHelper:ReleaseTable(last_best) end best_is_local = true last_best = trouts[#trouts] BetterRoute(last_best) end worst = math.max(worst, trouts[#trouts].distance) QH_Timeslice_Yield() end --QuestHelper:TextOut("Cleanup") local scale if worst == last_best.distance then scale = 1 else scale = 1 / (worst - last_best.distance) end QH_Timeslice_Yield() -- Next 3 loops are essentially pheromone update. for _, x in ipairs(ActiveNodes) do local wx = Weight[x] for _, y in ipairs(ActiveNodes) do --local idx = GetIndex(x, y) wx[y] = wx[y] * PheremonePreservation + UniversalBonus --ValidateNumber(Weight[idx]) end end QH_Timeslice_Yield() for _, x in ipairs(trouts) do local amount = 1 / x.distance for y = 1, #x - 1 do --local idx = GetIndex(x[y], x[y + 1]) Weight[x[y]][x[y + 1]] = Weight[x[y]][x[y + 1]] + amount --ValidateNumber(Weight[idx]) end end QH_Timeslice_Yield() local weitotal = 0 local weicount = 0 for _, x in ipairs(ActiveNodes) do local wx = Weight[x] for _, y in ipairs(ActiveNodes) do --local idx = GetIndex(x, y) weitotal = weitotal + wx[y] weicount = weicount + 1 end end QH_Timeslice_Yield() weight_ave = weitotal / weicount for k, v in pairs(trouts) do if v ~= last_best then QuestHelper:ReleaseTable(v) end end QuestHelper:ReleaseTable(trouts) QH_Timeslice_Yield() -- "heh" --QuestHelper:TextOut("Done") end -- End core loop function QH_Core_Bump() for _, x in ipairs(ActiveNodes) do local wx = Weight[x] for _, y in ipairs(ActiveNodes) do wx[y] = weight_ave end end QH_Route_Core_EarlyExit() end -- Ignore/unignore local function RecursiveIgnoreCount(clustid, accum) if accum == 0 then return end --print(clustid, accum) if ClusterIgnoredCount[clustid] == 0 then QuestHelper: Assert(accum > 0) DespliceCN(clustid) end ClusterIgnoredCount[clustid] = ClusterIgnoredCount[clustid] + accum if ClusterIgnoredCount[clustid] == 0 then QuestHelper: Assert(accum < 0) end -- Item being added, we'll handle this at the beginning of run if DependencyLinksReverse[clustid] then for _, v in pairs(DependencyLinksReverse[clustid]) do RecursiveIgnoreCount(v, accum) end end end local function Internal_IgnoreCluster(clustid, reason) QuestHelper: Assert(clustid) if not ClusterIgnored[clustid][reason] then ClusterIgnored[clustid][reason] = true RecursiveIgnoreCount(clustid, 1) end end local function Internal_UnignoreCluster(clustid, reason) QuestHelper: Assert(clustid) if ClusterIgnored[clustid][reason] then ClusterIgnored[clustid][reason] = nil RecursiveIgnoreCount(clustid, -1) end end function QH_Route_Core_IgnoreCluster(clust, reason) QuestHelper: Assert(type(reason) == "table") local clustid = ClusterTableLookup[clust] if not clustid then -- This can just happen due to the lag introduced by the controller, so, whatever --QuestHelper:TextOut("Attempted to ignore a cluster that no longer exists") return end Internal_IgnoreCluster(clustid, reason) end function QH_Route_Core_UnignoreCluster(clust, reason) QuestHelper: Assert(type(reason) == "table") local clustid = ClusterTableLookup[clust] if not clustid then -- This can just happen due to the lag introduced by the controller, so, whatever --QuestHelper:TextOut("Attempted to unignore a cluster that no longer exists") return end Internal_UnignoreCluster(clustid, reason) end function QH_Route_Core_IgnoreNode(node, reason) QuestHelper: Assert(type(reason) == "table") local nid = NodeLookup[node] if not nid then -- This can just happen due to the lag introduced by the controller, so, whatever --QuestHelper:TextOut("Attempted to ignore a node that no longer exists") return end QuestHelper: Assert(nid) if not NodeIgnored[nid][reason] then NodeIgnored[nid][reason] = true NodeIgnoredCount[nid] = NodeIgnoredCount[nid] + 1 if NodeIgnoredCount[nid] == 1 then DespliceCN(nil, nid) if ClusterLookup[nid] then local cloost = ClusterLookup[nid] ClusterIgnoredNodeActive[cloost] = ClusterIgnoredNodeActive[cloost] - 1 if ClusterIgnoredNodeActive[cloost] == 0 then Internal_IgnoreCluster(cloost, "internal_node_ignored") end end end end end function QH_Route_Core_UnignoreNode(node, reason) QuestHelper: Assert(type(reason) == "table") local nid = NodeLookup[node] if not nid then -- This can just happen due to the lag introduced by the controller, so, whatever --QuestHelper:TextOut("Attempted to unignore a node that no longer exists") return end QuestHelper: Assert(nid) if NodeIgnored[nid][reason] then NodeIgnored[nid][reason] = nil NodeIgnoredCount[nid] = NodeIgnoredCount[nid] - 1 if NodeIgnoredCount[nid] == 0 then -- Item being added if ClusterLookup[nid] then -- Item being added ClusterIgnoredNodeActive[ClusterLookup[nid]] = ClusterIgnoredNodeActive[ClusterLookup[nid]] + 1 if ClusterIgnoredNodeActive[ClusterLookup[nid]] == 1 then Internal_UnignoreCluster(ClusterLookup[nid], "internal_node_ignored") end end end end end local QH_Route_Core_UnignoreCluster = QH_Route_Core_UnignoreCluster -- we're just saving this so it doesn't get splattered -- End ignore/unignore -- Node allocation and deallocation -- this is only separate so we can use it for the crazy optimization hackery local function Expand() for _, v in ipairs(Distance) do table.insert(v, 0) end for _, v in ipairs(Weight) do table.insert(v, 0) end table.insert(Distance, {}) table.insert(Weight, {}) for k = 1, CurrentNodes + 1 do table.insert(Distance[#Distance], 0) table.insert(Weight[#Weight], 0) end CurrentNodes = CurrentNodes + 1 end -- This is pretty bad overall. Going from 0 nodes to N nodes is an O(n^3) operation. Eugh. Todo: allocate more than one at a time? local function AllocateExtraNode() if #DeadNodes > 0 then local nod = table.remove(DeadNodes) table.insert(ActiveNodes, nod) table.sort(ActiveNodes) return nod end -- We always allocate on the end, so we know this is safe. Expand() table.insert(DeadNodes, CurrentNodes) return AllocateExtraNode() -- ha ha end -- Set the start location function QH_Route_Core_SetStart(stt) -- We do some kind of ghastly things here. --TestShit() if last_best and #last_best > 1 then last_best.distance = last_best.distance - Distance[last_best[1]][last_best[2]] end NodeLookup[StartNode] = nil NodeList[1] = stt StartNode = stt NodeLookup[StartNode] = 1 local tlnod = QuestHelper:CreateTable("routecore setstart tlnod") for _, v in ipairs(ActiveNodes) do if v ~= 1 then table.insert(tlnod, NodeList[v]) end end local forward = DistBatch(NodeList[1], tlnod) local ct = 1 for _, v in ipairs(ActiveNodes) do if v ~= 1 then QuestHelper: Assert(forward[ct]) Distance[1][v] = forward[ct] ct = ct + 1 Distance[v][1] = 65500 -- this should never be used anyway end end if last_best and #last_best > 1 then last_best.distance = last_best.distance + Distance[last_best[1]][last_best[2]] end QuestHelper:ReleaseTable(forward) QuestHelper:ReleaseTable(tlnod) Storage_Distance_StoreFromIDToAll(1) --TestShit() -- TODO: properly deallocate old startnode? end QH_Route_Core_NodeAdd_Internal = function (nod, used_idx) --QuestHelper:TextOut(tostring(nod)) --TestShit() QuestHelper: Assert(nod) if NodeLookup[nod] then -- ughhh QuestHelper: Assert(not NodeLookup[nod], QuestHelper:StringizeTable(nod)) end local idx if used_idx then QuestHelper: Assert(OptimizationHackery) QuestHelper: Assert(not NodeList[used_idx]) idx = used_idx table.insert(ActiveNodes, used_idx) table.sort(ActiveNodes) if not Distance[idx] then Expand() QuestHelper: Assert(Distance[idx]) end else idx = AllocateExtraNode() end --RTO("|cffFF8080AEN: " .. tostring(idx)) NodeLookup[nod] = idx NodeList[idx] = nod NodeIgnored[idx] = {} NodeIgnoredCount[idx] = 0 for _, v in ipairs(ActiveNodes) do Weight[v][idx] = weight_ave Weight[idx][v] = weight_ave end DistanceWaiting[idx] = true -- Item being added return idx end -- Remove a node with the given location QH_Route_Core_NodeRemove_Internal = function (nod, used_idx) --TestShit() QuestHelper: Assert(nod) local idx if used_idx then QuestHelper: Assert(OptimizationHackery) QuestHelper: Assert(NodeList[used_idx]) idx = used_idx else QuestHelper: Assert(NodeLookup[nod]) idx = NodeLookup[nod] end --RTO("|cffFF8080RFN: " .. tostring(NodeLookup[nod])) NodeList[idx] = nil table.insert(DeadNodes, idx) local oas = #ActiveNodes for k, v in pairs(ActiveNodes) do if v == idx then table.remove(ActiveNodes, k) break end end -- this is pretty awful QuestHelper: Assert(#ActiveNodes < oas) NodeLookup[nod] = nil -- We don't have to modify the table itself, some sections are just "dead". --TestShit() DistanceWaiting[idx] = nil -- just in case we haven't updated it in the intervening time -- If we're a standalone node, nothing depends on us. If we're part of a cluster, the cluster's getting smoked anyway. NodeIgnored[idx] = nil NodeIgnoredCount[idx] = nil DespliceCN(nil, idx) return idx end -- End node allocation and deallocation function QH_Route_Core_ClusterAdd(clust, clustid_used) local clustid if clustid_used then QuestHelper: Assert(OptimizationHackery) QuestHelper: Assert(not Cluster[clustid_used]) clustid = clustid_used else QuestHelper: Assert(#clust > 0) clustid = table.remove(ClusterDead) if not clustid then clustid = #Cluster + 1 end end if debug_output then QuestHelper:TextOut(string.format("Adding cluster %d", clustid)) end Cluster[clustid] = {} ClusterTableLookup[clust] = clustid ClusterIgnored[clustid] = {} ClusterIgnoredCount[clustid] = 0 ClusterIgnoredNodeActive[clustid] = #clust ClusterPriority[clustid] = 0 if not PriorityCount[0] then table.insert(Priorities, 0) table.sort(Priorities) end PriorityCount[0] = (PriorityCount[0] or 0) + 1 -- if we're doing hackery, clust will just be an empty table and we'll retrofit stuff later for _, v in ipairs(clust) do local idx = QH_Route_Core_NodeAdd_Internal(v) Storage_NodeAdded(idx) ClusterLookup[idx] = clustid table.insert(Cluster[clustid], idx) end DependencyCounts[clustid] = 0 Storage_ClusterCreated(clustid) end function QH_Route_Core_ClusterRemove(clust, clustid_used) local clustid if clustid_used then QuestHelper: Assert(OptimizationHackery) QuestHelper: Assert(Cluster[clustid_used]) clustid = clustid_used for _, v in ipairs(Cluster[clustid]) do QH_Route_Core_NodeRemove_Internal({}, v) ClusterLookup[v] = nil end else clustid = ClusterTableLookup[clust] end do local ct = 0 local abort repeat QuestHelper: Assert(ct < 100) abort = true for k, v in pairs(ClusterIgnored[clustid]) do abort = false Internal_UnignoreCluster(clustid, k) ct = ct + 1 break end until abort -- Imagine a->b->c. a is ignored, and b is deleted. This decouples a from c (todo: should it?) but we need to reduce c's ignore count appropriately so it's unignored. RecursiveIgnoreCount(clustid, -ClusterIgnoredCount[clustid]) QuestHelper: Assert(ClusterIgnoredCount[clustid] == 0) end if debug_output then QuestHelper:TextOut(string.format("Removing cluster %d", clustid)) end for _, v in ipairs(clust) do local idx = QH_Route_Core_NodeRemove_Internal(v) ClusterLookup[idx] = nil end DependencyCounts[clustid] = nil if DependencyLinks[clustid] then for k, v in pairs(DependencyLinks[clustid]) do for m, f in pairs(DependencyLinksReverse[v]) do if f == clustid then if debug_output then QuestHelper:TextOut(string.format("Unlinking cluster %d needs %d", clustid, v)) end table.remove(DependencyLinksReverse[v], m) break end end end end DependencyLinks[clustid] = nil if DependencyLinksReverse[clustid] then for k, v in pairs(DependencyLinksReverse[clustid]) do for m, f in pairs(DependencyLinks[v]) do if f == clustid then if debug_output then QuestHelper:TextOut(string.format("Unlinking cluster %d needs %d", v, clustid)) end table.remove(DependencyLinks[v], m) DependencyCounts[v] = DependencyCounts[v] - 1 break end end end end DependencyLinksReverse[clustid] = nil Cluster[clustid] = nil ClusterTableLookup[clust] = nil table.insert(ClusterDead, clustid) ClusterIgnored[clustid] = nil ClusterIgnoredCount[clustid] = nil ClusterIgnoredNodeActive[clustid] = nil local pri = ClusterPriority[clustid] PriorityCount[pri] = PriorityCount[pri] - 1 if PriorityCount[pri] == 0 then PriorityCount[pri] = nil for k, v in ipairs(Priorities) do if v == pri then Priorities[k] = Priorities[#Priorities] table.remove(Priorities) table.sort(Priorities) break end end end ClusterPriority[clustid] = nil Storage_ClusterDestroyed(clustid) end local QH_Route_Core_SetClusterPriority_Internal -- Add a note that node 1 requires node 2. function QH_Route_Core_ClusterRequires(a, b, hackery) local aidx local bidx if hackery then QuestHelper: Assert(OptimizationHackery) QuestHelper: Assert(Cluster[a]) QuestHelper: Assert(Cluster[b]) aidx, bidx = a, b else aidx = ClusterTableLookup[a] bidx = ClusterTableLookup[b] end QuestHelper: Assert(aidx) QuestHelper: Assert(bidx) QuestHelper: Assert(aidx ~= bidx) if debug_output then QuestHelper:TextOut(string.format("Linking cluster %d needs %d", aidx, bidx)) end DependencyCounts[aidx] = DependencyCounts[aidx] + 1 if not DependencyLinks[aidx] then DependencyLinks[aidx] = {} end table.insert(DependencyLinks[aidx], bidx) if not DependencyLinksReverse[bidx] then DependencyLinksReverse[bidx] = {} end table.insert(DependencyLinksReverse[bidx], aidx) DespliceCN(aidx) DespliceCN(bidx) Storage_ClusterDependency(aidx, bidx) QH_Route_Core_SetClusterPriority_Internal(bidx, ClusterPriority[bidx], true) end function QH_Route_Core_GetClusterPriority(clust) return ClusterPriority[ClusterTableLookup[clust]] end function QH_Route_Core_SetClusterPriority_Internal(clustid, new_pri, force) QuestHelper: Assert(clustid) if not force and ClusterPriority[clustid] == new_pri then return end --QuestHelper:TextOut(string.format("Setting %d to %d", clustid, new_pri)) local pri = ClusterPriority[clustid] QuestHelper: Assert(pri) PriorityCount[pri] = PriorityCount[pri] - 1 if PriorityCount[pri] == 0 then PriorityCount[pri] = nil for k, v in ipairs(Priorities) do if v == pri then Priorities[k] = Priorities[#Priorities] table.remove(Priorities) table.sort(Priorities) break end end end ClusterPriority[clustid] = new_pri if not PriorityCount[new_pri] then table.insert(Priorities, new_pri) table.sort(Priorities) end PriorityCount[new_pri] = (PriorityCount[new_pri] or 0) + 1 DespliceCN(clustid) -- NOTE: These are recursive functions. It is vitally important that these not be called if nothing is changing, and it is vitally important that we change the local node first, otherwise we'll get infinite recursion and explosions. Or even EXPLOISIONS. -- Clusters that this one depends on. Must happen first (i.e. have a smaller or equal priority) if DependencyLinks[clustid] then for _, v in ipairs(DependencyLinks[clustid]) do if ClusterPriority[v] > new_pri then QH_Route_Core_SetClusterPriority_Internal(v, new_pri) end end end -- Clusters that depend on this one. Must happen last (i.e. have a greater or equal priority) if DependencyLinksReverse[clustid] then for _, v in ipairs(DependencyLinksReverse[clustid]) do if ClusterPriority[v] < new_pri then QH_Route_Core_SetClusterPriority_Internal(v, new_pri) end end end end function QH_Route_Core_SetClusterPriority(clust, new_pri) QuestHelper: Assert(clust) local clustid = ClusterTableLookup[clust] if clustid then QH_Route_Core_SetClusterPriority_Internal(clustid, new_pri) end end -- Wipe and re-cache all distances. function QH_Route_Core_DistanceClear(progress) local tlnod = {} for _, v in ipairs(ActiveNodes) do table.insert(tlnod, NodeList[v]) end for ani, idx in ipairs(ActiveNodes) do local forward = DistBatch(NodeList[idx], tlnod, false, true) for k, v in ipairs(ActiveNodes) do Distance[idx][v] = forward[k] end if QuestHelper.loading_preroll and #ActiveNodes > 1 then QuestHelper.loading_preroll:SetPercentage(ani / #ActiveNodes) end if progress then progress:SetPercentage(ani / #ActiveNodes) end end if last_best then last_best.distance = 0 for i = 1, #last_best - 1 do last_best.distance = last_best.distance + Distance[last_best[i]][last_best[i + 1]] end end Storage_Distance_StoreAll() end QH_Route_Core_DistanceClear_Local = QH_Route_Core_DistanceClear function findin(tab, val) local ct = 0 for k, v in pairs(tab) do if v == val then ct = ct + 1 end end return ct == 1 end function TestShit() --[[ for x = 1, #ActiveNodes do local ts = "" for y = 1, #ActiveNodes do ts = ts .. string.format("%f ", Distance[GetIndex(ActiveNodes[x], ActiveNodes[y])]) end RTO(ts) end ]] --[[ RTO("Lookup table") for x = 1, #ActiveNodes do RTO(tostring(ActiveNodes[x])) end RTO("Lookup table done") ]] --[=[ local fail = false for x = 1, #ActiveNodes do for y = 2, #ActiveNodes do if not (almost(Dist(NodeList[ActiveNodes[x]], NodeList[ActiveNodes[y]]), Distance[ActiveNodes[x]][ActiveNodes[y]])) then RTO(string.format("%d/%d (%d/%d) should be %f, is %f", x, y, ActiveNodes[x], ActiveNodes[y], Dist(NodeList[ActiveNodes[x]], NodeList[ActiveNodes[y]]),Distance[ActiveNodes[x]][ActiveNodes[y]])) fail = true end end end]=] for k, v in pairs(DependencyLinks) do QuestHelper: Assert(#v == DependencyCounts[k]) end for k, v in pairs(DependencyCounts) do QuestHelper: Assert(v == (DependencyLinks[k] and #DependencyLinks[k] or 0)) end for k, v in pairs(DependencyLinks) do for _, v2 in pairs(v) do QuestHelper: Assert(findin(DependencyLinksReverse[v2], k)) QuestHelper: Assert(ClusterPriority[v2] <= ClusterPriority[k]) end end for k, v in pairs(DependencyLinksReverse) do for _, v2 in pairs(v) do QuestHelper: Assert(findin(DependencyLinks[v2], k)) QuestHelper: Assert(ClusterPriority[v2] >= ClusterPriority[k]) end end QuestHelper: Assert(not fail) end --[=[ function HackeryDump() local st = "{" for k, v in pairs(ActiveNodes) do if v ~= 1 then st = st .. string.format("{c = %d, x = %f, y = %f}, ", NodeList[k].loc.c, NodeList[k].loc.x, NodeList[k].loc.y) end end st = st .. "}" QuestHelper: Assert(false, st) end]=] -- weeeeee