Change how that error shows up
[QuestHelper.git] / routing_controller.lua
blobd8506ba8d95de7f471ac273d6cf9151b87840cb3
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()
55 return setmetatable(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()
77 local ps = pcs(pathcache_inactive)
78 pathcache_active = new_pathcache_table()
79 pathcache_inactive = new_pathcache_table()
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 QuestHelper: Assert(not v.condense_type) -- no
125 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
126 table.insert(real_path, v)
127 if last_path[k + 1] then
128 local nrt = GetCachedPath(last_path[k], last_path[k + 1])
129 distance = distance + nrt.d
130 QuestHelper: Assert(nrt)
132 -- The "condense" is kind of weird - we're actually condensing descriptions, but we condense to the *last* item. Urgh.
133 local condense_start = nil
134 local condense_type = nil
135 local condense_to = nil
137 -- ugh this is just easier
138 local function condense_doit()
139 for i = condense_start, #real_path do
140 real_path[i].map_desc = condense_to
142 condense_start, condense_type, condense_to = nil, nil, nil
145 if #nrt > 0 then for _, wp in ipairs(nrt) do
146 QuestHelper: Assert(wp.c)
148 if condense_type and condense_type ~= wp.condense_type then condense_doit() end
150 local pathnode = QuestHelper:CreateTable("pathnode")
151 pathnode.loc = QuestHelper:CreateTable("pathnode.loc")
152 pathnode.loc.x = wp.x
153 pathnode.loc.y = wp.y
154 pathnode.loc.c = wp.c
155 pathnode.ignore = true
156 pathnode.map_desc = wp.map_desc
157 pathnode.map_desc_chain = last_path[k + 1]
158 pathnode.local_allocated = true
159 table.insert(real_path, pathnode) -- Technically, we'll end up with the distance to the next objective. I'm okay with this.
161 if not condense_type and wp.condense_type then
162 condense_start, condense_type, condense_to = #real_path, wp.condense_type, wp.map_desc
164 end end
166 if condense_type then condense_doit() end -- in case we have stuff left over
170 for _, v in pairs(notification_funcs) do
171 v(real_path)
174 -- I hate having to do this, I feel like I'm just begging for horrifying bugs
175 if cleanup_path then
176 for k, v in ipairs(cleanup_path) do
177 if v.local_allocated then
178 QuestHelper:ReleaseTable(v.loc)
179 QuestHelper:ReleaseTable(v)
183 QuestHelper:ReleaseTable(cleanup_path)
184 cleanup_path = nil
187 cleanup_path = real_path
190 local filters = {}
192 function QH_Route_RegisterFilter(filter)
193 QuestHelper: Assert(not filters[filter.name])
194 QuestHelper: Assert(filter)
195 filters[filter.name] = filter
198 -- 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
199 -- this is one reason the API is not considered stable :P
200 local function ScanNode(node, ...)
201 local stupid_lua = {...}
202 table.insert(pending, function ()
203 for k, v in pairs(filters) do
204 if v:Process(node, unpack(stupid_lua)) then
205 Route_Core_IgnoreNode(node, v)
206 else
207 Route_Core_UnignoreNode(node, v)
210 end)
213 local function ScanCluster(clust)
214 table.insert(pending, function ()
215 for _, v in ipairs(clust) do
216 ScanNode(v)
218 end)
221 function QH_Route_Filter_Rescan(name)
222 QuestHelper: Assert(not name or filters[name])
223 table.insert(pending, function ()
224 Route_Core_TraverseNodes(function (...)
225 ScanNode(...) -- yeah, so we're really rescanning every node, aren't we. laaaazy
226 end)
227 end)
230 function QH_Route_IgnoreNode(node, reason)
231 table.insert(pending, function () Route_Core_IgnoreNode(node, reason) end)
234 function QH_Route_UnignoreNode(node, reason)
235 table.insert(pending, function () Route_Core_UnignoreNode(node, reason) end)
238 function QH_Route_ClusterAdd(clust)
239 table.insert(pending, function () Route_Core_ClusterAdd(clust) ScanCluster(clust) end)
242 function QH_Route_ClusterRemove(clust)
243 table.insert(pending, function () Route_Core_ClusterRemove(clust) end)
246 function QH_Route_ClusterRequires(a, b)
247 table.insert(pending, function () Route_Core_ClusterRequires(a, b) end)
250 function QH_Route_IgnoreCluster(clust, reason)
251 table.insert(pending, function () Route_Core_IgnoreCluster(clust, reason) end)
254 function QH_Route_UnignoreCluster(clust, reason)
255 table.insert(pending, function () Route_Core_UnignoreCluster(clust, reason) end)
258 function QH_Route_UnignoreCluster(clust, pri)
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 function QH_Route_FlightPathRecalc()
267 table.insert(pending, function () QH_redo_flightpath() pathcache_active = new_pathcache_table() pathcache_inactive = new_pathcache_table() Route_Core_DistanceClear() ReplotPath() end)
269 QH_Route_FlightPathRecalc() -- heh heh
271 -- Right now we just defer to the existing ones
272 function QH_Route_TraverseNodes(func)
273 return Route_Core_TraverseNodes(func)
275 function QH_Route_TraverseClusters(func)
276 return Route_Core_TraverseClusters(func)
278 function QH_Route_IgnoredReasons_Cluster(clust, func)
279 return Route_Core_IgnoredReasons_Cluster(clust, func)
281 function QH_Route_IgnoredReasons_Node(node, func)
282 return Route_Core_IgnoredReasons_Node(node, func)
284 function QH_Route_Ignored_Cluster(clust)
285 return Route_Core_Ignored_Cluster(clust)
287 function QH_Route_GetClusterPriority(clust)
288 return Route_Core_GetClusterPriority(clust)
293 Route_Core_Init(
294 function(path)
295 last_path = path
296 ReplotPath()
297 end,
298 function(loc1, loctable, reverse)
299 QH_Timeslice_Yield()
301 QuestHelper: Assert(loc1)
302 QuestHelper: Assert(loc1.loc)
304 local lt = QuestHelper:CreateTable("route controller path shunt loctable")
305 for _, v in ipairs(loctable) do
306 QuestHelper: Assert(v.loc)
307 table.insert(lt, v.loc)
310 local rvv
312 if QuestHelper_Pref.precache then
313 if not reverse then
314 if not pathcache_active[loc1] then pathcache_active[loc1] = new_pathcache_table() end
315 if not pathcache_inactive[loc1] then pathcache_inactive[loc1] = new_pathcache_table() end
316 else
317 for _, v in ipairs(loctable) do
318 if not pathcache_active[v] then pathcache_active[v] = new_pathcache_table() end
319 if not pathcache_inactive[v] then pathcache_inactive[v] = new_pathcache_table() end
323 rvv = QuestHelper:CreateTable("route controller path shunt returnvalue")
324 local rv = QH_Graph_Pathmultifind(loc1.loc, lt, reverse, true)
326 for k, v in ipairs(lt) do
327 if not rv[k] then
328 QuestHelper:TextOut(QuestHelper:StringizeTable(loc1.loc))
329 QuestHelper:TextOut(QuestHelper:StringizeTable(lt[k]))
331 QuestHelper: Assert(rv[k], string.format("%d to %d", loc1.loc.p, loctable[k].loc.p))
332 QuestHelper: Assert(rv[k].d)
333 rvv[k] = rv[k].d
335 -- We're only setting the inactive to give the garbage collector potentially a little more to clean up (i.e. the old path.)
336 if not reverse then
337 pathcache_active[loc1][loctable[k]] = rv[k]
338 pathcache_inactive[loc1][loctable[k]] = rv[k]
339 else
340 pathcache_active[loctable[k]][loc1] = rv[k]
341 pathcache_inactive[loctable[k]][loc1] = rv[k]
345 QuestHelper:ReleaseTable(lt)
346 QuestHelper:ReleaseTable(rv) -- this had better be releasable
347 else
348 rvv = QH_Graph_Pathmultifind(loc1.loc, lt, reverse)
351 return rvv
355 local StartObjective = {desc = "Start", tracker_hidden = true} -- this should never be displayed
357 local lapa = GetTime()
358 local passcount = 0
360 local lc, lx, ly, lrc, lrz
362 local last_playerpos = nil
364 local function ReleaseShard(ki, shard)
365 for k, tv in pairs(shard) do
366 if not pathcache_active[ki] or not pathcache_active[ki][k] then QuestHelper:ReleaseTable(tv) end
368 QuestHelper:ReleaseTable(shard)
371 local function process()
373 local last_cull = 0
374 local last_movement = 0
376 local first = true
377 -- 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.
378 while true do
379 if last_cull + 120 < GetTime() then
380 last_cull = GetTime()
382 for k, v in pairs(pathcache_inactive) do
383 ReleaseShard(k, v)
385 QuestHelper:ReleaseTable(pathcache_inactive)
387 pathcache_inactive = pathcache_active
388 pathcache_active = new_pathcache_table()
391 if last_movement + 1 < GetTime() then
392 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
393 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
394 --local t = GetTime()
395 lc, lx, ly, lrc, lrz = c, x, y, rc, rz
397 local new_playerpos = {desc = "Start", why = StartObjective, loc = NewLoc(c, x, y, rc, rz), tracker_hidden = true, ignore = true}
398 Route_Core_SetStart(new_playerpos)
399 if last_path then last_path[1] = new_playerpos end
400 --QuestHelper: TextOut(string.format("SS takes %f", GetTime() - t))
401 ReplotPath()
403 if last_playerpos then
404 -- if it's in active, then it must be in inactive as well, so we do our actual deallocation in inactive only
405 if pathcache_active[last_playerpos] then QuestHelper:ReleaseTable(pathcache_active[last_playerpos]) pathcache_active[last_playerpos] = nil end
406 for k, v in pairs(pathcache_active) do v[last_playerpos] = nil end
408 if pathcache_inactive[last_playerpos] then ReleaseShard(last_playerpos, pathcache_inactive[last_playerpos]) pathcache_inactive[last_playerpos] = nil end
409 for k, v in pairs(pathcache_inactive) do if v[last_playerpos] then QuestHelper:ReleaseTable(v[last_playerpos]) v[last_playerpos] = nil end end
412 last_playerpos = new_playerpos
414 last_movement = GetTime()
418 if not first then
419 Route_Core_Process()
420 QH_Timeslice_Doneinit()
422 first = false
424 passcount = passcount + 1
425 if lapa + 60 < GetTime() then
426 if debug_output then QuestHelper:TextOut(string.format("%d passes in the last minute, %d nodes", passcount, Route_Core_NodeCount())) end
427 lapa = lapa + 60
428 passcount = 0
431 QH_Timeslice_Yield()
433 -- snag stuff so we don't accidentally end up changing pending in two things at once
434 while #pending > 0 do
435 local lpending = pending
436 pending = {}
438 for k, v in ipairs(lpending) do
440 QH_Timeslice_Yield()
446 QH_Timeslice_Add(process, "new_routing")