local GetTime = QuestHelper_GetTime QuestHelper_File["routing_controller.lua"] = "4.0.1.$svnversion$" QuestHelper_Loadtime["routing_controller.lua"] = GetTime() local debug_output = (QuestHelper_File["routing_controller.lua"] == "Development Version") local Route_Core_Process = QH_Route_Core_Process local Route_Core_NodeCount = QH_Route_Core_NodeCount local Route_Core_Init = QH_Route_Core_Init local Route_Core_SetStart = QH_Route_Core_SetStart local Route_Core_ClusterAdd = QH_Route_Core_ClusterAdd local Route_Core_ClusterRemove = QH_Route_Core_ClusterRemove local Route_Core_ClusterRequires = QH_Route_Core_ClusterRequires local Route_Core_DistanceClear = QH_Route_Core_DistanceClear local Route_Core_IgnoreNode = QH_Route_Core_IgnoreNode local Route_Core_UnignoreNode = QH_Route_Core_UnignoreNode local Route_Core_IgnoreCluster = QH_Route_Core_IgnoreCluster local Route_Core_UnignoreCluster = QH_Route_Core_UnignoreCluster local Route_Core_GetClusterPriority = QH_Route_Core_GetClusterPriority local Route_Core_SetClusterPriority = QH_Route_Core_SetClusterPriority local Route_Core_TraverseNodes = QH_Route_Core_TraverseNodes local Route_Core_TraverseClusters = QH_Route_Core_TraverseClusters local Route_Core_IgnoredReasons_Cluster = QH_Route_Core_IgnoredReasons_Cluster local Route_Core_IgnoredReasons_Node = QH_Route_Core_IgnoredReasons_Node local Route_Core_Ignored_Cluster = QH_Route_Core_Ignored_Cluster local Route_Core_Ignored_Cluster_Active = QH_Route_Core_Ignored_Cluster_Active local Route_Core_EarlyExit = QH_Route_Core_EarlyExit QH_Route_Core_Process = nil QH_Route_Core_Init = nil QH_Route_Core_SetStart = nil QH_Route_Core_NodeObsoletes = nil QH_Route_Core_NodeRequires = nil QH_Route_Core_DistanceClear = nil QH_Route_Core_IgnoreNode = nil QH_Route_Core_UnignoreNode = nil QH_Route_Core_IgnoreCluster = nil QH_Route_Core_UnignoreCluster = nil QH_Route_Core_GetClusterPriority = nil QH_Route_Core_SetClusterPriority = nil QH_Route_Core_TraverseNodes = nil QH_Route_Core_TraverseClusters = nil QH_Route_Core_IgnoredReasons_Cluster = nil QH_Route_Core_IgnoredReasons_Node = nil QH_Route_Core_Ignored_Cluster = nil QH_Route_Core_Ignored_Cluster_Active = nil QH_Route_Core_EarlyExit = nil local pending = {} local rescan_it_all = false local weak_key = {__mode="k"} local function new_pathcache_table(conservative) return setmetatable(conservative and {} or QuestHelper:CreateTable("controller cache"), weak_key) end -- Every minute or two, we dump the inactive and move active to inactive. Every time we touch something, we put it in active. local pathcache_active = new_pathcache_table() local pathcache_inactive = new_pathcache_table() local function pcs(tpcs) local ct = 0 for _, v in pairs(tpcs) do for _, t in pairs(v) do ct = ct + 1 end end return ct end function QH_PrintPathcacheSize() QuestHelper:TextOut(string.format("Active pathcache: %d", pcs(pathcache_active))) QuestHelper:TextOut(string.format("Inactive pathcache: %d", pcs(pathcache_inactive))) end function QH_ClearPathcache(conservative) local ps = pcs(pathcache_inactive) pathcache_active = new_pathcache_table(conservative) pathcache_inactive = new_pathcache_table(conservative) return ps end local notification_funcs = {} function QH_Route_RegisterNotification(func) table.insert(notification_funcs, func) end local hits = 0 local misses = 0 local function GetCachedPath(loc1, loc2) -- If it's in active, it's guaranteed to be in inactive. if not pathcache_inactive[loc1] or not pathcache_inactive[loc1][loc2] then -- Not in either, time to create misses = misses + 1 local nrt = QH_Graph_Pathfind(loc1.loc, loc2.loc, false, true) QuestHelper: Assert(nrt) if not pathcache_active[loc1] then pathcache_active[loc1] = new_pathcache_table() end if not pathcache_inactive[loc1] then pathcache_inactive[loc1] = new_pathcache_table() end pathcache_active[loc1][loc2] = nrt pathcache_inactive[loc1][loc2] = nrt return nrt else hits = hits + 1 if not pathcache_active[loc1] then pathcache_active[loc1] = new_pathcache_table() end pathcache_active[loc1][loc2] = pathcache_inactive[loc1][loc2] return pathcache_active[loc1][loc2] end end local last_path = nil local cleanup_path = nil local function ReplotPath(progress) if not last_path then return end -- siiigh local real_path = QuestHelper:CreateTable("path") hits = 0 misses = 0 local distance = 0 for k, v in ipairs(last_path) do QH_Timeslice_Yield() --QuestHelper: Assert(not v.condense_type) -- no 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 table.insert(real_path, v) if last_path[k + 1] then local nrt = GetCachedPath(last_path[k], last_path[k + 1]) distance = distance + nrt.d QuestHelper: Assert(nrt) -- The "condense" is kind of weird - we're actually condensing descriptions, but we condense to the *last* item. Urgh. local condense_start = nil local condense_class = nil local condense_to = nil -- ugh this is just easier local function condense_doit() --print("start condense doit, was", real_path[condense_start].map_desc[1]) for i = condense_start, #real_path do real_path[i].map_desc = condense_to end --print("end condense doit, now", real_path[condense_start].map_desc[1]) condense_start, condense_class, condense_to = nil, nil, nil end if #nrt > 0 then for _, wp in ipairs(nrt) do QuestHelper: Assert(wp.c) --print(wp.condense_class) if condense_class and condense_class ~= wp.condense_class then condense_doit() end local pathnode = QuestHelper:CreateTable("pathnode") pathnode.loc = QuestHelper:CreateTable("pathnode.loc") pathnode.loc.x = wp.x pathnode.loc.y = wp.y pathnode.loc.c = wp.c pathnode.ignore = true pathnode.map_desc = wp.map_desc pathnode.map_desc_chain = last_path[k + 1] pathnode.local_allocated = true pathnode.cluster = last_path[k + 1].cluster table.insert(real_path, pathnode) -- Technically, we'll end up with the distance to the next objective. I'm okay with this. if not condense_class and wp.condense_class then condense_start, condense_class = #real_path, wp.condense_class end if condense_class then condense_to = wp.map_desc end end end if condense_class then condense_doit() end -- in case we have stuff left over end if progress then progress:SetPercentage(k / #last_path) end end for _, v in pairs(notification_funcs) do QH_Timeslice_Yield() v(real_path) end -- I hate having to do this, I feel like I'm just begging for horrifying bugs if cleanup_path then for k, v in ipairs(cleanup_path) do if v.local_allocated then QuestHelper:ReleaseTable(v.loc) QuestHelper:ReleaseTable(v) end end QuestHelper:ReleaseTable(cleanup_path) cleanup_path = nil end cleanup_path = real_path QH_Timeslice_Yield() end local filters = {} function QH_Route_RegisterFilter(filter) QuestHelper: Assert(not filters[filter.name]) QuestHelper: Assert(filter) filters[filter.name] = filter end -- 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 -- this is one reason the API is not considered stable :P local function ScanNode(node, ...) local stupid_lua = {...} Route_Core_EarlyExit() table.insert(pending, function () for k, v in pairs(filters) do if v:Process(node, unpack(stupid_lua)) then Route_Core_IgnoreNode(node, v) else Route_Core_UnignoreNode(node, v) end end end) end local function ScanCluster(clust) Route_Core_EarlyExit() table.insert(pending, function () for _, v in ipairs(clust) do ScanNode(v) end end) end local show_debug_commands = false function QH_Route_Filter_Rescan_Now() Route_Core_TraverseNodes(function (...) ScanNode(...) end) end function QH_Route_Filter_Rescan(name, suppress_earlyexit) if not suppress_earlyexit then Route_Core_EarlyExit() --[[print("ee rscn", (debugstack(2, 1, 0):gsub("\n...", "")))]] end QuestHelper: Assert(not name or filters[name] or name == "user_manual_ignored") table.insert(pending, function () if show_debug_commands then print("delayed qrfr") end QH_Route_Filter_Rescan_Now() -- yeah, so we're really rescanning every node, aren't we. laaaazy end) end function QH_Route_IgnoreNode(node, reason) Route_Core_EarlyExit() --print("ee in") table.insert(pending, function () if show_debug_commands then print("delayed qrin") end Route_Core_IgnoreNode(node, reason) end) end function QH_Route_UnignoreNode(node, reason) Route_Core_EarlyExit() --print("ee uin") table.insert(pending, function () if show_debug_commands then print("delayed qrun") end Route_Core_UnignoreNode(node, reason) end) end function QH_Route_ClusterAdd(clust) for _, v in ipairs(clust) do QuestHelper: Assert(v.cluster == clust) end Route_Core_EarlyExit() --print("ee ca") table.insert(pending, function () if show_debug_commands then print("delayed qrca1") end Route_Core_ClusterAdd(clust) end) rescan_it_all = true end function QH_Route_ClusterRemove(clust) Route_Core_EarlyExit() --print("ee cre") table.insert(pending, function () if show_debug_commands then print("delayed qrcrm") end Route_Core_ClusterRemove(clust) end) end function QH_Route_ClusterRequires(a, b) Route_Core_EarlyExit() --print("ee cr") table.insert(pending, function () if show_debug_commands then print("delayed qrcrq") end Route_Core_ClusterRequires(a, b) end) end function QH_Route_IgnoreCluster(clust, reason) Route_Core_EarlyExit() --print("ee ic") table.insert(pending, function () if show_debug_commands then print("delayed qric") end Route_Core_IgnoreCluster(clust, reason) end) end function QH_Route_UnignoreCluster(clust, reason) Route_Core_EarlyExit() --print("ee uic") table.insert(pending, function () if show_debug_commands then print("delayed qruc") end Route_Core_UnignoreCluster(clust, reason) end) end function QH_Route_SetClusterPriority(clust, pri) QuestHelper: Assert(clust) Route_Core_EarlyExit() --print("ee scp") table.insert(pending, function () if show_debug_commands then print("delayed qrscp") end Route_Core_SetClusterPriority(clust, pri) end) end local pending_recalc = false function QH_Route_FlightPathRecalc() if not pending_recalc then pending_recalc = true Route_Core_EarlyExit() --print("ee recalc") table.insert(pending, function () if show_debug_commands then print("delayed qrfpr") end pending_recalc = false QH_redo_flightpath() pathcache_active = new_pathcache_table() pathcache_inactive = new_pathcache_table() Route_Core_DistanceClear() ReplotPath() end) end end QH_Route_FlightPathRecalc() -- heh heh -- Right now we just defer to the existing ones function QH_Route_TraverseNodes(func) return Route_Core_TraverseNodes(func) end function QH_Route_TraverseClusters(func) return Route_Core_TraverseClusters(func) end function QH_Route_IgnoredReasons_Cluster(clust, func) return Route_Core_IgnoredReasons_Cluster(clust, func) end function QH_Route_IgnoredReasons_Node(node, func) return Route_Core_IgnoredReasons_Node(node, func) end function QH_Route_Ignored_Cluster(clust) return Route_Core_Ignored_Cluster(clust) end function QH_Route_Ignored_Cluster_Active(clust) return Route_Core_Ignored_Cluster_Active(clust) end function QH_Route_GetClusterPriority(clust) return Route_Core_GetClusterPriority(clust) end Route_Core_Init( function(path, progress) last_path = path ReplotPath(progress) end, function(loc1, loctable, reverse, complete_pass) if #loctable == 0 then return QuestHelper:CreateTable("null response") end QH_Timeslice_Yield() QuestHelper: Assert(loc1) QuestHelper: Assert(loc1.loc) local lt = QuestHelper:CreateTable("route controller path shunt loctable") for _, v in ipairs(loctable) do QuestHelper: Assert(v.loc) QuestHelper: Assert(type(v.loc) == "table") table.insert(lt, v.loc) end QuestHelper: Assert(#loctable == #lt) local rvv if not reverse then if not pathcache_active[loc1] then pathcache_active[loc1] = new_pathcache_table() end if not pathcache_inactive[loc1] then pathcache_inactive[loc1] = new_pathcache_table() end else for _, v in ipairs(loctable) do if not pathcache_active[v] then pathcache_active[v] = new_pathcache_table() end if not pathcache_inactive[v] then pathcache_inactive[v] = new_pathcache_table() end end end rvv = QuestHelper:CreateTable("route controller path shunt returnvalue") local rv = QH_Graph_Pathmultifind(loc1.loc, lt, reverse, true) --QuestHelper: Assert(#lt == #rv, string.format("lt has %d items, rt has %d items. Both should be the same.", #lt, #rv)) -- We want to store the math.max(sqrt(#rv), 10) shortest paths local tostore = complete_pass and math.max(sqrt(#rv), 10) or #rv --print("would store", #rv, "am store", tostore) local linkity = QuestHelper:CreateTable("shortest path summary") for k, v in ipairs(rv) do local tk = QuestHelper:CreateTable("shortest path summary item") tk.k = k tk.v = v table.insert(linkity, tk) rvv[k] = rv[k].d end table.sort(linkity, function(a, b) return a.v.d < b.v.d end) while #linkity > tostore do local rip = table.remove(linkity) QuestHelper:ReleaseTable(rip.v) QuestHelper:ReleaseTable(rip) end for _, it in pairs(linkity) do local k, v = it.k, it.v if not rv[k] then QuestHelper:TextOut(QuestHelper:StringizeTable(loc1.loc)) QuestHelper:TextOut(QuestHelper:StringizeTable(lt[k])) end QuestHelper: Assert(rv[k], string.format("%d to %d", loc1.loc.p, loctable[k].loc.p)) QuestHelper: Assert(rv[k].d) -- We're only setting the inactive to give the garbage collector potentially a little more to clean up (i.e. the old path.) if not reverse then QuestHelper: Assert(pathcache_active[loc1]) QuestHelper: Assert(pathcache_inactive[loc1]) pathcache_active[loc1][loctable[k]] = rv[k] pathcache_inactive[loc1][loctable[k]] = rv[k] else QuestHelper: Assert(loctable[k]) QuestHelper: Assert(pathcache_active[loctable[k]]) QuestHelper: Assert(pathcache_inactive[loctable[k]]) pathcache_active[loctable[k]][loc1] = rv[k] pathcache_inactive[loctable[k]][loc1] = rv[k] end end for _, v in ipairs(linkity) do -- we do not release v.v, since that's now stored in our path cache QuestHelper:ReleaseTable(v) end QuestHelper:ReleaseTable(linkity) QuestHelper:ReleaseTable(lt) QuestHelper:ReleaseTable(rv) -- this had better be releasable return rvv end ) local StartObjective = {desc = "Start", tracker_hidden = true} -- this should never be displayed local lapa = GetTime() local passcount = 0 local lc, lx, ly, lrc, lrz local last_playerpos = nil local function ReleaseShard(ki, shard) for k, tv in pairs(shard) do if not pathcache_active[ki] or not pathcache_active[ki][k] then QuestHelper:ReleaseTable(tv) end end QuestHelper:ReleaseTable(shard) end local function process() local last_cull = 0 local last_movement = 0 local first = true -- 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. while true do if last_cull + 120 < GetTime() then last_cull = GetTime() for k, v in pairs(pathcache_inactive) do ReleaseShard(k, v) end QuestHelper:ReleaseTable(pathcache_inactive) pathcache_inactive = pathcache_active pathcache_active = new_pathcache_table() end if last_movement + 1 < GetTime() then local c, x, y, rc, rz = QuestHelper.routing_ac, QuestHelper.routing_ax, QuestHelper.routing_ay, QuestHelper.routing_c, QuestHelper.routing_z -- ugh we need a better solution to this, but with this weird "planes" hybrid there just isn't one right now 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 --local t = GetTime() lc, lx, ly, lrc, lrz = c, x, y, rc, rz local new_playerpos = {desc = "Start", why = StartObjective, loc = NewLoc(c, x, y, rc, rz), tracker_hidden = true, ignore = true} Route_Core_SetStart(new_playerpos) if last_path then last_path[1] = new_playerpos end --QuestHelper: TextOut(string.format("SS takes %f", GetTime() - t)) ReplotPath() if last_playerpos then -- if it's in active, then it must be in inactive as well, so we do our actual deallocation in inactive only if pathcache_active[last_playerpos] then QuestHelper:ReleaseTable(pathcache_active[last_playerpos]) pathcache_active[last_playerpos] = nil end for k, v in pairs(pathcache_active) do v[last_playerpos] = nil end if pathcache_inactive[last_playerpos] then ReleaseShard(last_playerpos, pathcache_inactive[last_playerpos]) pathcache_inactive[last_playerpos] = nil end for k, v in pairs(pathcache_inactive) do if v[last_playerpos] then QuestHelper:ReleaseTable(v[last_playerpos]) v[last_playerpos] = nil end end end last_playerpos = new_playerpos last_movement = GetTime() end end if not first then Route_Core_Process() QH_Timeslice_Doneinit() -- Wackyland code here if QH_WACKYLAND then QH_Route_Filter_Rescan() QH_Route_TraverseClusters(function (clust) QH_Route_SetClusterPriority(clust, math.random(-2, 2)) end) end end first = false passcount = passcount + 1 if lapa + 60 < GetTime() then if debug_output then QuestHelper:TextOut(string.format("%d passes in the last minute, %d nodes", passcount, Route_Core_NodeCount())) end lapa = lapa + 60 passcount = 0 end QH_Timeslice_Yield() -- snag stuff so we don't accidentally end up changing pending in two things at once while #pending > 0 do local lpending = pending pending = {} for k, v in ipairs(lpending) do v() QH_Timeslice_Yield() end end if rescan_it_all then rescan_it_all = false --[[ assert(#pending == 0) ]] -- assert that everything is consistent after the scan QH_Route_Filter_Rescan_Now() end end end -- 4.0.3a Breakage related QH_Timeslice_Add(process, "new_routing")