Automated update from: http://smariot.no-ip.org/translate
[QuestHelper.git] / graph_core.lua
blob99914bc0fd1962586fc0410227402f0a913a019d
1 QuestHelper_File["graph_core.lua"] = "Development Version"
2 QuestHelper_Loadtime["graph_core.lua"] = GetTime()
4 -- Alright so what's the interface here
5 -- There's the obvious "find a path from A to B"
6 -- function QH_Graph_Pathfind(st, nd, make_path)
7 -- Right now, we're pretending that the world consists of infinite planes with links between them. That's where the whole "between-zone" thing occurs. So one way or another, we need to add links. We'll use name as an identifier so we can get rid of flight paths links later, and the coords will include a plane ID number.
8 -- function QH_Graph_Plane_Makelink(name, coord1, coord2, cost)
9 -- And then we need a way to get rid of links, so:
11 -- how does flying work zorba
12 -- how can we fly
13 -- howwwwww
15 -- Make a map from "phase" to "flyphase". Store all the links we're being told to make. When placing links, if the flyphase is flyable, we use the flyphase instead of the phase for placing. We don't place if it's an internal boundary (there's a few ways we can detect this, but let's just use the hacky one where we just look at the ID.) From there it's all pretty standard.
17 -- performance hack :(
18 local QH_Timeslice_Yield = QH_Timeslice_Yield
19 local tinsert, tremove = table.insert, table.remove
20 local pairs, ipairs = pairs, ipairs
21 local sqrt = math.sqrt
22 local GetTime = GetTime
24 local function heap_left(x) return (2*x) end
25 local function heap_right(x) return (2*x + 1) end
27 local function heap_sane(heap)
28 local dmp = ""
29 local finishbefore = 2
30 for i = 1, #heap do
31 if i == finishbefore then
32 print(dmp)
33 dmp = ""
34 finishbefore = finishbefore * 2
35 end
36 dmp = dmp .. string.format("%f ", heap[i].c)
37 end
38 print(dmp)
39 print("")
40 for i = 1, #heap do
41 assert(not heap[heap_left(i)] or heap[i].c <= heap[heap_left(i)].c)
42 assert(not heap[heap_right(i)] or heap[i].c <= heap[heap_right(i)].c)
43 end
44 end
46 local function heap_insert(heap, item)
47 assert(item)
48 tinsert(heap, item)
49 local pt = #heap
50 while pt > 1 do
51 local ptd2 = math.floor(pt / 2)
52 if heap[ptd2].c <= heap[pt].c then
53 break
54 end
55 local tmp = heap[pt]
56 heap[pt] = heap[ptd2]
57 heap[ptd2] = tmp
58 pt = ptd2
59 end
60 --heap_sane(heap)
61 end
63 local function heap_extract(heap)
64 local rv = heap[1]
65 if #heap == 1 then tremove(heap) return rv end
66 heap[1] = tremove(heap)
67 local idx = 1
68 while idx < #heap do
69 local minix = idx
70 --local hl, hr = heap_left(idx), heap_right(idx)
71 local hl, hr = 2*idx, 2*idx+1 -- these had better be equivalent to the line above one
72 if heap[hl] and heap[hl].c < heap[minix].c then minix = hl end
73 if heap[hr] and heap[hr].c < heap[minix].c then minix = hr end
74 if minix ~= idx then
75 local tx = heap[minix]
76 heap[minix] = heap[idx]
77 heap[idx] = tx
78 idx = minix
79 else
80 break
81 end
82 end
83 --heap_sane(heap)
84 return rv
85 end
87 -- incremented on each graph iteration, so we don't have to clear everything. "GRaph ID" :D
88 local grid = 0
90 -- Plane format: each plane contains an array of nodes that exist in that plane.
91 -- Each node contains both its parent ID and its coordinates within that plane. It may contain a node it links to, along with cost.
92 local plane = {}
94 local plane_to_flyplane = {}
95 local continent_to_flyplane = {}
96 local flyplanes_enabled = {}
97 local plane_multiplier = {}
98 local plane_cull = {}
100 -- canonical plane :D
101 local function canoplane(plane)
102 if flyplanes_enabled[plane_to_flyplane[plane]] then return plane_to_flyplane[plane] else return plane end
105 local function xydist_raw(plane, stx, sty, ndx, ndy)
106 local dx, dy = stx - ndx, sty - ndy
107 return sqrt(dx * dx + dy * dy) / (plane_multiplier[plane] or 7)
109 local function xydist(st, nd)
110 --QuestHelper: Assert(canoplane(st.p) == canoplane(nd.p))
111 local dx, dy = st.x - nd.x, st.y - nd.y
112 return sqrt(dx * dx + dy * dy) / (plane_multiplier[canoplane(nd.p)] or 7) -- we're getting a result in seconds, not in yards
119 function QH_Graph_Pathfind(st, nd, reverse, make_path)
120 return QH_Graph_Pathmultifind(st, {nd}, reverse, make_path)[1]
123 local active = false
124 local prepared = false
127 local extrctime = 0
128 local wextrctime = 0
129 local actualtime = 0
130 local planetime = 0
131 local linktime = 0
133 function QH_Graph_Pathmultifind(st, nda, reverse, make_path)
134 --QuestHelper:TextOut("Starting PMF")
135 QuestHelper: Assert(not active)
136 QuestHelper: Assert(prepared)
137 active = true -- The fun thing about coroutines is that this is actually safe.
138 local out = QuestHelper:CreateTable("graphcore output") -- THIS HAD BETTER BE RELEASABLE OR IT WILL BE BAD
140 local undone = QuestHelper:CreateTable("graphcore undone")
141 local remaining = 0
143 local link = QuestHelper:CreateTable("graphcore link")
145 --local stats = QuestHelper:CreateTable("graphcore --stats")
147 QuestHelper: Assert(st.x and st.y and st.p)
149 --stats.dests_complex = 0
150 --stats.dests_total = 0
152 for k, v in ipairs(nda) do
153 QuestHelper: Assert(v.x and v.y and v.p)
154 local cpvp = canoplane(v.p)
155 if plane[cpvp] then
156 --print("Destination plane insertion")
157 local dest = QuestHelper:CreateTable("graphcore destination")
158 dest.x, dest.y, dest.p, dest.goal = v.x, v.y, cpvp, k
159 link[k] = dest
160 tinsert(plane[cpvp], link[k])
161 undone[k] = true
162 remaining = remaining + 1
163 --stats.dests_complex = --stats.dests_complex + 1
165 --stats.dests_total = --stats.dests_total + 1
168 local link_id = reverse and "rlinks" or "links"
170 --stats.node_initialized_first = 0
171 --stats.node_done = 0
172 --stats.node_done_already = 0
173 --stats.node_modified_before_use = 0
174 --stats.node_link_reprocessed = 0
175 --stats.node_link_first = 0
176 --stats.node_link_alreadydone = 0
177 --stats.node_inner_reprocessed = 0
178 --stats.node_inner_first = 0
179 --stats.node_inner_alreadydone = 0
181 local dijheap = QuestHelper:CreateTable("graphcore heap container")
182 local stnode = QuestHelper:CreateTable("graphcore stnode")
184 stnode.x, stnode.y, stnode.p, stnode.c, stnode.scan_id, stnode.scan_cost = st.x, st.y, canoplane(st.p), st.c, grid, 0
185 QuestHelper: Assert(plane[canoplane(st.p)])
186 tinsert(plane[stnode.p], stnode)
188 local hep = QuestHelper:CreateTable("graphcore heap")
189 hep.c, hep.n = 0, stnode -- more than the subtraction, less than the minimum
190 heap_insert(dijheap, hep)
193 --stats.heap_max = #dijheap
195 --QuestHelper:TextOut("Starting routing")
197 local extrc = 0
198 local actual = 0
199 local planescan = 0
200 local linkscan = 0
202 local planeyes = 0
203 local planeno = 0
205 local linkyes = 0
206 local linkno = 0
208 while remaining > 0 and #dijheap > 0 do
209 QH_Timeslice_Yield()
210 local ett = GetTime()
211 extrc = extrc + 1
212 --stats.heap_max = math.max(--stats.heap_max, #dijheap)
213 local cdj = heap_extract(dijheap)
215 local snode = cdj.n
216 --print(string.format("Extracted cost %f/%s pointing at %d/%f/%f", cdj.c, tostring(cdj.n.scan_cost), cdj.n.p, cdj.n.x, cdj.n.y))
217 if snode.scan_cost == cdj.c then -- if we've modified it since then, don't bother
218 local actt = GetTime()
219 actual = actual + 1
220 -- Are we at an end node?
221 if snode.goal then
222 -- we wouldn't be here if we hadn't found a better solution
223 --QuestHelper: Assert(undone[snode.goal])
224 out[snode.goal] = cdj.c
225 undone[snode.goal] = nil
226 remaining = remaining - 1
229 -- Link to everything else on the plane
230 if not cdj.pi then -- Did we come from this plane? If so, there's no reason to scan it again (flag means "plane internal")
231 local planet = GetTime()
232 planescan = planescan + 1
233 for _, v in ipairs(plane[snode.p]) do
234 if v.scan_id ~= grid or v.scan_cost > snode.scan_cost then
235 planeyes = planeyes + 1
236 local dst = xydist(snode, v)
237 local modcost = snode.scan_cost + dst
238 --print(string.format("Doing %d/%f vs %s/%s at %d/%f/%f", grid, modcost, tostring(v.scan_id), tostring(v.scan_cost), v.p, v.x, v.y))
239 if v.scan_id ~= grid or v.scan_cost > modcost then
240 v.scan_id = grid
241 v.scan_cost = modcost
242 v.scan_from = snode
243 v.scan_processed = false
244 v.scan_outnode = nil
245 v.scan_outnode_from = nil
248 local ncdj = QuestHelper:CreateTable("graphcore heap")
249 ncdj.c, ncdj.n, ncdj.pi = modcost, v, true
250 heap_insert(dijheap, ncdj)
251 --print(string.format("Inserting %f at %d/%f/%f, plane", snude.c, v.p, v.x, v.y))
254 else
255 planeno = planeno + 1
258 planetime = planetime + GetTime() - planet
261 -- Link to everything we link to
262 if not cdj.li and snode[link_id] then
263 local linkt = GetTime()
264 linkscan = linkscan + 1
265 for _, lnk in pairs(snode[link_id]) do
266 local mc2 = snode.scan_cost + lnk.cost
267 local linkto = lnk.link
268 if linkto.scan_id ~= grid or linkto.scan_cost > mc2 then
269 linkyes = linkyes + 1
270 linkto.scan_id = grid
271 linkto.scan_cost = mc2
272 linkto.scan_from = snode
273 linkto.scan_outnode = lnk.outnode_to
274 linkto.scan_outnode_from = lnk.outnode_from
276 local hep = QuestHelper:CreateTable("graphcore heap")
277 hep.c, hep.n, hep.li = mc2, linkto, true
278 heap_insert(dijheap, hep)
279 --print(string.format("Inserting %f at %d/%f/%f, link", hep.c, linkto.p, linkto.x, linkto.y))
280 else
281 linkno = linkno + 1
284 linktime = linktime + GetTime() - linkt
286 actualtime = actualtime + GetTime() - actt
287 extrctime = extrctime + GetTime() - ett
288 else
289 wextrctime = wextrctime + GetTime() - ett
291 QuestHelper:ReleaseTable(cdj)
294 --QuestHelper:TextOut(string.format("%d extracted, %d processed, %d planescan, %d linkscan, %d/%d plane, %d/%d link", extrc, actual, planescan, linkscan, planeyes, planeno, linkyes, linkno))
295 --QuestHelper:TextOut(string.format("times: %f/%f extracted, %f processed, %f planescan, %f linkscan", extrctime, wextrctime, actualtime, planetime, linktime))
297 for _, v in ipairs(dijheap) do
298 QuestHelper:ReleaseTable(v)
300 QuestHelper:ReleaseTable(dijheap)
301 dijheap = nil
303 --QuestHelper:TextOut(string.format("Earlyout with %d/%d remaining", #dijheap, remaining))
304 if remaining > 0 then
305 for k, v in ipairs(nda) do
306 if not out[k] then
307 QuestHelper: Assert(false, string.format("Couldn't find path to %d/%f/%f from %d/%f/%f", nda[k].p, nda[k].x, nda[k].y, st.p, st.x, st.y))
311 QuestHelper: Assert(remaining == 0)
313 grid = grid + 1
314 QuestHelper: Assert(grid < 1000000000) -- if this ever triggers I will be amazed
316 if make_path then
317 --print("mpath")
318 for k, v in pairs(out) do
319 QH_Timeslice_Yield()
320 --print(out[k])
321 local rp = QuestHelper:CreateTable("graphcore return")
322 rp.d = v
324 out[k] = rp
325 --print(out[k])
327 if link[k] then
328 QuestHelper: Assert(link[k].scan_from)
329 local tpath = reverse and rp or QuestHelper:CreateTable("graphcore path reversal")
330 local cpx = link[k].scan_from
331 local mdlast = nil
332 while cpx do
333 QuestHelper: Assert(cpx)
335 if cpx.scan_outnode then
336 tinsert(tpath, cpx.scan_outnode)
338 if cpx.scan_outnode_from then
339 tinsert(tpath, cpx.scan_outnode_from)
342 cpx = cpx.scan_from
344 QuestHelper: Assert(not mdlast)
346 if not reverse then
347 for i = #tpath, 1, -1 do
348 tinsert(rp, tpath[i])
351 QuestHelper: Assert(tpath ~= rp)
352 QuestHelper:ReleaseTable(tpath)
358 QuestHelper:ReleaseTable(table.remove(plane[stnode.p])) -- always added last, so we remove it first
359 for k, v in ipairs(nda) do
360 if plane[canoplane(v.p)] and plane[canoplane(v.p)][#plane[canoplane(v.p)]].goal then -- might not be the exact one, but we'll remove 'em all once we get there anyway :D
361 QuestHelper:ReleaseTable(table.remove(plane[canoplane(v.p)]))
365 QuestHelper:ReleaseTable(link)
366 QuestHelper:ReleaseTable(undone)
368 --QuestHelper:TextOut("Finishing")
370 --for k, v in pairs(stats) do
371 --print(k, v)
372 --end
374 active = false
375 return out -- THIS HAD BETTER BE RELEASABLE OR IT WILL BE BAD
378 function QH_Graph_Init()
379 for c, d in pairs(QuestHelper_IndexLookup) do
380 if type(c) == "number" then
381 QuestHelper: Assert(d[0])
382 continent_to_flyplane[c] = d[0]
383 for z, p in pairs(d) do
384 if type(z) == "number" then
385 --QuestHelper:TextOut(string.format("%d/%d: %d", c, z, p))
386 plane_to_flyplane[p] = d[0]
393 local linkages = {}
395 local function findnode(coord)
396 local p = canoplane(coord.p)
397 if not plane[p] then plane[p] = {} end
398 for _, v in ipairs(plane[p]) do
399 if v.x == coord.x and v.y == coord.y and v.name == coord.name then
400 return v
404 local nd = {x = coord.x, y = coord.y, p = p, name = coord.name}
405 tinsert(plane[p], nd)
406 return nd
409 function QH_Graph_Plane_Makelink(name, coord1, coord2, cost, cost_reverse)
410 QuestHelper: Assert(not active)
411 prepared = false
413 --QuestHelper: TextOut(string.format("Link '%s' made from %d/%f/%f to %d/%f/%f of cost %f, asymflag %s", name, coord1.p, coord1.x, coord1.y, coord2.p, coord2.x, coord2.y, cost, tostring(not not asymmetrical)))
414 QuestHelper: Assert(name)
415 QuestHelper: Assert(coord1)
416 QuestHelper: Assert(coord2)
417 QuestHelper: Assert(cost)
419 QuestHelper: Assert(cost >= 0)
420 QuestHelper: Assert(not cost_reverse or cost_reverse >= 0)
422 --cost = math.max(cost, 0.01)
423 --if cost_reverse then cost_reverse = math.max(cost_reverse, 0.01) end
425 local tlink = {name, coord1, coord2, cost, cost_reverse}
426 if not linkages[name] then linkages[name] = {} end
427 tinsert(linkages[name], tlink)
428 --print(name, coord1.map_desc[1], coord2.map_desc[1], coord)
431 function QH_Graph_Plane_Destroylinks(name)
432 QuestHelper: Assert(not active)
433 prepared = false
435 linkages[name] = nil
437 -- we'll actually clean things out once the refresh function is called
440 function QH_Graph_Flyplaneset(fpset, speed, cull)
441 QuestHelper: Assert(not active)
442 prepared = false
444 local index = QuestHelper_IndexLookup[fpset][0]
445 if not flyplanes_enabled[index] or plane_multiplier[index] ~= speed or plane_cull[index] ~= cull then
446 flyplanes_enabled[index] = true
447 plane_multiplier[index] = speed
448 plane_cull[index] = cull
451 -- we'll actually clean things out once the refresh function is called
454 function QH_Graph_Plane_Refresh()
455 --QuestHelper:TextOut("Graph plane refresh now")
456 QuestHelper: Assert(not active)
458 plane = {} -- reset it all
460 -- we take all our links, process them, and jam them together
461 -- mmmm
462 -- jam
464 for name, v in pairs(linkages) do
465 --print("Starting linkage", name)
466 local titx = {}
467 local nodeitems = {}
468 local nodedests = {}
470 local function makenodedest(outnode, package)
471 if not nodedests[outnode] then
472 nodedests[outnode] = {x = package.x, y = package.y, p = package.p, c = package.c, map_desc = package.map_desc, condense_class = package.condense_class} -- note: this is where the actual node objects that eventually get passed into the routing controller's path replot engine come from. So if you intend for anything to exist in that module, it's gotta be inserted here.
476 for _, i in ipairs(v) do
477 local name, coord1, coord2, cost, cost_reverse = unpack(i)
479 QuestHelper: Assert(name)
480 QuestHelper: Assert(coord1)
481 QuestHelper: Assert(coord2)
482 QuestHelper: Assert(cost)
484 i.n1, i.n2 = findnode(coord1), findnode(coord2)
486 table.insert(titx, i)
488 if not nodeitems[i.n1] then nodeitems[i.n1] = QuestHelper:CreateTable("graph plane refresh") end
489 if not nodeitems[i.n2] then nodeitems[i.n2] = QuestHelper:CreateTable("graph plane refresh") end
491 table.insert(nodeitems[i.n1], i)
492 table.insert(nodeitems[i.n2], i)
494 makenodedest(i.n1, coord1)
495 makenodedest(i.n2, coord2)
498 --QuestHelper:TextOut(string.format("%d titx", #titx))
500 -- all nodes are created, links are posted
502 local nodedone = {}
503 local mark
504 mark = function(tnode, acum)
505 if acum[tnode] then return end
506 acum[tnode] = true
507 nodedone[tnode] = true
509 for _, d in ipairs(nodeitems[tnode]) do
510 mark(d.n1, acum)
511 mark(d.n2, acum)
515 local infinity = 1e10
517 for _, connect in ipairs(titx) do
518 QH_Timeslice_Yield()
520 QuestHelper: Assert(nodedone[connect.n1] == nodedone[connect.n2])
522 if not nodedone[connect.n1] then
523 local nods = QuestHelper:CreateTable("graph plane nods")
524 local nods_reverse = QuestHelper:CreateTable("graph plane nods_reverse")
525 mark(connect.n1, nods)
527 local lookupindex = 1
528 for k, v in pairs(nods) do
529 nods[k] = lookupindex
530 nods_reverse[lookupindex] = k
531 lookupindex = lookupindex + 1
534 --QuestHelper:TextOut(string.format("Processing cluster of %d", lookupindex))
536 local tab = QuestHelper:CreateTable("graph plane floyd core")
537 for r = 1, lookupindex do
538 local inner = QuestHelper:CreateTable("graph plane floyd inner")
539 table.insert(tab, inner)
540 for tr = 1, lookupindex do
541 table.insert(inner, infinity)
545 for k, _ in pairs(nods) do
546 for _, titem in pairs(nodeitems[k]) do
547 local a, b = nods[titem.n1], nods[titem.n2]
548 tab[a][b] = math.min(tab[a][b], titem[4])
549 if titem[5] then
550 tab[b][a] = math.min(tab[b][a], titem[5])
555 for pivot in ipairs(tab) do
556 for s in ipairs(tab) do
557 for e in ipairs(tab) do
558 tab[s][e] = math.min(tab[s][e], tab[s][pivot] + tab[pivot][e])
563 -- add node link destinations here (we only need one sample per item)
564 for s, t in ipairs(tab) do
565 local n1 = nods_reverse[s]
566 for e, c in ipairs(t) do
567 local n2 = nods_reverse[e]
569 local doit = true
571 if c == infinity then doit = false end
572 if doit then
573 local n1p = canoplane(nodedests[n1].p)
574 local n2p = canoplane(nodedests[n2].p)
576 if n1p == n2p then
577 if name == "static_transition" then doit = false end -- ha ha, yep, that's how we find out, tooootally reliable
578 if plane_cull[n1p] then doit = false end -- DENIED
580 if doit then
581 local xyd = xydist_raw(n1p, nodedests[n1].x, nodedests[n1].y, nodedests[n2].x, nodedests[n2].y)
583 if c >= xyd then doit = false end -- it's faster to just fly directly. this won't fuck with the total-connectedness at all, because if it is faster to just fly directly from cluster A to cluster B, we'll just pick up cluster B when we get there
589 if doit then
590 if not n1.links then n1.links = {} end
591 if not n2.rlinks then n2.rlinks = {} end
593 --if name == "flightpath" then print("linking from", nodedests[n1].map_desc[1], "to", nodedests[n2].map_desc[1]) end
594 tinsert(n1.links, {cost = c, link = n2, outnode_to = nodedests[n2], outnode_from = nodedests[n1]})
595 tinsert(n2.rlinks, {cost = c, link = n1, outnode_to = nodedests[n1], outnode_from = nodedests[n2]})
600 QuestHelper:ReleaseTable(nods)
601 QuestHelper:ReleaseTable(nods_reverse)
602 for _, v in ipairs(tab) do QuestHelper:ReleaseTable(v) end
603 QuestHelper:ReleaseTable(tab)
607 for _, v in pairs(titx) do v.n1, v.n2 = nil, nil end
608 for _, v in pairs(nodeitems) do QuestHelper:ReleaseTable(v) end
611 prepared = true
612 --QuestHelper:TextOut("Graph plane refresh done")
617 --[[
618 local function QH_Graph_Plane_ReallyMakeLink(item)
619 local name, coord1, coord2, cost, cost_reverse = unpack(item)
621 QuestHelper: Assert(not active)
623 --QuestHelper: TextOut(string.format("Link '%s' made from %d/%f/%f to %d/%f/%f of cost %f, asymflag %s", name, coord1.p, coord1.x, coord1.y, coord2.p, coord2.x, coord2.y, cost, tostring(not not asymmetrical)))
624 QuestHelper: Assert(name)
625 QuestHelper: Assert(coord1)
626 QuestHelper: Assert(coord2)
627 QuestHelper: Assert(cost)
629 local n1p = canoplane(coord1.p)
630 local n2p = canoplane(coord2.p)
632 if n1p == n2p then
633 if name == "static_transition" then return end -- ha ha, yep, that's how we find out, tooootally reliable
635 local xyd = xydist_raw(n1p, coord1.x, coord1.y, coord2.x, coord2.y)
636 if plane_cull[n1p] then return end -- DENIED
637 if cost >= xyd and (not cost_reverse or cost_reverse >= xyd) then
638 return -- DENIED
642 local node1 = findnode(coord1)
643 local node2 = findnode(coord2)
645 local n1d = {x = coord1.x, y = coord1.y, p = coord1.p, c = coord1.c, map_desc = coord1.map_desc, condense_class = coord1.condense_class}
646 local n2d = {x = coord2.x, y = coord2.y, p = coord2.p, c = coord2.c, map_desc = coord2.map_desc, condense_class = coord2.condense_class}
648 if not node1.links then node1.links = {} end
649 if not node2.rlinks then node2.rlinks = {} end
651 tinsert(node1.links, {cost = cost, link = node2, outnode_to = n2d, outnode_from = n1d})
652 tinsert(node2.rlinks, {cost = cost, link = node1, outnode_to = n1d, outnode_from = n2d})
654 if cost_reverse then
655 if not node1.rlinks then node1.rlinks = {} end
656 if not node2.links then node2.links = {} end
658 tinsert(node1.rlinks, {cost = cost_reverse, link = node2, outnode_to = n2d, outnode_from = n1d})
659 tinsert(node2.links, {cost = cost_reverse, link = node1, outnode_to = n1d, outnode_from = n2d})