try to track this thing down better
[QuestHelper.git] / graph_core.lua
blobccf97b45c326a8f605a7629540f86863a95ab371
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 function heap_left(x) return (2*x) end
18 local function heap_right(x) return (2*x + 1) end
20 local function heap_sane(heap)
21 local dmp = ""
22 local finishbefore = 2
23 for i = 1, #heap do
24 if i == finishbefore then
25 print(dmp)
26 dmp = ""
27 finishbefore = finishbefore * 2
28 end
29 dmp = dmp .. string.format("%f ", heap[i].c)
30 end
31 print(dmp)
32 print("")
33 for i = 1, #heap do
34 assert(not heap[heap_left(i)] or heap[i].c <= heap[heap_left(i)].c)
35 assert(not heap[heap_right(i)] or heap[i].c <= heap[heap_right(i)].c)
36 end
37 end
39 local function heap_insert(heap, item)
40 assert(item)
41 table.insert(heap, item)
42 local pt = #heap
43 while pt > 1 do
44 local ptd2 = math.floor(pt / 2)
45 if heap[ptd2].c <= heap[pt].c then
46 break
47 end
48 local tmp = heap[pt]
49 heap[pt] = heap[ptd2]
50 heap[ptd2] = tmp
51 pt = ptd2
52 end
53 --heap_sane(heap)
54 end
56 local function heap_extract(heap)
57 local rv = heap[1]
58 if #heap == 1 then table.remove(heap) return rv end
59 heap[1] = table.remove(heap)
60 local idx = 1
61 while idx < #heap do
62 local minix = idx
63 if heap[heap_left(idx)] and heap[heap_left(idx)].c < heap[minix].c then minix = heap_left(idx) end
64 if heap[heap_right(idx)] and heap[heap_right(idx)].c < heap[minix].c then minix = heap_right(idx) end
65 if minix ~= idx then
66 local tx = heap[minix]
67 heap[minix] = heap[idx]
68 heap[idx] = tx
69 idx = minix
70 else
71 break
72 end
73 end
74 --heap_sane(heap)
75 return rv
76 end
78 -- incremented on each graph iteration, so we don't have to clear everything. "GRaph ID" :D
79 local grid = 0
81 -- Plane format: each plane contains an array of nodes that exist in that plane.
82 -- Each node contains both its parent ID and its coordinates within that plane. It may contain a node it links to, along with cost.
83 local plane = {}
85 local plane_to_flyplane = {}
86 local continent_to_flyplane = {}
87 local flyplanes_enabled = {}
88 local plane_multiplier = {}
90 -- canonical plane :D
91 local function canoplane(plane)
92 if flyplanes_enabled[plane_to_flyplane[plane]] then return plane_to_flyplane[plane] else return plane end
93 end
95 local function xydist(st, nd)
96 QuestHelper: Assert(canoplane(st.p) == canoplane(nd.p))
97 local dx, dy = st.x - nd.x, st.y - nd.y
98 return math.sqrt(dx * dx + dy * dy) / (plane_multiplier[canoplane(nd.p)] or 7) -- we're getting a result in seconds, not in yards
99 end
105 function QH_Graph_Pathfind(st, nd, reverse, make_path)
106 return QH_Graph_Pathmultifind(st, {nd}, reverse, make_path)[1]
109 local active = false
111 function QH_Graph_Pathmultifind(st, nda, reverse, make_path)
112 QuestHelper: Assert(not active)
113 active = true -- The fun thing about coroutines is that this is actually safe.
114 local out = QuestHelper:CreateTable("graphcore output") -- THIS HAD BETTER BE RELEASABLE OR IT WILL BE BAD
116 local undone = QuestHelper:CreateTable("graphcore undone")
117 local remaining = 0
119 local link = QuestHelper:CreateTable("graphcore link")
121 QuestHelper: Assert(st.x and st.y and st.p)
123 for k, v in ipairs(nda) do
124 QuestHelper: Assert(v.x and v.y and v.p)
125 local cpvp = canoplane(v.p)
126 if canoplane(st.p) == cpvp then
127 out[k] = xydist(st, v)
128 else
129 if plane[cpvp] then
130 --print("Destination plane insertion")
131 -- ugh I hate this optimization
132 local dest = QuestHelper:CreateTable("graphcore destination")
133 dest.x, dest.y, dest.p, dest.goal = v.x, v.y, cpvp, k
134 link[k] = dest
135 table.insert(plane[cpvp], link[k])
136 undone[k] = true
137 remaining = remaining + 1
142 local link_id = reverse and "rlink" or "link"
143 local link_cost_id = reverse and "rlink_cost" or "link_cost"
145 local dijheap = QuestHelper:CreateTable("graphcore heap center")
146 if plane[canoplane(st.p)] then
147 --print("ST plane insertion")
148 for _, v in ipairs(plane[canoplane(st.p)]) do
149 if v[link_id] then
150 QuestHelper: Assert(not v.goal)
151 local dst = xydist(st, v)
152 v.scan_id = grid
153 v.scan_cost = dst
154 v.scan_from = nil
156 local hep = QuestHelper:CreateTable("graphcore heap")
157 hep.c, hep.cs, hep.n = dst + v[link_cost_id], dst, v
158 heap_insert(dijheap, hep)
163 while remaining > 0 and #dijheap > 0 do
164 QH_Timeslice_Yield()
165 local cdj = heap_extract(dijheap)
166 if cdj.done then
167 if undone[cdj.done] then
168 undone[cdj.done] = nil
169 remaining = remaining - 1
171 else
172 --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))
173 QuestHelper: Assert(cdj.n[link_id])
174 if cdj.n.scan_cost == cdj.cs then -- if we've modified it since then, don't bother
175 local linkto = cdj.n[link_id]
176 local basecost = cdj.c + cdj.n[link_cost_id]
177 if linkto.scan_id ~= grid or linkto.scan_cost > basecost then
178 linkto.scan_id = grid
179 linkto.scan_cost = basecost
180 linkto.scan_from = nil
182 for _, v in ipairs(plane[linkto.p]) do
183 if v.goal then
184 -- One way or another, we gotta calculate this.
185 local goalcost = basecost + xydist(linkto, v)
186 if not out[v.goal] or out[v.goal] > goalcost then
187 out[v.goal] = goalcost
188 v.scan_from = cdj.n
190 local hep = QuestHelper:CreateTable("graphcore heap")
191 hep.c, hep.done = goalcost, v.goal
192 heap_insert(dijheap, hep)
194 elseif v[link_id] and (v.scan_id ~= grid or v.scan_cost > basecost) then
195 local goalcost = basecost + xydist(linkto, v)
196 if v.scan_id ~= grid or v.scan_cost > goalcost then
197 v.scan_id = grid
198 v.scan_cost = goalcost
199 v.scan_from = cdj.n
201 local hep = QuestHelper:CreateTable("graphcore heap")
202 hep.c, hep.cs, hep.n = goalcost + v[link_cost_id], goalcost, v
203 heap_insert(dijheap, hep)
210 QuestHelper:ReleaseTable(cdj)
213 for _, v in ipairs(dijheap) do
214 QuestHelper:ReleaseTable(v)
216 QuestHelper:ReleaseTable(dijheap)
217 dijheap = nil
219 --QuestHelper:TextOut(string.format("Earlyout with %d/%d remaining", #dijheap, remaining))
220 if remaining > 0 then
221 for k, v in ipairs(nda) do
222 if not out[k] then
223 QuestHelper: Assert(false, string.format("Couldn't find path to %d/%f/%f", nda[k].p, nda[k].x, nda[k].y))
227 QuestHelper: Assert(remaining == 0)
229 grid = grid + 1
230 QuestHelper: Assert(grid < 1000000000) -- if this ever triggers I will be amazed
232 if make_path then
233 --print("mpath")
234 for k, v in pairs(out) do
235 --print(out[k])
236 local rp = QuestHelper:CreateTable("graphcore return")
237 rp.d = v
239 out[k] = rp
240 --print(out[k])
242 if link[k] then
243 QuestHelper: Assert(link[k].scan_from)
244 local tpath = reverse and rp or QuestHelper:CreateTable("graphcore path reversal")
245 local cpx = link[k].scan_from
246 while cpx do
247 if reverse then
248 QuestHelper: Assert(cpx.rlink)
249 table.insert(tpath, cpx.rlink)
250 else
251 QuestHelper: Assert(cpx.link)
252 table.insert(tpath, cpx.link)
254 QuestHelper: Assert(cpx)
255 table.insert(tpath, cpx)
257 cpx = cpx.scan_from
260 if not reverse then
261 for i = #tpath, 1, -1 do
262 table.insert(rp, tpath[i])
265 QuestHelper: Assert(tpath ~= rp)
266 QuestHelper:ReleaseTable(tpath)
272 for k, v in ipairs(nda) do
273 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
274 QuestHelper:ReleaseTable(table.remove(plane[canoplane(v.p)]))
278 QuestHelper:ReleaseTable(link)
279 QuestHelper:ReleaseTable(undone)
281 active = false
282 return out -- THIS HAD BETTER BE RELEASABLE OR IT WILL BE BAD
285 function QH_Graph_Init()
286 for c, d in pairs(QuestHelper_IndexLookup) do
287 if type(c) == "number" then
288 QuestHelper: Assert(d[0])
289 continent_to_flyplane[c] = d[0]
290 for z, p in pairs(d) do
291 if type(z) == "number" then
292 --QuestHelper:TextOut(string.format("%d/%d: %d", c, z, p))
293 plane_to_flyplane[p] = d[0]
300 local linkages = {}
302 local function QH_Graph_Plane_ReallyMakeLink(item)
303 local name, coord1, coord2, cost, cost_reverse = unpack(item)
305 QuestHelper: Assert(not active)
307 --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)))
308 QuestHelper: Assert(name)
309 QuestHelper: Assert(coord1)
310 QuestHelper: Assert(coord2)
311 QuestHelper: Assert(cost)
313 local node1 = {x = coord1.x, y = coord1.y, p = canoplane(coord1.p), c = coord1.c, map_desc = coord1.map_desc, condense_class = coord1.condense_class, name = name}
314 local node2 = {x = coord2.x, y = coord2.y, p = canoplane(coord2.p), c = coord2.c, map_desc = coord2.map_desc, condense_class = coord2.condense_class, name = name}
316 if node1.p == node2.p then
317 -- if they're the same location, we don't want to include them
318 -- right now, "the same location" is being done really, really cheaply
319 -- hey look me! I'm kind of a bastard!
321 if name == "static_transition" then return end -- ha ha, yep, that's how we find out, tooootally reliable
324 if not plane[node1.p] then plane[node1.p] = {} end
325 if not plane[node2.p] then plane[node2.p] = {} end
327 node1.link, node1.link_cost, node2.rlink, node2.rlink_cost = node2, cost, node1, cost
328 if cost_reverse then node1.rlink, node1.rlink_cost, node2.link, node2.link_cost = node2, cost_reverse, node1, cost_reverse end
330 table.insert(plane[node1.p], node1)
331 table.insert(plane[node2.p], node2)
334 function QH_Graph_Plane_Makelink(name, coord1, coord2, cost, cost_reverse)
335 QuestHelper: Assert(not active)
337 --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)))
338 QuestHelper: Assert(name)
339 QuestHelper: Assert(coord1)
340 QuestHelper: Assert(coord2)
341 QuestHelper: Assert(cost)
343 local tlink = {name, coord1, coord2, cost, cost_reverse}
344 if not linkages[name] then linkages[name] = {} end
345 table.insert(linkages[name], tlink)
347 QH_Graph_Plane_ReallyMakeLink(tlink)
350 local function QH_Graph_Plane_Destroylinkslocal(name)
351 QuestHelper: Assert(not active)
353 for k, v in pairs(plane) do
354 local repl = {}
355 for tk, tv in ipairs(v) do
356 if tv.name ~= name then
357 table.insert(repl, tv)
360 plane[k] = repl
364 function QH_Graph_Plane_Destroylinks(name)
365 QuestHelper: Assert(not active)
367 linkages[name] = nil
369 QH_Graph_Plane_Destroylinkslocal(name)
372 function QH_Graph_Flyplaneset(fpset, speed)
373 QuestHelper: Assert(not active)
375 if not flyplanes_enabled[QuestHelper_IndexLookup[fpset][0]] then
376 flyplanes_enabled[QuestHelper_IndexLookup[fpset][0]] = true
377 for k, v in pairs(linkages) do
378 QH_Graph_Plane_Destroylinkslocal(k)
380 for _, ite in pairs(v) do
381 QH_Graph_Plane_ReallyMakeLink(ite)
385 plane_multiplier[QuestHelper_IndexLookup[fpset][0]] = speed