add more explicit removal of flightpaths
[QuestHelper.git] / graph_core.lua
blob3016adacd34d5454938e414b57080bde22e32859
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 = {}
94 local plane_cull = {}
96 -- canonical plane :D
97 local function canoplane(plane)
98 if flyplanes_enabled[plane_to_flyplane[plane]] then return plane_to_flyplane[plane] else return plane end
99 end
101 local function xydist_raw(plane, stx, sty, ndx, ndy)
102 local dx, dy = stx - ndx, sty - ndy
103 return math.sqrt(dx * dx + dy * dy) / (plane_multiplier[plane] or 7)
105 local function xydist(st, nd)
106 QuestHelper: Assert(canoplane(st.p) == canoplane(nd.p))
107 local dx, dy = st.x - nd.x, st.y - nd.y
108 return math.sqrt(dx * dx + dy * dy) / (plane_multiplier[canoplane(nd.p)] or 7) -- we're getting a result in seconds, not in yards
115 function QH_Graph_Pathfind(st, nd, reverse, make_path)
116 return QH_Graph_Pathmultifind(st, {nd}, reverse, make_path)[1]
119 local active = false
121 function QH_Graph_Pathmultifind(st, nda, reverse, make_path)
122 --QuestHelper:TextOut("Starting PMF")
123 QuestHelper: Assert(not active)
124 active = true -- The fun thing about coroutines is that this is actually safe.
125 local out = QuestHelper:CreateTable("graphcore output") -- THIS HAD BETTER BE RELEASABLE OR IT WILL BE BAD
127 local undone = QuestHelper:CreateTable("graphcore undone")
128 local remaining = 0
130 local link = QuestHelper:CreateTable("graphcore link")
132 --local stats = QuestHelper:CreateTable("graphcore --stats")
134 QuestHelper: Assert(st.x and st.y and st.p)
136 --stats.dests_complex = 0
137 --stats.dests_total = 0
139 for k, v in ipairs(nda) do
140 QuestHelper: Assert(v.x and v.y and v.p)
141 local cpvp = canoplane(v.p)
142 if plane[cpvp] then
143 --print("Destination plane insertion")
144 local dest = QuestHelper:CreateTable("graphcore destination")
145 dest.x, dest.y, dest.p, dest.goal = v.x, v.y, cpvp, k
146 link[k] = dest
147 tinsert(plane[cpvp], link[k])
148 undone[k] = true
149 remaining = remaining + 1
150 --stats.dests_complex = --stats.dests_complex + 1
152 --stats.dests_total = --stats.dests_total + 1
155 local link_id = reverse and "rlinks" or "links"
157 --stats.node_initialized_first = 0
158 --stats.node_done = 0
159 --stats.node_done_already = 0
160 --stats.node_modified_before_use = 0
161 --stats.node_link_reprocessed = 0
162 --stats.node_link_first = 0
163 --stats.node_link_alreadydone = 0
164 --stats.node_inner_reprocessed = 0
165 --stats.node_inner_first = 0
166 --stats.node_inner_alreadydone = 0
168 local dijheap = QuestHelper:CreateTable("graphcore heap container")
169 local stnode = QuestHelper:CreateTable("graphcore stnode")
171 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
172 QuestHelper: Assert(plane[canoplane(st.p)])
173 tinsert(plane[stnode.p], stnode)
175 local hep = QuestHelper:CreateTable("graphcore heap")
176 hep.c, hep.n = 0, stnode -- more than the subtraction, less than the minimum
177 heap_insert(dijheap, hep)
180 --stats.heap_max = #dijheap
182 --QuestHelper:TextOut("Starting routing")
184 while remaining > 0 and #dijheap > 0 do
185 QH_Timeslice_Yield()
186 --stats.heap_max = math.max(--stats.heap_max, #dijheap)
187 local cdj = heap_extract(dijheap)
189 local snode = cdj.n
190 --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))
191 if snode.scan_cost == cdj.c then -- if we've modified it since then, don't bother
192 -- Are we at an end node?
193 if snode.goal then
194 -- we wouldn't be here if we hadn't found a better solution
195 QuestHelper: Assert(undone[snode.goal])
196 out[snode.goal] = cdj.c
197 undone[snode.goal] = nil
198 remaining = remaining - 1
201 -- Link to everything else on the plane
202 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")
203 for _, v in ipairs(plane[snode.p]) do
204 if v.scan_id ~= grid or v.scan_cost > snode.scan_cost then
205 local dst = xydist(snode, v)
206 local modcost = snode.scan_cost + dst
207 --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))
208 if v.scan_id ~= grid or v.scan_cost > modcost then
209 v.scan_id = grid
210 v.scan_cost = modcost
211 v.scan_from = snode
212 v.scan_processed = false
213 v.scan_outnode = nil
214 v.scan_outnode_from = nil
217 local snude = QuestHelper:CreateTable("graphcore heap")
218 snude.c, snude.n, snude.pi = modcost, v, true
219 heap_insert(dijheap, snude)
220 --print(string.format("Inserting %f at %d/%f/%f, plane", snude.c, v.p, v.x, v.y))
227 -- Link to everything we link to
228 if snode[link_id] then
229 for _, lnk in pairs(snode[link_id]) do
230 local mc2 = snode.scan_cost + lnk.cost
231 local linkto = lnk.link
232 if linkto.scan_id ~= grid or linkto.scan_cost > mc2 then
233 linkto.scan_id = grid
234 linkto.scan_cost = mc2
235 linkto.scan_from = snode
236 linkto.scan_outnode = lnk.outnode_to
237 linkto.scan_outnode_from = lnk.outnode_from
239 local hep = QuestHelper:CreateTable("graphcore heap")
240 hep.c, hep.n = mc2, linkto
241 heap_insert(dijheap, hep)
242 --print(string.format("Inserting %f at %d/%f/%f, link", hep.c, linkto.p, linkto.x, linkto.y))
247 QuestHelper:ReleaseTable(cdj)
250 --QuestHelper:TextOut("Starting pathing")
252 for _, v in ipairs(dijheap) do
253 QuestHelper:ReleaseTable(v)
255 QuestHelper:ReleaseTable(dijheap)
256 dijheap = nil
258 --QuestHelper:TextOut(string.format("Earlyout with %d/%d remaining", #dijheap, remaining))
259 if remaining > 0 then
260 for k, v in ipairs(nda) do
261 if not out[k] then
262 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))
266 QuestHelper: Assert(remaining == 0)
268 grid = grid + 1
269 QuestHelper: Assert(grid < 1000000000) -- if this ever triggers I will be amazed
271 if make_path then
272 --print("mpath")
273 for k, v in pairs(out) do
274 QH_Timeslice_Yield()
275 --print(out[k])
276 local rp = QuestHelper:CreateTable("graphcore return")
277 rp.d = v
279 out[k] = rp
280 --print(out[k])
282 if link[k] then
283 QuestHelper: Assert(link[k].scan_from)
284 local tpath = reverse and rp or QuestHelper:CreateTable("graphcore path reversal")
285 local cpx = link[k].scan_from
286 local mdlast = nil
287 while cpx do
288 QuestHelper: Assert(cpx)
290 if cpx.scan_outnode then
291 tinsert(tpath, cpx.scan_outnode)
293 if cpx.scan_outnode_from then
294 tinsert(tpath, cpx.scan_outnode_from)
297 cpx = cpx.scan_from
299 QuestHelper: Assert(not mdlast)
301 if not reverse then
302 for i = #tpath, 1, -1 do
303 tinsert(rp, tpath[i])
306 QuestHelper: Assert(tpath ~= rp)
307 QuestHelper:ReleaseTable(tpath)
313 QuestHelper:ReleaseTable(table.remove(plane[stnode.p])) -- always added last, so we remove it first
314 for k, v in ipairs(nda) do
315 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
316 QuestHelper:ReleaseTable(table.remove(plane[canoplane(v.p)]))
320 QuestHelper:ReleaseTable(link)
321 QuestHelper:ReleaseTable(undone)
323 --QuestHelper:TextOut("Finishing")
325 --for k, v in pairs(stats) do
326 --print(k, v)
327 --end
329 active = false
330 return out -- THIS HAD BETTER BE RELEASABLE OR IT WILL BE BAD
333 function QH_Graph_Init()
334 for c, d in pairs(QuestHelper_IndexLookup) do
335 if type(c) == "number" then
336 QuestHelper: Assert(d[0])
337 continent_to_flyplane[c] = d[0]
338 for z, p in pairs(d) do
339 if type(z) == "number" then
340 --QuestHelper:TextOut(string.format("%d/%d: %d", c, z, p))
341 plane_to_flyplane[p] = d[0]
348 local linkages = {}
350 local function findnode(coord)
351 local p = canoplane(coord.p)
352 if not plane[p] then plane[p] = {} end
353 for _, v in ipairs(plane[p]) do
354 if v.x == coord.x and v.y == coord.y and v.name == coord.name then
355 return v
359 local nd = {x = coord.x, y = coord.y, p = p, name = coord.name}
360 tinsert(plane[p], nd)
361 return nd
364 local function QH_Graph_Plane_ReallyMakeLink(item)
365 local name, coord1, coord2, cost, cost_reverse = unpack(item)
367 QuestHelper: Assert(not active)
369 --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)))
370 QuestHelper: Assert(name)
371 QuestHelper: Assert(coord1)
372 QuestHelper: Assert(coord2)
373 QuestHelper: Assert(cost)
375 local n1p = canoplane(coord1.p)
376 local n2p = canoplane(coord2.p)
378 if n1p == n2p then
379 if name == "static_transition" then return end -- ha ha, yep, that's how we find out, tooootally reliable
381 local xyd = xydist_raw(n1p, coord1.x, coord1.y, coord2.x, coord2.y)
382 if plane_cull[n1p] then return end -- DENIED
383 if cost >= xyd and (not cost_reverse or cost_reverse >= xyd) then
384 return -- DENIED
388 local node1 = findnode(coord1)
389 local node2 = findnode(coord2)
391 local n1d = {x = coord1.x, y = coord1.y, p = coord1.p, c = coord1.c, map_desc = coord1.map_desc, condense_class = coord1.condense_class}
392 local n2d = {x = coord2.x, y = coord2.y, p = coord2.p, c = coord2.c, map_desc = coord2.map_desc, condense_class = coord2.condense_class}
394 if not node1.links then node1.links = {} end
395 if not node2.rlinks then node2.rlinks = {} end
397 tinsert(node1.links, {cost = cost, link = node2, outnode_to = n2d, outnode_from = n1d})
398 tinsert(node2.rlinks, {cost = cost, link = node1, outnode_to = n1d, outnode_from = n2d})
400 if cost_reverse then
401 if not node1.rlinks then node1.rlinks = {} end
402 if not node2.links then node2.links = {} end
404 tinsert(node1.rlinks, {cost = cost_reverse, link = node2, outnode_to = n2d, outnode_from = n1d})
405 tinsert(node2.links, {cost = cost_reverse, link = node1, outnode_to = n1d, outnode_from = n2d})
409 function QH_Graph_Plane_Makelink(name, coord1, coord2, cost, cost_reverse)
410 QuestHelper: Assert(not active)
412 --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)))
413 QuestHelper: Assert(name)
414 QuestHelper: Assert(coord1)
415 QuestHelper: Assert(coord2)
416 QuestHelper: Assert(cost)
418 QuestHelper: Assert(cost >= 0)
419 QuestHelper: Assert(not cost_reverse or cost_reverse >= 0)
421 --cost = math.max(cost, 0.01)
422 --if cost_reverse then cost_reverse = math.max(cost_reverse, 0.01) end
424 local tlink = {name, coord1, coord2, cost, cost_reverse}
425 if not linkages[name] then linkages[name] = {} end
426 tinsert(linkages[name], tlink)
428 QH_Graph_Plane_ReallyMakeLink(tlink)
431 local function QH_Graph_Plane_Destroylinkslocal(name)
432 QuestHelper: Assert(not active)
434 for k, v in pairs(plane) do
435 local repl = {}
436 for tk, tv in ipairs(v) do
437 if tv.name ~= name then
438 tinsert(repl, tv)
441 plane[k] = repl
445 function QH_Graph_Plane_Destroylinks(name)
446 QuestHelper: Assert(not active)
448 linkages[name] = nil
450 QH_Graph_Plane_Destroylinkslocal(name)
453 function QH_Graph_Flyplaneset(fpset, speed, cull)
454 QuestHelper: Assert(not active)
456 local index = QuestHelper_IndexLookup[fpset][0]
457 if not flyplanes_enabled[index] or plane_multiplier[index] ~= speed or plane_cull[index] ~= cull then
459 flyplanes_enabled[index] = true
460 plane_multiplier[index] = speed
461 plane_cull[index] = cull
463 for k, v in pairs(linkages) do
464 QH_Graph_Plane_Destroylinkslocal(k)
466 for _, ite in pairs(v) do
467 QH_Graph_Plane_ReallyMakeLink(ite)