--[[ 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 --The files we want clients to load local clienttoload = {} local searchpaths = { {"gamemodes/artery/gamemode","GAME"}, {"artery/global","DATA"}, {"data/artery/global","GAME"}, } if SERVER then --Network messages util.AddNetworkString("artery_downloadfile") util.AddNetworkString("art_downloadfiles") util.AddNetworkString("art_requestfile") util.AddNetworkString("art_respondfile") util.AddNetworkString("artery_request_client_download") --[[Server tells the client to load a file]] util.AddNetworkString("artery_loadfile") --[[Client tells server we don't have that file]] util.AddNetworkString("artery_requestcsfile") --[[Server gives client the contents of a file]] util.AddNetworkString("artery_respondfile") util.AddNetworkString("art_requestclientreload") util.AddNetworkString("art_client_reload_file") util.AddNetworkString("art_refresh") util.AddNetworkString("art_manualrefresh") end --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%s",table.ToString(pathparts,"pathparts",true))) 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 = {} local ntbl = {} local reqtbl = {} --Holds already nrequire()'d things local lastcall = {} --Holds when things were loaded local location = {} --Holds the location of files ("DATA","GAME","LUA",...) local deptbl = {} --Holds the dependencies between files local pathstack = {} function nrequire(req,...) local varargs = {...} local tpath-- Find the full path for the nrequired() module if CLIENT then xpcall(function() tpath = scan(ntbl,req) end,function() tpath = scan(ntbl,req .. ".txt") end) else tpath = scan(ntbl,req) end if tpath == nil then MsgC(Color(209,96,196), "Unable to find file for include:" .. req) error(debug.traceback()) end local trace = debug.getinfo(2,"flnSu") local source = trace.source --print("Adding to deptbl, src:",source,"tpath:",tpath) deptbl[source] = tpath --If we've already run it (and its up to date), just return it local filetime = file.Time(tpath[1],tpath[2]) if reqtbl[tpath[1]] and lastcall[tpath[1]] and lastcall[tpath[1]] > filetime 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] --add this to circular dependency checker local filetxt = file.Read(tpath[1],tpath[2]) if #filetxt == 0 then pathstack[#pathstack] = nil return end --Compile the file local filefunc = CompileString(filetxt,tpath[1],false) if type(filefunc) ~= "function" then error(filefunc) else xpcall(function() --and run it local m = filefunc(varargs) if reqtbl[tpath[1]] then --We already included the file before, we're updating setmetatable(reqtbl[tpath[1]],{__index = m}) else local m_proxy = {} setmetatable(m_proxy,{__index = m}) reqtbl[tpath[1]] = m_proxy end hook.Run("artery_file_included",tpath,m) --print(string.format("In nrequire(), not skipping compile and setting lastcall[%s] to %d",tpath[1],file.Time(tpath[1],tpath[2]))) lastcall[tpath[1]] = os.time() location[tpath[1]] = tpath[2] end,function(err) MsgC(Color(209,96,196),debug.traceback(),"\n") MsgC(Color(209,96,196),"nrequire Error:",err,"\n") end) end pathstack[#pathstack] = nil return reqtbl[tpath[1]] end local update_file_co = coroutine.create(function() while true do for k,v in pairs(reqtbl) do local loc = location[k] if file.Time(k,loc) > lastcall[k] then print(string.format("Found updated file:%s",k)) nrequire(k) lastcall[k] = os.time() --file.Time(k,loc) for ind,tbl in pairs(clienttoload) do if tbl.name == k then clienttoload[ind] = { name = k, hash = util.CRC(file.Read(k,loc)), directory = loc, } end end net.Start("art_client_reload_file") net.WriteString(k) net.WriteString(loc) net.WriteString(file.Read(k,loc)) net.Broadcast() end coroutine.yield() end end end) hook.Add("Tick","nrequire_update_tick",function() if coroutine.status(update_file_co) == "suspended" then coroutine.resume(update_file_co) end end) if SERVER then net.Receive("artery_requestcsfile",function(ln,ply) local which = net.ReadString() local path = net.ReadString() local found = false for k,v in pairs(clienttoload) do if v.name == which and v.directory == path then net.Start("artery_respondfile") net.WriteString(which) net.WriteString(path) net.WriteString(file.Read(which,path)) net.Send(ply) return end end end) else local function run_csfile(txt,name) assert(#txt > 0, "File was size 0 on: " .. name) local ptr = CompileString(txt,name,false) local ret = nil if type(ptr) == "function" then xpcall(function() ret = ptr() end,function(err) MsgC(Color(209,96,196),debug.traceback(),"\n") MsgC(Color(209,96,196),"nrequire Error:",err,"\n") end) else error(ptr) end return ret end net.Receive("artery_loadfile",function() local filename = net.ReadString() local hash = net.ReadUInt(32) local path = net.ReadString() local cache = file.Read("artery/client/files/" .. path .. "/" .. filename,"DATA") if cache == nil then --We don't have this file downloaded! net.Start("artery_requestcsfile") net.WriteString(filename) net.WriteString(path) net.SendToServer() return end local thash = tonumber(util.CRC(cache)) if hash != thash then print("I need to load a file",filename," but my hash was old, redownloading!") net.Start("artery_requestcsfile") net.WriteString(filename) net.WriteString(path) net.SendToServer() else run_csfile(cache,filename) end end) net.Receive("artery_respondfile",function() local filename = net.ReadString() local filepath = net.ReadString() local filetext = net.ReadString() local dirname = string.GetPathFromFilename(filename) file.CreateDir("artery/client/files/" .. dirname) assert(#filetext > 0, "Retreived a size 0 file: " .. filename) file.Write("artery/client/files/" .. filename,filetext) local mod = run_csfile(filetext,filename) local mod_proxy = {} setmetatable(mod_proxy,{__index = mod}) reqtbl[filepath] = mod_proxy lastcall[filepath] = os.time() end) 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 clienttoload[#clienttoload+1] = { name = filename, hash = util.CRC(file.Read(filename,v[2])), directory = v[2] } end else if SERVER then clienttoload[#clienttoload + 1] = { name = filename, hash = util.CRC(file.Read(filename,v[2])), directory = v[2] } end nrequire(filename) end pathstack = {} end end local function redoincludes() print("redoing includes") paths = {} -- Defined above somewhere for k,v in pairs(searchpaths) do -- print("Searching path:") -- PrintTable(v) TraverseFolder(v,function(n) -- print("found file:") -- PrintTable(n) paths[#paths + 1] = n end) end ntbl = BuildIncludeTable(paths) doincludes() hook.Call("artery_core_loaded") end if SERVER then redoincludes() concommand.Add("artery_doincludes",function(ply,cmd,args) redoincludes() end) end local runclient = nil if SERVER then local function downloadclient(who) print("Loading client",who) net.Start("art_downloadfiles") net.WriteTable(clienttoload) net.Send(who) -- local routine = coroutine.create(function() -- for k,v in pairs(clienttoload) do -- print("Asking client to load",k) -- net.Start("artery_downloadfile") -- net.WriteString(v.name) -- net.WriteUInt(v.hash,32) -- net.WriteString(v.directory) -- net.WriteString(file.Read(v.name,v.directory)) -- net.Send(who) -- coroutine.yield() -- end -- end) -- timer.Create("artery_downloadload_client_timer",1,#clienttoload,function() -- coroutine.resume(routine) -- end) end net.Receive("artery_request_client_download",function(ln,pl) print("artery_request_client_download received by server") downloadclient(pl) end) net.Receive("art_requestfile",function(ln,pl) local filedata = net.ReadTable() local filetxt = file.Read(filedata.name,filedata.directory) net.Start("art_respondfile") net.WriteTable({ name = filedata.name, directory = filedata.directory, text = filetxt }) net.Send(pl) end) else net.Receive("art_client_reload_file",function(ln,pl) local filename = net.ReadString() local directory = net.ReadString() local filetext = net.ReadString() local fullpath = "artery/client/" .. directory .. "/" .. filename .. ".txt" file.Write(fullpath,filetext) redoincludes() end) net.Receive("art_downloadfiles",function(ln,pl) local clientfiles = net.ReadTable() local todownload = {} local needs_restart = false for k,v in pairs(clientfiles) do local fullpath = "artery/client/" .. v.directory .. "/" .. v.name .. ".txt" if not file.Exists(fullpath,"DATA") then local thisdownload = { name = v.name, directory = v.directory } todownload[#todownload + 1] = thisdownload needs_restart = true elseif util.CRC(file.Read(fullpath,"DATA")) ~= v.hash then local thisdownload = { name = v.name, directory = v.directory } todownload[#todownload + 1] = thisdownload needs_restart = true -- else file is up to date and dosn't need re-downloading. end end local routine = coroutine.create(function() for k,v in pairs(todownload) do net.Start("art_requestfile") net.WriteTable(v) net.SendToServer() coroutine.yield() end runclient() end) timer.Create("artery_download_files_timer",0.1,#todownload,function() coroutine.resume(routine) end) end) net.Receive("art_respondfile",function(ln,pl) local tbl = net.ReadTable() print("Received response file:",tbl.name) local fullpath = "artery/client/" .. tbl.directory .. "/" .. tbl.name .. ".txt" local path = string.GetPathFromFilename(fullpath) file.CreateDir(path) print("Writing to ", fullpath) file.Write(fullpath,tbl.text) nrequire(tbl.name) --timer.Simple(10,function() RunConsoleCommand("retry") end) redoincludes() end) net.Receive("artery_downloadfile",function(ln,pl) print("Asked to download file") local clientfiles = net.ReadTable() -- local filename = net.ReadString() -- local filehash = net.ReadUInt(32) -- local filedir = net.ReadString() -- local filetext = net.ReadString() -- print("got filename:",filename,"and filedir:",filedir) -- local dirname = string.GetPathFromFilename(filename) -- file.CreateDir("artery/client/files/" .. dirname) -- assert(#filetext > 0, "Retreived a size 0 file: " .. filename) -- file.Write("artery/client/files/" .. filename,"") end) local function request_file_download() print("Requesting to download client files") net.Start("artery_request_client_download") net.SendToServer() end timer.Simple(0,function() request_file_download() end) concommand.Add("artery_DownloadClient",function(ply,cmd,args) request_file_download() end) end concommand.Add("PrintDepTbl",function(ply,cmd,args) PrintTable(deptbl) end) concommand.Add("PrintReqTbl",function(ply,cmd,args) PrintTable(reqtbl) end) if SERVER then concommand.Add("PrintClientToLoad",function(ply,cmd,args) PrintTable(clienttoload) end) end if SERVER then net.Receive("art_requestclientreload",function(ln,pl) loadclient(pl) end) else concommand.Add("artery_LoadClient",function(ply,cmd,args) net.Start("art_requestclientreload") net.SendToServer() --loadclient(ply) end) runclient = function() searchpaths = { {"artery/client/game/","DATA"}, {"artery/client/" .. game.GetMap() .. "/", "DATA"} } redoincludes() end runclient() concommand.Add("artery_RunClient",function(ply,cmd,args) runclient() end) end if SERVER then concommand.Add("art_refresh",function(ply,cmd,args) print("Doing soft refresh") doincludes() net.Start("art_refresh") net.Broadcast() end) else net.Receive("art_manualrefresh",doincludes) end if SERVER then 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_manualrefresh",doincludes) end concommand.Add("artery_manualrefresh",function(ply,cmd,args) doincludes() end)