update changes
[QuestHelper.git] / routing_controller.lua
blob7b0bc506a17062b604a85d184fa9ac6297b90bf1
1 QuestHelper_File["routing_controller.lua"] = "Development Version"
2 QuestHelper_Loadtime["routing_controller.lua"] = GetTime()
4 local debug_output = (QuestHelper_File["routing_controller.lua"] == "Development Version")
6 local Route_Core_Process = QH_Route_Core_Process
8 local Route_Core_NodeCount = QH_Route_Core_NodeCount
10 local Route_Core_Init = QH_Route_Core_Init
11 local Route_Core_SetStart = QH_Route_Core_SetStart
13 local Route_Core_ClusterAdd = QH_Route_Core_ClusterAdd
14 local Route_Core_ClusterRemove = QH_Route_Core_ClusterRemove
15 local Route_Core_ClusterRequires = QH_Route_Core_ClusterRequires
16 local Route_Core_DistanceClear = QH_Route_Core_DistanceClear
18 local Route_Core_IgnoreNode = QH_Route_Core_IgnoreNode
19 local Route_Core_UnignoreNode = QH_Route_Core_UnignoreNode
20 local Route_Core_IgnoreCluster = QH_Route_Core_IgnoreCluster
21 local Route_Core_UnignoreCluster = QH_Route_Core_UnignoreCluster
23 local Route_Core_GetClusterPriority = QH_Route_Core_GetClusterPriority
24 local Route_Core_SetClusterPriority = QH_Route_Core_SetClusterPriority
26 local Route_Core_TraverseNodes = QH_Route_Core_TraverseNodes
27 local Route_Core_TraverseClusters = QH_Route_Core_TraverseClusters
28 local Route_Core_IgnoredReasons_Cluster = QH_Route_Core_IgnoredReasons_Cluster
29 local Route_Core_IgnoredReasons_Node = QH_Route_Core_IgnoredReasons_Node
30 local Route_Core_Ignored_Cluster = QH_Route_Core_Ignored_Cluster
32 QH_Route_Core_Process = nil
33 QH_Route_Core_Init = nil
34 QH_Route_Core_SetStart = nil
35 QH_Route_Core_NodeObsoletes = nil
36 QH_Route_Core_NodeRequires = nil
37 QH_Route_Core_DistanceClear = nil
38 QH_Route_Core_IgnoreNode = nil
39 QH_Route_Core_UnignoreNode = nil
40 QH_Route_Core_IgnoreCluster = nil
41 QH_Route_Core_UnignoreCluster = nil
42 QH_Route_Core_GetClusterPriority = nil
43 QH_Route_Core_SetClusterPriority = nil
44 QH_Route_Core_TraverseNodes = nil
45 QH_Route_Core_TraverseClusters = nil
46 QH_Route_Core_IgnoredReasons_Cluster = nil
47 QH_Route_Core_IgnoredReasons_Node = nil
48 QH_Route_Core_Ignored_Cluster = nil
50 local pending = {}
52 local weak_key = {__mode="k"}
54 local function new_pathcache_table(conservative)
55 return setmetatable(conservative and {} or QuestHelper:CreateTable("controller cache"), weak_key)
56 end
58 -- Every minute or two, we dump the inactive and move active to inactive. Every time we touch something, we put it in active.
59 local pathcache_active = new_pathcache_table()
60 local pathcache_inactive = new_pathcache_table()
62 local function pcs(tpcs)
63 local ct = 0
64 for _, v in pairs(tpcs) do
65 for _, t in pairs(v) do
66 ct = ct + 1
67 end
68 end
69 return ct
70 end
72 function QH_PrintPathcacheSize()
73 QuestHelper:TextOut(string.format("Active pathcache: %d", pcs(pathcache_active)))
74 QuestHelper:TextOut(string.format("Inactive pathcache: %d", pcs(pathcache_inactive)))
75 end
76 function QH_ClearPathcache(conservative)
77 local ps = pcs(pathcache_inactive)
78 pathcache_active = new_pathcache_table(conservative)
79 pathcache_inactive = new_pathcache_table(conservative)
80 return ps
81 end
83 local notification_funcs = {}
85 function QH_Route_RegisterNotification(func)
86 table.insert(notification_funcs, func)
87 end
89 local hits = 0
90 local misses = 0
92 local function GetCachedPath(loc1, loc2)
93 -- If it's in active, it's guaranteed to be in inactive.
94 if not pathcache_inactive[loc1] or not pathcache_inactive[loc1][loc2] then
95 -- Not in either, time to create
96 misses = misses + 1
97 local nrt = QH_Graph_Pathfind(loc1.loc, loc2.loc, false, true)
98 QuestHelper: Assert(nrt)
99 if not pathcache_active[loc1] then pathcache_active[loc1] = new_pathcache_table() end
100 if not pathcache_inactive[loc1] then pathcache_inactive[loc1] = new_pathcache_table() end
101 pathcache_active[loc1][loc2] = nrt
102 pathcache_inactive[loc1][loc2] = nrt
103 return nrt
104 else
105 hits = hits + 1
106 if not pathcache_active[loc1] then pathcache_active[loc1] = new_pathcache_table() end
107 pathcache_active[loc1][loc2] = pathcache_inactive[loc1][loc2]
108 return pathcache_active[loc1][loc2]
112 local last_path = nil
113 local cleanup_path = nil
115 local function ReplotPath()
116 if not last_path then return end -- siiigh
118 local real_path = QuestHelper:CreateTable("path")
119 hits = 0
120 misses = 0
122 local distance = 0
123 for k, v in ipairs(last_path) do
124 QH_Timeslice_Yield()
125 QuestHelper: Assert(not v.condense_type) -- no
126 v.distance = distance -- I'm not a huge fan of mutating things like this, but it is safe, and these nodes are technically designed to be modified during runtime anyway
127 table.insert(real_path, v)
128 if last_path[k + 1] then
129 local nrt = GetCachedPath(last_path[k], last_path[k + 1])
130 distance = distance + nrt.d
131 QuestHelper: Assert(nrt)
133 -- The "condense" is kind of weird - we're actually condensing descriptions, but we condense to the *last* item. Urgh.
134 local condense_start = nil
135 local condense_type = nil
136 local condense_to = nil
138 -- ugh this is just easier
139 local function condense_doit()
140 for i = condense_start, #real_path do
141 real_path[i].map_desc = condense_to
143 condense_start, condense_type, condense_to = nil, nil, nil
146 if #nrt > 0 then for _, wp in ipairs(nrt) do
147 QuestHelper: Assert(wp.c)
149 if condense_type and condense_type ~= wp.condense_type then condense_doit() end
151 local pathnode = QuestHelper:CreateTable("pathnode")
152 pathnode.loc = QuestHelper:CreateTable("pathnode.loc")
153 pathnode.loc.x = wp.x
154 pathnode.loc.y = wp.y
155 pathnode.loc.c = wp.c
156 pathnode.ignore = true
157 pathnode.map_desc = wp.map_desc
158 pathnode.map_desc_chain = last_path[k + 1]
159 pathnode.local_allocated = true
160 table.insert(real_path, pathnode) -- Technically, we'll end up with the distance to the next objective. I'm okay with this.
162 if not condense_type and wp.condense_type then
163 condense_start, condense_type, condense_to = #real_path, wp.condense_type, wp.map_desc
165 end end
167 if condense_type then condense_doit() end -- in case we have stuff left over
171 for _, v in pairs(notification_funcs) do
172 QH_Timeslice_Yield()
173 v(real_path)
176 -- I hate having to do this, I feel like I'm just begging for horrifying bugs
177 if cleanup_path then
178 for k, v in ipairs(cleanup_path) do
179 if v.local_allocated then
180 QuestHelper:ReleaseTable(v.loc)
181 QuestHelper:ReleaseTable(v)
185 QuestHelper:ReleaseTable(cleanup_path)
186 cleanup_path = nil
189 cleanup_path = real_path
191 QH_Timeslice_Yield()
194 local filters = {}
196 function QH_Route_RegisterFilter(filter)
197 QuestHelper: Assert(not filters[filter.name])
198 QuestHelper: Assert(filter)
199 filters[filter.name] = filter
202 -- we deal very badly with changing the requirement links after things are set up, so right now we're just relying on the fact that everything is unchanging afterwards
203 -- this is one reason the API is not considered stable :P
204 local function ScanNode(node, ...)
205 local stupid_lua = {...}
206 table.insert(pending, function ()
207 for k, v in pairs(filters) do
208 if v:Process(node, unpack(stupid_lua)) then
209 Route_Core_IgnoreNode(node, v)
210 else
211 Route_Core_UnignoreNode(node, v)
214 end)
217 local function ScanCluster(clust)
218 table.insert(pending, function ()
219 for _, v in ipairs(clust) do
220 ScanNode(v)
222 end)
225 function QH_Route_Filter_Rescan(name)
226 QuestHelper: Assert(not name or filters[name] or name == "user_manual_ignored")
227 table.insert(pending, function ()
228 Route_Core_TraverseNodes(function (...)
229 ScanNode(...) -- yeah, so we're really rescanning every node, aren't we. laaaazy
230 end)
231 end)
234 function QH_Route_IgnoreNode(node, reason)
235 table.insert(pending, function () Route_Core_IgnoreNode(node, reason) end)
238 function QH_Route_UnignoreNode(node, reason)
239 table.insert(pending, function () Route_Core_UnignoreNode(node, reason) end)
242 function QH_Route_ClusterAdd(clust)
243 table.insert(pending, function () Route_Core_ClusterAdd(clust) ScanCluster(clust) end)
246 function QH_Route_ClusterRemove(clust)
247 table.insert(pending, function () Route_Core_ClusterRemove(clust) end)
250 function QH_Route_ClusterRequires(a, b)
251 table.insert(pending, function () Route_Core_ClusterRequires(a, b) end)
254 function QH_Route_IgnoreCluster(clust, reason)
255 table.insert(pending, function () Route_Core_IgnoreCluster(clust, reason) end)
258 function QH_Route_UnignoreCluster(clust, reason)
259 table.insert(pending, function () Route_Core_UnignoreCluster(clust, reason) end)
262 function QH_Route_SetClusterPriority(clust, pri)
263 table.insert(pending, function () Route_Core_SetClusterPriority(clust, pri) end)
266 local pending_recalc = false
267 function QH_Route_FlightPathRecalc()
268 if not pending_recalc then
269 pending_recalc = true
270 table.insert(pending, function () pending_recalc = false QH_redo_flightpath() pathcache_active = new_pathcache_table() pathcache_inactive = new_pathcache_table() Route_Core_DistanceClear() ReplotPath() end)
273 QH_Route_FlightPathRecalc() -- heh heh
275 -- Right now we just defer to the existing ones
276 function QH_Route_TraverseNodes(func)
277 return Route_Core_TraverseNodes(func)
279 function QH_Route_TraverseClusters(func)
280 return Route_Core_TraverseClusters(func)
282 function QH_Route_IgnoredReasons_Cluster(clust, func)
283 return Route_Core_IgnoredReasons_Cluster(clust, func)
285 function QH_Route_IgnoredReasons_Node(node, func)
286 return Route_Core_IgnoredReasons_Node(node, func)
288 function QH_Route_Ignored_Cluster(clust)
289 return Route_Core_Ignored_Cluster(clust)
291 function QH_Route_GetClusterPriority(clust)
292 return Route_Core_GetClusterPriority(clust)
297 Route_Core_Init(
298 function(path)
299 last_path = path
300 ReplotPath()
301 end,
302 function(loc1, loctable, reverse, complete_pass)
303 QH_Timeslice_Yield()
305 QuestHelper: Assert(loc1)
306 QuestHelper: Assert(loc1.loc)
308 local lt = QuestHelper:CreateTable("route controller path shunt loctable")
309 for _, v in ipairs(loctable) do
310 QuestHelper: Assert(v.loc)
311 table.insert(lt, v.loc)
313 QuestHelper: Assert(#loctable == #lt)
315 local rvv
317 if not complete_pass then
318 if not reverse then
319 if not pathcache_active[loc1] then pathcache_active[loc1] = new_pathcache_table() end
320 if not pathcache_inactive[loc1] then pathcache_inactive[loc1] = new_pathcache_table() end
321 else
322 for _, v in ipairs(loctable) do
323 if not pathcache_active[v] then pathcache_active[v] = new_pathcache_table() end
324 if not pathcache_inactive[v] then pathcache_inactive[v] = new_pathcache_table() end
328 rvv = QuestHelper:CreateTable("route controller path shunt returnvalue")
329 local rv = QH_Graph_Pathmultifind(loc1.loc, lt, reverse, true)
331 QuestHelper: Assert(#lt == #rv)
332 for k, v in ipairs(lt) do
333 if not rv[k] then
334 QuestHelper:TextOut(QuestHelper:StringizeTable(loc1.loc))
335 QuestHelper:TextOut(QuestHelper:StringizeTable(lt[k]))
337 QuestHelper: Assert(rv[k], string.format("%d to %d", loc1.loc.p, loctable[k].loc.p))
338 QuestHelper: Assert(rv[k].d)
339 rvv[k] = rv[k].d
341 -- We're only setting the inactive to give the garbage collector potentially a little more to clean up (i.e. the old path.)
342 if not reverse then
343 QuestHelper: Assert(pathcache_active[loc1])
344 QuestHelper: Assert(pathcache_inactive[loc1])
345 pathcache_active[loc1][loctable[k]] = rv[k]
346 pathcache_inactive[loc1][loctable[k]] = rv[k]
347 else
348 pathcache_active[loctable[k]][loc1] = rv[k]
349 pathcache_inactive[loctable[k]][loc1] = rv[k]
353 QuestHelper:ReleaseTable(lt)
354 QuestHelper:ReleaseTable(rv) -- this had better be releasable
355 else
356 rvv = QH_Graph_Pathmultifind(loc1.loc, lt, reverse)
359 return rvv
363 local StartObjective = {desc = "Start", tracker_hidden = true} -- this should never be displayed
365 local lapa = GetTime()
366 local passcount = 0
368 local lc, lx, ly, lrc, lrz
370 local last_playerpos = nil
372 local function ReleaseShard(ki, shard)
373 for k, tv in pairs(shard) do
374 if not pathcache_active[ki] or not pathcache_active[ki][k] then QuestHelper:ReleaseTable(tv) end
376 QuestHelper:ReleaseTable(shard)
379 local function process()
381 local last_cull = 0
382 local last_movement = 0
384 local first = true
385 -- Order here is important. We don't want to update the location, then wait for a while as we add nodes. We also need the location updated before the first nodes are added. This way, it all works and we don't need anything outside the loop.
386 while true do
387 if last_cull + 120 < GetTime() then
388 last_cull = GetTime()
390 for k, v in pairs(pathcache_inactive) do
391 ReleaseShard(k, v)
393 QuestHelper:ReleaseTable(pathcache_inactive)
395 pathcache_inactive = pathcache_active
396 pathcache_active = new_pathcache_table()
399 if last_movement + 1 < GetTime() then
400 local c, x, y, rc, rz = QuestHelper.collect_ac, QuestHelper.collect_ax, QuestHelper.collect_ay, QuestHelper.c, QuestHelper.z -- ugh we need a better solution to this, but with this weird "planes" hybrid there just isn't one right now
401 if c and x and y and rc and rz and (c ~= lc or x ~= lx or y ~= ly or rc ~= lrc or rz ~= lrz) then
402 --local t = GetTime()
403 lc, lx, ly, lrc, lrz = c, x, y, rc, rz
405 local new_playerpos = {desc = "Start", why = StartObjective, loc = NewLoc(c, x, y, rc, rz), tracker_hidden = true, ignore = true}
406 Route_Core_SetStart(new_playerpos)
407 if last_path then last_path[1] = new_playerpos end
408 --QuestHelper: TextOut(string.format("SS takes %f", GetTime() - t))
409 ReplotPath()
411 if last_playerpos then
412 -- if it's in active, then it must be in inactive as well, so we do our actual deallocation in inactive only
413 if pathcache_active[last_playerpos] then QuestHelper:ReleaseTable(pathcache_active[last_playerpos]) pathcache_active[last_playerpos] = nil end
414 for k, v in pairs(pathcache_active) do v[last_playerpos] = nil end
416 if pathcache_inactive[last_playerpos] then ReleaseShard(last_playerpos, pathcache_inactive[last_playerpos]) pathcache_inactive[last_playerpos] = nil end
417 for k, v in pairs(pathcache_inactive) do if v[last_playerpos] then QuestHelper:ReleaseTable(v[last_playerpos]) v[last_playerpos] = nil end end
420 last_playerpos = new_playerpos
422 last_movement = GetTime()
426 if not first then
427 Route_Core_Process()
428 QH_Timeslice_Doneinit()
430 -- Wackyland code here
431 if QH_WACKYLAND then
432 QH_Route_Filter_Rescan()
433 QH_Route_TraverseClusters(function (clust) QH_Route_SetClusterPriority(clust, math.random(-2, 2)) end)
436 first = false
438 passcount = passcount + 1
439 if lapa + 60 < GetTime() then
440 if debug_output then QuestHelper:TextOut(string.format("%d passes in the last minute, %d nodes", passcount, Route_Core_NodeCount())) end
441 lapa = lapa + 60
442 passcount = 0
445 QH_Timeslice_Yield()
447 -- snag stuff so we don't accidentally end up changing pending in two things at once
448 while #pending > 0 do
449 local lpending = pending
450 pending = {}
452 for k, v in ipairs(lpending) do
454 QH_Timeslice_Yield()
460 QH_Timeslice_Add(process, "new_routing")