--[[ A thing that kinda works like require, or at least, works how I wish require worked. calls hooks: artery_nrequire_defined artery_core_loaded ]] if nrequire ~= nil then return end local searchpaths = { {"gamemodes/artery/gamemode","GAME"}, {"artery/global","DATA"}, {"data/artery/global","GAME"}, } --dir is a table of {string_path, string_directory} local function TraverseFolder(dir,func) local find_path = table.concat({dir[1],"/*"},"") local files,directories = file.Find(find_path,dir[2]) for k,v in pairs(files) do local callpath = table.concat({dir[1],"/",v},"") func({callpath,dir[2]}) end for k,v in pairs(directories) do local npath = table.concat({dir[1],"/",v},"") TraverseFolder({npath,dir[2]},func) end end local function BuildIncludeTable(tbl) local output = {} for k,v in pairs(tbl) do local filepath = v[1] local fileloc = v[2] local pathparts = {} for part in filepath:gmatch("/?[%w_]+/") do local foldername = part:gsub("/$",""):gsub("^/","") pathparts[#pathparts+1] = foldername end local filename = filepath:gfind("[%w_]+%.[%w_]+$")() if output[filename] == nil then output[filename] = {} end local cursor = output[filename] for folder= #pathparts, 2, -1 do if cursor[pathparts[folder]] == nil then cursor[pathparts[folder]] = {} end cursor = cursor[pathparts[folder]] end assert(cursor[pathparts[1]] == nil, string.format("Found 2 files with the same path:\n\t%q",pathparts[1])) cursor[pathparts[1]] = v end return output end --Finds the number of elements in a table, even if it is not an array local function tbllen(tbl) local count = 0 for k,v in pairs(tbl) do count = count + 1 end return count end --[[ Finds all the paths from a include table ]] local function collect_paths(pretbl) local ret = {} for k,v in pairs(pretbl) do if v[1] == nil then local resp = collect_paths(v) for i,j in pairs(resp) do ret[#ret + 1] = j end else ret[#ret + 1] = string.format("%s (%s)",v[1],v[2]) end end return ret end --[[ Scans the prefix table built by rebuild_include_table to find the file path for the partial name of an included file. If two files are named the same, you will need to include part of the file path until it can be resolved. ]] local function scan(pretbl, name) local pathparts = {} for part in name:gmatch("/?[%w_]+/") do pathparts[#pathparts + 1] = part:gsub("/$",""):gsub("^/","") end local filename = name:gfind("[%w_]+%.[%w_]+$")() assert(filename ~= nil,string.format("Could not file a filename for %q from parts: %s valid files are:\n\t%s",name,table.concat(pathparts,","),table.concat(collect_paths(pretbl),"\n\t"))) local cursor = pretbl[filename] assert(cursor ~= nil,string.format("Scan did not find a file named %q.\n",filename)) local rev = {} for i = 1, #pathparts do rev[#pathparts - i + 1] = pathparts[i] end for k,v in ipairs(rev) do if cursor == nil then error(string.format("Scan could not complete file path translation while translateing %q at %s",table.concat(rev,","),v)) end cursor = cursor[v] end while cursor ~= nil and #cursor ~= 2 do assert(type(cursor) ~= "nil",string.format("Could not find a valid file for path %q, file paths:\n\t%s",name,table.concat(collect_paths(pretbl),"\n\t"))) assert(tbllen(cursor) == 1,string.format("Ambiguous scan, there are two or more paths that match %q\n\t%s\nSpecify more of the file path.",name,table.concat(collect_paths(cursor),"\n\t"))) cursor = cursor[next(cursor)] end assert(type(cursor) ~= "nil", string.format("Could not find %s under %s, found under\n\t %s",filename,name, table.concat(collect_paths(pretbl[filename]),"\n\t"))) return cursor end local paths = {} for k,v in pairs(searchpaths) do TraverseFolder(v,function(n) paths[#paths + 1] = n end) end local ntbl = BuildIncludeTable(paths) local reqtbl = {} --Holds already nrequire()'d things local lastcall = {} --Holds when things were loaded local deptbl = {} --Holds the dependencies between files local pathstack = {} function nrequire(req,...) local tpath = scan(ntbl,req) -- Find the full path for the nrequired() module local trace = debug.getinfo(1,"flnSu") local source = trace.source deptbl[source] = tpath --If we've already run it, just return it if reqtbl[tpath[1]] then return reqtbl[tpath[1]] end --Otherwise, make sure we don't have a circular depedency for k,v in pairs(pathstack) do assert(v ~= tpath[1],string.format("Circular dependancy detected:\n\t%s\n\t\t|\n\t\tV\n\t%s",table.concat(pathstack,"\n\t\t|\n\t\tV\n\t"),v)) end --Actually run the file pathstack[#pathstack + 1] = tpath[1] local filetxt = file.Read(tpath[1],tpath[2]) local filetime = file.Time(tpath[1],tpath[2]) local filefunc = CompileString(filetxt,tpath[1],false) if type(filefunc) ~= "function" then error(filefunc) else xpcall(function() local m = filefunc() hook.Run("artery_file_included",tpath,m) reqtbl[tpath[1]] = m lastcall[tpath[1]] = filetime end,function(err) MsgC(Color(209,96,196),"nrequire Error:",err,"\n") end) end pathstack[#pathstack] = nil return reqtbl[tpath[1]] end --[[ Automatically include all the files in the gamemode directory based on the file name. If the file starts with cl_ it will only be included on the client, if it starts with sv_ it will only be included on the server. If it starts with anything else, it will be shared. Will detect and error on circuar dependancy. ]] local function doincludes() paths = {} for k,v in pairs(searchpaths) do TraverseFolder(v,function(n) paths[#paths + 1] = n end) end ntbl = BuildIncludeTable(paths) reqtbl = reqtbl or {} for k,v in pairs(paths) do local filename = v[1] MsgN("Filename:",filename) if filename == "gamemodes/artery/gamemode/init.lua" then continue end if filename:match("/?sv_[%w_]+%.[%w_]+$") then if SERVER then nrequire(filename) end elseif filename:match("/?cl_[%w_]+%.[%w_]+$") then if CLIENT then nrequire(filename) else AddCSLuaFile(filename) end else if SERVER then AddCSLuaFile(filename) end nrequire(filename) end pathstack = {} end end doincludes() concommand.Add("PrintDepTbl",function(ply,cmd,args) PrintTable(deptbl) end) concommand.Add("PrintReqTbl",function(ply,cmd,args) PrintTable(reqtbl) end) if SERVER then util.AddNetworkString("art_refresh") concommand.Add("art_manualrefresh",function(ply,cmd,args) if not ply:IsAdmin() then return end reqtbl = {} doincludes() net.Start("art_refresh") net.Broadcast() end) end if CLIENT then net.Receive("art_refresh",doincludes) end concommand.Add("artery_manualrefresh",function(ply,cmd,args) doincludes() end)