update changes
[QuestHelper.git] / graph_core.lua
blobc718f2c89413db2661f504448176ed8c93617029
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 local QH_Timeslice_Yield = QH_Timeslice_Yield -- performance hack :(
19 local function heap_left(x) return (2*x) end
20 local function heap_right(x) return (2*x + 1) end
22 local function heap_sane(heap)
23 local dmp = ""
24 local finishbefore = 2
25 for i = 1, #heap do
26 if i == finishbefore then
27 print(dmp)
28 dmp = ""
29 finishbefore = finishbefore * 2
30 end
31 dmp = dmp .. string.format("%f ", heap[i].c)
32 end
33 print(dmp)
34 print("")
35 for i = 1, #heap do
36 assert(not heap[heap_left(i)] or heap[i].c <= heap[heap_left(i)].c)
37 assert(not heap[heap_right(i)] or heap[i].c <= heap[heap_right(i)].c)
38 end
39 end
41 local function heap_insert(heap, item)
42 assert(item)
43 table.insert(heap, item)
44 local pt = #heap
45 while pt > 1 do
46 local ptd2 = math.floor(pt / 2)
47 if heap[ptd2].c <= heap[pt].c then
48 break
49 end
50 local tmp = heap[pt]
51 heap[pt] = heap[ptd2]
52 heap[ptd2] = tmp
53 pt = ptd2
54 end
55 --heap_sane(heap)
56 end
58 local function heap_extract(heap)
59 local rv = heap[1]
60 if #heap == 1 then table.remove(heap) return rv end
61 heap[1] = table.remove(heap)
62 local idx = 1
63 while idx < #heap do
64 local minix = idx
65 if heap[heap_left(idx)] and heap[heap_left(idx)].c < heap[minix].c then minix = heap_left(idx) end
66 if heap[heap_right(idx)] and heap[heap_right(idx)].c < heap[minix].c then minix = heap_right(idx) end
67 if minix ~= idx then
68 local tx = heap[minix]
69 heap[minix] = heap[idx]
70 heap[idx] = tx
71 idx = minix
72 else
73 break
74 end
75 end
76 --heap_sane(heap)
77 return rv
78 end
80 -- incremented on each graph iteration, so we don't have to clear everything. "GRaph ID" :D
81 local grid = 0
83 -- Plane format: each plane contains an array of nodes that exist in that plane.
84 -- Each node contains both its parent ID and its coordinates within that plane. It may contain a node it links to, along with cost.
85 local plane = {}
87 local plane_to_flyplane = {}
88 local continent_to_flyplane = {}
89 local flyplanes_enabled = {}
90 local plane_multiplier = {}
92 -- canonical plane :D
93 local function canoplane(plane)
94 if flyplanes_enabled[plane_to_flyplane[plane]] then return plane_to_flyplane[plane] else return plane end
95 end
97 local function xydist_raw(plane, stx, sty, ndx, ndy)
98 local dx, dy = stx - ndx, sty - ndy
99 return math.sqrt(dx * dx + dy * dy) / (plane_multiplier[plane] or 7)
101 local function xydist(st, nd)
102 QuestHelper: Assert(canoplane(st.p) == canoplane(nd.p))
103 local dx, dy = st.x - nd.x, st.y - nd.y
104 return math.sqrt(dx * dx + dy * dy) / (plane_multiplier[canoplane(nd.p)] or 7) -- we're getting a result in seconds, not in yards
111 function QH_Graph_Pathfind(st, nd, reverse, make_path)
112 return QH_Graph_Pathmultifind(st, {nd}, reverse, make_path)[1]
115 local active = false
117 function QH_Graph_Pathmultifind(st, nda, reverse, make_path)
118 --QuestHelper:TextOut("Starting PMF")
119 QuestHelper: Assert(not active)
120 active = true -- The fun thing about coroutines is that this is actually safe.
121 local out = QuestHelper:CreateTable("graphcore output") -- THIS HAD BETTER BE RELEASABLE OR IT WILL BE BAD
123 local undone = QuestHelper:CreateTable("graphcore undone")
124 local remaining = 0
126 local link = QuestHelper:CreateTable("graphcore link")
128 --local stats = QuestHelper:CreateTable("graphcore --stats")
130 QuestHelper: Assert(st.x and st.y and st.p)
132 --stats.dests_complex = 0
133 --stats.dests_total = 0
135 for k, v in ipairs(nda) do
136 QuestHelper: Assert(v.x and v.y and v.p)
137 local cpvp = canoplane(v.p)
138 if plane[cpvp] then
139 --print("Destination plane insertion")
140 local dest = QuestHelper:CreateTable("graphcore destination")
141 dest.x, dest.y, dest.p, dest.goal = v.x, v.y, cpvp, k
142 link[k] = dest
143 table.insert(plane[cpvp], link[k])
144 undone[k] = true
145 remaining = remaining + 1
146 --stats.dests_complex = --stats.dests_complex + 1
148 --stats.dests_total = --stats.dests_total + 1
151 local link_id = reverse and "rlinks" or "links"
153 --stats.node_initialized_first = 0
154 --stats.node_done = 0
155 --stats.node_done_already = 0
156 --stats.node_modified_before_use = 0
157 --stats.node_link_reprocessed = 0
158 --stats.node_link_first = 0
159 --stats.node_link_alreadydone = 0
160 --stats.node_inner_reprocessed = 0
161 --stats.node_inner_first = 0
162 --stats.node_inner_alreadydone = 0
164 local dijheap = QuestHelper:CreateTable("graphcore heap container")
165 local stnode = QuestHelper:CreateTable("graphcore stnode")
167 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
168 QuestHelper: Assert(plane[canoplane(st.p)])
169 table.insert(plane[stnode.p], stnode)
171 local hep = QuestHelper:CreateTable("graphcore heap")
172 hep.c, hep.n = 0, stnode
173 heap_insert(dijheap, hep)
176 --stats.heap_max = #dijheap
178 --QuestHelper:TextOut("Starting routing")
180 while remaining > 0 and #dijheap > 0 do
181 QH_Timeslice_Yield()
182 --stats.heap_max = math.max(--stats.heap_max, #dijheap)
183 local cdj = heap_extract(dijheap)
184 if cdj.done then
185 if undone[cdj.done] then
186 out[cdj.done] = cdj.c
187 undone[cdj.done] = nil
188 remaining = remaining - 1
189 --stats.node_done = --stats.node_done + 1
190 else
191 --stats.node_done_already = --stats.node_done_already + 1
193 else
194 local snode = cdj.n
195 --print(string.format("Extracted cost %f/%s pointing at %f/%f/%d", cdj.c, tostring(cdj.n.scan_cost), cdj.n.x, cdj.n.y, cdj.n.p))
196 if snode.scan_cost == cdj.c then -- if we've modified it since then, don't bother
197 for _, v in ipairs(plane[snode.p]) do
198 if v.scan_id ~= grid or v.scan_cost > snode.scan_cost then
199 local dst = xydist(snode, v)
200 local modcost = snode.scan_cost + dst
201 if v.scan_id ~= grid or v.scan_cost > modcost then
202 v.scan_id = grid
203 v.scan_cost = modcost
204 v.scan_from = snode
205 v.scan_outnode = nil
206 v.scan_outnode_from = nil
208 if v.goal then
209 -- we wouldn't be here if we hadn't found a better solution
210 local hep = QuestHelper:CreateTable("graphcore heap")
211 hep.c, hep.done = modcost, v.goal
212 heap_insert(dijheap, hep)
213 QuestHelper: Assert(not v[link_id])
214 elseif v[link_id] then
215 for _, lnk in pairs(v[link_id]) do
216 local mc2 = modcost + lnk.cost
217 local linkto = lnk.link
218 if linkto.scan_id ~= grid or linkto.scan_cost > mc2 then
219 linkto.scan_id = grid
220 linkto.scan_cost = mc2
221 linkto.scan_from = v
222 linkto.scan_outnode = lnk.outnode_to
223 linkto.scan_outnode_from = lnk.outnode_from
225 local hep = QuestHelper:CreateTable("graphcore heap")
226 hep.c, hep.n = mc2, linkto
227 heap_insert(dijheap, hep)
236 QuestHelper:ReleaseTable(cdj)
239 --QuestHelper:TextOut("Starting pathing")
241 for _, v in ipairs(dijheap) do
242 QuestHelper:ReleaseTable(v)
244 QuestHelper:ReleaseTable(dijheap)
245 dijheap = nil
247 --QuestHelper:TextOut(string.format("Earlyout with %d/%d remaining", #dijheap, remaining))
248 if remaining > 0 then
249 for k, v in ipairs(nda) do
250 if not out[k] then
251 QuestHelper: Assert(false, string.format("Couldn't find path to %d/%f/%f", nda[k].p, nda[k].x, nda[k].y))
255 QuestHelper: Assert(remaining == 0)
257 grid = grid + 1
258 QuestHelper: Assert(grid < 1000000000) -- if this ever triggers I will be amazed
260 if make_path then
261 --print("mpath")
262 for k, v in pairs(out) do
263 QH_Timeslice_Yield()
264 --print(out[k])
265 local rp = QuestHelper:CreateTable("graphcore return")
266 rp.d = v
268 out[k] = rp
269 --print(out[k])
271 if link[k] then
272 QuestHelper: Assert(link[k].scan_from)
273 local tpath = reverse and rp or QuestHelper:CreateTable("graphcore path reversal")
274 local cpx = link[k].scan_from
275 local mdlast = nil
276 while cpx do
277 QuestHelper: Assert(cpx)
279 if cpx.scan_outnode then
280 table.insert(tpath, cpx.scan_outnode)
282 if cpx.scan_outnode_from then
283 table.insert(tpath, cpx.scan_outnode_from)
286 cpx = cpx.scan_from
288 QuestHelper: Assert(not mdlast)
290 if not reverse then
291 for i = #tpath, 1, -1 do
292 table.insert(rp, tpath[i])
295 QuestHelper: Assert(tpath ~= rp)
296 QuestHelper:ReleaseTable(tpath)
302 QuestHelper:ReleaseTable(table.remove(plane[stnode.p])) -- always added last, so we remove it first
303 for k, v in ipairs(nda) do
304 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
305 QuestHelper:ReleaseTable(table.remove(plane[canoplane(v.p)]))
309 QuestHelper:ReleaseTable(link)
310 QuestHelper:ReleaseTable(undone)
312 --QuestHelper:TextOut("Finishing")
314 --for k, v in pairs(stats) do
315 --print(k, v)
316 --end
318 active = false
319 return out -- THIS HAD BETTER BE RELEASABLE OR IT WILL BE BAD
322 function QH_Graph_Init()
323 for c, d in pairs(QuestHelper_IndexLookup) do
324 if type(c) == "number" then
325 QuestHelper: Assert(d[0])
326 continent_to_flyplane[c] = d[0]
327 for z, p in pairs(d) do
328 if type(z) == "number" then
329 --QuestHelper:TextOut(string.format("%d/%d: %d", c, z, p))
330 plane_to_flyplane[p] = d[0]
337 local linkages = {}
339 local function findnode(coord)
340 local p = canoplane(coord.p)
341 if not plane[p] then plane[p] = {} end
342 for _, v in ipairs(plane[p]) do
343 if v.x == coord.x and v.y == coord.y and v.name == coord.name then
344 return v
348 local nd = {x = coord.x, y = coord.y, p = p, name = coord.name}
349 table.insert(plane[p], nd)
350 return nd
353 local function QH_Graph_Plane_ReallyMakeLink(item)
354 local name, coord1, coord2, cost, cost_reverse = unpack(item)
356 QuestHelper: Assert(not active)
358 --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)))
359 QuestHelper: Assert(name)
360 QuestHelper: Assert(coord1)
361 QuestHelper: Assert(coord2)
362 QuestHelper: Assert(cost)
364 local n1p = canoplane(coord1.p)
365 local n2p = canoplane(coord2.p)
367 if canoplane(coord1.p) == canoplane(coord2.p) then
368 if name == "static_transition" then return end -- ha ha, yep, that's how we find out, tooootally reliable
370 local xyd = xydist_raw(canoplane(coord1.p), coord1.x, coord1.y, coord2.x, coord2.y)
371 if cost >= xyd and (not cost_reverse or cost_reverse >= xyd) then
372 return -- DENIED
376 local node1 = findnode(coord1)
377 local node2 = findnode(coord2)
379 local n1d = {x = coord1.x, y = coord1.y, p = coord1.p, c = coord1.c, map_desc = coord1.map_desc, condense_class = coord1.condense_class}
380 local n2d = {x = coord2.x, y = coord2.y, p = coord2.p, c = coord2.c, map_desc = coord2.map_desc, condense_class = coord2.condense_class}
382 if not node1.links then node1.links = {} end
383 if not node2.rlinks then node2.rlinks = {} end
385 table.insert(node1.links, {cost = cost, link = node2, outnode_to = n2d, outnode_from = n1d})
386 table.insert(node2.rlinks, {cost = cost, link = node1, outnode_to = n1d, outnode_from = n2d})
388 if cost_reverse then
389 if not node1.rlinks then node1.rlinks = {} end
390 if not node2.links then node2.links = {} end
392 table.insert(node1.rlinks, {cost = cost_reverse, link = node2, outnode_to = n2d, outnode_from = n1d})
393 table.insert(node2.links, {cost = cost_reverse, link = node1, outnode_to = n1d, outnode_from = n2d})
397 function QH_Graph_Plane_Makelink(name, coord1, coord2, cost, cost_reverse)
398 QuestHelper: Assert(not active)
400 --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)))
401 QuestHelper: Assert(name)
402 QuestHelper: Assert(coord1)
403 QuestHelper: Assert(coord2)
404 QuestHelper: Assert(cost)
406 QuestHelper: Assert(cost >= 0)
407 QuestHelper: Assert(not cost_reverse or cost_reverse >= 0)
409 local tlink = {name, coord1, coord2, cost, cost_reverse}
410 if not linkages[name] then linkages[name] = {} end
411 table.insert(linkages[name], tlink)
413 QH_Graph_Plane_ReallyMakeLink(tlink)
416 local function QH_Graph_Plane_Destroylinkslocal(name)
417 QuestHelper: Assert(not active)
419 for k, v in pairs(plane) do
420 local repl = {}
421 for tk, tv in ipairs(v) do
422 if tv.name ~= name then
423 table.insert(repl, tv)
426 plane[k] = repl
430 function QH_Graph_Plane_Destroylinks(name)
431 QuestHelper: Assert(not active)
433 linkages[name] = nil
435 QH_Graph_Plane_Destroylinkslocal(name)
438 function QH_Graph_Flyplaneset(fpset, speed)
439 QuestHelper: Assert(not active)
441 if not flyplanes_enabled[QuestHelper_IndexLookup[fpset][0]] then
442 flyplanes_enabled[QuestHelper_IndexLookup[fpset][0]] = true
443 for k, v in pairs(linkages) do
444 QH_Graph_Plane_Destroylinkslocal(k)
446 for _, ite in pairs(v) do
447 QH_Graph_Plane_ReallyMakeLink(ite)
451 plane_multiplier[QuestHelper_IndexLookup[fpset][0]] = speed