Merge branch 'master' of git://cams.pavlovian.net/questhelper
[QuestHelper.git] / graph_core.lua
blob7827565bece374eaf9aed17c252c4985f5eadf89
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 -- more than the subtraction, less than the minimum
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)
185 local snode = cdj.n
186 --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))
187 if snode.scan_cost == cdj.c then -- if we've modified it since then, don't bother
188 -- Are we at an end node?
189 if snode.goal then
190 -- we wouldn't be here if we hadn't found a better solution
191 QuestHelper: Assert(undone[snode.goal])
192 out[snode.goal] = cdj.c
193 undone[snode.goal] = nil
194 remaining = remaining - 1
197 -- Link to everything else on the plane
198 if not snode.pi then -- Did we come from this plane? If so, there's no reason to scan it again (flag means "plane internal")
199 for _, v in ipairs(plane[snode.p]) do
200 if v.scan_id ~= grid or v.scan_cost > snode.scan_cost then
201 local dst = xydist(snode, v)
202 local modcost = snode.scan_cost + dst
203 --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))
204 if v.scan_id ~= grid or v.scan_cost > modcost then
205 v.scan_id = grid
206 v.scan_cost = modcost
207 v.scan_from = snode
208 v.scan_processed = false
209 v.scan_outnode = nil
210 v.scan_outnode_from = nil
213 local snude = QuestHelper:CreateTable("graphcore heap")
214 snude.c, snude.n, snude.pi = modcost, v, true
215 heap_insert(dijheap, snude)
216 --print(string.format("Inserting %f at %d/%f/%f, plane", snude.c, v.p, v.x, v.y))
223 -- Link to everything we link to
224 if snode[link_id] then
225 for _, lnk in pairs(snode[link_id]) do
226 local mc2 = snode.scan_cost + lnk.cost
227 local linkto = lnk.link
228 if linkto.scan_id ~= grid or linkto.scan_cost > mc2 then
229 linkto.scan_id = grid
230 linkto.scan_cost = mc2
231 linkto.scan_from = snode
232 linkto.scan_outnode = lnk.outnode_to
233 linkto.scan_outnode_from = lnk.outnode_from
235 local hep = QuestHelper:CreateTable("graphcore heap")
236 hep.c, hep.n = mc2, linkto
237 heap_insert(dijheap, hep)
238 --print(string.format("Inserting %f at %d/%f/%f, link", hep.c, linkto.p, linkto.x, linkto.y))
243 QuestHelper:ReleaseTable(cdj)
246 --QuestHelper:TextOut("Starting pathing")
248 for _, v in ipairs(dijheap) do
249 QuestHelper:ReleaseTable(v)
251 QuestHelper:ReleaseTable(dijheap)
252 dijheap = nil
254 --QuestHelper:TextOut(string.format("Earlyout with %d/%d remaining", #dijheap, remaining))
255 if remaining > 0 then
256 for k, v in ipairs(nda) do
257 if not out[k] then
258 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))
262 QuestHelper: Assert(remaining == 0)
264 grid = grid + 1
265 QuestHelper: Assert(grid < 1000000000) -- if this ever triggers I will be amazed
267 if make_path then
268 --print("mpath")
269 for k, v in pairs(out) do
270 QH_Timeslice_Yield()
271 --print(out[k])
272 local rp = QuestHelper:CreateTable("graphcore return")
273 rp.d = v
275 out[k] = rp
276 --print(out[k])
278 if link[k] then
279 QuestHelper: Assert(link[k].scan_from)
280 local tpath = reverse and rp or QuestHelper:CreateTable("graphcore path reversal")
281 local cpx = link[k].scan_from
282 local mdlast = nil
283 while cpx do
284 QuestHelper: Assert(cpx)
286 if cpx.scan_outnode then
287 table.insert(tpath, cpx.scan_outnode)
289 if cpx.scan_outnode_from then
290 table.insert(tpath, cpx.scan_outnode_from)
293 cpx = cpx.scan_from
295 QuestHelper: Assert(not mdlast)
297 if not reverse then
298 for i = #tpath, 1, -1 do
299 table.insert(rp, tpath[i])
302 QuestHelper: Assert(tpath ~= rp)
303 QuestHelper:ReleaseTable(tpath)
309 QuestHelper:ReleaseTable(table.remove(plane[stnode.p])) -- always added last, so we remove it first
310 for k, v in ipairs(nda) do
311 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
312 QuestHelper:ReleaseTable(table.remove(plane[canoplane(v.p)]))
316 QuestHelper:ReleaseTable(link)
317 QuestHelper:ReleaseTable(undone)
319 --QuestHelper:TextOut("Finishing")
321 --for k, v in pairs(stats) do
322 --print(k, v)
323 --end
325 active = false
326 return out -- THIS HAD BETTER BE RELEASABLE OR IT WILL BE BAD
329 function QH_Graph_Init()
330 for c, d in pairs(QuestHelper_IndexLookup) do
331 if type(c) == "number" then
332 QuestHelper: Assert(d[0])
333 continent_to_flyplane[c] = d[0]
334 for z, p in pairs(d) do
335 if type(z) == "number" then
336 --QuestHelper:TextOut(string.format("%d/%d: %d", c, z, p))
337 plane_to_flyplane[p] = d[0]
344 local linkages = {}
346 local function findnode(coord)
347 local p = canoplane(coord.p)
348 if not plane[p] then plane[p] = {} end
349 for _, v in ipairs(plane[p]) do
350 if v.x == coord.x and v.y == coord.y and v.name == coord.name then
351 return v
355 local nd = {x = coord.x, y = coord.y, p = p, name = coord.name}
356 table.insert(plane[p], nd)
357 return nd
360 local function QH_Graph_Plane_ReallyMakeLink(item)
361 local name, coord1, coord2, cost, cost_reverse = unpack(item)
363 QuestHelper: Assert(not active)
365 --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)))
366 QuestHelper: Assert(name)
367 QuestHelper: Assert(coord1)
368 QuestHelper: Assert(coord2)
369 QuestHelper: Assert(cost)
371 local n1p = canoplane(coord1.p)
372 local n2p = canoplane(coord2.p)
374 if canoplane(coord1.p) == canoplane(coord2.p) then
375 if name == "static_transition" then return end -- ha ha, yep, that's how we find out, tooootally reliable
377 local xyd = xydist_raw(canoplane(coord1.p), coord1.x, coord1.y, coord2.x, coord2.y)
378 if cost >= xyd and (not cost_reverse or cost_reverse >= xyd) then
379 return -- DENIED
383 local node1 = findnode(coord1)
384 local node2 = findnode(coord2)
386 local n1d = {x = coord1.x, y = coord1.y, p = coord1.p, c = coord1.c, map_desc = coord1.map_desc, condense_class = coord1.condense_class}
387 local n2d = {x = coord2.x, y = coord2.y, p = coord2.p, c = coord2.c, map_desc = coord2.map_desc, condense_class = coord2.condense_class}
389 if not node1.links then node1.links = {} end
390 if not node2.rlinks then node2.rlinks = {} end
392 table.insert(node1.links, {cost = cost, link = node2, outnode_to = n2d, outnode_from = n1d})
393 table.insert(node2.rlinks, {cost = cost, link = node1, outnode_to = n1d, outnode_from = n2d})
395 if cost_reverse then
396 if not node1.rlinks then node1.rlinks = {} end
397 if not node2.links then node2.links = {} end
399 table.insert(node1.rlinks, {cost = cost_reverse, link = node2, outnode_to = n2d, outnode_from = n1d})
400 table.insert(node2.links, {cost = cost_reverse, link = node1, outnode_to = n1d, outnode_from = n2d})
404 function QH_Graph_Plane_Makelink(name, coord1, coord2, cost, cost_reverse)
405 QuestHelper: Assert(not active)
407 --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)))
408 QuestHelper: Assert(name)
409 QuestHelper: Assert(coord1)
410 QuestHelper: Assert(coord2)
411 QuestHelper: Assert(cost)
413 QuestHelper: Assert(cost >= 0)
414 QuestHelper: Assert(not cost_reverse or cost_reverse >= 0)
416 --cost = math.max(cost, 0.01)
417 --if cost_reverse then cost_reverse = math.max(cost_reverse, 0.01) end
419 local tlink = {name, coord1, coord2, cost, cost_reverse}
420 if not linkages[name] then linkages[name] = {} end
421 table.insert(linkages[name], tlink)
423 QH_Graph_Plane_ReallyMakeLink(tlink)
426 local function QH_Graph_Plane_Destroylinkslocal(name)
427 QuestHelper: Assert(not active)
429 for k, v in pairs(plane) do
430 local repl = {}
431 for tk, tv in ipairs(v) do
432 if tv.name ~= name then
433 table.insert(repl, tv)
436 plane[k] = repl
440 function QH_Graph_Plane_Destroylinks(name)
441 QuestHelper: Assert(not active)
443 linkages[name] = nil
445 QH_Graph_Plane_Destroylinkslocal(name)
448 function QH_Graph_Flyplaneset(fpset, speed)
449 QuestHelper: Assert(not active)
451 if not flyplanes_enabled[QuestHelper_IndexLookup[fpset][0]] then
452 flyplanes_enabled[QuestHelper_IndexLookup[fpset][0]] = true
453 for k, v in pairs(linkages) do
454 QH_Graph_Plane_Destroylinkslocal(k)
456 for _, ite in pairs(v) do
457 QH_Graph_Plane_ReallyMakeLink(ite)
461 plane_multiplier[QuestHelper_IndexLookup[fpset][0]] = speed