try to track this thing down better
[QuestHelper.git] / routing_core.lua
bloba70cd8c106131c8b525f62842d468246dae691ce
1 QuestHelper_File["routing_core.lua"] = "Development Version"
2 QuestHelper_Loadtime["routing_core.lua"] = GetTime()
4 local debug_output = (QuestHelper_File["routing_core.lua"] == "Development Version")
6 --[[
8 let's think about clustering
10 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.
12 So, "add cluster" is intrinsically different.
14 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.
16 We're going to restrict it so that dependencies can only be done with clusters, just for the sake of my sanity.
17 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.
19 I think this works tomorrow.
23 --[[
25 hey hey ignoring is complicated so let's discuss that!
27 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.
29 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.
31 Later, we'll provide something similar on items (or just dump items entirely? it's not like they're used for anything)
35 local OptimizationHackery = false
37 if OptimizationHackery then debug_output = false end -- :ughh:
40 -- 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.
41 -- Weight adjustment: Weight[x,y] = Weight[x,y]*weightadj + sum(alltravels)(1/distance_of_travel) (note: this is somewhat out of date)
43 -- Configuration
44 local PheremonePreservation = 0.98 -- must be within 0 and 1 exclusive
45 local AntCount = 20 -- number of ants to run before doing a pheremone pass
47 -- Weighting for the various factors
48 local WeightFactor = 0.61
49 local DistanceFactor = -2.5
50 local DistanceDeweight = 1.4 -- Add this to all distances to avoid sqrt(-1) deals
52 -- 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
53 local UniversalBonus = 0.06
54 -- End configuration
56 local Notifier
57 local DistBatch
59 -- Node storage and data structures
60 local CurrentNodes = 1
61 local ActiveNodes = {1}
62 local DeadNodes = {}
64 local NodeIgnored = {[1] = {}}
65 local NodeIgnoredCount = {[1] = 0}
67 -- Clusters
68 local Cluster = {} -- Goes from cluster ID to node IDs
69 local ClusterLookup = {} -- Goes from node ID to cluster ID
70 local ClusterTableLookup = {} -- Goes from the actual cluster table to the cluster ID
71 local ClusterDead = {} -- List of dead clusters that can be reclaimed
73 local ClusterIgnored = {} -- key-to-table-of-reasons-ignored
74 local ClusterIgnoredCount = {}
75 local ClusterIgnoredNodeActive = {}
77 local ClusterPriority = {}
78 local Priorities = {}
79 local PriorityCount = {}
81 local DependencyLinks = {} -- Every cluster that cluster X depends on
82 local DependencyLinksReverse = {} -- Every cluster that is depended on by cluster X
83 local DependencyCounts = {} -- How many different nodes cluster X depends on
85 local StartNode = {ignore = true, loc = {x = 37690, y = 19671, p = 25, c = 0}} -- Ironforge mailbox :)
87 local NodeLookup = {[StartNode] = 1}
88 local NodeList = {[1] = StartNode}
89 local Distance = {{0}}
90 local Weight = {{0}}
92 local DistanceWaiting = {} -- which node indices are waiting for distance data
94 weight_ave = 0.001
95 -- End node storage and data structures
97 --[[
98 ----------------------------------
99 Here's that wacky storage system.
100 ----------------------------------]]
102 local function unsigned2b(c)
103 if c > 65535 then -- ughh. again.
104 print(c)
105 c = 65535
108 if not (c < 65536) then
109 print(c)
111 QuestHelper: Assert(c < 65536)
113 QuestHelper: Assert(bit.mod(c, 256))
114 QuestHelper: Assert(bit.rshift(c, 8))
115 local strix = strchar(bit.mod(c, 256), bit.rshift(c, 8))
116 QuestHelper: Assert(#strix == 2)
117 return strix
120 -- L
121 local loopcount = 0
122 local function Storage_Loop()
123 loopcount = loopcount + 1
125 local function Storage_LoopFlush()
126 if loopcount > 0 then
127 QH_Merger_Add(QH_Collect_Routing_Dump, "L" .. unsigned2b(loopcount) .. "L")
128 loopcount = 0
132 -- -
133 local function Storage_Distance_StoreFromIDToAll(id)
134 if not QH_Collect_Routing_Dump then return end
135 Storage_LoopFlush()
137 QH_Merger_Add(QH_Collect_Routing_Dump, "-" .. unsigned2b(id))
138 for _, v in ipairs(ActiveNodes) do
139 QH_Merger_Add(QH_Collect_Routing_Dump, unsigned2b(Distance[id][v]))
141 QH_Merger_Add(QH_Collect_Routing_Dump, "-")
144 -- X
145 local function Storage_Distance_StoreCrossID(id)
146 if not QH_Collect_Routing_Dump then return end
147 Storage_LoopFlush()
149 QH_Merger_Add(QH_Collect_Routing_Dump, "X")
150 for _, v in ipairs(ActiveNodes) do
151 QH_Merger_Add(QH_Collect_Routing_Dump, unsigned2b(Distance[id][v]))
152 if v ~= id then QH_Merger_Add(QH_Collect_Routing_Dump, unsigned2b(Distance[v][id])) end
154 QH_Merger_Add(QH_Collect_Routing_Dump, "X")
157 -- #
158 local function Storage_Distance_StoreAll()
159 if not QH_Collect_Routing_Dump then return end
160 Storage_LoopFlush()
162 QH_Merger_Add(QH_Collect_Routing_Dump, "#")
163 for _, v in ipairs(ActiveNodes) do
164 for _, w in ipairs(ActiveNodes) do
165 QH_Merger_Add(QH_Collect_Routing_Dump, unsigned2b(Distance[v][w]))
168 QH_Merger_Add(QH_Collect_Routing_Dump, "#")
171 -- A
172 local function Storage_NodeAdded(id)
173 if not QH_Collect_Routing_Dump then return end
174 Storage_LoopFlush()
176 QH_Merger_Add(QH_Collect_Routing_Dump, "A" .. unsigned2b(id))
177 Storage_Distance_StoreCrossID(id)
178 QH_Merger_Add(QH_Collect_Routing_Dump, "A")
181 -- R
182 local function Storage_NodeRemoved(id)
183 if not QH_Collect_Routing_Dump then return end
184 Storage_LoopFlush()
186 QH_Merger_Add(QH_Collect_Routing_Dump, "R" .. unsigned2b(id) .. "R")
189 -- C
190 local function Storage_ClusterCreated(id)
191 if not QH_Collect_Routing_Dump then return end
192 Storage_LoopFlush()
194 QH_Merger_Add(QH_Collect_Routing_Dump, "C" .. unsigned2b(id) .. unsigned2b(#Cluster[id]))
195 for _, v in ipairs(Cluster[id]) do
196 QH_Merger_Add(QH_Collect_Routing_Dump, unsigned2b(v))
198 QH_Merger_Add(QH_Collect_Routing_Dump, "C")
201 -- D
202 local function Storage_ClusterDestroyed(id)
203 if not QH_Collect_Routing_Dump then return end
204 Storage_LoopFlush()
206 QH_Merger_Add(QH_Collect_Routing_Dump, "D" .. unsigned2b(id) .. "D")
209 -- >
210 local function Storage_ClusterDependency(from, to)
211 if not QH_Collect_Routing_Dump then return end
212 Storage_LoopFlush()
214 QH_Merger_Add(QH_Collect_Routing_Dump, ">" .. unsigned2b(from) .. unsigned2b(to) .. ">")
217 --[[
218 ----------------------------------
219 and here's the other end of the wacky storage system
220 ----------------------------------]]
222 -- we may need to play with these
223 local QH_Route_Core_NodeAdd_Internal
224 local QH_Route_Core_NodeRemove_Internal
226 if OptimizationHackery then
227 function Unstorage_SetDists(newdists)
228 local tc = 1
229 QuestHelper: Assert(#newdists == #ActiveNodes * #ActiveNodes)
230 for _, v in ipairs(ActiveNodes) do
231 for _, w in ipairs(ActiveNodes) do
232 Distance[v][w] = newdists[tc]
233 tc = tc + 1
236 QuestHelper: Assert(tc - 1 == #newdists)
239 function Unstorage_SetDistsX(pivot, newdists)
240 local tc = 1
241 QuestHelper: Assert(#newdists == #ActiveNodes * 2 - 1)
242 for _, v in ipairs(ActiveNodes) do
243 Distance[pivot][v] = newdists[tc]
244 tc = tc + 1
245 if v ~= pivot then
246 Distance[v][pivot] = newdists[tc]
247 tc = tc + 1
250 QuestHelper: Assert(tc - 1 == #newdists)
253 function Unstorage_SetDistsLine(pivot, newdists)
254 local tc = 1
255 QuestHelper: Assert(#newdists == #ActiveNodes)
257 if pivot == 1 then
258 if last_best and #last_best > 1 then
259 last_best.distance = last_best.distance - Distance[last_best[1]][last_best[2]]
263 for _, v in ipairs(ActiveNodes) do
264 Distance[pivot][v] = newdists[tc]
265 tc = tc + 1
267 QuestHelper: Assert(tc - 1 == #newdists)
269 if pivot == 1 then
270 if last_best and #last_best > 1 then
271 last_best.distance = last_best.distance + Distance[last_best[1]][last_best[2]]
276 function Unstorage_Add(nod)
277 QH_Route_Core_NodeAdd_Internal({}, nod)
280 function Unstorage_Remove(nod)
281 QH_Route_Core_NodeRemove_Internal({}, nod)
284 function Unstorage_ClusterAdd(nod, tab)
285 QH_Route_Core_ClusterAdd({}, nod)
286 for _, v in ipairs(tab) do
287 QuestHelper: Assert(NodeList[v])
288 ClusterLookup[v] = nod
289 table.insert(Cluster[nod], v)
293 function Unstorage_ClusterRemove(nod)
294 QH_Route_Core_ClusterRemove({}, nod)
297 function Unstorage_Link(a, b)
298 QH_Route_Core_ClusterRequires(a, b, true)
301 function Unstorage_Nastyscan()
302 for _, v in ipairs(ActiveNodes) do
303 for _, w in ipairs(ActiveNodes) do
304 QuestHelper: Assert(Distance[v][w])
305 QuestHelper: Assert(Weight[v][w])
310 function Unstorage_Magic(tab)
311 local touched = {}
313 PheremonePreservation = tab.PheremonePreservation QuestHelper: Assert(PheremonePreservation) touched.PheremonePreservation = true
314 AntCount = tab.AntCount QuestHelper: Assert(AntCount) touched.AntCount = true
315 WeightFactor = tab.WeightFactor QuestHelper: Assert(WeightFactor) touched.WeightFactor = true
316 DistanceFactor = tab.DistanceFactor QuestHelper: Assert(DistanceFactor) touched.DistanceFactor = true
317 DistanceDeweight = tab.DistanceDeweight QuestHelper: Assert(DistanceDeweight) touched.DistanceDeweight = true
318 UniversalBonus = tab.UniversalBonus QuestHelper: Assert(UniversalBonus) touched.UniversalBonus = true
320 for k, v in pairs(tab) do
321 QuestHelper: Assert(touched[k])
326 --[[
327 ----------------------------------
328 here ends the butt of the wacky storage system. yeah, that's right. I said butt. Butt. Hee hee. Butt.
329 ----------------------------------]]
332 function QH_Route_Core_NodeCount()
333 return #ActiveNodes
336 function QH_Route_Core_TraverseNodes(func)
337 for _, v in ipairs(ActiveNodes) do
338 if v ~= 1 then
339 local blocked = false
340 if ClusterLookup[v] and DependencyLinks[ClusterLookup[v]] and #DependencyLinks[ClusterLookup[v]] > 0 then blocked = true end
341 --print("nlx", NodeList[v], blocked)
342 func(NodeList[v], blocked)
347 function QH_Route_Core_TraverseClusters(func)
348 for k, v in pairs(ClusterTableLookup) do
349 func(k)
353 function QH_Route_Core_IgnoredReasons_Cluster(clust, func)
354 for k, _ in pairs(ClusterIgnored[ClusterTableLookup[clust]]) do
355 if type(k) == "table" then
356 func(k)
361 function QH_Route_Core_IgnoredReasons_Node(node, func)
362 for k, _ in pairs(NodeIgnored[NodeLookup[node]]) do
363 if type(k) == "table" then
364 func(k)
369 function QH_Route_Core_Ignored_Cluster(clust)
370 return ClusterIgnoredCount[ClusterTableLookup[clust]] ~= 0
373 -- fuck floating-point
374 local function almost(a, b)
375 if a == b then return true end
376 if type(a) ~= "number" or type(b) ~= "number" then return false end
377 if a == 0 or b == 0 then return false end
378 return math.abs(a / b - 1) < 0.0001
381 -- Initialization
382 function QH_Route_Core_Init(PathNotifier, DistanceBatch)
383 Notifier = PathNotifier
384 DistBatch = DistanceBatch
385 QuestHelper: Assert(Notifier)
386 QuestHelper: Assert(DistBatch)
388 -- End initialization
390 local last_best = nil
391 local last_best_tweaked = false
393 local function ValidateNumber(x)
394 QuestHelper: Assert(x == x)
395 QuestHelper: Assert(x ~= math.huge)
396 QuestHelper: Assert(x ~= -math.huge)
399 local function GetWeight(x, y)
400 if x == y then return 0.00000000001 end -- sigh
401 --local idx = GetIndex(x, y)
402 --local revidx = GetIndex(y, x)
403 if not Weight[x][y] or not Distance[x][y] then
404 RTO(string.format("%d/%d %d", x, y, CurrentNodes))
405 QuestHelper: Assert(x <= CurrentNodes)
406 QuestHelper: Assert(y <= CurrentNodes)
407 QuestHelper: Assert(false)
409 local weight = math.pow(Weight[x][y], WeightFactor) * math.pow(Distance[x][y] + DistanceDeweight, DistanceFactor)
410 --print(Weight[idx], Weight[revidx], bonus, WeightFactor, Distance[idx], DistanceFactor)
411 --ValidateNumber(weight)
412 return weight
415 -- Realtime splice
416 local function DespliceCN(cluster, node)
417 QuestHelper: Assert(not cluster or not node)
418 QuestHelper: Assert(cluster or node)
419 if not last_best then return end
421 local ct = 0
422 for i = 2, #last_best do
423 if last_best[i] and (last_best[i] == node or ClusterLookup[last_best[i]] == cluster) then
424 -- Splice it out!
425 last_best.distance = last_best.distance - Distance[last_best[i - 1]][last_best[i]]
426 if i ~= #last_best then
427 last_best.distance = last_best.distance - Distance[last_best[i]][last_best[i + 1]]
429 table.remove(last_best, i)
430 if i ~= #last_best + 1 then
431 last_best.distance = last_best.distance + Distance[last_best[i - 1]][last_best[i]]
434 ct = ct + 1
435 i = i - 1
439 last_best_tweaked = true
441 --QuestHelper:TextOut("Despliced with " .. ct)
444 local function SpliceIn(index, touched)
445 QuestHelper: Assert(index)
446 if touched[index] then return end
448 QH_Timeslice_Yield()
450 -- First, try to splice everything it depends on
451 if DependencyLinks[index] then for _, v in ipairs(DependencyLinks[index]) do
452 SpliceIn(v, touched)
453 end end
455 local dl_lookup = QuestHelper:CreateTable("splice dl lookup")
456 local dlr_lookup = QuestHelper:CreateTable("splice dlr lookup")
457 if DependencyLinks[index] then for _, v in ipairs(DependencyLinks[index]) do dl_lookup[v] = true end end
458 if DependencyLinksReverse[index] then for _, v in ipairs(DependencyLinksReverse[index]) do dlr_lookup[v] = true end end
460 local start_bound = 2
461 local end_bound
463 -- Next, figure out where it can go
464 for i = 2, #last_best do
465 --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])
466 if dl_lookup[ClusterLookup[last_best[i]]] then start_bound = i + 1 end
467 if dlr_lookup[ClusterLookup[last_best[i]]] and not end_bound then end_bound = i end
468 if ClusterPriority[ClusterLookup[last_best[i]]] < ClusterPriority[index] then start_bound = i + 1 end
469 if ClusterPriority[ClusterLookup[last_best[i]]] > ClusterPriority[index] and not end_bound then end_bound = i end
471 if not end_bound then end_bound = #last_best + 1 end
472 --QuestHelper: TextOut(string.format("Placed cluster %d between %d and %d", index, start_bound, end_bound))
473 QuestHelper: Assert(end_bound >= start_bound, string.format("%d and %d", start_bound, end_bound))
475 -- Figure out the best place to put it
476 local best_spot = nil
477 local best_node = nil
478 local best_cost = nil
479 for i = start_bound, end_bound do
480 for _, nindex in ipairs(Cluster[index]) do
481 if NodeIgnoredCount[nindex] == 0 then
482 local tcost = Distance[last_best[i - 1]][nindex] -- Cost of that-node-to-this-one
483 if i <= #last_best then
484 tcost = tcost + Distance[nindex][last_best[i]] - Distance[last_best[i - 1]][last_best[i]]
486 if not best_cost or tcost < best_cost then
487 best_spot, best_node, best_cost = i, nindex, tcost
493 QuestHelper: Assert(best_spot)
494 table.insert(last_best, best_spot, best_node)
495 last_best.distance = last_best.distance + best_cost
497 touched[index] = true
498 last_best_tweaked = true
500 QuestHelper:ReleaseTable(dl_lookup)
501 QuestHelper:CreateTable(dlr_lookup)
503 -- end realtime splice
505 -- 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.
506 local function RunAnt()
507 local route = NewRoute()
508 route[1] = 1
509 route.distance = 0
511 local dependencies = QuestHelper:CreateTable("route_core_dependencies")
513 local needed = QuestHelper:CreateTable("route_core_needed")
514 local needed_count = 0
515 local needed_ready_count = 0
517 for k, v in pairs(DependencyCounts) do
518 dependencies[k] = v
519 QuestHelper: Assert(dependencies[k] >= 0)
522 local curloc = 1
524 local gwc = QuestHelper:CreateTable("route_core_gwc")
526 --TestShit()
528 for k, v in ipairs(Priorities) do
529 if Priorities[k + 1] then
530 QuestHelper: Assert(Priorities[k] < Priorities[k + 1])
534 for _, current_pri in ipairs(Priorities) do
536 -- Here is we add the new batch of nodes
537 for _, v in ipairs(ActiveNodes) do
538 if v ~= 1 then -- if it's ignored, then we just plain don't do anything
539 local clustid = ClusterLookup[v]
540 QuestHelper: Assert(clustid)
542 if ClusterPriority[clustid] < current_pri then
543 QuestHelper: Assert(dependencies[clustid] == -1 or NodeIgnoredCount[v] > 0 or ClusterIgnoredCount[clustid] >= 0)
544 elseif ClusterPriority[clustid] == current_pri then
545 if NodeIgnoredCount[v] == 0 and ClusterIgnoredCount[clustid] == 0 then
546 local need = false
548 QuestHelper: Assert(dependencies[clustid])
549 if dependencies[clustid] == 0 then
550 needed[v] = true
551 needed_ready_count = needed_ready_count + 1
554 needed_count = needed_count + 1
556 else
557 QuestHelper: Assert(dependencies[clustid] ~= -1, clustid)
562 if not (needed_ready_count > 0 or needed_count == 0) then
563 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
566 while needed_count > 0 do
567 QuestHelper: Assert(needed_ready_count > 0)
569 local accumulated_weight = 0
570 local tweight = 0
571 for k, _ in pairs(needed) do
572 local tw = GetWeight(curloc, k)
573 gwc[k] = tw
574 accumulated_weight = accumulated_weight + tw
577 tweight = accumulated_weight
578 accumulated_weight = accumulated_weight * math.random()
580 local nod = nil
581 for k, _ in pairs(needed) do
582 accumulated_weight = accumulated_weight - gwc[k]
583 if accumulated_weight < 0 then
584 nod = k
585 break
589 if not nod then
590 RTO(string.format("no nod :( %f/%f", accumulated_weight, tweight))
591 for k, _ in pairs(needed) do
592 nod = k
593 break
597 -- Now we've chosen stuff. Bookkeeping.
598 local clust = ClusterLookup[nod]
599 QuestHelper: Assert(clust)
601 -- Obliterate other cluster items. Guaranteed to be at the same priority level.
602 for _, v in pairs(Cluster[clust]) do
603 if NodeIgnoredCount[v] == 0 then
604 needed[v] = nil
605 needed_count = needed_count - 1
606 needed_ready_count = needed_ready_count - 1
610 -- Dependency links.
611 if DependencyLinksReverse[clust] then for _, v in ipairs(DependencyLinksReverse[clust]) do
612 dependencies[v] = dependencies[v] - 1
613 QuestHelper: Assert(dependencies[v] >= 0)
614 if dependencies[v] == 0 and ClusterIgnoredCount[v] == 0 and ClusterPriority[v] == current_pri then
615 for _, v in pairs(Cluster[v]) do
616 if NodeIgnoredCount[v] == 0 then
617 needed[v] = true
618 needed_ready_count = needed_ready_count + 1
622 end end
624 QuestHelper: Assert(dependencies[clust] == 0)
625 QuestHelper: Assert(ClusterPriority[clust] == current_pri)
626 dependencies[clust] = -1
628 --print(needed_count, needed_ready_count)
630 route.distance = route.distance + Distance[curloc][nod]
631 table.insert(route, nod)
632 curloc = nod
635 QuestHelper: Assert(needed_ready_count == 0 and needed_count == 0)
638 QuestHelper:ReleaseTable(gwc)
639 QuestHelper:ReleaseTable(dependencies)
640 QuestHelper:ReleaseTable(needed)
642 return route
645 -- Lots of doublechecks to make sure our route is both sane and complete
646 local function CheckRoute(route)
647 --print("starting check")
649 QuestHelper: Assert(route[1] == 1) -- starting at the beginning
651 local td = 0
652 for i = 1, #route - 1 do
653 td = td + Distance[route[i]][route[i + 1]]
655 --print(td, route.distance)
656 QuestHelper: Assert(abs(td - route.distance) < 5 or abs(td / route.distance - 1) < 0.0001)
658 local seen = QuestHelper:CreateTable("check seen")
660 local cpri = nil
661 for i = 1, #route do
662 QuestHelper: Assert(NodeIgnoredCount[route[i]] == 0)
664 local clust = ClusterLookup[route[i]]
665 if clust then
666 --print("seeing cluster ", clust, cpri, ClusterPriority[clust])
667 QuestHelper: Assert(ClusterIgnoredCount[clust] == 0)
668 QuestHelper: Assert(not seen[clust])
669 seen[clust] = true
670 QuestHelper: Assert(not cpri or cpri <= ClusterPriority[clust])
671 cpri = ClusterPriority[clust]
673 if DependencyLinks[clust] then for _, v in ipairs(DependencyLinks[clust]) do QuestHelper: Assert(seen[v]) end end
674 if DependencyLinksReverse[clust] then for _, v in ipairs(DependencyLinksReverse[clust]) do QuestHelper: Assert(not seen[v]) end end
678 for k, v in pairs(ClusterIgnoredCount) do
679 QuestHelper: Assert(not seen[k] == (ClusterIgnoredCount[k] > 0))
682 QuestHelper:ReleaseTable(seen)
684 --print("ending check")
687 local function BetterRoute(route)
688 CheckRoute(route)
689 local rt = {}
690 for k, v in ipairs(route) do
691 rt[k] = NodeList[v]
693 rt.distance = route.distance -- this is probably temporary
694 Notifier(rt)
697 local QH_Route_Core_DistanceClear_Local -- sigh
698 -- Core process function
699 function QH_Route_Core_Process()
700 Storage_Loop()
702 -- First we check to see if we need to add more distances, and if so, we do so
704 local refreshcount = 0
705 for k, v in pairs(DistanceWaiting) do
706 refreshcount = refreshcount + 1
709 if refreshcount > 0 then
710 if debug_output then QuestHelper:TextOut(string.format("Refreshing %d", refreshcount)) end
711 if refreshcount >= #ActiveNodes / 2 then
712 -- Refresh everything!
713 QH_Route_Core_DistanceClear_Local()
714 else
715 local tlnod = QuestHelper:CreateTable("routecore distance tlnod")
716 for _, v in ipairs(ActiveNodes) do
717 table.insert(tlnod, NodeList[v])
720 for idx, _ in pairs(DistanceWaiting) do
721 -- Refresh a subset of things.
722 local forward = DistBatch(NodeList[idx], tlnod)
723 local backward = DistBatch(NodeList[idx], tlnod, true)
725 for k, v in ipairs(ActiveNodes) do
726 Distance[idx][v] = forward[k]
727 Distance[v][idx] = backward[k]
730 QuestHelper:ReleaseTable(forward)
731 QuestHelper:ReleaseTable(backward)
733 QuestHelper:ReleaseTable(tlnod)
735 QuestHelper:ReleaseTable(DistanceWaiting)
736 DistanceWaiting = QuestHelper:CreateTable("routecore distance waiting")
740 -- Next we see if last_best needs tweaking
741 if last_best then
742 local touched_clusts = QuestHelper:CreateTable("routing touched")
743 for i = 2, #last_best do
744 local clust = ClusterLookup[last_best[i]]
745 QuestHelper: Assert(clust)
746 QuestHelper: Assert(not touched_clusts[clust])
747 touched_clusts[clust] = true
750 for k, _ in pairs(Cluster) do
751 local exists = touched_clusts[k]
752 local ignored = (ClusterIgnoredCount[k] ~= 0)
753 QuestHelper: Assert(not (ignored and exists)) -- something went wrong
755 if not ignored and not exists then
756 -- here we go
757 SpliceIn(k, touched_clusts)
758 last_best_tweaked = true
761 QuestHelper:ReleaseTable(touched_clusts)
764 if last_best_tweaked then
765 --QuestHelper:TextOut("Pushing tweaked")
766 BetterRoute(last_best)
767 last_best_tweaked = false
770 local worst = 0
772 local best_is_local = false
774 local trouts = QuestHelper:CreateTable("routing_core_trouts")
775 for x = 1, AntCount do
776 table.insert(trouts, RunAnt())
777 --if last_best then RTO(string.format("Path generated: %s vs %s", PathToString(trouts[#trouts]), PathToString(last_best))) end
778 if not last_best or last_best.distance > trouts[#trouts].distance then
779 if last_best and not best_is_local then QuestHelper:ReleaseTable(last_best) end
781 best_is_local = true
782 last_best = trouts[#trouts]
783 BetterRoute(last_best)
786 worst = math.max(worst, trouts[#trouts].distance)
788 QH_Timeslice_Yield()
791 local scale
792 if worst == last_best.distance then
793 scale = 1
794 else
795 scale = 1 / (worst - last_best.distance)
798 for _, x in ipairs(ActiveNodes) do
799 for _, y in ipairs(ActiveNodes) do
800 --local idx = GetIndex(x, y)
801 Weight[x][y] = Weight[x][y] * PheremonePreservation + UniversalBonus
802 --ValidateNumber(Weight[idx])
806 for _, x in ipairs(trouts) do
807 local amount = 1 / x.distance
808 for y = 1, #x - 1 do
809 --local idx = GetIndex(x[y], x[y + 1])
810 Weight[x[y]][x[y + 1]] = Weight[x[y]][x[y + 1]] + amount
811 --ValidateNumber(Weight[idx])
815 local weitotal = 0
816 local weicount = 0
817 for _, x in ipairs(ActiveNodes) do
818 for _, y in ipairs(ActiveNodes) do
819 --local idx = GetIndex(x, y)
820 weitotal = weitotal + Weight[x][y]
821 weicount = weicount + 1
825 weight_ave = weitotal / weicount
827 for k, v in pairs(trouts) do
828 if v ~= last_best then
829 QuestHelper:ReleaseTable(v)
832 QuestHelper:ReleaseTable(trouts)
834 QH_Timeslice_Yield() -- "heh"
836 -- End core loop
838 -- Ignore/unignore
839 local function RecursiveIgnoreCount(clustid, accum)
840 if accum == 0 then return end
841 --print(clustid, accum)
843 if ClusterIgnoredCount[clustid] == 0 then QuestHelper: Assert(accum > 0) DespliceCN(clustid) end
844 ClusterIgnoredCount[clustid] = ClusterIgnoredCount[clustid] + accum
845 if ClusterIgnoredCount[clustid] == 0 then QuestHelper: Assert(accum < 0) end -- Item being added, we'll handle this at the beginning of run
847 if DependencyLinksReverse[clustid] then
848 for _, v in pairs(DependencyLinksReverse[clustid]) do
849 RecursiveIgnoreCount(v, accum)
854 local function Internal_IgnoreCluster(clustid, reason)
855 QuestHelper: Assert(clustid)
857 if not ClusterIgnored[clustid][reason] then
858 ClusterIgnored[clustid][reason] = true
859 RecursiveIgnoreCount(clustid, 1)
863 local function Internal_UnignoreCluster(clustid, reason)
864 QuestHelper: Assert(clustid)
865 if ClusterIgnored[clustid][reason] then
866 ClusterIgnored[clustid][reason] = nil
867 RecursiveIgnoreCount(clustid, -1)
871 function QH_Route_Core_IgnoreCluster(clust, reason)
872 QuestHelper: Assert(type(reason) == "table")
873 local clustid = ClusterTableLookup[clust]
874 if not clustid then
875 -- This can just happen due to the lag introduced by the controller, so, whatever
876 --QuestHelper:TextOut("Attempted to ignore a cluster that no longer exists")
877 return
880 Internal_IgnoreCluster(clustid, reason)
883 function QH_Route_Core_UnignoreCluster(clust, reason)
884 QuestHelper: Assert(type(reason) == "table")
885 local clustid = ClusterTableLookup[clust]
886 if not clustid then
887 -- This can just happen due to the lag introduced by the controller, so, whatever
888 --QuestHelper:TextOut("Attempted to unignore a cluster that no longer exists")
889 return
891 Internal_UnignoreCluster(clustid, reason)
894 function QH_Route_Core_IgnoreNode(node, reason)
895 QuestHelper: Assert(type(reason) == "table")
896 local nid = NodeLookup[node]
897 if not nid then
898 -- This can just happen due to the lag introduced by the controller, so, whatever
899 --QuestHelper:TextOut("Attempted to ignore a node that no longer exists")
900 return
903 QuestHelper: Assert(nid)
905 if not NodeIgnored[nid][reason] then
906 NodeIgnored[nid][reason] = true
908 NodeIgnoredCount[nid] = NodeIgnoredCount[nid] + 1
909 if NodeIgnoredCount[nid] == 1 then
910 DespliceCN(nil, nid)
912 if ClusterLookup[nid] then
913 ClusterIgnoredNodeActive[ClusterLookup[nid]] = ClusterIgnoredNodeActive[ClusterLookup[nid]] - 1
914 if ClusterIgnoredNodeActive[ClusterLookup[nid]] == 0 then
915 Internal_IgnoreCluster(ClusterLookup[nid], "internal_node_ignored")
922 function QH_Route_Core_UnignoreNode(node, reason)
923 QuestHelper: Assert(type(reason) == "table")
924 local nid = NodeLookup[node]
925 if not nid then
926 -- This can just happen due to the lag introduced by the controller, so, whatever
927 --QuestHelper:TextOut("Attempted to unignore a node that no longer exists")
928 return
931 QuestHelper: Assert(nid)
933 if NodeIgnored[nid][reason] then
934 NodeIgnored[nid][reason] = nil
936 NodeIgnoredCount[nid] = NodeIgnoredCount[nid] - 1
937 if NodeIgnoredCount[nid] == 0 then
938 -- Item being added
940 if ClusterLookup[nid] then
941 -- Item being added
942 ClusterIgnoredNodeActive[ClusterLookup[nid]] = ClusterIgnoredNodeActive[ClusterLookup[nid]] + 1
943 if ClusterIgnoredNodeActive[ClusterLookup[nid]] == 1 then
944 Internal_UnignoreCluster(ClusterLookup[nid], "internal_node_ignored")
951 local QH_Route_Core_UnignoreCluster = QH_Route_Core_UnignoreCluster -- we're just saving this so it doesn't get splattered
952 -- End ignore/unignore
954 -- Node allocation and deallocation
955 -- this is only separate so we can use it for the crazy optimization hackery
956 local function Expand()
957 for _, v in ipairs(Distance) do
958 table.insert(v, 0)
960 for _, v in ipairs(Weight) do
961 table.insert(v, 0)
963 table.insert(Distance, {})
964 table.insert(Weight, {})
966 for k = 1, CurrentNodes + 1 do
967 table.insert(Distance[#Distance], 0)
968 table.insert(Weight[#Weight], 0)
971 CurrentNodes = CurrentNodes + 1
974 -- 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?
975 local function AllocateExtraNode()
976 if #DeadNodes > 0 then
977 local nod = table.remove(DeadNodes)
978 table.insert(ActiveNodes, nod)
979 table.sort(ActiveNodes)
980 return nod
983 -- We always allocate on the end, so we know this is safe.
984 Expand()
986 table.insert(DeadNodes, CurrentNodes)
987 return AllocateExtraNode() -- ha ha
990 -- Set the start location
991 function QH_Route_Core_SetStart(stt)
992 -- We do some kind of ghastly things here.
993 --TestShit()
994 if last_best and #last_best > 1 then
995 last_best.distance = last_best.distance - Distance[last_best[1]][last_best[2]]
998 NodeLookup[StartNode] = nil
999 NodeList[1] = stt
1000 StartNode = stt
1001 NodeLookup[StartNode] = 1
1003 local tlnod = QuestHelper:CreateTable("routecore setstart tlnod")
1005 for _, v in ipairs(ActiveNodes) do
1006 if v ~= 1 then
1007 table.insert(tlnod, NodeList[v])
1011 local forward = DistBatch(NodeList[1], tlnod)
1013 local ct = 1
1014 for _, v in ipairs(ActiveNodes) do
1015 if v ~= 1 then
1016 QuestHelper: Assert(forward[ct])
1017 Distance[1][v] = forward[ct]
1018 ct = ct + 1
1020 Distance[v][1] = 65500 -- this should never be used anyway
1024 if last_best and #last_best > 1 then
1025 last_best.distance = last_best.distance + Distance[last_best[1]][last_best[2]]
1028 QuestHelper:ReleaseTable(forward)
1029 QuestHelper:ReleaseTable(tlnod)
1031 Storage_Distance_StoreFromIDToAll(1)
1032 --TestShit()
1033 -- TODO: properly deallocate old startnode?
1036 QH_Route_Core_NodeAdd_Internal = function (nod, used_idx)
1037 --QuestHelper:TextOut(tostring(nod))
1038 --TestShit()
1039 QuestHelper: Assert(nod)
1040 if NodeLookup[nod] then
1041 -- ughhh
1042 QuestHelper: Assert(not NodeLookup[nod], QuestHelper:StringizeTable(nod))
1045 local idx
1046 if used_idx then
1047 QuestHelper: Assert(OptimizationHackery)
1048 QuestHelper: Assert(not NodeList[used_idx])
1049 idx = used_idx
1050 table.insert(ActiveNodes, used_idx)
1051 table.sort(ActiveNodes)
1052 if not Distance[idx] then Expand() QuestHelper: Assert(Distance[idx]) end
1053 else
1054 idx = AllocateExtraNode()
1057 --RTO("|cffFF8080AEN: " .. tostring(idx))
1058 NodeLookup[nod] = idx
1059 NodeList[idx] = nod
1061 NodeIgnored[idx] = {}
1062 NodeIgnoredCount[idx] = 0
1064 for _, v in ipairs(ActiveNodes) do
1065 Weight[v][idx] = weight_ave
1066 Weight[idx][v] = weight_ave
1069 DistanceWaiting[idx] = true
1071 -- Item being added
1073 return idx
1076 -- Remove a node with the given location
1077 QH_Route_Core_NodeRemove_Internal = function (nod, used_idx)
1078 --TestShit()
1079 QuestHelper: Assert(nod)
1081 local idx
1082 if used_idx then
1083 QuestHelper: Assert(OptimizationHackery)
1084 QuestHelper: Assert(NodeList[used_idx])
1085 idx = used_idx
1086 else
1087 QuestHelper: Assert(NodeLookup[nod])
1088 idx = NodeLookup[nod]
1091 --RTO("|cffFF8080RFN: " .. tostring(NodeLookup[nod]))
1092 NodeList[idx] = nil
1093 table.insert(DeadNodes, idx)
1094 local oas = #ActiveNodes
1095 for k, v in pairs(ActiveNodes) do if v == idx then table.remove(ActiveNodes, k) break end end -- this is pretty awful
1096 QuestHelper: Assert(#ActiveNodes < oas)
1097 NodeLookup[nod] = nil
1098 -- We don't have to modify the table itself, some sections are just "dead".
1099 --TestShit()
1101 DistanceWaiting[idx] = nil -- just in case we haven't updated it in the intervening time
1103 -- If we're a standalone node, nothing depends on us. If we're part of a cluster, the cluster's getting smoked anyway.
1104 NodeIgnored[idx] = nil
1105 NodeIgnoredCount[idx] = nil
1107 DespliceCN(nil, idx)
1109 return idx
1111 -- End node allocation and deallocation
1113 function QH_Route_Core_ClusterAdd(clust, clustid_used)
1114 local clustid
1115 if clustid_used then
1116 QuestHelper: Assert(OptimizationHackery)
1117 QuestHelper: Assert(not Cluster[clustid_used])
1118 clustid = clustid_used
1119 else
1120 QuestHelper: Assert(#clust > 0)
1121 clustid = table.remove(ClusterDead)
1122 if not clustid then clustid = #Cluster + 1 end
1125 if debug_output then QuestHelper:TextOut(string.format("Adding cluster %d", clustid)) end
1127 Cluster[clustid] = {}
1128 ClusterTableLookup[clust] = clustid
1130 ClusterIgnored[clustid] = {}
1131 ClusterIgnoredCount[clustid] = 0
1132 ClusterIgnoredNodeActive[clustid] = #clust
1134 ClusterPriority[clustid] = 0
1135 if not PriorityCount[0] then table.insert(Priorities, 0) table.sort(Priorities) end
1136 PriorityCount[0] = (PriorityCount[0] or 0) + 1
1138 -- if we're doing hackery, clust will just be an empty table and we'll retrofit stuff later
1139 for _, v in ipairs(clust) do
1140 local idx = QH_Route_Core_NodeAdd_Internal(v)
1141 Storage_NodeAdded(idx)
1142 ClusterLookup[idx] = clustid
1143 table.insert(Cluster[clustid], idx)
1146 DependencyCounts[clustid] = 0
1148 Storage_ClusterCreated(clustid)
1151 function QH_Route_Core_ClusterRemove(clust, clustid_used)
1152 local clustid
1153 if clustid_used then
1154 QuestHelper: Assert(OptimizationHackery)
1155 QuestHelper: Assert(Cluster[clustid_used])
1156 clustid = clustid_used
1158 for _, v in ipairs(Cluster[clustid]) do
1159 QH_Route_Core_NodeRemove_Internal({}, v)
1160 ClusterLookup[v] = nil
1162 else
1163 clustid = ClusterTableLookup[clust]
1167 local ct = 0
1168 local abort
1169 repeat
1170 QuestHelper: Assert(ct < 100)
1171 abort = true
1172 for k, v in pairs(ClusterIgnored[clustid]) do
1173 abort = false
1174 Internal_UnignoreCluster(clustid, k)
1175 ct = ct + 1
1176 break
1178 until abort
1179 -- 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.
1180 RecursiveIgnoreCount(clustid, -ClusterIgnoredCount[clustid])
1181 QuestHelper: Assert(ClusterIgnoredCount[clustid] == 0)
1184 if debug_output then QuestHelper:TextOut(string.format("Removing cluster %d", clustid)) end
1186 for _, v in ipairs(clust) do
1187 local idx = QH_Route_Core_NodeRemove_Internal(v)
1188 ClusterLookup[idx] = nil
1191 DependencyCounts[clustid] = nil
1193 if DependencyLinks[clustid] then
1194 for k, v in pairs(DependencyLinks[clustid]) do
1195 for m, f in pairs(DependencyLinksReverse[v]) do
1196 if f == clustid then
1197 if debug_output then QuestHelper:TextOut(string.format("Unlinking cluster %d needs %d", clustid, v)) end
1198 table.remove(DependencyLinksReverse[v], m)
1199 break
1204 DependencyLinks[clustid] = nil
1206 if DependencyLinksReverse[clustid] then
1207 for k, v in pairs(DependencyLinksReverse[clustid]) do
1208 for m, f in pairs(DependencyLinks[v]) do
1209 if f == clustid then
1210 if debug_output then QuestHelper:TextOut(string.format("Unlinking cluster %d needs %d", v, clustid)) end
1211 table.remove(DependencyLinks[v], m)
1212 DependencyCounts[v] = DependencyCounts[v] - 1
1213 break
1218 DependencyLinksReverse[clustid] = nil
1220 Cluster[clustid] = nil
1221 ClusterTableLookup[clust] = nil
1222 table.insert(ClusterDead, clustid)
1224 ClusterIgnored[clustid] = nil
1225 ClusterIgnoredCount[clustid] = nil
1226 ClusterIgnoredNodeActive[clustid] = nil
1228 local pri = ClusterPriority[clustid]
1229 PriorityCount[pri] = PriorityCount[pri] - 1
1230 if PriorityCount[pri] == 0 then
1231 PriorityCount[pri] = nil
1233 for k, v in ipairs(Priorities) do
1234 if v == pri then
1235 Priorities[k] = Priorities[#Priorities]
1236 table.remove(Priorities)
1237 table.sort(Priorities)
1238 break
1242 ClusterPriority[clustid] = nil
1244 Storage_ClusterDestroyed(clustid)
1247 -- Add a note that node 1 requires node 2.
1248 function QH_Route_Core_ClusterRequires(a, b, hackery)
1249 local aidx
1250 local bidx
1251 if hackery then
1252 QuestHelper: Assert(OptimizationHackery)
1253 QuestHelper: Assert(Cluster[a])
1254 QuestHelper: Assert(Cluster[b])
1255 aidx, bidx = a, b
1256 else
1257 aidx = ClusterTableLookup[a]
1258 bidx = ClusterTableLookup[b]
1260 QuestHelper: Assert(aidx)
1261 QuestHelper: Assert(bidx)
1262 QuestHelper: Assert(aidx ~= bidx)
1264 if debug_output then QuestHelper:TextOut(string.format("Linking cluster %d needs %d", aidx, bidx)) end
1266 DependencyCounts[aidx] = DependencyCounts[aidx] + 1
1268 if not DependencyLinks[aidx] then DependencyLinks[aidx] = {} end
1269 table.insert(DependencyLinks[aidx], bidx)
1271 if not DependencyLinksReverse[bidx] then DependencyLinksReverse[bidx] = {} end
1272 table.insert(DependencyLinksReverse[bidx], aidx)
1274 DespliceCN(aidx)
1275 DespliceCN(bidx)
1277 Storage_ClusterDependency(aidx, bidx)
1280 function QH_Route_Core_GetClusterPriority(clust)
1281 return ClusterPriority[ClusterTableLookup[clust]]
1284 local function QH_Route_Core_SetClusterPriority_Internal(clustid, new_pri)
1285 QuestHelper: Assert(clustid)
1286 if ClusterPriority[clustid] == new_pri then return end
1287 --QuestHelper:TextOut(string.format("Setting %d to %d", clustid, new_pri))
1289 local pri = ClusterPriority[clustid]
1290 QuestHelper: Assert(pri)
1291 PriorityCount[pri] = PriorityCount[pri] - 1
1292 if PriorityCount[pri] == 0 then
1293 PriorityCount[pri] = nil
1295 for k, v in ipairs(Priorities) do
1296 if v == pri then
1297 Priorities[k] = Priorities[#Priorities]
1298 table.remove(Priorities)
1299 table.sort(Priorities)
1300 break
1305 ClusterPriority[clustid] = new_pri
1306 if not PriorityCount[new_pri] then table.insert(Priorities, new_pri) table.sort(Priorities) end
1307 PriorityCount[new_pri] = (PriorityCount[new_pri] or 0) + 1
1309 DespliceCN(clustid)
1311 -- 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.
1313 -- Clusters that this one depends on. Must happen first (i.e. have a smaller or equal priority)
1314 if DependencyLinks[clustid] then for _, v in ipairs(DependencyLinks[clustid]) do
1315 if ClusterPriority[v] > new_pri then QH_Route_Core_SetClusterPriority_Internal(v, new_pri) end
1316 end end
1318 -- Clusters that depend on this one. Must happen last (i.e. have a greater or equal priority)
1319 if DependencyLinksReverse[clustid] then for _, v in ipairs(DependencyLinksReverse[clustid]) do
1320 if ClusterPriority[v] < new_pri then QH_Route_Core_SetClusterPriority_Internal(v, new_pri) end
1321 end end
1324 function QH_Route_Core_SetClusterPriority(clust, new_pri)
1325 QuestHelper: Assert(clust)
1326 local clustid = ClusterTableLookup[clust]
1328 QH_Route_Core_SetClusterPriority_Internal(clustid, new_pri)
1331 -- Wipe and re-cache all distances.
1332 function QH_Route_Core_DistanceClear()
1333 local tlnod = {}
1334 for _, v in ipairs(ActiveNodes) do
1335 table.insert(tlnod, NodeList[v])
1338 for ani, idx in ipairs(ActiveNodes) do
1339 local forward = DistBatch(NodeList[idx], tlnod)
1341 for k, v in ipairs(ActiveNodes) do
1342 Distance[idx][v] = forward[k]
1345 if QuestHelper.loading_preroll and #ActiveNodes > 1 then QuestHelper.loading_preroll:SetPercentage(ani / #ActiveNodes) end
1348 if last_best then
1349 last_best.distance = 0
1350 for i = 1, #last_best - 1 do
1351 last_best.distance = last_best.distance + Distance[last_best[i]][last_best[i + 1]]
1355 Storage_Distance_StoreAll()
1357 QH_Route_Core_DistanceClear_Local = QH_Route_Core_DistanceClear
1359 --[==[
1360 function findin(tab, val)
1361 local ct = 0
1362 for k, v in pairs(tab) do
1363 if v == val then ct = ct + 1 end
1365 return ct == 1
1368 function TestShit()
1369 --[[
1370 for x = 1, #ActiveNodes do
1371 local ts = ""
1372 for y = 1, #ActiveNodes do
1373 ts = ts .. string.format("%f ", Distance[GetIndex(ActiveNodes[x], ActiveNodes[y])])
1375 RTO(ts)
1379 --[[
1380 RTO("Lookup table")
1381 for x = 1, #ActiveNodes do
1382 RTO(tostring(ActiveNodes[x]))
1384 RTO("Lookup table done")
1387 --[=[
1388 local fail = false
1389 for x = 1, #ActiveNodes do
1390 for y = 2, #ActiveNodes do
1391 if not (almost(Dist(NodeList[ActiveNodes[x]], NodeList[ActiveNodes[y]]), Distance[ActiveNodes[x]][ActiveNodes[y]])) then
1392 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]]))
1393 fail = true
1396 end]=]
1398 for k, v in pairs(DependencyLinks) do
1399 QuestHelper: Assert(#v == DependencyCounts[k])
1402 for k, v in pairs(DependencyCounts) do
1403 QuestHelper: Assert(v == (DependencyLinks[k] and #DependencyLinks[k] or 0))
1406 for k, v in pairs(DependencyLinks) do
1407 for _, v2 in pairs(v) do
1408 QuestHelper: Assert(findin(DependencyLinksReverse[v2], k))
1412 for k, v in pairs(DependencyLinksReverse) do
1413 for _, v2 in pairs(v) do
1414 QuestHelper: Assert(findin(DependencyLinks[v2], k))
1418 QuestHelper: Assert(not fail)
1420 ]==]
1421 --[=[
1422 function HackeryDump()
1423 local st = "{"
1424 for k, v in pairs(ActiveNodes) do
1425 if v ~= 1 then
1426 st = st .. string.format("{c = %d, x = %f, y = %f}, ", NodeList[k].loc.c, NodeList[k].loc.x, NodeList[k].loc.y)
1429 st = st .. "}"
1430 QuestHelper: Assert(false, st)
1431 end]=]
1433 -- weeeeee