Files
wnsrc/lua/pac3/editor/client/saved_parts.lua

729 lines
18 KiB
Lua
Raw Normal View History

2024-08-04 22:55:00 +03:00
--[[
| 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, "<!DOCTYPE html>" ) 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