--[[ | This file was obtained through the combined efforts | of Madbluntz & Plymouth Antiquarian Society. | | Credits: lifestorm, Gregory Wayne Rossel JR., | Maloy, DrPepper10 @ RIP, Atle! | | Visit for more: https://plymouth.thetwilightzone.ru/ --]] local PREFIX = '[PAC3] ' local PREFIX_COLOR = Color(255, 255, 0) local DEFAULT_TEXT_COLOR = Color(200, 200, 200) local BOOLEAN_COLOR = Color(33, 83, 226) local NUMBER_COLOR = Color(245, 199, 64) local STEAMID_COLOR = Color(255, 255, 255) local ENTITY_COLOR = Color(180, 232, 180) local FUNCTION_COLOR = Color(62, 106, 255) local TABLE_COLOR = Color(107, 200, 224) local URL_COLOR = Color(174, 124, 192) function pac.RepackMessage(strIn) local output = {} for line in string.gmatch(strIn, '([^ ]+)') do if #output ~= 0 then table.insert(output, ' ') end table.insert(output, line) end return output end local function FormatMessage(tabIn) local prevColor = DEFAULT_TEXT_COLOR local output = {prevColor} for i, val in ipairs(tabIn) do local valType = type(val) if valType == 'number' then table.insert(output, NUMBER_COLOR) table.insert(output, tostring(val)) table.insert(output, prevColor) elseif valType == 'string' then if val:find('^https?://') then table.insert(output, URL_COLOR) table.insert(output, val) table.insert(output, prevColor) else table.insert(output, val) end elseif valType == 'Player' then if team then table.insert(output, team.GetColor(val:Team()) or ENTITY_COLOR) else table.insert(output, ENTITY_COLOR) end table.insert(output, val:Nick()) if val.SteamName and val:SteamName() ~= val:Nick() then table.insert(output, ' (' .. val:SteamName() .. ')') end table.insert(output, '<') table.insert(output, val:SteamID()) table.insert(output, '>') table.insert(output, prevColor) elseif valType == 'Entity' or valType == 'NPC' or valType == 'Vehicle' then table.insert(output, ENTITY_COLOR) table.insert(output, tostring(val)) table.insert(output, prevColor) elseif IsColor(val) then table.insert(output, val) prevColor = val elseif valType == 'table' then table.insert(output, TABLE_COLOR) table.insert(output, tostring(val)) table.insert(output, prevColor) elseif valType == 'function' then table.insert(output, FUNCTION_COLOR) table.insert(output, string.format('function - %p', val)) table.insert(output, prevColor) elseif valType == 'boolean' then table.insert(output, BOOLEAN_COLOR) table.insert(output, tostring(val)) table.insert(output, prevColor) else table.insert(output, tostring(val)) end end return output end pac.FormatMessage = FormatMessage function pac.Message(...) local formatted = FormatMessage({...}) MsgC(PREFIX_COLOR, PREFIX, unpack(formatted)) MsgC('\n') return formatted end function pac.dprint(fmt, ...) if not pac.debug then return end MsgN("\n") MsgN(">>>PAC3>>>") MsgN(fmt:format(...)) if pac.debug_trace then MsgN("==TRACE==") debug.Trace() MsgN("==TRACE==") end MsgN("<< #b.buffer end) for i, v in ipairs(files) do if v.file_name:EndsWith(".mdl") then local name = v.file_name:match("(.+)%.mdl") for _, v2 in ipairs(files) do if v2.file_name:EndsWith(name .. ".ani") then v.ani = v2 break end end if v.ani then v.file_name = v.file_name:gsub(".-(%..+)", "i"..count.."%1"):lower() v.ani.file_name = v.ani.file_name:gsub(".-(%..+)", "i"..count.."%1"):lower() count = count + 1 else if not model_found or v.file_name:StartWith(model_found) then model_found = v.file_name:match("(.-)%.") v.file_name = v.file_name:gsub(".-(%..+)", "model%1"):lower() else table.insert(other_models, v.file_name) end end elseif v.file_name:EndsWith(".vtx") or v.file_name:EndsWith(".vvd") or v.file_name:EndsWith(".phy") then if not model_found or v.file_name:StartWith(model_found) then model_found = v.file_name:match("(.-)%.") v.file_name = v.file_name:gsub(".-(%..+)", "model%1"):lower() else table.insert(other_models, v.file_name) end end end if other_models[1] and ply == pac.LocalPlayer then pac.Message(Color(255, 200, 50), url, ": the archive contains more than one model.") pac.Message(Color(255, 200, 50), url, ": " .. model_found .. " was selected.") pac.Message(Color(255, 200, 50), url, ": these are ignored:") PrintTable(other_models) end if VERBOSE then print("FILES:") for i, v in ipairs(files) do print(v.file_name) end end if not ok then onfail(err) local str = file.Read(path) file.Delete(path) pac.Message(Color(255, 50,50), err) pac.Message(Color(255, 50,50), "the zip archive downloaded (", string.NiceSize(#str) ,") could not be parsed") local is_binary = false for i = 1, #str do local b = str:byte(i) if b == 0 then is_binary = true break end end if not is_binary then pac.Message(Color(255, 50,50), "the url isn't a binary zip archive. Is it a html website? here's the content:") print(str) elseif ply == pac.LocalPlayer then file.Write("pac3_cache/failed_zip_download.dat", str) pac.Message("the zip archive was stored to garrysmod/data/pac3_cache/failed_zip_download.dat (rename extension to .zip) if you want to inspect it") end return end local required = { ".mdl", ".vvd", ".dx90.vtx", } local found = {} for k,v in pairs(files) do for _, ext in ipairs(required) do if v.file_name:EndsWith(ext) then table.insert(found, ext) break end end end if #found < #required then local str = {} for _, ext in ipairs(required) do if not table.HasValue(found, ext) then table.insert(str, ext) end end onfail("could not find " .. table.concat(str, " or ") .. " in zip archive") return end do -- hex models local found_vmt_directories = {} for i, data in ipairs(files) do if data.file_name:EndsWith(".mdl") then local found_materials = {} local found_materialdirs = {} local found_mdl_includes = {} local vtf_dir_offset local vtf_dir_count local material_offset local material_count local include_mdl_dir_offset local include_mdl_dir_count if DEBUG_MDL then file.Write(data.file_name..".debug.old.dat", data.buffer) end local f = pac.StringStream(data.buffer) local id = f:read(4) local version = f:readUInt32() local checksum = f:readUInt32() local name_offset = f:tell() local name = f:read(64) local size_offset = f:tell() local size = f:readUInt32() f:skip(12 * 6) -- skips over all the vec3 stuff f:skip(4) -- flags f:skip(8) -- bone f:skip(8) -- bone controller f:skip(8) -- hitbox f:skip(8) -- local anim f:skip(8) -- sequences f:skip(8) -- activitylistversion + eventsindexed do material_count = f:readUInt32() material_offset = f:readUInt32() + 1 -- +1 to convert 0 indexed to 1 indexed local old_pos = f:tell() f:seek(material_offset) for i = 1, material_count do local material_start = f:tell() local material_name_offset = f:readInt32() f:skip(60) local material_end = f:tell() local material_name_pos = material_start + material_name_offset f:seek(material_name_pos) local material_name = (f:readString() .. ".vmt"):lower() local found = false for i, v in pairs(files) do if v.file_name == material_name then found = v.file_path break elseif v.file_path == ("materials/" .. material_name) then v.file_name = material_name found = v.file_path break end end if not found then for i, v in pairs(files) do if string.find(v.file_path, material_name, 1, true) or string.find(material_name, v.file_name, 1, true) then table.insert(files, {file_name = material_name, buffer = v.buffer, crc = v.crc, file_path = v.file_path}) found = v.file_path break end end end if not found then if ply == pac.LocalPlayer then pac.Message(Color(255, 50,50), url, " the model wants to find ", material_name , " but it was not found in the zip archive") end local dummy = "VertexLitGeneric\n{\n\t$basetexture \"error\"\n}" table.insert(files, {file_name = material_name, buffer = dummy, crc = util.CRC(dummy), file_path = material_name}) end table.insert(found_materials, {name = material_name, offset = material_name_pos}) f:seek(material_end) end if ply == pac.LocalPlayer and #found_materials == 0 then pac.Message(Color(255, 200, 50), url, ": could not find any materials in this model") end f:seek(old_pos) end do vtf_dir_count = f:readUInt32() vtf_dir_offset = f:readUInt32() + 1 -- +1 to convert 0 indexed to 1 indexed local old_pos = f:tell() f:seek(vtf_dir_offset) for i = 1, vtf_dir_count do local offset_pos = f:tell() local offset = f:readUInt32() + 1 -- +1 to convert 0 indexed to 1 indexed local old_pos = f:tell() f:seek(offset) local dir = f:readString() table.insert(found_materialdirs, {offset_pos = offset_pos, offset = offset, dir = dir}) table.insert(found_vmt_directories, {dir = dir}) f:seek(old_pos) end table.sort(found_vmt_directories, function(a,b) return #a.dir>#b.dir end) f:seek(old_pos) end f:skip(4 + 8) -- skin f:skip(8) -- bodypart f:skip(8) -- attachment f:skip(4 + 8) -- localnode f:skip(8) -- flex f:skip(8) -- flex rules f:skip(8) -- ik f:skip(8) -- mouth f:skip(8) -- localpose f:skip(4) -- render2dprop f:skip(8) -- keyvalues f:skip(8) -- iklock f:skip(12) -- mass f:skip(4) -- contents do include_mdl_dir_count = f:readUInt32() include_mdl_dir_offset = f:readUInt32() + 1 -- +1 to convert 0 indexed to 1 indexed local old_pos = f:tell() f:seek(include_mdl_dir_offset) for i = 1, include_mdl_dir_count do local base_pos = f:tell() f:skip(4) local file_name_offset = f:readUInt32() local old_pos = f:tell() f:seek(base_pos + file_name_offset) table.insert(found_mdl_includes, {base_pos = base_pos, path = f:readString()}) f:seek(old_pos) end f:seek(old_pos) end f:skip(4) -- virtual pointer local anim_name_offset_pos = f:tell() if VERBOSE or DEBUG_MDL then print(data.file_name, "MATERIAL DIRECTORIES:") PrintTable(found_materialdirs) print("============") print(data.file_name, "MATERIALS:") PrintTable(found_materials) print("============") print(data.file_name, "MDL_INCLUDES:") PrintTable(found_mdl_includes) print("============") end do -- replace the mdl name (max size is 64 bytes) local newname = string.sub(dir .. data.file_name:lower(), 1, 63) f:seek(name_offset) f:write(newname .. string.rep("\0", 64-#newname)) end for i,v in ipairs(found_mdl_includes) do local file_name = (v.path:match(".+/(.+)") or v.path) local found = false for _, info in ipairs(files) do if info.file_path == file_name then file_name = info.file_name found = true break end end if found then local path = "models/" .. dir .. file_name local newoffset = f:size() + 1 f:seek(newoffset) f:writeString(path) f:seek(v.base_pos + 4) f:writeInt32(newoffset - v.base_pos) elseif ply == pac.LocalPlayer and not file.Exists(v.path, "GAME") then pac.Message(Color(255, 50, 50), "the model want to include ", v.path, " but it doesn't exist") end end -- if we extend the mdl file with vmt directories we don't have to change any offsets cause nothing else comes after it if data.file_name == "model.mdl" then for i,v in ipairs(found_materialdirs) do local newoffset = f:size() + 1 f:seek(newoffset) f:writeString(dir) f:seek(v.offset_pos) f:writeInt32(newoffset - 1) -- -1 to convert 1 indexed to 0 indexed end else local new_name = "models/" .. dir .. data.file_name:gsub("mdl$", "ani") local newoffset = f:size() + 1 f:seek(newoffset) f:writeString(new_name) f:seek(anim_name_offset_pos) f:writeInt32(newoffset - 1) -- -1 to convert 1 indexed to 0 indexed end local cursize = f:size() -- Add nulls to align to 4 bytes local padding = 4-cursize%4 if padding<4 then f:seek(cursize+1) f:write(string.rep("\0",padding)) cursize = cursize + padding end f:seek(size_offset) f:writeInt32(cursize) data.buffer = f:getString() if DEBUG_MDL then file.Write(data.file_name..".debug.new.dat", data.buffer) end local crc = pac.StringStream() crc:writeInt32(tonumber(util.CRC(data.buffer))) data.crc = crc:getString() end end for i, data in ipairs(files) do if data.file_name:EndsWith(".vmt") then local proxies = data.buffer:match('("?%f[%w_]P?p?roxies%f[^%w_]"?%s*%b{})') data.buffer = data.buffer:lower():gsub("\\", "/") if proxies then data.buffer = data.buffer:gsub('("?%f[%w_]proxies%f[^%w_]"?%s*%b{})', proxies) end if DEBUG_MDL or VERBOSE then print(data.file_name .. ":") end for shader_param in pairs(texture_keys) do data.buffer = data.buffer:gsub('("?%$?%f[%w_]' .. shader_param .. '%f[^%w_]"?%s+"?)([^"%c]+)("?%s?)', function(l, vtf_path, r) if vtf_path == "env_cubemap" then return end local new_path for _, info in ipairs(found_vmt_directories) do if info.dir == "" then continue end new_path, count = vtf_path:gsub("^" .. info.dir:gsub("\\", "/"):lower(), dir) if count == 0 then new_path = nil else break end end if not new_path then for _, info in ipairs(files) do local vtf_name = (vtf_path:match(".+/(.+)") or vtf_path) if info.file_name:EndsWith(".vtf") then if info.file_name == vtf_name .. ".vtf" or info.file_name == vtf_name then new_path = dir .. vtf_name break end elseif (info.file_name:EndsWith(".vmt") and l:StartWith("include")) then if info.file_name == vtf_name then new_path = "materials/" .. dir .. vtf_name break end end end end if not new_path then if not file.Exists("materials/" .. vtf_path .. ".vtf", "GAME") then if ply == pac.LocalPlayer then pac.Message(Color(255, 50, 50), "vmt ", data.file_name, " wants to find texture materials/", vtf_path, ".vtf for $", shader_param ," but it doesn't exist") print(data.buffer) end end new_path = vtf_path -- maybe it's a special texture? in that case i need to it end if DEBUG_MDL or VERBOSE then print("\t" .. vtf_path .. " >> " .. new_path) end return l .. new_path .. r end) end local crc = pac.StringStream() crc:writeInt32(tonumber(util.CRC(data.buffer))) data.crc = crc:getString() end end end if skip_cache then id = id .. "_temp" end local path = "pac3_cache/downloads/" .. id .. ".dat" local f = file.Open(path, "wb", "DATA") if not f then onfail("unable to open file " .. path .. " for writing") pac.Message(Color(255, 50, 50), "unable to write to ", path, " for some reason") if file.Exists(path, "DATA") then pac.Message(Color(255, 50, 50), "the file exists and its size is ", string.NiceSize(file.Size(path, "DATA"))) pac.Message(Color(255, 50, 50), "is it locked or in use by something else?") else pac.Message(Color(255, 50, 50), "the file does not exist") pac.Message(Color(255, 50, 50), "are you out of disk space?") end return end f:Write("GMAD") f:WriteByte(3) f:WriteLong(0)f:WriteLong(0) f:WriteLong(0)f:WriteLong(0) f:WriteByte(0) f:Write("name here")f:WriteByte(0) f:Write("description here")f:WriteByte(0) f:Write("author here")f:WriteByte(0) f:WriteLong(1) for i, data in ipairs(files) do f:WriteLong(i) if data.file_name:EndsWith(".vtf") or data.file_name:EndsWith(".vmt") then f:Write("materials/" .. dir .. data.file_name:lower())f:WriteByte(0) else f:Write("models/" .. dir .. data.file_name:lower())f:WriteByte(0) end f:WriteLong(#data.buffer)f:WriteLong(0) f:WriteLong(data.crc) end f:WriteLong(0) for i, data in ipairs(files) do f:Write(data.buffer) end f:Flush() local content = file.Read("pac3_cache/downloads/" .. id .. ".dat", "DATA") f:Write(util.CRC(content)) f:Close() end local ok, tbl = game.MountGMA("data/pac3_cache/downloads/" .. id .. ".dat") if not ok then onfail("failed to mount gma mdl") return end for k,v in pairs(tbl) do if v:EndsWith("model.mdl") then if VERBOSE and not DEBUG_MDL then print("util.IsValidModel: ", tostring(util.IsValidModel(v))) local dev = GetConVar("developer"):GetFloat() if dev == 0 then RunConsoleCommand("developer", "3") timer.Simple(0.1, function() if CLIENT then ClientsideModel(v):Remove() else local ent = ents.Create("prop_dynamic") ent:SetModel(v) ent:Spawn() ent:Remove() end print("created and removed model") RunConsoleCommand("developer", "0") end) else if CLIENT then ClientsideModel(v):Remove() else local ent = ents.Create("prop_dynamic") ent:SetModel(v) ent:Spawn() ent:Remove() end end end cached_paths[url] = v callback(v) file.Delete("pac3_cache/downloads/" .. id .. ".dat") break end end end, onfail) end