Proper support for metric distances.
[QuestHelper.git] / routing_core.lua
blob469810225b2be149b02c2fff9e495ff12cd9f73c
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 QH_Timeslice_Yield = QH_Timeslice_Yield -- performance hack :(
37 local OptimizationHackery = false
39 if OptimizationHackery then debug_output = false end -- :ughh:
42 -- 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.
43 -- Weight adjustment: Weight[x,y] = Weight[x,y]*weightadj + sum(alltravels)(1/distance_of_travel) (note: this is somewhat out of date)
45 -- Configuration
46 local PheremonePreservation = 0.98 -- must be within 0 and 1 exclusive
47 local AntCount = 20 -- number of ants to run before doing a pheremone pass
49 -- Weighting for the various factors
50 local WeightFactor = 0.61
51 local DistanceFactor = -2.5
52 local DistanceDeweight = 1.4 -- Add this to all distances to avoid sqrt(-1) deals
54 -- 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
55 local UniversalBonus = 0.06
56 -- End configuration
58 local Notifier
59 local DistBatch
61 -- Node storage and data structures
62 local CurrentNodes = 1
63 local ActiveNodes = {1}
64 local DeadNodes = {}
66 local NodeIgnored = {[1] = {}}
67 local NodeIgnoredCount = {[1] = 0}
69 -- Clusters
70 local Cluster = {} -- Goes from cluster ID to node IDs
71 local ClusterLookup = {} -- Goes from node ID to cluster ID
72 local ClusterTableLookup = {} -- Goes from the actual cluster table to the cluster ID
73 local ClusterDead = {} -- List of dead clusters that can be reclaimed
75 local ClusterIgnored = {} -- key-to-table-of-reasons-ignored
76 local ClusterIgnoredCount = {}
77 local ClusterIgnoredNodeActive = {}
79 local ClusterPriority = {}
80 local Priorities = {}
81 local PriorityCount = {}
83 local DependencyLinks = {} -- Every cluster that cluster X depends on
84 local DependencyLinksReverse = {} -- Every cluster that is depended on by cluster X
85 local DependencyCounts = {} -- How many different nodes cluster X depends on
87 local StartNode = {ignore = true, loc = {x = 37690, y = 19671, p = 25, c = 0}} -- Ironforge mailbox :)
89 local NodeLookup = {[StartNode] = 1}
90 local NodeList = {[1] = StartNode}
91 local Distance = {{0}}
92 local Weight = {{0}}
94 local DistanceWaiting = {} -- which node indices are waiting for distance data
96 weight_ave = 0.001
97 -- End node storage and data structures
99 --[[
100 ----------------------------------
101 Here's that wacky storage system.
102 ----------------------------------]]
104 local function unsigned2b(c)
105 if c > 65535 then -- ughh. again.
106 print(c)
107 c = 65535
110 if not (c < 65536) then
111 print(c)
113 QuestHelper: Assert(c < 65536)
115 QuestHelper: Assert(bit.mod(c, 256))
116 QuestHelper: Assert(bit.rshift(c, 8))
117 local strix = strchar(bit.mod(c, 256), bit.rshift(c, 8))
118 QuestHelper: Assert(#strix == 2)
119 return strix
122 -- L
123 local loopcount = 0
124 local function Storage_Loop()
125 loopcount = loopcount + 1
127 local function Storage_LoopFlush()
128 if loopcount > 0 then
129 QH_Merger_Add(QH_Collect_Routing_Dump, "L" .. unsigned2b(loopcount) .. "L")
130 loopcount = 0
134 -- -
135 local function Storage_Distance_StoreFromIDToAll(id)
136 if not QH_Collect_Routing_Dump then return end
137 Storage_LoopFlush()
139 QH_Merger_Add(QH_Collect_Routing_Dump, "-" .. unsigned2b(id))
140 for _, v in ipairs(ActiveNodes) do
141 QH_Merger_Add(QH_Collect_Routing_Dump, unsigned2b(Distance[id][v]))
143 QH_Merger_Add(QH_Collect_Routing_Dump, "-")
146 -- X
147 local function Storage_Distance_StoreCrossID(id)
148 if not QH_Collect_Routing_Dump then return end
149 Storage_LoopFlush()
151 QH_Merger_Add(QH_Collect_Routing_Dump, "X")
152 for _, v in ipairs(ActiveNodes) do
153 QH_Merger_Add(QH_Collect_Routing_Dump, unsigned2b(Distance[id][v]))
154 if v ~= id then QH_Merger_Add(QH_Collect_Routing_Dump, unsigned2b(Distance[v][id])) end
156 QH_Merger_Add(QH_Collect_Routing_Dump, "X")
159 -- #
160 local function Storage_Distance_StoreAll()
161 if not QH_Collect_Routing_Dump then return end
162 Storage_LoopFlush()
164 QH_Merger_Add(QH_Collect_Routing_Dump, "#")
165 for _, v in ipairs(ActiveNodes) do
166 for _, w in ipairs(ActiveNodes) do
167 QH_Merger_Add(QH_Collect_Routing_Dump, unsigned2b(Distance[v][w]))
170 QH_Merger_Add(QH_Collect_Routing_Dump, "#")
173 -- A
174 local function Storage_NodeAdded(id)
175 if not QH_Collect_Routing_Dump then return end
176 Storage_LoopFlush()
178 QH_Merger_Add(QH_Collect_Routing_Dump, "A" .. unsigned2b(id))
179 Storage_Distance_StoreCrossID(id)
180 QH_Merger_Add(QH_Collect_Routing_Dump, "A")
183 -- R
184 local function Storage_NodeRemoved(id)
185 if not QH_Collect_Routing_Dump then return end
186 Storage_LoopFlush()
188 QH_Merger_Add(QH_Collect_Routing_Dump, "R" .. unsigned2b(id) .. "R")
191 -- C
192 local function Storage_ClusterCreated(id)
193 if not QH_Collect_Routing_Dump then return end
194 Storage_LoopFlush()
196 QH_Merger_Add(QH_Collect_Routing_Dump, "C" .. unsigned2b(id) .. unsigned2b(#Cluster[id]))
197 for _, v in ipairs(Cluster[id]) do
198 QH_Merger_Add(QH_Collect_Routing_Dump, unsigned2b(v))
200 QH_Merger_Add(QH_Collect_Routing_Dump, "C")
203 -- D
204 local function Storage_ClusterDestroyed(id)
205 if not QH_Collect_Routing_Dump then return end
206 Storage_LoopFlush()
208 QH_Merger_Add(QH_Collect_Routing_Dump, "D" .. unsigned2b(id) .. "D")
211 -- >
212 local function Storage_ClusterDependency(from, to)
213 if not QH_Collect_Routing_Dump then return end
214 Storage_LoopFlush()
216 QH_Merger_Add(QH_Collect_Routing_Dump, ">" .. unsigned2b(from) .. unsigned2b(to) .. ">")
219 --[[
220 ----------------------------------
221 and here's the other end of the wacky storage system
222 ----------------------------------]]
224 -- we may need to play with these
225 local QH_Route_Core_NodeAdd_Internal
226 local QH_Route_Core_NodeRemove_Internal
228 if OptimizationHackery then
229 function Unstorage_SetDists(newdists)
230 local tc = 1
231 QuestHelper: Assert(#newdists == #ActiveNodes * #ActiveNodes)
232 for _, v in ipairs(ActiveNodes) do
233 for _, w in ipairs(ActiveNodes) do
234 Distance[v][w] = newdists[tc]
235 tc = tc + 1
238 QuestHelper: Assert(tc - 1 == #newdists)
241 function Unstorage_SetDistsX(pivot, newdists)
242 local tc = 1
243 QuestHelper: Assert(#newdists == #ActiveNodes * 2 - 1)
244 for _, v in ipairs(ActiveNodes) do
245 Distance[pivot][v] = newdists[tc]
246 tc = tc + 1
247 if v ~= pivot then
248 Distance[v][pivot] = newdists[tc]
249 tc = tc + 1
252 QuestHelper: Assert(tc - 1 == #newdists)
255 function Unstorage_SetDistsLine(pivot, newdists)
256 local tc = 1
257 QuestHelper: Assert(#newdists == #ActiveNodes)
259 if pivot == 1 then
260 if last_best and #last_best > 1 then
261 last_best.distance = last_best.distance - Distance[last_best[1]][last_best[2]]
265 for _, v in ipairs(ActiveNodes) do
266 Distance[pivot][v] = newdists[tc]
267 tc = tc + 1
269 QuestHelper: Assert(tc - 1 == #newdists)
271 if pivot == 1 then
272 if last_best and #last_best > 1 then
273 last_best.distance = last_best.distance + Distance[last_best[1]][last_best[2]]
278 function Unstorage_Add(nod)
279 QH_Route_Core_NodeAdd_Internal({}, nod)
282 function Unstorage_Remove(nod)
283 QH_Route_Core_NodeRemove_Internal({}, nod)
286 function Unstorage_ClusterAdd(nod, tab)
287 QH_Route_Core_ClusterAdd({}, nod)
288 for _, v in ipairs(tab) do
289 QuestHelper: Assert(NodeList[v])
290 ClusterLookup[v] = nod
291 table.insert(Cluster[nod], v)
295 function Unstorage_ClusterRemove(nod)
296 QH_Route_Core_ClusterRemove({}, nod)
299 function Unstorage_Link(a, b)
300 QH_Route_Core_ClusterRequires(a, b, true)
303 function Unstorage_Nastyscan()
304 for _, v in ipairs(ActiveNodes) do
305 for _, w in ipairs(ActiveNodes) do
306 QuestHelper: Assert(Distance[v][w])
307 QuestHelper: Assert(Weight[v][w])
312 function Unstorage_Magic(tab)
313 local touched = {}
315 PheremonePreservation = tab.PheremonePreservation QuestHelper: Assert(PheremonePreservation) touched.PheremonePreservation = true
316 AntCount = tab.AntCount QuestHelper: Assert(AntCount) touched.AntCount = true
317 WeightFactor = tab.WeightFactor QuestHelper: Assert(WeightFactor) touched.WeightFactor = true
318 DistanceFactor = tab.DistanceFactor QuestHelper: Assert(DistanceFactor) touched.DistanceFactor = true
319 DistanceDeweight = tab.DistanceDeweight QuestHelper: Assert(DistanceDeweight) touched.DistanceDeweight = true
320 UniversalBonus = tab.UniversalBonus QuestHelper: Assert(UniversalBonus) touched.UniversalBonus = true
322 for k, v in pairs(tab) do
323 QuestHelper: Assert(touched[k])
328 --[[
329 ----------------------------------
330 here ends the butt of the wacky storage system. yeah, that's right. I said butt. Butt. Hee hee. Butt.
331 ----------------------------------]]
334 function QH_Route_Core_NodeCount()
335 return #ActiveNodes
338 function QH_Route_Core_TraverseNodes(func)
339 for _, v in ipairs(ActiveNodes) do
340 if v ~= 1 then
341 local blocked = false
342 if ClusterLookup[v] and DependencyLinks[ClusterLookup[v]] and #DependencyLinks[ClusterLookup[v]] > 0 then blocked = true end
343 --print("nlx", NodeList[v], blocked)
344 func(NodeList[v], blocked)
349 function QH_Route_Core_TraverseClusters(func)
350 for k, v in pairs(ClusterTableLookup) do
351 func(k)
355 function QH_Route_Core_IgnoredReasons_Cluster(clust, func)
356 for k, _ in pairs(ClusterIgnored[ClusterTableLookup[clust]]) do
357 if type(k) == "table" then
358 func(k)
363 function QH_Route_Core_IgnoredReasons_Node(node, func)
364 for k, _ in pairs(NodeIgnored[NodeLookup[node]]) do
365 if type(k) == "table" then
366 func(k)
371 function QH_Route_Core_Ignored_Cluster(clust)
372 return ClusterIgnoredCount[ClusterTableLookup[clust]] ~= 0
375 -- fuck floating-point
376 local function almost(a, b)
377 if a == b then return true end
378 if type(a) ~= "number" or type(b) ~= "number" then return false end
379 if a == 0 or b == 0 then return false end
380 return math.abs(a / b - 1) < 0.0001
383 -- Initialization
384 function QH_Route_Core_Init(PathNotifier, DistanceBatch)
385 Notifier = PathNotifier
386 DistBatch = DistanceBatch
387 QuestHelper: Assert(Notifier)
388 QuestHelper: Assert(DistBatch)
390 -- End initialization
392 local last_best = nil
393 local last_best_tweaked = false
395 local function ValidateNumber(x)
396 QuestHelper: Assert(x == x)
397 QuestHelper: Assert(x ~= math.huge)
398 QuestHelper: Assert(x ~= -math.huge)
401 local function GetWeight(x, y)
402 if x == y then return 0.00000000001 end -- sigh
403 --local idx = GetIndex(x, y)
404 --local revidx = GetIndex(y, x)
405 if not Weight[x][y] or not Distance[x][y] then
406 RTO(string.format("%d/%d %d", x, y, CurrentNodes))
407 QuestHelper: Assert(x <= CurrentNodes)
408 QuestHelper: Assert(y <= CurrentNodes)
409 QuestHelper: Assert(false)
411 local weight = math.pow(Weight[x][y], WeightFactor) * math.pow(Distance[x][y] + DistanceDeweight, DistanceFactor)
412 --print(Weight[idx], Weight[revidx], bonus, WeightFactor, Distance[idx], DistanceFactor)
413 --ValidateNumber(weight)
414 return weight
417 -- Realtime splice
418 local function DespliceCN(cluster, node)
419 QuestHelper: Assert(not cluster or not node)
420 QuestHelper: Assert(cluster or node)
421 if not last_best then return end
423 local ct = 0
424 for i = 2, #last_best do
425 if last_best[i] and (last_best[i] == node or ClusterLookup[last_best[i]] == cluster) then
426 -- Splice it out!
427 last_best.distance = last_best.distance - Distance[last_best[i - 1]][last_best[i]]
428 if i ~= #last_best then
429 last_best.distance = last_best.distance - Distance[last_best[i]][last_best[i + 1]]
431 table.remove(last_best, i)
432 if i ~= #last_best + 1 then
433 last_best.distance = last_best.distance + Distance[last_best[i - 1]][last_best[i]]
436 ct = ct + 1
437 i = i - 1
441 last_best_tweaked = true
443 --QuestHelper:TextOut("Despliced with " .. ct)
446 local function SpliceIn(index, touched)
447 QuestHelper: Assert(index)
448 if touched[index] then return end
450 QH_Timeslice_Yield()
452 -- First, try to splice everything it depends on
453 if DependencyLinks[index] then for _, v in ipairs(DependencyLinks[index]) do
454 if SpliceIn(v, touched) then
455 return true
457 end end
459 local dl_lookup = QuestHelper:CreateTable("splice dl lookup")
460 local dlr_lookup = QuestHelper:CreateTable("splice dlr lookup")
461 if DependencyLinks[index] then for _, v in ipairs(DependencyLinks[index]) do dl_lookup[v] = true end end
462 if DependencyLinksReverse[index] then for _, v in ipairs(DependencyLinksReverse[index]) do dlr_lookup[v] = true end end
464 local start_bound = 2
465 local end_bound
467 -- Next, figure out where it can go
468 for i = 2, #last_best do
469 --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])
470 if dl_lookup[ClusterLookup[last_best[i]]] then start_bound = i + 1 end
471 if dlr_lookup[ClusterLookup[last_best[i]]] and not end_bound then end_bound = i end
472 if ClusterPriority[ClusterLookup[last_best[i]]] < ClusterPriority[index] then start_bound = i + 1 end
473 if ClusterPriority[ClusterLookup[last_best[i]]] > ClusterPriority[index] and not end_bound then end_bound = i end
475 if not end_bound then end_bound = #last_best + 1 end
476 --QuestHelper: TextOut(string.format("Placed cluster %d between %d and %d", index, start_bound, end_bound))
478 if end_bound < start_bound then
479 -- arrrrgh
480 -- this should never happen, but I don't want it to show up all the time, sooooo
481 QuestHelper_ErrorCatcher_ExplicitError(false, string.format("Routing paradox: %d and %d, panicking and restarting", start_bound, end_bound))
482 return true
485 -- Figure out the best place to put it
486 local best_spot = nil
487 local best_node = nil
488 local best_cost = nil
489 for i = start_bound, end_bound do
490 for _, nindex in ipairs(Cluster[index]) do
491 if NodeIgnoredCount[nindex] == 0 then
492 local tcost = Distance[last_best[i - 1]][nindex] -- Cost of that-node-to-this-one
493 if i <= #last_best then
494 tcost = tcost + Distance[nindex][last_best[i]] - Distance[last_best[i - 1]][last_best[i]]
496 if not best_cost or tcost < best_cost then
497 best_spot, best_node, best_cost = i, nindex, tcost
503 QuestHelper: Assert(best_spot)
504 table.insert(last_best, best_spot, best_node)
505 last_best.distance = last_best.distance + best_cost
507 touched[index] = true
508 last_best_tweaked = true
510 QuestHelper:ReleaseTable(dl_lookup)
511 QuestHelper:CreateTable(dlr_lookup)
513 -- end realtime splice
515 -- 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.
516 local function RunAnt()
517 local route = NewRoute()
518 route[1] = 1
519 route.distance = 0
521 local dependencies = QuestHelper:CreateTable("route_core_dependencies")
523 local needed = QuestHelper:CreateTable("route_core_needed")
524 local needed_count = 0
525 local needed_ready_count = 0
527 for k, v in pairs(DependencyCounts) do
528 dependencies[k] = v
529 QuestHelper: Assert(dependencies[k] >= 0)
532 local curloc = 1
534 local gwc = QuestHelper:CreateTable("route_core_gwc")
536 --TestShit()
538 for k, v in ipairs(Priorities) do
539 if Priorities[k + 1] then
540 QuestHelper: Assert(Priorities[k] < Priorities[k + 1])
544 for _, current_pri in ipairs(Priorities) do
546 -- Here is we add the new batch of nodes
547 for _, v in ipairs(ActiveNodes) do
548 QH_Timeslice_Yield()
549 if v ~= 1 then -- if it's ignored, then we just plain don't do anything
550 local clustid = ClusterLookup[v]
551 QuestHelper: Assert(clustid)
553 if ClusterPriority[clustid] < current_pri then
554 QuestHelper: Assert(dependencies[clustid] == -1 or NodeIgnoredCount[v] > 0 or ClusterIgnoredCount[clustid] >= 0)
555 elseif ClusterPriority[clustid] == current_pri then
556 if NodeIgnoredCount[v] == 0 and ClusterIgnoredCount[clustid] == 0 then
557 local need = false
559 QuestHelper: Assert(dependencies[clustid])
560 if dependencies[clustid] == 0 then
561 needed[v] = true
562 needed_ready_count = needed_ready_count + 1
565 needed_count = needed_count + 1
567 else
568 QuestHelper: Assert(dependencies[clustid] ~= -1, clustid)
573 if not (needed_ready_count > 0 or needed_count == 0) then
574 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
577 while needed_count > 0 do
578 QH_Timeslice_Yield()
579 QuestHelper: Assert(needed_ready_count > 0)
581 local accumulated_weight = 0
582 local tweight = 0
583 for k, _ in pairs(needed) do
584 local tw = GetWeight(curloc, k)
585 gwc[k] = tw
586 accumulated_weight = accumulated_weight + tw
589 tweight = accumulated_weight
590 accumulated_weight = accumulated_weight * math.random()
592 QH_Timeslice_Yield()
594 local nod = nil
595 for k, _ in pairs(needed) do
596 accumulated_weight = accumulated_weight - gwc[k]
597 if accumulated_weight < 0 then
598 nod = k
599 break
603 if not nod then
604 RTO(string.format("no nod :( %f/%f", accumulated_weight, tweight))
605 for k, _ in pairs(needed) do
606 nod = k
607 break
611 -- Now we've chosen stuff. Bookkeeping.
612 local clust = ClusterLookup[nod]
613 QuestHelper: Assert(clust)
615 -- Obliterate other cluster items. Guaranteed to be at the same priority level.
616 for _, v in pairs(Cluster[clust]) do
617 if NodeIgnoredCount[v] == 0 then
618 needed[v] = nil
619 needed_count = needed_count - 1
620 needed_ready_count = needed_ready_count - 1
624 -- Dependency links.
625 if DependencyLinksReverse[clust] then for _, v in ipairs(DependencyLinksReverse[clust]) do
626 dependencies[v] = dependencies[v] - 1
627 QuestHelper: Assert(dependencies[v] >= 0)
628 if dependencies[v] == 0 and ClusterIgnoredCount[v] == 0 and ClusterPriority[v] == current_pri then
629 for _, v in pairs(Cluster[v]) do
630 if NodeIgnoredCount[v] == 0 then
631 needed[v] = true
632 needed_ready_count = needed_ready_count + 1
636 end end
638 QuestHelper: Assert(dependencies[clust] == 0)
639 QuestHelper: Assert(ClusterPriority[clust] == current_pri)
640 dependencies[clust] = -1
642 --print(needed_count, needed_ready_count)
644 route.distance = route.distance + Distance[curloc][nod]
645 table.insert(route, nod)
646 curloc = nod
649 QuestHelper: Assert(needed_ready_count == 0 and needed_count == 0)
652 QuestHelper:ReleaseTable(gwc)
653 QuestHelper:ReleaseTable(dependencies)
654 QuestHelper:ReleaseTable(needed)
656 return route
659 -- Lots of doublechecks to make sure our route is both sane and complete
660 local function CheckRoute(route)
661 --print("starting check")
663 QuestHelper: Assert(route[1] == 1) -- starting at the beginning
665 local td = 0
666 for i = 1, #route - 1 do
667 td = td + Distance[route[i]][route[i + 1]]
669 --print(td, route.distance)
670 QuestHelper: Assert(abs(td - route.distance) < 5 or abs(td / route.distance - 1) < 0.0001)
672 local seen = QuestHelper:CreateTable("check seen")
674 local cpri = nil
675 for i = 1, #route do
676 QuestHelper: Assert(NodeIgnoredCount[route[i]] == 0)
678 local clust = ClusterLookup[route[i]]
679 if clust then
680 --print("seeing cluster ", clust, cpri, ClusterPriority[clust])
681 QuestHelper: Assert(ClusterIgnoredCount[clust] == 0)
682 QuestHelper: Assert(not seen[clust])
683 seen[clust] = true
684 QuestHelper: Assert(not cpri or cpri <= ClusterPriority[clust])
685 cpri = ClusterPriority[clust]
687 if DependencyLinks[clust] then for _, v in ipairs(DependencyLinks[clust]) do QuestHelper: Assert(seen[v]) end end
688 if DependencyLinksReverse[clust] then for _, v in ipairs(DependencyLinksReverse[clust]) do QuestHelper: Assert(not seen[v]) end end
692 for k, v in pairs(ClusterIgnoredCount) do
693 QuestHelper: Assert(not seen[k] == (ClusterIgnoredCount[k] > 0))
696 QuestHelper:ReleaseTable(seen)
698 --print("ending check")
701 local function BetterRoute(route)
702 CheckRoute(route)
703 local rt = {}
704 for k, v in ipairs(route) do
705 rt[k] = NodeList[v]
707 rt.distance = route.distance -- this is probably temporary
708 Notifier(rt)
711 local QH_Route_Core_DistanceClear_Local -- sigh
712 -- Core process function
713 function QH_Route_Core_Process()
714 Storage_Loop()
716 -- First we check to see if we need to add more distances, and if so, we do so
718 local refreshcount = 0
719 for k, v in pairs(DistanceWaiting) do
720 refreshcount = refreshcount + 1
723 if refreshcount > 0 then
724 if debug_output then QuestHelper:TextOut(string.format("Refreshing %d", refreshcount)) end
725 if refreshcount >= #ActiveNodes / 2 then
726 -- Refresh everything!
727 QH_Route_Core_DistanceClear_Local()
728 else
729 local tlnod = QuestHelper:CreateTable("routecore distance tlnod")
730 for _, v in ipairs(ActiveNodes) do
731 table.insert(tlnod, NodeList[v])
734 for idx, _ in pairs(DistanceWaiting) do
735 -- Refresh a subset of things.
736 local forward = DistBatch(NodeList[idx], tlnod)
737 local backward = DistBatch(NodeList[idx], tlnod, true)
739 for k, v in ipairs(ActiveNodes) do
740 Distance[idx][v] = forward[k]
741 Distance[v][idx] = backward[k]
744 QuestHelper:ReleaseTable(forward)
745 QuestHelper:ReleaseTable(backward)
747 QuestHelper:ReleaseTable(tlnod)
749 QuestHelper:ReleaseTable(DistanceWaiting)
750 DistanceWaiting = QuestHelper:CreateTable("routecore distance waiting")
754 -- Next we see if last_best needs tweaking
755 if last_best then
756 local touched_clusts = QuestHelper:CreateTable("routing touched")
757 for i = 2, #last_best do
758 local clust = ClusterLookup[last_best[i]]
759 QuestHelper: Assert(clust)
760 QuestHelper: Assert(not touched_clusts[clust])
761 touched_clusts[clust] = true
764 for k, _ in pairs(Cluster) do
765 local exists = touched_clusts[k]
766 local ignored = (ClusterIgnoredCount[k] ~= 0)
767 QuestHelper: Assert(not (ignored and exists)) -- something went wrong
769 if not ignored and not exists then
770 -- here we go
771 if SpliceIn(k, touched_clusts) then
772 last_best = nil
773 break
775 last_best_tweaked = true
778 QuestHelper:ReleaseTable(touched_clusts)
781 if last_best_tweaked and last_best then
782 --QuestHelper:TextOut("Pushing tweaked")
783 BetterRoute(last_best)
784 last_best_tweaked = false
787 local worst = 0
789 local best_is_local = false
791 local trouts = QuestHelper:CreateTable("routing_core_trouts")
792 for x = 1, AntCount do
793 table.insert(trouts, RunAnt())
794 --if last_best then RTO(string.format("Path generated: %s vs %s", PathToString(trouts[#trouts]), PathToString(last_best))) end
795 if not last_best or last_best.distance > trouts[#trouts].distance then
796 if last_best and not best_is_local then QuestHelper:ReleaseTable(last_best) end
798 best_is_local = true
799 last_best = trouts[#trouts]
800 BetterRoute(last_best)
803 worst = math.max(worst, trouts[#trouts].distance)
805 QH_Timeslice_Yield()
808 local scale
809 if worst == last_best.distance then
810 scale = 1
811 else
812 scale = 1 / (worst - last_best.distance)
815 QH_Timeslice_Yield()
817 for _, x in ipairs(ActiveNodes) do
818 local wx = Weight[x]
819 for _, y in ipairs(ActiveNodes) do
820 --local idx = GetIndex(x, y)
821 wx[y] = wx[y] * PheremonePreservation + UniversalBonus
822 --ValidateNumber(Weight[idx])
826 QH_Timeslice_Yield()
828 for _, x in ipairs(trouts) do
829 local amount = 1 / x.distance
830 for y = 1, #x - 1 do
831 --local idx = GetIndex(x[y], x[y + 1])
832 Weight[x[y]][x[y + 1]] = Weight[x[y]][x[y + 1]] + amount
833 --ValidateNumber(Weight[idx])
837 QH_Timeslice_Yield()
839 local weitotal = 0
840 local weicount = 0
841 for _, x in ipairs(ActiveNodes) do
842 local wx = Weight[x]
843 for _, y in ipairs(ActiveNodes) do
844 --local idx = GetIndex(x, y)
845 weitotal = weitotal + wx[y]
846 weicount = weicount + 1
850 QH_Timeslice_Yield()
852 weight_ave = weitotal / weicount
854 for k, v in pairs(trouts) do
855 if v ~= last_best then
856 QuestHelper:ReleaseTable(v)
859 QuestHelper:ReleaseTable(trouts)
861 QH_Timeslice_Yield() -- "heh"
863 -- End core loop
865 -- Ignore/unignore
866 local function RecursiveIgnoreCount(clustid, accum)
867 if accum == 0 then return end
868 --print(clustid, accum)
870 if ClusterIgnoredCount[clustid] == 0 then QuestHelper: Assert(accum > 0) DespliceCN(clustid) end
871 ClusterIgnoredCount[clustid] = ClusterIgnoredCount[clustid] + accum
872 if ClusterIgnoredCount[clustid] == 0 then QuestHelper: Assert(accum < 0) end -- Item being added, we'll handle this at the beginning of run
874 if DependencyLinksReverse[clustid] then
875 for _, v in pairs(DependencyLinksReverse[clustid]) do
876 RecursiveIgnoreCount(v, accum)
881 local function Internal_IgnoreCluster(clustid, reason)
882 QuestHelper: Assert(clustid)
884 if not ClusterIgnored[clustid][reason] then
885 ClusterIgnored[clustid][reason] = true
886 RecursiveIgnoreCount(clustid, 1)
890 local function Internal_UnignoreCluster(clustid, reason)
891 QuestHelper: Assert(clustid)
892 if ClusterIgnored[clustid][reason] then
893 ClusterIgnored[clustid][reason] = nil
894 RecursiveIgnoreCount(clustid, -1)
898 function QH_Route_Core_IgnoreCluster(clust, reason)
899 QuestHelper: Assert(type(reason) == "table")
900 local clustid = ClusterTableLookup[clust]
901 if not clustid then
902 -- This can just happen due to the lag introduced by the controller, so, whatever
903 --QuestHelper:TextOut("Attempted to ignore a cluster that no longer exists")
904 return
907 Internal_IgnoreCluster(clustid, reason)
910 function QH_Route_Core_UnignoreCluster(clust, reason)
911 QuestHelper: Assert(type(reason) == "table")
912 local clustid = ClusterTableLookup[clust]
913 if not clustid then
914 -- This can just happen due to the lag introduced by the controller, so, whatever
915 --QuestHelper:TextOut("Attempted to unignore a cluster that no longer exists")
916 return
918 Internal_UnignoreCluster(clustid, reason)
921 function QH_Route_Core_IgnoreNode(node, reason)
922 QuestHelper: Assert(type(reason) == "table")
923 local nid = NodeLookup[node]
924 if not nid then
925 -- This can just happen due to the lag introduced by the controller, so, whatever
926 --QuestHelper:TextOut("Attempted to ignore a node that no longer exists")
927 return
930 QuestHelper: Assert(nid)
932 if not NodeIgnored[nid][reason] then
933 NodeIgnored[nid][reason] = true
935 NodeIgnoredCount[nid] = NodeIgnoredCount[nid] + 1
936 if NodeIgnoredCount[nid] == 1 then
937 DespliceCN(nil, nid)
939 if ClusterLookup[nid] then
940 ClusterIgnoredNodeActive[ClusterLookup[nid]] = ClusterIgnoredNodeActive[ClusterLookup[nid]] - 1
941 if ClusterIgnoredNodeActive[ClusterLookup[nid]] == 0 then
942 Internal_IgnoreCluster(ClusterLookup[nid], "internal_node_ignored")
949 function QH_Route_Core_UnignoreNode(node, reason)
950 QuestHelper: Assert(type(reason) == "table")
951 local nid = NodeLookup[node]
952 if not nid then
953 -- This can just happen due to the lag introduced by the controller, so, whatever
954 --QuestHelper:TextOut("Attempted to unignore a node that no longer exists")
955 return
958 QuestHelper: Assert(nid)
960 if NodeIgnored[nid][reason] then
961 NodeIgnored[nid][reason] = nil
963 NodeIgnoredCount[nid] = NodeIgnoredCount[nid] - 1
964 if NodeIgnoredCount[nid] == 0 then
965 -- Item being added
967 if ClusterLookup[nid] then
968 -- Item being added
969 ClusterIgnoredNodeActive[ClusterLookup[nid]] = ClusterIgnoredNodeActive[ClusterLookup[nid]] + 1
970 if ClusterIgnoredNodeActive[ClusterLookup[nid]] == 1 then
971 Internal_UnignoreCluster(ClusterLookup[nid], "internal_node_ignored")
978 local QH_Route_Core_UnignoreCluster = QH_Route_Core_UnignoreCluster -- we're just saving this so it doesn't get splattered
979 -- End ignore/unignore
981 -- Node allocation and deallocation
982 -- this is only separate so we can use it for the crazy optimization hackery
983 local function Expand()
984 for _, v in ipairs(Distance) do
985 table.insert(v, 0)
987 for _, v in ipairs(Weight) do
988 table.insert(v, 0)
990 table.insert(Distance, {})
991 table.insert(Weight, {})
993 for k = 1, CurrentNodes + 1 do
994 table.insert(Distance[#Distance], 0)
995 table.insert(Weight[#Weight], 0)
998 CurrentNodes = CurrentNodes + 1
1001 -- 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?
1002 local function AllocateExtraNode()
1003 if #DeadNodes > 0 then
1004 local nod = table.remove(DeadNodes)
1005 table.insert(ActiveNodes, nod)
1006 table.sort(ActiveNodes)
1007 return nod
1010 -- We always allocate on the end, so we know this is safe.
1011 Expand()
1013 table.insert(DeadNodes, CurrentNodes)
1014 return AllocateExtraNode() -- ha ha
1017 -- Set the start location
1018 function QH_Route_Core_SetStart(stt)
1019 -- We do some kind of ghastly things here.
1020 --TestShit()
1021 if last_best and #last_best > 1 then
1022 last_best.distance = last_best.distance - Distance[last_best[1]][last_best[2]]
1025 NodeLookup[StartNode] = nil
1026 NodeList[1] = stt
1027 StartNode = stt
1028 NodeLookup[StartNode] = 1
1030 local tlnod = QuestHelper:CreateTable("routecore setstart tlnod")
1032 for _, v in ipairs(ActiveNodes) do
1033 if v ~= 1 then
1034 table.insert(tlnod, NodeList[v])
1038 local forward = DistBatch(NodeList[1], tlnod)
1040 local ct = 1
1041 for _, v in ipairs(ActiveNodes) do
1042 if v ~= 1 then
1043 QuestHelper: Assert(forward[ct])
1044 Distance[1][v] = forward[ct]
1045 ct = ct + 1
1047 Distance[v][1] = 65500 -- this should never be used anyway
1051 if last_best and #last_best > 1 then
1052 last_best.distance = last_best.distance + Distance[last_best[1]][last_best[2]]
1055 QuestHelper:ReleaseTable(forward)
1056 QuestHelper:ReleaseTable(tlnod)
1058 Storage_Distance_StoreFromIDToAll(1)
1059 --TestShit()
1060 -- TODO: properly deallocate old startnode?
1063 QH_Route_Core_NodeAdd_Internal = function (nod, used_idx)
1064 --QuestHelper:TextOut(tostring(nod))
1065 --TestShit()
1066 QuestHelper: Assert(nod)
1067 if NodeLookup[nod] then
1068 -- ughhh
1069 QuestHelper: Assert(not NodeLookup[nod], QuestHelper:StringizeTable(nod))
1072 local idx
1073 if used_idx then
1074 QuestHelper: Assert(OptimizationHackery)
1075 QuestHelper: Assert(not NodeList[used_idx])
1076 idx = used_idx
1077 table.insert(ActiveNodes, used_idx)
1078 table.sort(ActiveNodes)
1079 if not Distance[idx] then Expand() QuestHelper: Assert(Distance[idx]) end
1080 else
1081 idx = AllocateExtraNode()
1084 --RTO("|cffFF8080AEN: " .. tostring(idx))
1085 NodeLookup[nod] = idx
1086 NodeList[idx] = nod
1088 NodeIgnored[idx] = {}
1089 NodeIgnoredCount[idx] = 0
1091 for _, v in ipairs(ActiveNodes) do
1092 Weight[v][idx] = weight_ave
1093 Weight[idx][v] = weight_ave
1096 DistanceWaiting[idx] = true
1098 -- Item being added
1100 return idx
1103 -- Remove a node with the given location
1104 QH_Route_Core_NodeRemove_Internal = function (nod, used_idx)
1105 --TestShit()
1106 QuestHelper: Assert(nod)
1108 local idx
1109 if used_idx then
1110 QuestHelper: Assert(OptimizationHackery)
1111 QuestHelper: Assert(NodeList[used_idx])
1112 idx = used_idx
1113 else
1114 QuestHelper: Assert(NodeLookup[nod])
1115 idx = NodeLookup[nod]
1118 --RTO("|cffFF8080RFN: " .. tostring(NodeLookup[nod]))
1119 NodeList[idx] = nil
1120 table.insert(DeadNodes, idx)
1121 local oas = #ActiveNodes
1122 for k, v in pairs(ActiveNodes) do if v == idx then table.remove(ActiveNodes, k) break end end -- this is pretty awful
1123 QuestHelper: Assert(#ActiveNodes < oas)
1124 NodeLookup[nod] = nil
1125 -- We don't have to modify the table itself, some sections are just "dead".
1126 --TestShit()
1128 DistanceWaiting[idx] = nil -- just in case we haven't updated it in the intervening time
1130 -- If we're a standalone node, nothing depends on us. If we're part of a cluster, the cluster's getting smoked anyway.
1131 NodeIgnored[idx] = nil
1132 NodeIgnoredCount[idx] = nil
1134 DespliceCN(nil, idx)
1136 return idx
1138 -- End node allocation and deallocation
1140 function QH_Route_Core_ClusterAdd(clust, clustid_used)
1141 local clustid
1142 if clustid_used then
1143 QuestHelper: Assert(OptimizationHackery)
1144 QuestHelper: Assert(not Cluster[clustid_used])
1145 clustid = clustid_used
1146 else
1147 QuestHelper: Assert(#clust > 0)
1148 clustid = table.remove(ClusterDead)
1149 if not clustid then clustid = #Cluster + 1 end
1152 if debug_output then QuestHelper:TextOut(string.format("Adding cluster %d", clustid)) end
1154 Cluster[clustid] = {}
1155 ClusterTableLookup[clust] = clustid
1157 ClusterIgnored[clustid] = {}
1158 ClusterIgnoredCount[clustid] = 0
1159 ClusterIgnoredNodeActive[clustid] = #clust
1161 ClusterPriority[clustid] = 0
1162 if not PriorityCount[0] then table.insert(Priorities, 0) table.sort(Priorities) end
1163 PriorityCount[0] = (PriorityCount[0] or 0) + 1
1165 -- if we're doing hackery, clust will just be an empty table and we'll retrofit stuff later
1166 for _, v in ipairs(clust) do
1167 local idx = QH_Route_Core_NodeAdd_Internal(v)
1168 Storage_NodeAdded(idx)
1169 ClusterLookup[idx] = clustid
1170 table.insert(Cluster[clustid], idx)
1173 DependencyCounts[clustid] = 0
1175 Storage_ClusterCreated(clustid)
1178 function QH_Route_Core_ClusterRemove(clust, clustid_used)
1179 local clustid
1180 if clustid_used then
1181 QuestHelper: Assert(OptimizationHackery)
1182 QuestHelper: Assert(Cluster[clustid_used])
1183 clustid = clustid_used
1185 for _, v in ipairs(Cluster[clustid]) do
1186 QH_Route_Core_NodeRemove_Internal({}, v)
1187 ClusterLookup[v] = nil
1189 else
1190 clustid = ClusterTableLookup[clust]
1194 local ct = 0
1195 local abort
1196 repeat
1197 QuestHelper: Assert(ct < 100)
1198 abort = true
1199 for k, v in pairs(ClusterIgnored[clustid]) do
1200 abort = false
1201 Internal_UnignoreCluster(clustid, k)
1202 ct = ct + 1
1203 break
1205 until abort
1206 -- 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.
1207 RecursiveIgnoreCount(clustid, -ClusterIgnoredCount[clustid])
1208 QuestHelper: Assert(ClusterIgnoredCount[clustid] == 0)
1211 if debug_output then QuestHelper:TextOut(string.format("Removing cluster %d", clustid)) end
1213 for _, v in ipairs(clust) do
1214 local idx = QH_Route_Core_NodeRemove_Internal(v)
1215 ClusterLookup[idx] = nil
1218 DependencyCounts[clustid] = nil
1220 if DependencyLinks[clustid] then
1221 for k, v in pairs(DependencyLinks[clustid]) do
1222 for m, f in pairs(DependencyLinksReverse[v]) do
1223 if f == clustid then
1224 if debug_output then QuestHelper:TextOut(string.format("Unlinking cluster %d needs %d", clustid, v)) end
1225 table.remove(DependencyLinksReverse[v], m)
1226 break
1231 DependencyLinks[clustid] = nil
1233 if DependencyLinksReverse[clustid] then
1234 for k, v in pairs(DependencyLinksReverse[clustid]) do
1235 for m, f in pairs(DependencyLinks[v]) do
1236 if f == clustid then
1237 if debug_output then QuestHelper:TextOut(string.format("Unlinking cluster %d needs %d", v, clustid)) end
1238 table.remove(DependencyLinks[v], m)
1239 DependencyCounts[v] = DependencyCounts[v] - 1
1240 break
1245 DependencyLinksReverse[clustid] = nil
1247 Cluster[clustid] = nil
1248 ClusterTableLookup[clust] = nil
1249 table.insert(ClusterDead, clustid)
1251 ClusterIgnored[clustid] = nil
1252 ClusterIgnoredCount[clustid] = nil
1253 ClusterIgnoredNodeActive[clustid] = nil
1255 local pri = ClusterPriority[clustid]
1256 PriorityCount[pri] = PriorityCount[pri] - 1
1257 if PriorityCount[pri] == 0 then
1258 PriorityCount[pri] = nil
1260 for k, v in ipairs(Priorities) do
1261 if v == pri then
1262 Priorities[k] = Priorities[#Priorities]
1263 table.remove(Priorities)
1264 table.sort(Priorities)
1265 break
1269 ClusterPriority[clustid] = nil
1271 Storage_ClusterDestroyed(clustid)
1274 -- Add a note that node 1 requires node 2.
1275 function QH_Route_Core_ClusterRequires(a, b, hackery)
1276 local aidx
1277 local bidx
1278 if hackery then
1279 QuestHelper: Assert(OptimizationHackery)
1280 QuestHelper: Assert(Cluster[a])
1281 QuestHelper: Assert(Cluster[b])
1282 aidx, bidx = a, b
1283 else
1284 aidx = ClusterTableLookup[a]
1285 bidx = ClusterTableLookup[b]
1287 QuestHelper: Assert(aidx)
1288 QuestHelper: Assert(bidx)
1289 QuestHelper: Assert(aidx ~= bidx)
1291 if debug_output then QuestHelper:TextOut(string.format("Linking cluster %d needs %d", aidx, bidx)) end
1293 DependencyCounts[aidx] = DependencyCounts[aidx] + 1
1295 if not DependencyLinks[aidx] then DependencyLinks[aidx] = {} end
1296 table.insert(DependencyLinks[aidx], bidx)
1298 if not DependencyLinksReverse[bidx] then DependencyLinksReverse[bidx] = {} end
1299 table.insert(DependencyLinksReverse[bidx], aidx)
1301 DespliceCN(aidx)
1302 DespliceCN(bidx)
1304 Storage_ClusterDependency(aidx, bidx)
1307 function QH_Route_Core_GetClusterPriority(clust)
1308 return ClusterPriority[ClusterTableLookup[clust]]
1311 local function QH_Route_Core_SetClusterPriority_Internal(clustid, new_pri)
1312 QuestHelper: Assert(clustid)
1313 if ClusterPriority[clustid] == new_pri then return end
1314 --QuestHelper:TextOut(string.format("Setting %d to %d", clustid, new_pri))
1316 local pri = ClusterPriority[clustid]
1317 QuestHelper: Assert(pri)
1318 PriorityCount[pri] = PriorityCount[pri] - 1
1319 if PriorityCount[pri] == 0 then
1320 PriorityCount[pri] = nil
1322 for k, v in ipairs(Priorities) do
1323 if v == pri then
1324 Priorities[k] = Priorities[#Priorities]
1325 table.remove(Priorities)
1326 table.sort(Priorities)
1327 break
1332 ClusterPriority[clustid] = new_pri
1333 if not PriorityCount[new_pri] then table.insert(Priorities, new_pri) table.sort(Priorities) end
1334 PriorityCount[new_pri] = (PriorityCount[new_pri] or 0) + 1
1336 DespliceCN(clustid)
1338 -- 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.
1340 -- Clusters that this one depends on. Must happen first (i.e. have a smaller or equal priority)
1341 if DependencyLinks[clustid] then for _, v in ipairs(DependencyLinks[clustid]) do
1342 if ClusterPriority[v] > new_pri then QH_Route_Core_SetClusterPriority_Internal(v, new_pri) end
1343 end end
1345 -- Clusters that depend on this one. Must happen last (i.e. have a greater or equal priority)
1346 if DependencyLinksReverse[clustid] then for _, v in ipairs(DependencyLinksReverse[clustid]) do
1347 if ClusterPriority[v] < new_pri then QH_Route_Core_SetClusterPriority_Internal(v, new_pri) end
1348 end end
1351 function QH_Route_Core_SetClusterPriority(clust, new_pri)
1352 QuestHelper: Assert(clust)
1353 local clustid = ClusterTableLookup[clust]
1355 QH_Route_Core_SetClusterPriority_Internal(clustid, new_pri)
1358 -- Wipe and re-cache all distances.
1359 function QH_Route_Core_DistanceClear()
1360 local tlnod = {}
1361 for _, v in ipairs(ActiveNodes) do
1362 table.insert(tlnod, NodeList[v])
1365 for ani, idx in ipairs(ActiveNodes) do
1366 local forward = DistBatch(NodeList[idx], tlnod, false, true)
1368 for k, v in ipairs(ActiveNodes) do
1369 Distance[idx][v] = forward[k]
1372 if QuestHelper.loading_preroll and #ActiveNodes > 1 then QuestHelper.loading_preroll:SetPercentage(ani / #ActiveNodes) end
1375 if last_best then
1376 last_best.distance = 0
1377 for i = 1, #last_best - 1 do
1378 last_best.distance = last_best.distance + Distance[last_best[i]][last_best[i + 1]]
1382 Storage_Distance_StoreAll()
1384 QH_Route_Core_DistanceClear_Local = QH_Route_Core_DistanceClear
1386 --[==[
1387 function findin(tab, val)
1388 local ct = 0
1389 for k, v in pairs(tab) do
1390 if v == val then ct = ct + 1 end
1392 return ct == 1
1395 function TestShit()
1396 --[[
1397 for x = 1, #ActiveNodes do
1398 local ts = ""
1399 for y = 1, #ActiveNodes do
1400 ts = ts .. string.format("%f ", Distance[GetIndex(ActiveNodes[x], ActiveNodes[y])])
1402 RTO(ts)
1406 --[[
1407 RTO("Lookup table")
1408 for x = 1, #ActiveNodes do
1409 RTO(tostring(ActiveNodes[x]))
1411 RTO("Lookup table done")
1414 --[=[
1415 local fail = false
1416 for x = 1, #ActiveNodes do
1417 for y = 2, #ActiveNodes do
1418 if not (almost(Dist(NodeList[ActiveNodes[x]], NodeList[ActiveNodes[y]]), Distance[ActiveNodes[x]][ActiveNodes[y]])) then
1419 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]]))
1420 fail = true
1423 end]=]
1425 for k, v in pairs(DependencyLinks) do
1426 QuestHelper: Assert(#v == DependencyCounts[k])
1429 for k, v in pairs(DependencyCounts) do
1430 QuestHelper: Assert(v == (DependencyLinks[k] and #DependencyLinks[k] or 0))
1433 for k, v in pairs(DependencyLinks) do
1434 for _, v2 in pairs(v) do
1435 QuestHelper: Assert(findin(DependencyLinksReverse[v2], k))
1439 for k, v in pairs(DependencyLinksReverse) do
1440 for _, v2 in pairs(v) do
1441 QuestHelper: Assert(findin(DependencyLinks[v2], k))
1445 QuestHelper: Assert(not fail)
1447 ]==]
1448 --[=[
1449 function HackeryDump()
1450 local st = "{"
1451 for k, v in pairs(ActiveNodes) do
1452 if v ~= 1 then
1453 st = st .. string.format("{c = %d, x = %f, y = %f}, ", NodeList[k].loc.c, NodeList[k].loc.x, NodeList[k].loc.y)
1456 st = st .. "}"
1457 QuestHelper: Assert(false, st)
1458 end]=]
1460 -- weeeeee