--[[ | 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 L = pace.LanguageString -- load only when hovered above local function add_expensive_submenu_load(pnl, callback) local old = pnl.OnCursorEntered pnl.OnCursorEntered = function(...) callback() pnl.OnCursorEntered = old return old(...) end end file.CreateDir("pac3") file.CreateDir("pac3/__backup/") file.CreateDir("pac3/__backup_save/") function pace.SaveParts(name, prompt_name, override_part, overrideAsUsual) if not name or prompt_name then Derma_StringRequest( L"save parts", L"filename:", prompt_name or pace.LastSaveName or "autoload", function(name) pace.LastSaveName = name pace.SaveParts(name, nil, override_part, overrideAsUsual) pace.RefreshFiles() end ) return end pac.dprint("saving parts %s", name) local data = {} if not overrideAsUsual then if pace.use_current_part_for_saveload and pace.current_part:IsValid() then override_part = pace.current_part end if override_part then data = override_part:ToSaveTable() end elseif override_part then table.insert(data, override_part:ToSaveTable()) override_part = nil end if #data == 0 then for key, part in pairs(pac.GetLocalParts()) do if not part:HasParent() and part:GetShowInEditor() then table.insert(data, part:ToSaveTable()) end end end data = hook.Run("pac_pace.SaveParts", data) or data if not override_part and #file.Find("pac3/sessions/*", "DATA") > 0 and not name:find("/") then pace.luadata.WriteFile("pac3/sessions/" .. name .. ".txt", data) else if file.Exists("pac3/" .. name .. ".txt", "DATA") then local date = os.date("%y-%m-%d-%H_%M_%S") local read = file.Read("pac3/" .. name .. ".txt", "DATA") file.Write("pac3/__backup_save/" .. name .. "_" .. date .. ".txt", read) local files, folders = file.Find("pac3/__backup_save/*", "DATA") if #files > 30 then local targetFiles = {} for i, filename in ipairs(files) do local time = file.Time("pac3/__backup_save/" .. filename, "DATA") table.insert(targetFiles, {"pac3/__backup_save/" .. filename, time}) end table.sort(targetFiles, function(a, b) return a[2] > b[2] end) for i = 31, #files do file.Delete(targetFiles[i][1]) end end end pace.luadata.WriteFile("pac3/" .. name .. ".txt", data) end pace.Backup(data, name) end local last_backup local maxBackups = CreateConVar("pac_backup_limit", "100", {FCVAR_ARCHIVE}, "Maximal amount of backups") function pace.Backup(data, name) name = name or "" if not data then data = {} for key, part in pairs(pac.GetLocalParts()) do if not part:HasParent() and part:GetShowInEditor() then table.insert(data, part:ToSaveTable()) end end end if #data > 0 then local files, folders = file.Find("pac3/__backup/*", "DATA") if #files > maxBackups:GetInt() then local temp = {} for key, name in pairs(files) do local time = file.Time("pac3/__backup/" .. name, "DATA") table.insert(temp, {path = "pac3/__backup/" .. name, time = time}) end table.sort(temp, function(a, b) return a.time > b.time end) for i = maxBackups:GetInt() + 1, #files do file.Delete(temp[i].path, "DATA") end end local date = os.date("%y-%m-%d-%H_%M_%S") local str = pace.luadata.Encode(data) if str ~= last_backup then file.Write("pac3/__backup/" .. (name=="" and name or (name..'_')) .. date .. ".txt", str) last_backup = str end end end function pace.LoadParts(name, clear, override_part) if not name then local frm = vgui.Create("DFrame") frm:SetTitle(L"parts") local pnl = pace.CreatePanel("browser", frm) pnl.OnLoad = function(node) pace.LoadParts(node.FileName, clear, override_part) end if #file.Find("pac3/sessions/*", "DATA") > 0 then pnl:SetDir("sessions/") else pnl:SetDir("") end pnl:Dock(FILL) frm:SetSize(300, 500) frm:MakePopup() frm:Center() local btn = vgui.Create("DButton", frm) btn:Dock(BOTTOM) btn:SetText(L"load from url") btn.DoClick = function() Derma_StringRequest( L"load part", L"pastebin urls also work!", "", function(name) pace.LoadParts(name, clear, override_part) end ) end else if hook.Run("PrePACLoadOutfit", name) == false then return end pac.dprint("loading Parts %s", name) if name:find("https?://") then local function callback(str) if string.find( str, "" ) then pace.MessagePrompt("Invalid URL, the website returned a HTML file. If you're using Github then use the RAW option.", "URL Failed", "OK") return end local data, err = pace.luadata.Decode(str) if not data then local message = string.format("URL fail: %s : %s\n", name, err) pace.MessagePrompt(message, "URL Failed", "OK") return end pace.LoadPartsFromTable(data, clear, override_part) end pac.HTTPGet(name, callback, function(err) pace.MessagePrompt(err, "HTTP Request Failed for " .. name, "OK") end) else name = name:gsub("%.txt", "") local data,err = pace.luadata.ReadFile("pac3/" .. name .. ".txt") if name == "autoload" and (not data or not next(data)) then local err data,err = pace.luadata.ReadFile("pac3/sessions/" .. name .. ".txt",nil,true) if not data then if err then ErrorNoHalt(("Autoload failed: %s\n"):format(err)) end return end elseif not data then ErrorNoHalt(("Decoding %s failed: %s\n"):format(name,err)) return end pace.LoadPartsFromTable(data, clear, override_part) end end end concommand.Add('pac_load_url', function(ply, cmd, args) if not args[1] then return print('[PAC3] No URL specified') end local url = args[1]:Trim() if not url:find("https?://") then return print('[PAC3] Invalid URL specified') end pac.Message('Loading specified URL') if args[2] == nil then args[2] = '1' end pace.LoadParts(url, tobool(args[2])) end) function pace.LoadPartsFromTable(data, clear, override_part) if pace.use_current_part_for_saveload and pace.current_part:IsValid() then override_part = pace.current_part end if clear then pace.ClearParts() pace.ClearUndo() else --pace.RecordUndoHistory() end local partsLoaded = {} local copy_id = tostring(data) if data.self then local part if override_part then part = override_part part:SetTable(data) else part = override_part or pac.CreatePart(data.self.ClassName, nil, data, pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), data.self.UniqueID):IsValid() and copy_id) end table.insert(partsLoaded, part) else data = pace.FixBadGrouping(data) data = pace.FixUniqueIDs(data) for key, tbl in pairs(data) do local part = pac.CreatePart(tbl.self.ClassName, nil, tbl, pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), tbl.self.UniqueID):IsValid() and copy_id) table.insert(partsLoaded, part) end end pace.RefreshTree(true) for i, part in ipairs(partsLoaded) do part:CallRecursive('OnOutfitLoaded') part:CallRecursive('PostApplyFixes') end pac.LocalPlayer.pac_fix_show_from_render = SysTime() + 1 pace.RecordUndoHistory() end local function add_files(tbl, dir) local files, folders = file.Find("pac3/" .. dir .. "/*", "DATA") if folders then for key, folder in pairs(folders) do if folder == "__backup" or folder == "objcache" or folder == "__animations" or folder == "__backup_save" then goto CONTINUE end tbl[folder] = {} add_files(tbl[folder], dir .. "/" .. folder) ::CONTINUE:: end end if files then for i, name in pairs(files) do if name:find("%.txt") then local path = "pac3/" .. dir .. "/" .. name if file.Exists(path, "DATA") then local data = {} data.Name = name:gsub("%.txt", "") data.FileName = name data.Size = string.NiceSize(file.Size(path, "DATA")) local time = file.Time(path, "DATA") data.LastModified = os.date("%m/%d/%Y %H:%M", time) data.Time = file.Time(path, "DATA") data.Path = path data.RelativePath = (dir .. "/" .. data.Name):sub(2) local dat,err=pace.luadata.ReadFile(path) data.Content = dat if dat then table.insert(tbl, data) else pac.dprint(("Decoding %s failed: %s\n"):format(path,err)) chat.AddText(("Could not load: %s\n"):format(path)) end end end end end table.sort(tbl, function(a,b) if a.Time and b.Time then return a.Name < b.Name end return true end) end function pace.GetSavedParts(dir) if pace.CachedFiles then return pace.CachedFiles end local out = {} add_files(out, dir or "") pace.CachedFiles = out return out end local function populate_part(menu, part, override_part, clear) local name = part.self.Name or "" if name == "" then name = part.self.ClassName .. " (no name)" end if #part.children > 0 then local menu, pnl = menu:AddSubMenu(name, function() pace.LoadPartsFromTable(part, nil, override_part) end) pnl:SetImage(part.self.Icon) menu.GetDeleteSelf = function() return false end local old = menu.Open menu.Open = function(...) if not menu.pac_opened then for key, part in pairs(part.children) do populate_part(menu, part, override_part, clear) end menu.pac_opened = true end return old(...) end else menu:AddOption(name, function() pace.LoadPartsFromTable(part, clear, override_part) end):SetImage(part.self.Icon) end end local function populate_parts(menu, tbl, override_part, clear) for key, data in pairs(tbl) do if not data.Path then local menu, pnl = menu:AddSubMenu(key, function()end, data) pnl:SetImage(pace.MiscIcons.load) menu.GetDeleteSelf = function() return false end local old = menu.Open menu.Open = function(...) if not menu.pac_opened then populate_parts(menu, data, override_part, clear) menu.pac_opened = true end return old(...) end else local icon = pace.MiscIcons.outfit local parts = data.Content if parts.self then icon = parts.self.Icon parts = {parts} end local outfit, pnl = menu:AddSubMenu(data.Name, function() pace.LoadParts(data.RelativePath, clear, override_part) end) pnl:SetImage(icon) outfit.GetDeleteSelf = function() return false end local old = outfit.Open outfit.Open = function(...) if not outfit.pac_opened then for key, part in pairs(parts) do populate_part(outfit, part, override_part, clear) end outfit.pac_opened = true end return old(...) end end end end function pace.AddSavedPartsToMenu(menu, clear, override_part) menu.GetDeleteSelf = function() return false end menu:AddOption(L"load from url", function() Derma_StringRequest( L"load parts", L"Some indirect urls from on pastebin, dropbox, github, etc are handled automatically. Pasting the outfit's file contents into the input field will also work.", "", function(name) pace.LoadParts(name, clear, override_part) end ) end):SetImage(pace.MiscIcons.url) menu:AddOption(L"load from clipboard", function() pace.MultilineStringRequest( L"load parts from clipboard", L"Paste the outfits content here.", "", function(name) local data,err = pace.luadata.Decode(name) if data then pace.LoadPartsFromTable(data, clear, override_part) end end ) end):SetImage(pace.MiscIcons.paste) if not override_part and pace.example_outfits then local examples, pnl = menu:AddSubMenu(L"examples") pnl:SetImage(pace.MiscIcons.help) examples.GetDeleteSelf = function() return false end local sorted = {} for k,v in pairs(pace.example_outfits) do sorted[#sorted + 1] = {k = k, v = v} end table.sort(sorted, function(a, b) return a.k < b.k end) for _, data in pairs(sorted) do examples:AddOption(data.k, function() pace.LoadPartsFromTable(data.v) end) :SetImage(pace.MiscIcons.outfit) end end menu:AddSpacer() local tbl = pace.GetSavedParts() populate_parts(menu, tbl, override_part, clear) menu:AddSpacer() local backups, pnl = menu:AddSubMenu(L"backups") pnl:SetImage(pace.MiscIcons.clone) backups.GetDeleteSelf = function() return false end add_expensive_submenu_load(pnl, function() local files = file.Find("pac3/__backup/*", "DATA") local files2 = {} for i, filename in ipairs(files) do table.insert(files2, {filename, file.Time("pac3/__backup/" .. filename, "DATA")}) end table.sort(files2, function(a, b) return a[2] > b[2] end) for _, data in pairs(files2) do local name = data[1] local full_path = "pac3/__backup/" .. name local friendly_name = os.date("%m/%d/%Y %H:%M:%S ", file.Time(full_path, "DATA")) .. string.NiceSize(file.Size(full_path, "DATA")) backups:AddOption(friendly_name, function() pace.LoadParts("__backup/" .. name, true) end) :SetImage(pace.MiscIcons.outfit) end end) local backups, pnl = menu:AddSubMenu(L"outfit backups") pnl:SetImage(pace.MiscIcons.clone) backups.GetDeleteSelf = function() return false end add_expensive_submenu_load(pnl, function() local files = file.Find("pac3/__backup_save/*", "DATA") local files2 = {} for i, filename in ipairs(files) do table.insert(files2, {filename, file.Time("pac3/__backup_save/" .. filename, "DATA")}) end table.sort(files2, function(a, b) return a[2] > b[2] end) for _, data in pairs(files2) do local name = data[1] local stamp = data[2] local nicename = name local date = os.date("_%y-%m-%d-%H_%M_%S", stamp) if nicename:find(date, 1, true) then nicename = nicename:Replace(date, os.date(" %m/%d/%Y %H:%M:%S", stamp)) end backups:AddOption(nicename:Replace(".txt", "") .. " (" .. string.NiceSize(file.Size("pac3/__backup_save/" .. name, "DATA")) .. ")", function() pace.LoadParts("__backup_save/" .. name, true) end) :SetImage(pace.MiscIcons.outfit) end end) end local function populate_parts(menu, tbl, dir, override_part) dir = dir or "" menu:AddOption(L"new file", function() pace.SaveParts(nil, dir .. "/", override_part) end) :SetImage("icon16/page_add.png") menu:AddOption(L"new directory", function() Derma_StringRequest( L"new directory", L"name:", "", function(name) file.CreateDir("pac3/" .. dir .. "/" .. name) pace.RefreshFiles() end ) end) :SetImage("icon16/folder_add.png") menu:AddOption(L"to clipboard", function() local data = {} for key, part in pairs(pac.GetLocalParts()) do if not part:HasParent() and part:GetShowInEditor() then table.insert(data, part:ToSaveTable()) end end SetClipboardText(pace.luadata.Encode(data):sub(1, -1)) end) :SetImage(pace.MiscIcons.copy) menu:AddSpacer() for key, data in pairs(tbl) do if not data.Path then local menu, pnl = menu:AddSubMenu(key, function()end, data) pnl:SetImage(pace.MiscIcons.load) menu.GetDeleteSelf = function() return false end populate_parts(menu, data, dir .. "/" .. key, override_part) else local parts = data.Content if parts[1] then local menu, pnl = menu:AddSubMenu(data.Name, function() pace.SaveParts(nil, data.RelativePath, override_part) end) menu.GetDeleteSelf = function() return false end pnl:SetImage(pace.MiscIcons.outfit) menu:AddOption(L"delete", function() file.Delete("pac3/" .. data.RelativePath .. ".txt", "DATA") pace.RefreshFiles() end):SetImage(pace.MiscIcons.clear) pnl:SetImage(pace.MiscIcons.outfit) elseif parts.self then menu:AddOption(data.Name, function() pace.SaveParts(nil, data.RelativePath, override_part) end) :SetImage(parts.self.Icon) end end end if dir ~= "" then menu:AddSpacer() menu:AddOption(L"delete directory", function() Derma_Query( L"Are you sure you want to delete data/pac3" .. dir .. "/* and all its files?\nThis cannot be undone!", L"delete directory", L"yes", function() local function delete_directory(dir) local files, folders = file.Find(dir .. "*", "DATA") for k,v in ipairs(files) do file.Delete(dir .. v) end for k,v in ipairs(folders) do delete_directory(dir .. v .. "/") end if file.Find(dir .. "*", "DATA")[1] then Derma_Message("Cannot remove the directory.\nMaybe it contains hidden files?", "unable to remove directory", L"ok") else file.Delete(dir) end end delete_directory("pac3/" .. dir .. "/") pace.RefreshFiles() end, L"no", function() end ) end):SetImage("icon16/folder_delete.png") end end function pace.AddSaveMenuToMenu(menu, override_part) menu.GetDeleteSelf = function() return false end if not override_part then menu:AddOption(L"auto load (your spawn outfit)", function() pace.SaveParts("autoload", nil, override_part) pace.RefreshFiles() end) :SetImage(pace.MiscIcons.autoload) menu:AddSpacer() end local tbl = pace.GetSavedParts() populate_parts(menu, tbl, nil, override_part) end -- this fixes parts that are using the same uniqueid as other parts because of some bugs in older versions function pace.FixUniqueIDs(data) local ids = {} local function iterate(part) ids[part.self.UniqueID] = ids[part.self.UniqueID] or {} table.insert(ids[part.self.UniqueID], part) for key, part in pairs(part.children) do iterate(part) end end for key, part in pairs(data) do iterate(part) end for key, val in pairs(ids) do if #val > 1 then for key, part in pairs(val) do pac.dprint("Part (%s using model %s) named %q has %i other parts with the same unique id. Fixing!", part.self.ClassName, part.self.Name, part.self.Model or "", #val) part.self.UniqueID = pac.Hash() end end end return data end -- this is for fixing parts that are not in a group function pace.FixBadGrouping(data) local parts = {} local other = {} for key, part in pairs(data) do if part.self.ClassName ~= "group" then table.insert(parts, part) else table.insert(other, part) end end if #parts > 0 then local out = { { ["self"] = { ["EditorExpand"] = true, ["ClassName"] = "group", ["UniqueID"] = pac.Hash(), ["Name"] = "automatic group", }, ["children"] = parts, }, } for k,v in pairs(other) do table.insert(out, v) end return out end return data end