Merge branch 'master' of git://cams.pavlovian.net/questhelper
[QuestHelper.git] / graph_core.lua
blob0df8ec97e832cc9886049fabfeb92d1cd9b9758d
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 = table.insert
20 local pairs, ipairs = pairs, ipairs
22 local function heap_left(x) return (2*x) end
23 local function heap_right(x) return (2*x + 1) end
25 local function heap_sane(heap)
26 local dmp = ""
27 local finishbefore = 2
28 for i = 1, #heap do
29 if i == finishbefore then
30 print(dmp)
31 dmp = ""
32 finishbefore = finishbefore * 2
33 end
34 dmp = dmp .. string.format("%f ", heap[i].c)
35 end
36 print(dmp)
37 print("")
38 for i = 1, #heap do
39 assert(not heap[heap_left(i)] or heap[i].c <= heap[heap_left(i)].c)
40 assert(not heap[heap_right(i)] or heap[i].c <= heap[heap_right(i)].c)
41 end
42 end
44 local function heap_insert(heap, item)
45 assert(item)
46 tinsert(heap, item)
47 local pt = #heap
48 while pt > 1 do
49 local ptd2 = math.floor(pt / 2)
50 if heap[ptd2].c <= heap[pt].c then
51 break
52 end
53 local tmp = heap[pt]
54 heap[pt] = heap[ptd2]
55 heap[ptd2] = tmp
56 pt = ptd2
57 end
58 --heap_sane(heap)
59 end
61 local function heap_extract(heap)
62 local rv = heap[1]
63 if #heap == 1 then table.remove(heap) return rv end
64 heap[1] = table.remove(heap)
65 local idx = 1
66 while idx < #heap do
67 local minix = idx
68 if heap[heap_left(idx)] and heap[heap_left(idx)].c < heap[minix].c then minix = heap_left(idx) end
69 if heap[heap_right(idx)] and heap[heap_right(idx)].c < heap[minix].c then minix = heap_right(idx) end
70 if minix ~= idx then
71 local tx = heap[minix]
72 heap[minix] = heap[idx]
73 heap[idx] = tx
74 idx = minix
75 else
76 break
77 end
78 end
79 --heap_sane(heap)
80 return rv
81 end
83 -- incremented on each graph iteration, so we don't have to clear everything. "GRaph ID" :D
84 local grid = 0
86 -- Plane format: each plane contains an array of nodes that exist in that plane.
87 -- Each node contains both its parent ID and its coordinates within that plane. It may contain a node it links to, along with cost.
88 local plane = {}
90 local plane_to_flyplane = {}
91 local continent_to_flyplane = {}
92 local flyplanes_enabled = {}
93 local plane_multiplier = {}
95 -- canonical plane :D
96 local function canoplane(plane)
97 if flyplanes_enabled[plane_to_flyplane[plane]] then return plane_to_flyplane[plane] else return plane end
98 end
100 local function xydist_raw(plane, stx, sty, ndx, ndy)
101 local dx, dy = stx - ndx, sty - ndy
102 return math.sqrt(dx * dx + dy * dy) / (plane_multiplier[plane] or 7)
104 local function xydist(st, nd)
105 QuestHelper: Assert(canoplane(st.p) == canoplane(nd.p))
106 local dx, dy = st.x - nd.x, st.y - nd.y
107 return math.sqrt(dx * dx + dy * dy) / (plane_multiplier[canoplane(nd.p)] or 7) -- we're getting a result in seconds, not in yards
114 function QH_Graph_Pathfind(st, nd, reverse, make_path)
115 return QH_Graph_Pathmultifind(st, {nd}, reverse, make_path)[1]
118 local active = false
120 function QH_Graph_Pathmultifind(st, nda, reverse, make_path)
121 --QuestHelper:TextOut("Starting PMF")
122 QuestHelper: Assert(not active)
123 active = true -- The fun thing about coroutines is that this is actually safe.
124 local out = QuestHelper:CreateTable("graphcore output") -- THIS HAD BETTER BE RELEASABLE OR IT WILL BE BAD
126 local undone = QuestHelper:CreateTable("graphcore undone")
127 local remaining = 0
129 local link = QuestHelper:CreateTable("graphcore link")
131 --local stats = QuestHelper:CreateTable("graphcore --stats")
133 QuestHelper: Assert(st.x and st.y and st.p)
135 --stats.dests_complex = 0
136 --stats.dests_total = 0
138 for k, v in ipairs(nda) do
139 QuestHelper: Assert(v.x and v.y and v.p)
140 local cpvp = canoplane(v.p)
141 if plane[cpvp] then
142 --print("Destination plane insertion")
143 local dest = QuestHelper:CreateTable("graphcore destination")
144 dest.x, dest.y, dest.p, dest.goal = v.x, v.y, cpvp, k
145 link[k] = dest
146 tinsert(plane[cpvp], link[k])
147 undone[k] = true
148 remaining = remaining + 1
149 --stats.dests_complex = --stats.dests_complex + 1
151 --stats.dests_total = --stats.dests_total + 1
154 local link_id = reverse and "rlinks" or "links"
156 --stats.node_initialized_first = 0
157 --stats.node_done = 0
158 --stats.node_done_already = 0
159 --stats.node_modified_before_use = 0
160 --stats.node_link_reprocessed = 0
161 --stats.node_link_first = 0
162 --stats.node_link_alreadydone = 0
163 --stats.node_inner_reprocessed = 0
164 --stats.node_inner_first = 0
165 --stats.node_inner_alreadydone = 0
167 local dijheap = QuestHelper:CreateTable("graphcore heap container")
168 local stnode = QuestHelper:CreateTable("graphcore stnode")
170 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
171 QuestHelper: Assert(plane[canoplane(st.p)])
172 tinsert(plane[stnode.p], stnode)
174 local hep = QuestHelper:CreateTable("graphcore heap")
175 hep.c, hep.n = 0, stnode -- more than the subtraction, less than the minimum
176 heap_insert(dijheap, hep)
179 --stats.heap_max = #dijheap
181 --QuestHelper:TextOut("Starting routing")
183 while remaining > 0 and #dijheap > 0 do
184 QH_Timeslice_Yield()
185 --stats.heap_max = math.max(--stats.heap_max, #dijheap)
186 local cdj = heap_extract(dijheap)
188 local snode = cdj.n
189 --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))
190 if snode.scan_cost == cdj.c then -- if we've modified it since then, don't bother
191 -- Are we at an end node?
192 if snode.goal then
193 -- we wouldn't be here if we hadn't found a better solution
194 QuestHelper: Assert(undone[snode.goal])
195 out[snode.goal] = cdj.c
196 undone[snode.goal] = nil
197 remaining = remaining - 1
200 -- Link to everything else on the plane
201 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")
202 for _, v in ipairs(plane[snode.p]) do
203 if v.scan_id ~= grid or v.scan_cost > snode.scan_cost then
204 local dst = xydist(snode, v)
205 local modcost = snode.scan_cost + dst
206 --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))
207 if v.scan_id ~= grid or v.scan_cost > modcost then
208 v.scan_id = grid
209 v.scan_cost = modcost
210 v.scan_from = snode
211 v.scan_processed = false
212 v.scan_outnode = nil
213 v.scan_outnode_from = nil
216 local snude = QuestHelper:CreateTable("graphcore heap")
217 snude.c, snude.n, snude.pi = modcost, v, true
218 heap_insert(dijheap, snude)
219 --print(string.format("Inserting %f at %d/%f/%f, plane", snude.c, v.p, v.x, v.y))
226 -- Link to everything we link to
227 if snode[link_id] then
228 for _, lnk in pairs(snode[link_id]) do
229 local mc2 = snode.scan_cost + lnk.cost
230 local linkto = lnk.link
231 if linkto.scan_id ~= grid or linkto.scan_cost > mc2 then
232 linkto.scan_id = grid
233 linkto.scan_cost = mc2
234 linkto.scan_from = snode
235 linkto.scan_outnode = lnk.outnode_to
236 linkto.scan_outnode_from = lnk.outnode_from
238 local hep = QuestHelper:CreateTable("graphcore heap")
239 hep.c, hep.n = mc2, linkto
240 heap_insert(dijheap, hep)
241 --print(string.format("Inserting %f at %d/%f/%f, link", hep.c, linkto.p, linkto.x, linkto.y))
246 QuestHelper:ReleaseTable(cdj)
249 --QuestHelper:TextOut("Starting pathing")
251 for _, v in ipairs(dijheap) do
252 QuestHelper:ReleaseTable(v)
254 QuestHelper:ReleaseTable(dijheap)
255 dijheap = nil
257 --QuestHelper:TextOut(string.format("Earlyout with %d/%d remaining", #dijheap, remaining))
258 if remaining > 0 then
259 for k, v in ipairs(nda) do
260 if not out[k] then
261 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))
265 QuestHelper: Assert(remaining == 0)
267 grid = grid + 1
268 QuestHelper: Assert(grid < 1000000000) -- if this ever triggers I will be amazed
270 if make_path then
271 --print("mpath")
272 for k, v in pairs(out) do
273 QH_Timeslice_Yield()
274 --print(out[k])
275 local rp = QuestHelper:CreateTable("graphcore return")
276 rp.d = v
278 out[k] = rp
279 --print(out[k])
281 if link[k] then
282 QuestHelper: Assert(link[k].scan_from)
283 local tpath = reverse and rp or QuestHelper:CreateTable("graphcore path reversal")
284 local cpx = link[k].scan_from
285 local mdlast = nil
286 while cpx do
287 QuestHelper: Assert(cpx)
289 if cpx.scan_outnode then
290 tinsert(tpath, cpx.scan_outnode)
292 if cpx.scan_outnode_from then
293 tinsert(tpath, cpx.scan_outnode_from)
296 cpx = cpx.scan_from
298 QuestHelper: Assert(not mdlast)
300 if not reverse then
301 for i = #tpath, 1, -1 do
302 tinsert(rp, tpath[i])
305 QuestHelper: Assert(tpath ~= rp)
306 QuestHelper:ReleaseTable(tpath)
312 QuestHelper:ReleaseTable(table.remove(plane[stnode.p])) -- always added last, so we remove it first
313 for k, v in ipairs(nda) do
314 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
315 QuestHelper:ReleaseTable(table.remove(plane[canoplane(v.p)]))
319 QuestHelper:ReleaseTable(link)
320 QuestHelper:ReleaseTable(undone)
322 --QuestHelper:TextOut("Finishing")
324 --for k, v in pairs(stats) do
325 --print(k, v)
326 --end
328 active = false
329 return out -- THIS HAD BETTER BE RELEASABLE OR IT WILL BE BAD
332 function QH_Graph_Init()
333 for c, d in pairs(QuestHelper_IndexLookup) do
334 if type(c) == "number" then
335 QuestHelper: Assert(d[0])
336 continent_to_flyplane[c] = d[0]
337 for z, p in pairs(d) do
338 if type(z) == "number" then
339 --QuestHelper:TextOut(string.format("%d/%d: %d", c, z, p))
340 plane_to_flyplane[p] = d[0]
347 local linkages = {}
349 local function findnode(coord)
350 local p = canoplane(coord.p)
351 if not plane[p] then plane[p] = {} end
352 for _, v in ipairs(plane[p]) do
353 if v.x == coord.x and v.y == coord.y and v.name == coord.name then
354 return v
358 local nd = {x = coord.x, y = coord.y, p = p, name = coord.name}
359 tinsert(plane[p], nd)
360 return nd
363 local function QH_Graph_Plane_ReallyMakeLink(item)
364 local name, coord1, coord2, cost, cost_reverse = unpack(item)
366 QuestHelper: Assert(not active)
368 --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)))
369 QuestHelper: Assert(name)
370 QuestHelper: Assert(coord1)
371 QuestHelper: Assert(coord2)
372 QuestHelper: Assert(cost)
374 local n1p = canoplane(coord1.p)
375 local n2p = canoplane(coord2.p)
377 if canoplane(coord1.p) == canoplane(coord2.p) then
378 if name == "static_transition" then return end -- ha ha, yep, that's how we find out, tooootally reliable
380 local xyd = xydist_raw(canoplane(coord1.p), coord1.x, coord1.y, coord2.x, coord2.y)
381 if cost >= xyd and (not cost_reverse or cost_reverse >= xyd) then
382 return -- DENIED
386 local node1 = findnode(coord1)
387 local node2 = findnode(coord2)
389 local n1d = {x = coord1.x, y = coord1.y, p = coord1.p, c = coord1.c, map_desc = coord1.map_desc, condense_class = coord1.condense_class}
390 local n2d = {x = coord2.x, y = coord2.y, p = coord2.p, c = coord2.c, map_desc = coord2.map_desc, condense_class = coord2.condense_class}
392 if not node1.links then node1.links = {} end
393 if not node2.rlinks then node2.rlinks = {} end
395 tinsert(node1.links, {cost = cost, link = node2, outnode_to = n2d, outnode_from = n1d})
396 tinsert(node2.rlinks, {cost = cost, link = node1, outnode_to = n1d, outnode_from = n2d})
398 if cost_reverse then
399 if not node1.rlinks then node1.rlinks = {} end
400 if not node2.links then node2.links = {} end
402 tinsert(node1.rlinks, {cost = cost_reverse, link = node2, outnode_to = n2d, outnode_from = n1d})
403 tinsert(node2.links, {cost = cost_reverse, link = node1, outnode_to = n1d, outnode_from = n2d})
407 function QH_Graph_Plane_Makelink(name, coord1, coord2, cost, cost_reverse)
408 QuestHelper: Assert(not active)
410 --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)))
411 QuestHelper: Assert(name)
412 QuestHelper: Assert(coord1)
413 QuestHelper: Assert(coord2)
414 QuestHelper: Assert(cost)
416 QuestHelper: Assert(cost >= 0)
417 QuestHelper: Assert(not cost_reverse or cost_reverse >= 0)
419 --cost = math.max(cost, 0.01)
420 --if cost_reverse then cost_reverse = math.max(cost_reverse, 0.01) end
422 local tlink = {name, coord1, coord2, cost, cost_reverse}
423 if not linkages[name] then linkages[name] = {} end
424 tinsert(linkages[name], tlink)
426 QH_Graph_Plane_ReallyMakeLink(tlink)
429 local function QH_Graph_Plane_Destroylinkslocal(name)
430 QuestHelper: Assert(not active)
432 for k, v in pairs(plane) do
433 local repl = {}
434 for tk, tv in ipairs(v) do
435 if tv.name ~= name then
436 tinsert(repl, tv)
439 plane[k] = repl
443 function QH_Graph_Plane_Destroylinks(name)
444 QuestHelper: Assert(not active)
446 linkages[name] = nil
448 QH_Graph_Plane_Destroylinkslocal(name)
451 function QH_Graph_Flyplaneset(fpset, speed)
452 QuestHelper: Assert(not active)
454 if not flyplanes_enabled[QuestHelper_IndexLookup[fpset][0]] then
455 flyplanes_enabled[QuestHelper_IndexLookup[fpset][0]] = true
456 for k, v in pairs(linkages) do
457 QH_Graph_Plane_Destroylinkslocal(k)
459 for _, ite in pairs(v) do
460 QH_Graph_Plane_ReallyMakeLink(ite)
464 plane_multiplier[QuestHelper_IndexLookup[fpset][0]] = speed