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
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
)
29 local finishbefore
= 2
31 if i
== finishbefore
then
34 finishbefore
= finishbefore
* 2
36 dmp
= dmp
.. string.format("%f ", heap
[i
].c
)
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
)
46 local function heap_insert(heap
, item
)
51 local ptd2
= math
.floor(pt
/ 2)
52 if heap
[ptd2
].c
<= heap
[pt
].c
then
63 local function heap_extract(heap
)
65 if #heap
== 1 then tremove(heap
) return rv
end
66 heap
[1] = tremove(heap
)
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
75 local tx
= heap
[minix
]
76 heap
[minix
] = heap
[idx
]
87 -- incremented on each graph iteration, so we don't have to clear everything. "GRaph ID" :D
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.
94 local plane_to_flyplane
= {}
95 local continent_to_flyplane
= {}
96 local flyplanes_enabled
= {}
97 local plane_multiplier
= {}
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]
124 local prepared
= false
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")
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
)
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
160 tinsert(plane
[cpvp
], link
[k
])
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")
208 while remaining
> 0 and #dijheap
> 0 do
210 local ett
= GetTime()
212 --stats.heap_max = math.max(--stats.heap_max, #dijheap)
213 local cdj
= heap_extract(dijheap
)
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()
220 -- Are we at an end node?
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
241 v
.scan_cost
= modcost
243 v
.scan_processed
= false
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))
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))
284 linktime
= linktime
+ GetTime() - linkt
286 actualtime
= actualtime
+ GetTime() - actt
287 extrctime
= extrctime
+ GetTime() - ett
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
)
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
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)
314 QuestHelper
: Assert(grid
< 1000000000) -- if this ever triggers I will be amazed
318 for k
, v
in pairs(out
) do
321 local rp
= QuestHelper
:CreateTable("graphcore return")
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
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
)
344 QuestHelper
: Assert(not mdlast
)
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
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]
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
404 local nd
= {x
= coord
.x
, y
= coord
.y
, p
= p
, name
= coord
.name
}
405 tinsert(plane
[p
], nd
)
409 function QH_Graph_Plane_Makelink(name
, coord1
, coord2
, cost
, cost_reverse
)
410 QuestHelper
: Assert(not active
)
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
)
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
)
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
464 for name
, v
in pairs(linkages
) do
465 --print("Starting linkage", name)
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
504 mark
= function(tnode
, acum
)
505 if acum
[tnode
] then return end
507 nodedone
[tnode
] = true
509 for _
, d
in ipairs(nodeitems
[tnode
]) do
515 local infinity
= 1e10
517 for _
, connect
in ipairs(titx
) do
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])
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
]
571 if c
== infinity
then doit
= false end
573 local n1p
= canoplane(nodedests
[n1
].p
)
574 local n2p
= canoplane(nodedests
[n2
].p
)
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
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
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
612 --QuestHelper:TextOut("Graph plane refresh done")
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)
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
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})
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})