--- The server side lazy loader for pac3 costumes. --PAC3 outfits are not downloaded until they are needed, we can keep the inital download to join the server pretty small this way. -- The downside is that the game might lag a little when someone wears something that is rare/new and everyone has to download it. --@module sv_pac.lua --[[ Console Commands: artery_reload_pacs The server will cache PAC's so it dosen't need to read form disk every time. If you're live editing pac's on the server, and are wondering why your changes aren't showing, use this command. There is no command to clear client cache, because applying a pac also sends a hash of the pac to the client, and the client re-downloads if the hashes are different. Functions: entity:ApplyPac(String costume) :: nil Find the file /data/pacs/.pac (on the server) and tell clients to apply it to ent. This will automatically download the pac to any clients nessessary. entity:RemovePac(String costume) :: nil Remove the pac from the entity. entity:GetPacs() :: table Retreives a list of the pacs an entity is wearing, as strings Network Strings: "artery_getworldpacs", "artery_giveworldpacs", "artery_applypac", "artery_removepac", "artery_requestpac", "artery_downloadpac" ]] local log = nrequire("log.lua") local p3 = {} local nwstrings = { "artery_getworldpacs", "artery_giveworldpacs", "artery_applypac", "artery_removepac", "artery_requestpac", "artery_downloadpac" } for _,v in pairs(nwstrings) do util.AddNetworkString(v) end --If the server has pac installed, restrict player's from putting on their own pacs hook.Add("PrePACConfigApply", "stoppacs", function(ply, outfit_data) if not ply:IsAdmin() then return false, "You don't have permission to do that!" end end) hook.Add( "PrePACEditorOpen", "stoppaceditor", function( ply ) return ply:IsSuperAdmin(), "This is accessable only to superadmins" end ) local pacsources = { ["data/artery/pacs/"] = "GAME" } --- Add a resource that we can load pacs from. -- Automatically tries to reload when a pac can't be found -- Starts out with "data/artery/pacs/" = "GAME" --@tparam string filepath the path to search for pacs --@tparam string|bool from the game path ("GAME" | "LUA" | "DATA" | "MOD"), can also be a boolean (true="GAME" | false="DATA") function p3.AddPacSource(filepath,from) pacsources[filepath] = from end --When the server starts, get all the pacs and calculate their hashes so we can index them quickly without haveing to read from disk each time. local pachashes = {} local function loadhashes() for path,part in pairs(pacsources) do local files,_ = file.Find(path .. "*",part) for _,v in ipairs(files) do local filepath = string.format("%s%s",path,v) local filetext = file.Read(filepath,part) local filehash = util.CRC(filetext) pachashes[string.StripExtension(v)] = tonumber(filehash) end end end loadhashes() ---Reloads the hashes. -- Run this if you changed a pac and want to reload it. ![Requires admin](./req_admin) --@concommand artery_reload_pac_hashes concommand.Add("artery_reload_pac_hashes",function(ply,cmd,args) if not ply:IsAdmin() then return end loadhashes() end) ---Print the pac hashes. -- Prints all the pac hashes that are loaded (for debugging). --@concommand artery_print_pac_hashes concommand.Add("artery_print_pac_hashes",function(ply,cmd,args) PrintTable(pachashes) end) local appliedpacs = {} ---Apply a pac to an entity. -- Applies a pac to an entity, pac should be a file name (not path!) without the .txt function p3.ApplyPac(what, name) log.debug(string.format("Applying pac %q to %q",name,tostring(what))) assert(pachashes[name],string.format("Tried to apply pac %s which didn't have a hash. Pac hashes are:%s",name,table.ToString(pachashes,"pachashes",true))) appliedpacs[what] = appliedpacs[what] or {} appliedpacs[what][name] = pachashes[name] net.Start("artery_applypac") net.WriteEntity(what) net.WriteString(name) --If this pac is from an addon that was loaded after this file was, we may need to reload hashes. if not pachashes[name] then loadhashes() end net.WriteUInt(pachashes[name],32) net.Broadcast() end ---Remove a pac to an entity. -- Removes a pac to an entity, pac should be a file name without the .txt function p3.RemovePac(what, name) log.debug(string.format("Removeing pac %q from %q",name,tostring(what))) assert(appliedpacs[what][name],"Attempted to remove a pac that an entity is not wearing!") appliedpacs[what][name] = nil if #appliedpacs[what] == 0 then appliedpacs[what] = nil end net.Start("artery_removepac") net.WriteEntity(what) net.WriteString(name) net.WriteUInt(pachashes[name],32) net.Broadcast() end ---Find the pacs applied to an entity. -- Returns an array of the names of all the pacs applied to a certain entity function p3.GetPacs(what) return appliedpacs[what] or {} end --If a player joins the server, tell them all about the pacs that are applied net.Receive("artery_getworldpacs",function(ln,ply) net.Start("artery_giveworldpacs") net.WriteTable(appliedpacs) net.Send(ply) end) local max_pacs_in_cache = 10 local pacs_in_cache = 0 local pac_cache = {} --Load something from our cache local function cacheload(key) --If it's already in the cache, just update the time it was last used and return the pac. if pac_cache[key] ~= nil then print("Pac was already in the cache.") pac_cache[key].time = CurTime() if pac_cache[key].pac == nil then PrintTable(pac_cache) error("Pac was loaded, but the txt was nil!") end return pac_cache[key].pac end --Otherwise, we need to load it. local pacpath = string.format("data/artery/pacs/%s.txt",key) local pacfile = file.Read(pacpath,"GAME") print("Pac was not in cache, reloading, pac txt is",pacfile) --If we haven't reached max cache yet, just put it in if pacs_in_cache < max_pacs_in_cache then pac_cache[key] = { ["pac"] = pacfile, ["time"] = CurTime() } pacs_in_cache = pacs_in_cache + 1 return pacfile else --We have max pac's, delete the oldest one, and put the new one in. local oldest,oldstr = CurTime(),"" for k,v in pairs(pac_cache) do if v.time < oldest then oldest = v.time oldstr = k end end pac_cache[oldstr] = nil pac_cache[key] = { ["pac"] = pacfile, ["time"] = CurTime() } return pacfile end end net.Receive("artery_requestpac",function(ln,ply) local pac_name = net.ReadString() log.debug(string.format("Player %q requested pac %q",tostring(ply),pac_name)) --Double check that we're not executing a directory traversal attack https://www.owasp.org/index.php/Path_Traversal if string.find(pac_name,"..",1,true) then log.report(string.format("Directory traversal attack attempted by %s:%s using artery_requestpac string %q",ply:Nick(),ply:SteamID64(),pac_name)) end local pac_txt = cacheload(pac_name) assert(pac_name,"Pac's name was nil!") assert(pac_txt, "Pac's txt was nil (from the cache)") net.Start("artery_downloadpac") net.WriteString(pac_name) net.WriteString(pac_txt) net.WriteUInt(pachashes[pac_name],32) net.Send(ply) end) ---Reload pacs. -- Does all the things needed to edit pac's live concommand.Add("artery_reload_pacs",function(ply,cmd,args) if not ply:IsAdmin() then return end pac_cache = {} pacs_in_cache = 0 loadhashes() end) return p3