From 23d7d5e9b27e4008c572e044dfb8ce1c13a4791a Mon Sep 17 00:00:00 2001 From: emspike Date: Sun, 30 Oct 2011 02:59:26 +0000 Subject: [PATCH] Initial commit. --- icon.tga | Bin 0 -> 2739 bytes info.txt | 11 + lua/advdupe2/cl_browser.lua | 1057 ++++++++++ lua/advdupe2/cl_file.lua | 60 + lua/advdupe2/cl_networking.lua | 144 ++ lua/advdupe2/nullesc.lua | 54 + lua/advdupe2/sv_clipboard.lua | 1283 ++++++++++++ lua/advdupe2/sv_codec.lua | 696 +++++++ lua/advdupe2/sv_file.lua | 92 + lua/advdupe2/sv_misc.lua | 131 ++ lua/advdupe2/sv_networking.lua | 310 +++ lua/autorun/client/advdupe2_cl_init.lua | 69 + lua/autorun/server/advdupe2_sv_init.lua | 88 + lua/entities/gmod_contr_spawner/cl_init.lua | 6 + lua/entities/gmod_contr_spawner/init.lua | 293 +++ lua/entities/gmod_contr_spawner/shared.lua | 10 + lua/weapons/gmod_tool/stools/advdupe2.lua | 2051 +++++++++++++++++++ materials/gui/ad2logo.tga | Bin 0 -> 144241 bytes materials/gui/silkicons/help.vmt | 8 + materials/gui/silkicons/help.vtf | Bin 0 -> 1227 bytes 20 files changed, 6363 insertions(+) create mode 100644 icon.tga create mode 100644 info.txt create mode 100644 lua/advdupe2/cl_browser.lua create mode 100644 lua/advdupe2/cl_file.lua create mode 100644 lua/advdupe2/cl_networking.lua create mode 100644 lua/advdupe2/nullesc.lua create mode 100644 lua/advdupe2/sv_clipboard.lua create mode 100644 lua/advdupe2/sv_codec.lua create mode 100644 lua/advdupe2/sv_file.lua create mode 100644 lua/advdupe2/sv_misc.lua create mode 100644 lua/advdupe2/sv_networking.lua create mode 100644 lua/autorun/client/advdupe2_cl_init.lua create mode 100644 lua/autorun/server/advdupe2_sv_init.lua create mode 100644 lua/entities/gmod_contr_spawner/cl_init.lua create mode 100644 lua/entities/gmod_contr_spawner/init.lua create mode 100644 lua/entities/gmod_contr_spawner/shared.lua create mode 100644 lua/weapons/gmod_tool/stools/advdupe2.lua create mode 100644 materials/gui/ad2logo.tga create mode 100644 materials/gui/silkicons/help.vmt create mode 100644 materials/gui/silkicons/help.vtf diff --git a/icon.tga b/icon.tga new file mode 100644 index 0000000000000000000000000000000000000000..6b9a7e6ca1a73507aefb5b609fcdc32836cd404b GIT binary patch literal 2739 zcmeH}OHZ6t7{`Y)ScXKq@dLPWS2r#-F)nICVl=5@nwZp;pTUKzmjW6WCRG!Yrd}eZ z)C!h643u#$lQd(G8(B$N#rl+SBkH+AzCY4AeEIlZ%3@ z2N|EmMO+pa{{8)ZtBSdhl~Kz1e8AV&*R`>+VKso04aEEt*oQT;3d5RJm^%W@QPxW! zJnTB4=VjL~pf3z^5g|7J{(s8Y+VpSPSVB7Hkazzj8!a>ygws?c01gfgluoB@RiT=c zvXKu5)q{G)DvONdJw?)t=StIMDoA&Cw^mkGEDME(qtw*p<>e>oaSK2n2oyLhgR zg@VD1yF{ScC?q#*Z{2_-b#-+b8XB^_%)vrl6u{j$gIeQRvQ91uJeSQtK$v{Jp|$6>9(eq3&1Q)AN83GidJj`!nX>nhpVIkLhqE zE<*#>q03k-fLdHkYb=q}VDH0>|IX_Qe*3aozr2zOeEp(=cVEpkmw&99n)`ae7%0}> z-fj(KBF2i!dWaD<2r%N3?oIC}NDx^{L>3^ES;B~mM*CIu;OG68>TiY`D?U}@cSU-1 z_f6H`E7tT(GL}cg7+(z%N4j77akcIALsAikHB(p zv_l#2LV*k(r$H$LG$(5k2Qb?Ic{IcBhC(50QHPrr+aSV(90o0G>{19Xkp#g=;607! zGwyaxVh9b%vA@cJ$f}c*6I-%~(9g(?JSf0O9-euGm`Rp%3VO^T*S@d3+z#`%wY90b zx>~ihwfelKrbZ4kFUiF=$%jQwPfwrV*vF^+!5@6@UoX1$LE(Fa@4j>O##@DNUwg&# ECwSvrdH?_b literal 0 HcmV?d00001 diff --git a/info.txt b/info.txt new file mode 100644 index 0000000..116967a --- /dev/null +++ b/info.txt @@ -0,0 +1,11 @@ +"AddonInfo" + +{ + "name" "Adv. Duplicator 2" + "version" "SVN" + "up_date" "SVN" + "author_name" "Advanced Duplicator 2 Team: TB, emspike" + "author_email" "advdupe2@gmail.com" + "author_url" "http://facepunch.com" + "info" "A new and much improved Adv. Duplicator." +} diff --git a/lua/advdupe2/cl_browser.lua b/lua/advdupe2/cl_browser.lua new file mode 100644 index 0000000..82cb7ee --- /dev/null +++ b/lua/advdupe2/cl_browser.lua @@ -0,0 +1,1057 @@ +--[[ + Title: Adv. Dupe 2 File Browser + + Desc: Displays and interfaces with duplication files. + + Author: TB + + Version: 1.0 +]] + +local PANEL = {} +local ClientFolderCount = 1 + +function PANEL:PopulateUpload(Search, Folders, Files, parent) + //Search = string.Left(Search,#Search-1) + local NodeP + if(parent==0)then + NodeP = self.Upload + else + NodeP = self.CNodes[parent] + end + + local Folder + for k,v in pairs(Folders)do + Folder = NodeP:AddNode(v) + self.CNodes[ClientFolderCount] = Folder + Folder.IsFile = false + Folder.Name = v + Folder.SortName = "A"..string.lower(v) + Folder.ID = ClientFolderCount + //table.insert(FolderSearch, {ID=Folder.ID, Name=v}) + ClientFolderCount = ClientFolderCount + 1 + for k,v in pairs(file.Find(Search..v.."/*.txt"))do + name = string.sub(v, 1, -5) + File = Folder:AddNode(name) + File.IsFile = true + File.Name = name + File.SortName = "B"..string.lower(name) + File.Icon:SetImage("vgui/spawnmenu/file") + end + self:PopulateUpload(Search..v.."/", file.FindDir(Search..v.."/*"), nil, Folder.ID) + //file.TFind(Search..v.."/*", function(Search2, Folders2, Files2) self:PopulateUpload(Search2, Folders2, Files2, Folder.ID) end) + end + + /*local name = "" + local File + for k,v in pairs(Files)do + name = string.sub(v, 1, -5) + File = NodeP:AddNode(name) + File.IsFile = true + File.Name = name + File.SortName = "B"..string.lower(name) + File.Icon:SetImage("vgui/spawnmenu/file") + end + + for k,v in pairs(FolderSearch)do + file.TFind(Search..v.Name.."/*", function(Search2, Folders2, Files2) self:PopulateUpload(Search2, Folders2, Files2, v.ID) end) + end*/ + +end + +function PANEL:UpdateClientFiles() + ClientFolderCount = 2 + local Folder = self.Upload:AddNode("=Adv Duplicator=") + self.CNodes[1] = Folder + Folder.IsFile = false + Folder.Name = "=Adv Duplicator=" + Folder.SortName = "A=adv duplicator=" + Folder.ID = 1 + + self:PopulateUpload("adv_duplicator/", file.FindDir("adv_duplicator/*"), nil, 1) + local name = "" + local File + for k,v in pairs(file.Find("adv_duplicator/*.txt"))do + name = string.sub(v, 1, -5) + File = Folder:AddNode(name) + File.IsFile = true + File.Name = name + File.SortName = "B"..string.lower(name) + File.Icon:SetImage("vgui/spawnmenu/file") + end + self:PopulateUpload(AdvDupe2.DataFolder.."/", file.FindDir(AdvDupe2.DataFolder.."/*"), nil, 0) + for k,v in pairs(file.Find(AdvDupe2.DataFolder.."/*.txt"))do + name = string.sub(v, 1, -5) + File = self.Upload:AddNode(name) + File.IsFile = true + File.Name = name + File.SortName = "B"..string.lower(name) + File.Icon:SetImage("vgui/spawnmenu/file") + end + + //file.TFind("Data/adv_duplicator/*", function(Search, Folders, Files) self:PopulateUpload(Search, Folders, Files, 1) end) + //file.TFind("Data/"..AdvDupe2.DataFolder.."/*", function(Search, Folders, Files) self:PopulateUpload(Search, Folders, Files, 0) end) + + self.Upload.Populated = true +end + +local function SortChildren(NodeP) + if(NodeP.ChildNodes)then + NodeP.ChildNodes:SortByMember("SortName") + NodeP:SetExpanded(false, true) + NodeP:SetExpanded(true, true) + else + NodeP:SortByMember("SortName") + NodeP:InvalidateLayout() + end +end + +local function GetNodeRoot(Node) + + local ReturnNode = false + local function PurgeNodes( PNode ) + if(PNode.Name == nil)then return end + ReturnNode = PNode + PurgeNodes(PNode:GetParentNode()) + end + PurgeNodes(Node:GetParentNode()) + + return ReturnNode +end + +local function GetNodePath( Node ) + local path = Node.Name + + local function PurgeNodes( PNode ) + if(PNode.Name == nil)then return end + path = PNode.Name.."/"..path + PurgeNodes(PNode:GetParentNode()) + end + + PurgeNodes(Node:GetParentNode()) + + return path +end + +local function ParsePath(path) + local area = 0 + local Alt = string.Explode("/", path)[1] + if(Alt=="=Adv Duplicator=")then + path = string.sub(path, 18) + area = 2 + elseif(Alt=="=Public=")then + path = string.sub(path, 10) + area = 1 + end + + return path, area +end + +local function CheckFileNameCl(path) + + if file.Exists(path..".txt") then + for i = 1, AdvDupe2.FileRenameTryLimit do + if not file.Exists(path.."_"..i..".txt") then + return path.."_"..i..".txt" + end + end + return false + end + + return path..".txt" +end + +function PANEL:FolderSelect(func, name, path, ArgNode) + + local txt = "" + local todo + local Frame + local Fldbrw + local Tree + local AltTree = nil + local path, area = ParsePath(GetNodePath(ArgNode)) + + if(func==1)then + txt = "Select the folder you want to save the Upload to." + Tree = self.Tree + todo = function() + local area2 = 0 + if(area==2)then area2=2 end + if(!Fldbrw:GetSelectedItem())then + AdvDupe2.InitializeUpload(path, area, name, area2, 0) + Frame:Remove() + return + end + AdvDupe2.InitializeUpload(path, area, GetNodePath(Fldbrw:GetSelectedItem()).."/"..name, area2, Fldbrw:GetSelectedItem().ID) + Frame:Remove() + end + elseif(func==2)then + txt = "Select the folder you want to save the Download to." + if(!self.Upload.Populated)then self:UpdateClientFiles() end + Tree = self.Upload + local Root = GetNodeRoot(ArgNode) + todo = function() + if(!Fldbrw:GetSelectedItem())then + + if(area==0 || area==1)then + AdvDupe2.SavePath = AdvDupe2.DataFolder.."/"..name..".txt" + AdvDupe2.SaveNode = 0 + elseif(area==2)then + AdvDupe2.SavePath = "adv_duplicator/"..name..".txt" + AdvDupe2.SaveNode = Root.ID + end + RunConsoleCommand("AdvDupe2_DownloadFile", path, area) + Frame:Remove() + return + end + local SavePath + if(area==0 || area==1)then + SavePath = AdvDupe2.DataFolder.."/"..GetNodePath(Fldbrw:GetSelectedItem()) + else + SavePath = "adv_duplicator/"..GetNodePath(Fldbrw:GetSelectedItem()) + end + AdvDupe2.SavePath = SavePath.."/"..name..".txt" + AdvDupe2.SaveNode = Fldbrw:GetSelectedItem().ID + RunConsoleCommand("AdvDupe2_DownloadFile", path, area) + Frame:Remove() + end + elseif(func==3)then //Move File Client + txt = "Select the folder you want to move the File to." + if SinglePlayer() then + Tree = self.Tree + else + if(!self.Upload.Populated)then self:UpdateClientFiles() end + Tree = self.Upload + end + local Root = GetNodeRoot(ArgNode) + if(Root && Root.Name=="=Adv Duplicator=")then AltTree=Root end + todo = function() + local base = AdvDupe2.DataFolder + local ParentNode + local savepath = "" + if(AltTree)then base = "adv_duplicator" end + + if(!Fldbrw:GetSelectedItem())then + savepath = base.."/" + ParentNode = 0 + if(AltTree)then ParentNode = 1 end + else + local NodePath = GetNodePath(Fldbrw:GetSelectedItem()) + savepath = base.."/"..NodePath.."/" + ParentNode = Fldbrw:GetSelectedItem().ID + end + savepath = savepath..name + savepath = CheckFileNameCl(savepath) + if(AltTree)then path = string.sub(path, 18) end + local OldFile = base.."/"..path..".txt" + local ReFile = file.Read(OldFile) + file.Write( savepath, ReFile) + file.Delete(OldFile) + local name2 = string.Explode("/", savepath) + name2 = string.sub(name2[#name2], 1, -5) + if(SinglePlayer())then + self:AddFile(name2, ParentNode, true) + else + self:AddFileToClient(name2, ParentNode, true) + end + local NodeP = ArgNode:GetParentNode() + for k,v in pairs(NodeP.Items or NodeP.ChildNodes.Items)do + if(v==ArgNode.Panel)then + table.remove(NodeP.Items or NodeP.ChildNodes.Items, k) + v:Remove() + NodeP:InvalidateLayout() + break + end + end + if(NodeP.ChildNodes)then + if(NodeP.m_bExpanded)then + NodeP:SetExpanded(false) + NodeP:SetExpanded(true) + end + end + Tree.m_pSelectedItem = nil + Frame:Remove() + end + elseif(func==4)then //Move File Server + txt = "Select the folder you want to move the File to." + local Root = GetNodeRoot(ArgNode) + if(Root && Root.Name=="=Adv Duplicator=")then AltTree=Root else Tree = self.Tree end + todo = function() + local path1, area1 = ParsePath(GetNodePath(ArgNode)) + local path2, area2 + + if(Fldbrw:GetSelectedItem())then + path2, area2 = ParsePath(GetNodePath(Fldbrw:GetSelectedItem())) + self.MoveToNode = Fldbrw:GetSelectedItem().ID + if(area1==2)then area2 = 2 end + else + if(area1==2)then + self.MoveToNode = Root.ID + else + self.MoveToNode = 0 + end + path2 = "" + area2 = area1 + end + + if((area1==2 && area2!=2) || (area2==2 && area1!=2))then AdvDupe2.Notify("Cannot move files between these directories.", NOTIFY_ERROR) return end + self.NodeToMove = ArgNode + RunConsoleCommand("AdvDupe2_MoveFile", area1, area2, path1, path2) + Frame:Remove() + end + end + + + Frame = vgui.Create("DFrame") + Frame:SetTitle( txt ) + Frame:SetSize(280, 475) + Frame:Center() + Frame:ShowCloseButton(true) + Frame:MakePopup() + + Fldbrw = vgui.Create("DTree", Frame) + Fldbrw:SetPadding(5) + Fldbrw:SetPos(10,40) + Fldbrw:SetSize(260,400) + + local function PurgeChildren(Node, Parent) + if(!Node.ChildNodes)then return end + local child + for k,v in pairs(Node.ChildNodes:GetItems())do + if(v.IsFile)then continue end + child = Parent:AddNode(v.Name) + child.Name = v.Name + child.ID = v.ID + PurgeChildren(v, child) + end + return + end + + local child + for k,v in pairs(AltTree && AltTree.ChildNodes.Items or Tree:GetItems())do + if(v.IsFile || v.Name=="=Adv Duplicator=")then continue end + child = Fldbrw:AddNode(v.Name) + child.Name = v.Name + child.ID = v.ID + PurgeChildren(v, child) + end + + local confirm = vgui.Create("DButton", Frame) + confirm:SetSize(75,25) + confirm:SetText("Ok") + confirm:AlignBottom(5) + confirm:AlignLeft(10) + confirm.DoClick = todo + + local Cancel = vgui.Create("DButton", Frame) + Cancel:SetSize(75,25) + Cancel:SetText("Cancel") + Cancel:AlignBottom(5) + Cancel:AlignRight(10) + Cancel.DoClick = function() Frame:Remove() end + +end + +local function DeleteFilesInFolders(Search, Folders, Files) + Search = string.sub(Search, 6, -2) + + for k,v in pairs(Files)do + file.Delete(Search..v) + end + + for k,v in pairs(Folders)do + file.TFind("Data/"..Search..v.."/*", DeleteFilesInFolders) + end + +end + +local function RemoveFileNodes(Node) + for k,v in pairs(Node.ChildNodes.Items)do + if(v.IsFile)then + table.remove(Node.ChildNodes.Items, k) + v:Remove() + elseif(v.ChildNodes)then + RemoveFileNodes(v) + end + end + Node:InvalidateLayout() + Node:SetExpanded(false) +end + +local function Delete(Tree, Folder, Server) + + local Node = Tree:GetSelectedItem() + local msg + + if(Server)then + msg = '" from the SERVER?' + else + msg = '" from your CLIENT?' + end + + if(!Folder)then + msg = 'Are you sure you want to delete the FILE, "'..Node.Name..msg + else + msg = 'Are you sure you want to delete the ENTIRE FOLDER, "'..Node.Name..msg + end + + local path, area = ParsePath(GetNodePath(Node)) + + local Frame = vgui.Create("DFrame") + Frame:SetTitle( "Are You Sure?" ) + Frame:SetSize(250, 100) + Frame:Center() + Frame:ShowCloseButton(false) + + local label = vgui.Create("DLabel", Frame) + label:AlignLeft(5) + label:AlignTop(30) + label:SetText(msg) + label:SetWide(240) + label:SetTall(25) + label:SetWrap(true) + + local confirm = vgui.Create("DButton", Frame) + confirm:SetSize(75,25) + confirm:SetText("Delete") + confirm:AlignBottom(5) + confirm:AlignLeft(10) + confirm.DoClick = function() + if(!Folder)then + if(Server)then + Tree:GetParent().NodeToDelete = Node + Tree:GetParent().ParentToDelete = Node:GetParentNode() + RunConsoleCommand("AdvDupe2_DeleteFile", path, area, "false") + else + if(area==1)then path = "=Public=/"..path end + path = AdvDupe2.DataFolder.."/"..path..".txt" + local NodeP = Node:GetParentNode() + + for k,v in pairs(NodeP.Items or NodeP.ChildNodes.Items)do + if(v==Node.Panel)then + table.remove(NodeP.Items or NodeP.ChildNodes.Items, k) + v:Remove() + NodeP:InvalidateLayout() + break + end + end + if(NodeP.ChildNodes)then + if(NodeP.m_bExpanded)then + NodeP:SetExpanded(false) + NodeP:SetExpanded(true) + end + end + Tree.m_pSelectedItem = nil + end + file.Delete(path) + else + if(Server)then + Tree:GetParent().NodeToDelete = false + Tree:GetParent().ParentToDelete = Node + RunConsoleCommand("AdvDupe2_DeleteFile", path, area, "true") + else + if(area==1)then path = "=Public=/"..path end + path = "Data/"..AdvDupe2.DataFolder.."/"..path.."/*" + if(Node.ChildNodes)then + RemoveFileNodes(Node) + end + Node:SetExpanded(false) + Tree.m_pSelectedItem = nil + end + file.TFind(path, DeleteFilesInFolders) + end + Frame:Remove() + end + + local Cancel = vgui.Create("DButton", Frame) + Cancel:SetSize(75,25) + Cancel:SetText("Cancel") + Cancel:AlignBottom(5) + Cancel:AlignRight(10) + Cancel.DoClick = function() Frame:Remove() end + Frame:SetVisible(true) + Frame:MakePopup() + +end + +function PANEL:AddNewFolder(Tree, base) + + local Node + if(base)then + Node = Tree + else + Node = Tree:GetSelectedItem() + end + + local name = self.Textbox:GetValue() + if(name=="" || name=="File_Name...")then AdvDupe2.Notify("Name is blank!", NOTIFY_ERROR) return end + name = name:gsub("%W","") + + local path, area + if(base)then + path = AdvDupe2.DataFolder.."/"..name + else + path, area = ParsePath(GetNodePath(Node)) + if(area==0)then + path = AdvDupe2.DataFolder.."/"..path.."/"..name + elseif(area==1)then + path = AdvDupe2.DataFolder.."/=Public=/"..path.."/"..name + else + path = "adv_duplicator/"..path.."/"..name + end + end + + if(file.IsDir(path))then AdvDupe2.Notify("Folder name alreayd exists.", NOTIFY_ERROR) return end + + file.CreateDir(path) + + local Folder = Node:AddNode(name) + self.CNodes[ClientFolderCount] = Folder + Folder.IsFile = false + Folder.Name = name + Folder.SortName = "A"..string.lower(name) + Folder.ID = ClientFolderCount + ClientFolderCount = ClientFolderCount + 1 + + SortChildren(Node) + + if(!Node.m_bExpanded)then + Node:SetExpanded(true) + end + + Tree:SetSelectedItem(Folder) +end + +local function Incomplete() + AdvDupe2.Notify("This feature is not yet complete!",NOTIFY_GENERIC,10) +end + +function PANEL:DoClick(Node) + if(!Node || !Node.IsFile)then return end + if(CurTime()-self.LastClick<=0.25 && self.LastNode==Node)then + local path, area = ParsePath(GetNodePath(Node)) + RunConsoleCommand("AdvDupe2_OpenFile", path, area) + end + self.LastNode = Node + self.LastClick = CurTime() +end + +local function RenameFileCl(Node, name) + if(name=="" || name=="File_Name...")then AdvDupe2.Notify("Enter a file name to rename file.", NOTIFY_ERROR) return end + local path, area = ParsePath(GetNodePath(Node)) + local File, FilePath, tempFilePath = "", "", "" + if(area==0)then + tempFilePath = AdvDupe2.DataFolder.."/"..path + elseif(area==1)then + tempFilePath = AdvDupe2.DataFolder.."/=Public=/"..path + elseif(area==2)then + tempFilePath = "adv_duplicator/"..path + end + + File = file.Read(tempFilePath..".txt") + FilePath = CheckFileNameCl(string.sub(tempFilePath, 1, #tempFilePath-#Node.Name)..name) + + if(!FilePath)then AdvDupe2.Notify("Rename limit exceeded, could not rename.", NOTIFY_ERROR) return end + file.Write(FilePath, File) + if(file.Exists(FilePath))then + file.Delete(tempFilePath..".txt") + local NewName = string.Explode("/", FilePath) + NewName = string.sub(NewName[#NewName], 1, -5) + Node:SetText(NewName) + Node.Name = NewName + Node.SortName = "B"..string.lower(NewName) + AdvDupe2.Notify("File renamed to "..NewName) + else + AdvDupe2.Notify("File was not renamed.", NOTIFY_ERROR) + end + + local NodeP = Node:GetParentNode() + SortChildren(NodeP) +end + +function PANEL:DoRightClick(Node) + if(Node==nil)then return end + self:SetSelectedItem(Node) + local parent = self:GetParent() + local Menu = DermaMenu() + + if(SinglePlayer())then + if(Node.IsFile)then + Menu:AddOption("Open", function() + local path, area = ParsePath(GetNodePath(Node)) + RunConsoleCommand("AdvDupe2_OpenFile", path, area) + end) + Menu:AddOption("Rename", function() + RenameFileCl(Node, parent.Textbox:GetValue()) + parent.Textbox:SetValue("File_Name...") + parent.DescBox:SetValue("Description...") + end) + Menu:AddOption("Move File", function() parent:FolderSelect( 3, Node.Name, GetNodePath(Node), Node) end) + Menu:AddOption("Delete", function() Delete(self, false, false) end) + else + Menu:AddOption("Save", function() + local name = parent.Textbox:GetValue() + if(name=="" || name=="File_Name...")then AdvDupe2.Notify("Name field is blank.", NOTIFY_ERROR) return end + local path, area = ParsePath(GetNodePath(Node) ) + local desc = parent.DescBox:GetValue() + if(desc=="Description...")then desc="" end + RunConsoleCommand("AdvDupe2_SaveFile", parent.Textbox:GetValue(), path, area, desc, Node.ID) + parent.Textbox:SetValue("File_Name...") + parent.DescBox:SetValue("Description...") + end) + Menu:AddOption("New Folder", function() + parent:AddNewFolder(self, false) + parent.Textbox:SetValue("File_Name...") + parent.DescBox:SetValue("Description...") + end) + Menu:AddOption("Delete", function() Delete(self, true, false) end) + end + + elseif(parent.Change.Server)then + + if(Node.IsFile)then + Menu:AddOption("Open", function() + local path, area = ParsePath(GetNodePath(Node)) + RunConsoleCommand("AdvDupe2_OpenFile", path, area) + end) + Menu:AddOption("Download", function() + parent:FolderSelect(2, Node.Name, nil, Node) + end) + Menu:AddOption("Rename", function() + local name = parent.Textbox:GetValue() + if(name=="" || name=="File_Name...")then AdvDupe2.Notify("Name field is blank!", NOTIFY_ERROR) return end + local path, area = ParsePath(GetNodePath(Node)) + parent.NodeToRename = Node + RunConsoleCommand("AdvDupe2_RenameFile", area, name, path ) + parent.Textbox:SetValue("File_Name...") + parent.DescBox:SetValue("Description...") + end) + Menu:AddOption("Move File", function() + parent:FolderSelect(4, nil, nil, Node) + end) + Menu:AddOption("Delete", function() Delete(self, false, true) end ) + else + Menu:AddOption("Save", function() + local name = parent.Textbox:GetValue() + if(name=="" || name=="File_Name...")then AdvDupe2.Notify("Name field is blank!", NOTIFY_ERROR) return end + local path, area = ParsePath(GetNodePath(Node)) + local desc = parent.DescBox:GetValue() + if(desc=="Description...")then desc="" end + RunConsoleCommand("AdvDupe2_SaveFile", parent.Textbox:GetValue(), path, area, desc, Node.ID) + parent.Textbox:SetValue("File_Name...") + parent.DescBox:SetValue("Description...") + end) + Menu:AddOption("New Folder", function() + local name = parent.Textbox:GetValue() + if(name=="" || name=="File_Name...")then AdvDupe2.Notify("Name field is blank!", NOTIFY_ERROR) return end + name = name:gsub("%W","") + local path, area = ParsePath(GetNodePath(Node)) + RunConsoleCommand("AdvDupe2_NewFolder", name, path, area, Node.ID) + parent.Textbox:SetValue("File_Name...") + parent.DescBox:SetValue("Description...") + end) + Menu:AddOption("Delete", function() Delete(self, true, true) end ) + end + else + if(Node.IsFile)then + Menu:AddOption("Upload", function() parent:FolderSelect(1, Node.Name, GetNodePath(Node), Node) end)//function() AdvDupe2.InitializeUpload(GetNodePath(Tree:GetSelectedItem())) end ) + Menu:AddOption("Rename", function() + RenameFileCl(Node, parent.Textbox:GetValue()) + parent.Textbox:SetValue("File_Name...") + parent.DescBox:SetValue("Description...") + end) + Menu:AddOption("Move File", function() parent:FolderSelect( 3, Node.Name, GetNodePath(Node), Node) end) + Menu:AddOption("Delete", function() Delete(self, false, false) end) + else + Menu:AddOption("New Folder", function() + parent:AddNewFolder(self) + parent.Textbox:SetValue("File_Name...") + parent.DescBox:SetValue("Description...") + end) + Menu:AddOption("Delete", function() Delete(self, true, false) end) + end + end + Menu:Open() +end + + + +function PANEL:Init() + + PANEL.Panel = self + self.Nodes = {} + self.CNodes = {} + local Wide, Tall = self:GetSize() + local TreeOff = 60 + local UpOff = 40 + + self.FileParent = 0 + + self.Textbox = vgui.Create("DTextEntry", self) + self.Textbox:SetMultiline(false) + self.Textbox:SetKeyboardInputEnabled( true ) + self.Textbox:SetEnabled( true ) + self.Textbox:SetAllowNonAsciiCharacters( true ) + self.Textbox:SetValue("File_Name...") + self.Textbox.OnMousePressed = function() + self.Textbox:OnGetFocus() + if(self.Textbox:GetValue()=="File_Name...")then + self.Textbox:SelectAllOnFocus(true) + end + end + self.Textbox:SetUpdateOnType(true) + self.Textbox.OnTextChanged = function() + local new, changed = self.Textbox:GetValue():gsub("[?.:\"*<>|]","") + if changed > 0 then + self.Textbox:SetValue(new) + end + end + self.Textbox.OnValueChange = function() + if(self.Textbox:GetValue()!="File_Name...")then + local new,changed = self.Textbox:GetValue():gsub("[?.:\"*<>|]","") + if changed > 0 then + self.Textbox:SetValue(new) + end + end + end + /*function() + //if(self.Textbox:GetValue()!="File_Name...")then + local new,changed = self.Textbox:GetValue():gsub("[?.:\"*<>|]","") + if changed > 0 then + self.Textbox:SetValue(new) + end + //end + end*/ + + self.Save = vgui.Create("DButton", self) + self.Save:SetText("Save") + self.Save:SetToolTip("Save to base folder") + self.Save.DoClick = function() + if(self.Textbox:GetValue()=="" || self.Textbox:GetValue()=="File_Name...")then AdvDupe2.Notify("Name field is blank!", NOTIFY_ERROR) return end + --[[local _,changed = self.Textbox:GetValue():gsub("[?.:\"*<>|]","") + if changed > 0 then + AdvDupe2.Notify("Filenames cannot contain ?.:\"*<>|",NOTIFY_ERROR) + return + end]] + local desc = self.DescBox:GetValue() + if(desc=="Description...")then desc="" end + RunConsoleCommand("AdvDupe2_SaveFile", self.Textbox:GetValue(), "", 0, desc, 0) + self.Textbox:SetValue("File_Name...") + self.DescBox:SetValue("Description...") + end + self.Textbox.OnEnter = self.Save.DoClick + + self.DescBox = vgui.Create("DTextEntry", self) + self.DescBox:SetMultiline(false) + self.DescBox:SetKeyboardInputEnabled( true ) + self.DescBox:SetEnabled( true ) + self.DescBox:SetValue("Description...") + self.DescBox.OnEnter = self.Save.DoClick + self.DescBox.OnMousePressed = function() + self.DescBox:OnGetFocus() + if(self.DescBox:GetValue()=="Description...")then + self.DescBox:SelectAllOnFocus(true) + end + end + + if(!SinglePlayer())then + self.Upload = vgui.Create("DTree", self) + self.Upload:SetPadding(5) + self.Upload.Populated = false + self.Upload.DoRightClick = self.DoRightClick + self.Upload:SetVisible(false) + self.Upload.LastClick = 0 + end + + self.Tree = vgui.Create("DTree", self) + self.Tree:SetPadding(5) + self.Tree.LastClick = 0 + self.Tree.DoClick = self.DoClick + self.Tree.DoRightClick = self.DoRightClick + self.CurrentParent = self.Tree + + self.Help = vgui.Create("DButton", self) + //self.Help:SetText("?") + self.Help:SetImage("gui/silkicons/help") + self.Help.DoClick = function(btn) + local Menu = DermaMenu() + Menu:AddOption("Forum", Incomplete) + Menu:AddOption("Bug Reporting", function() gui.OpenURL("http://code.google.com/p/advdupe2/issues/list") end) + Menu:AddOption("About", AdvDupe2.ShowSplash) + Menu:Open() + end + + self.Update = vgui.Create("DButton", self) + self.Update:SetText("Update") + self.Update:SetToolTip("Update files") + self.Update.DoClick = function(button) + local Wide, Tall = self:GetSize() + if(self.Change.Server) then + RunConsoleCommand("AdvDupe2_SendFiles", 0) + else + for k,v in pairs(self.Upload.Items)do + v:Remove() + end + self.Upload.Items = {} + self.Upload:InvalidateLayout() + self.CNodes = {} + self.Upload.m_pSelectedItem = nil + self:UpdateClientFiles() + end + end + + self.NewFolder = vgui.Create("DButton", self) + self.NewFolder:SetText("New Folder") + self.NewFolder:SetToolTip("Create a new folder in the base folder") + self.NewFolder.DoClick = function() + if(self.Change.Server)then + local name = self.Textbox:GetValue() + if(name=="" || name=="File_Name...")then AdvDupe2.Notify("Name field is blank!", NOTIFY_ERROR) return end + name = name:gsub("%W","") + RunConsoleCommand("AdvDupe2_NewFolder", name, "", 0) + self.Textbox:SetValue("File_Name...") + else + self:AddNewFolder(self.Upload, true) + end + end + + self.Change = vgui.Create("DButton", self) + self.Change.Server = true + if(SinglePlayer())then + self.Change:SetText("Local Files") + else + self.Change:SetText("Server Files") + self.Change:SetToolTip("Change between server and client files") + self.Change.DoClick = function(button) + if(self.Change.Server)then + if(SinglePlayer())then return end + self.Change:SetText("Client Files") + self.Change.Server = false + self.Tree:SetVisible(false) + if(!self.Upload.Populated)then self:UpdateClientFiles() end + self.Upload:SetVisible(true) + else + self.Change:SetText("Server Files") + self.Change.Server = true + self.Upload:SetVisible(false) + self.Tree:SetVisible(true) + end + end + end + + self:SetPaintBackground(false) + +end + +function PANEL:AdjustFiller(Menu) + local Tab = self.Panel:GetTable() + if(Menu)then + if(g_ContextMenu:GetTall() MaxTall ) then Tall = MaxTall end + Tab.Filler:SetTall(Tall-49) + else + Tab.Filler:SetTall(g_ContextMenu:GetTall()-49) + end + else + Tab.Filler:SetTall(Tab.Panel:GetParent():GetParent():GetParent():GetParent():GetParent():GetTall()-45) + end +end + +local CMenu = false +hook.Add("OnContextMenuOpen", "AD2MenuFormat", + function() + if(!GetControlPanel("advdupe2"):IsVisible() || !PANEL || !PANEL.Panel)then return end + CMenu = true + PANEL:AdjustFiller(true) + end) + +hook.Add("SpawnMenuOpen", "AD2MenuFormat", + function() + if(CMenu)then CMenu = false return end + if(!GetControlPanel("advdupe2"):IsVisible() || !PANEL || !PANEL.Panel)then return end + PANEL:AdjustFiller(false) + end) + +function PANEL:PerformLayout() + + local w = self:GetWide() + self.Change:SetPos(0, 0) + self.Change:SetSize(w, 20) + if(self.Upload)then + self.Upload:SetPos(0, 20) + self.Upload:SetSize(w,360) + end + self.Tree:SetPos(0, 20) + self.Tree:SetSize(w,360) + self.Textbox:SetPos(0,380) + self.Textbox:SetSize(w*.75, 20) + self.Save:SetPos(w*.75,380) + self.Save:SetSize(w*.25, 20) + self.DescBox:SetPos(0,400) + self.DescBox:SetSize(w, 20) + self.Help:SetPos(0, 420) + self.Help:SetSize(22, 20) + self.Update:SetPos(22, 420) + self.Update:SetSize(w/2-22, 20) + self.NewFolder:SetPos(w/2, 420) + self.NewFolder:SetSize(w/2, 20) +end + +function PANEL:AddFolder(Name, ID, Parent, New) + + local NodeP + if(Parent!=0)then + NodeP = self.Nodes[Parent] + else + NodeP = self.Tree + end + if(!ValidPanel(NodeP))then return end + local Folder = NodeP:AddNode(Name) + + self.Nodes[ID]=Folder + Folder.IsFile = false + Folder.Name = Name + Folder.SortName = "A"..string.lower(Name) + Folder.ID = ID + + if(NodeP.ChildNodes)then + NodeP.ChildNodes.Items[#NodeP.ChildNodes.Items].Name = Folder.Name + end + if(New)then + SortChildren(NodeP) + end + +end + +function PANEL:AddFile(Name, Parent, New) + + local NodeP + if(Parent!=0)then + NodeP = self.Nodes[Parent] + else + NodeP = self.Tree + end + if(!ValidPanel(NodeP))then return end + + local File = NodeP:AddNode(Name) + File.IsFile = true + File.Name = Name + File.SortName = "B"..string.lower(Name) + File.Icon:SetImage("vgui/spawnmenu/file") + + if(NodeP.ChildNodes)then + NodeP.ChildNodes.Items[#NodeP.ChildNodes.Items].IsFile = true + end + + if(New)then + SortChildren(NodeP) + end + +end + +function PANEL:AddFileToClient(Name, Parent, New) + local NodeP + if(Parent!=0)then + NodeP = self.CNodes[Parent] + else + NodeP = self.Upload + end + if(!ValidPanel(NodeP))then return end + + local File = NodeP:AddNode(Name) + File.IsFile = true + File.Name = Name + File.SortName = "B"..string.lower(Name) + File.Icon:SetImage("vgui/spawnmenu/file") + + if(NodeP.ChildNodes)then + NodeP.ChildNodes.Items[#NodeP.ChildNodes.Items].IsFile = true + end + if(New)then + SortChildren(NodeP) + end +end + +function PANEL:ClearBrowser() + for k,v in pairs(self.Tree:GetItems())do + v:Remove() + end + self.Tree.Items = {} + self.Tree:InvalidateLayout() + self.Nodes = {} + self.Tree.m_pSelectedItem = nil + RunConsoleCommand("AdvDupe2_SendFiles", 1) +end + +function PANEL:RenameNode(Name) + local Node = self.NodeToRename + Node:SetText(Name) + Node.Name = Name + Node.SortName = "B"..string.lower(Name) + local NodeP = Node:GetParentNode() + SortChildren(NodeP) + AdvDupe2.Notify("File was renamed to "..Name) + self.NodeToRename = nil +end + +function PANEL:MoveNode(Name) + local Node = self.NodeToMove + local NodeP = Node:GetParentNode() + Node:GetRoot().m_pSelectedItem = nil + for k,v in pairs(NodeP.Items or NodeP.ChildNodes.Items)do + if(v==Node.Panel)then + table.remove(NodeP.Items or NodeP.ChildNodes.Items, k) + v:Remove() + NodeP:InvalidateLayout() + break + end + end + if(NodeP.m_bExpanded)then + NodeP:SetExpanded(false) + NodeP:SetExpanded(true) + end + if(self.MoveToNode==0)then + NodeP = self.Tree + else + NodeP = self.Nodes[self.MoveToNode] + end + + local NewNode = NodeP:AddNode(Name) + NewNode.Name = Name + NewNode.SortName = "B"..string.lower(Name) + NewNode.Icon:SetImage("vgui/spawnmenu/file") + NewNode.IsFile = true + if(NodeP.ChildNodes)then + NodeP.ChildNodes.Items[#NodeP.ChildNodes.Items].IsFile = true + end + SortChildren(NodeP) + +end + +function PANEL:DeleteNode() + if(self.NodeToDelete==false)then + RemoveFileNodes(self.ParentToDelete) + else + for k,v in pairs(self.ParentToDelete.Items or self.ParentToDelete.ChildNodes.Items)do + if(v==self.NodeToDelete)then + table.remove(self.ParentToDelete.Items or self.ParentToDelete.ChildNodes.Items, k) + v:Remove() + self.ParentToDelete:InvalidateLayout() + break + end + end + if(self.ParentToDelete.ChildNodes)then + if(self.ParentToDelete.m_bExpanded)then + self.ParentToDelete:SetExpanded(false) + self.ParentToDelete:SetExpanded(true) + end + end + self.Tree.m_pSelectedItem = nil + end + self.ParentToDelete=nil + self.NodeToDelete=nil +end + +vgui.Register("advdupe2_browser", PANEL, "DPanel") \ No newline at end of file diff --git a/lua/advdupe2/cl_file.lua b/lua/advdupe2/cl_file.lua new file mode 100644 index 0000000..5ab06a0 --- /dev/null +++ b/lua/advdupe2/cl_file.lua @@ -0,0 +1,60 @@ +--[[ + Title: Adv. Dupe 2 Filing Clerk (Clientside) + + Desc: Reads/writes AdvDupe2 files. + + Author: AD2 Team + + Version: 1.0 +]] + +--[[ + Name: WriteAdvDupe2File + Desc: Writes a dupe file to the dupe folder. + Params: dupe, name + Return: success/ path +]] +function AdvDupe2.WriteFile(name, dupe) + + name = name:lower() + + if name:find("[<>:\\\"|%?%*%.]") then return false end + + name = name:gsub("//","/") + + local path = string.format("%q/%q", self.DataFolder, name) + + --if a file with this name already exists, we have to come up with a different name + if file.Exists(path..".txt") then + for i = 1, AdvDupe2.FileRenameTryLimit do + --check if theres already a file with the name we came up with, and retry if there is + --otherwise, we can exit the loop and write the file + if not file.Exists(path.."_"..i..".txt") then + path = path.."_"..i + break + end + end + --if we still can't find a unique name we give up + if file.Exists(path..".txt") then return false end + end + + --write the file + file.Write(path..".txt", dupe) + + --returns if the write was successful and the name the path ended up being saved under + return path..".txt", path:match("[^/]-$") + +end + +--[[ + Name: ReadAdvDupe2File + Desc: Reads a dupe file from the dupe folder. + Params: name + Return: contents +]] +function AdvDupe2.ReadFile(name, dirOverride) + + --infinitely simpler than WriteAdvDupe2 :3 + return file.Read(string.format("%q/%q.txt", dirOverride or AdvDupe2.DataFolder, name)) + +end \ No newline at end of file diff --git a/lua/advdupe2/cl_networking.lua b/lua/advdupe2/cl_networking.lua new file mode 100644 index 0000000..a0777c0 --- /dev/null +++ b/lua/advdupe2/cl_networking.lua @@ -0,0 +1,144 @@ +--[[ + Title: Adv. Dupe 2 Networking (Clientside) + + Desc: Handles file transfers and all that jazz. + + Author: TB + + Version: 1.0 +]] + +include "nullesc.lua" + +local function CheckFileNameCl(path) + if file.Exists(path) then + path = string.sub(path, 1, #path-4) + for i = 1, AdvDupe2.FileRenameTryLimit do + if not file.Exists(path.."_"..i..".txt") then + return path.."_"..i..".txt" + end + end + end + + return path +end + +--[[ + Name: AdvDupe2_RecieveFile + Desc: Recieve file data from the server when downloading to the client + Params: usermessage + Returns: +]] +local function AdvDupe2_RecieveFile(um) + local status = um:ReadShort() + + if(status==1)then AdvDupe2.NetFile = "" end + AdvDupe2.NetFile=AdvDupe2.NetFile..um:ReadString() + + if(status==2)then + local path = CheckFileNameCl(AdvDupe2.SavePath) + + file.Write(path, AdvDupe2.Null.invesc(AdvDupe2.NetFile)) + + local filename = string.Explode("/", path) + filename = string.sub(filename[#filename], 1, -5) + + AdvDupe2.FileBrowser:AddFileToClient(filename, AdvDupe2.SaveNode, true) + AdvDupe2.NetFile = "" + AdvDupe2.Notify("File successfully downloaded!",NOTIFY_GENERIC,5) + return + end + +end +usermessage.Hook("AdvDupe2_RecieveFile", AdvDupe2_RecieveFile) + +function AdvDupe2.RemoveProgressBar() + if !AdvDupe2 then AdvDupe2={} end + AdvDupe2.ProgressBar = {} +end + +local escseqnl = { "nwl", "newl", "nwli", "nline" } +local escseqquo = { "quo", "qte", "qwo", "quote" } +--[[ + Name: InitializeUpload + Desc: When the client clicks upload, prepares to send data to the server + Params: File Data, Path to save + Returns: +]] +function AdvDupe2.InitializeUpload(ReadPath, ReadArea, SavePath, SaveArea, ParentID) + if(ReadArea==0)then + ReadPath = AdvDupe2.DataFolder.."/"..ReadPath..".txt" + elseif(ReadArea==1)then + ReadPath = AdvDupe2.DataFolder.."/=Public=/"..ReadPath..".txt" + else + ReadPath = "adv_duplicator/"..ReadPath..".txt" + end + + if(!file.Exists(ReadPath))then return end + local nwl + local quo + local data = AdvDupe2.Null.esc(file.Read(ReadPath)) + + for k = 1, #escseqnl do + if(string.find(data, escseqnl[k]))then continue end + nwl = escseqnl[k] + data = string.gsub(data, "\10", escseqnl[k]) + break + end + + for k = 1, #escseqquo do + if(string.find(data, escseqquo[k]))then continue end + quo = escseqquo[k] + data = string.gsub(data, [["]], escseqquo[k]) + break + end + + AdvDupe2.File = data + AdvDupe2.LastPos = 0 + AdvDupe2.Length = string.len(data) + AdvDupe2.InitProgressBar("Uploading:") + + RunConsoleCommand("AdvDupe2_InitRecieveFile", SavePath, SaveArea, nwl, quo, ParentID) +end + +function AdvDupe2.UpdateProgressBar(percent) + AdvDupe2.ProgressBar.Percent = percent +end + +--[[ + Name: SendFileToServer + Desc: Send chunks of the file data to the server + Params: end of file + Returns: +]] +local function SendFileToServer(eof, chunks) + + for i=1,chunks do + if(AdvDupe2.LastPos+eof>AdvDupe2.Length)then + eof = AdvDupe2.Length + end + + local data = string.sub(AdvDupe2.File, AdvDupe2.LastPos, AdvDupe2.LastPos+eof) + AdvDupe2.LastPos = AdvDupe2.LastPos+eof+1 + AdvDupe2.UpdateProgressBar(math.floor((AdvDupe2.LastPos/AdvDupe2.Length)*100)) + local status = 0 + if(AdvDupe2.LastPos>=AdvDupe2.Length)then + status=1 + AdvDupe2.RemoveProgressBar() + RunConsoleCommand("AdvDupe2_RecieveFile", status, data) + break + end + RunConsoleCommand("AdvDupe2_RecieveFile", status, data) + end +end + +usermessage.Hook("AdvDupe2_RecieveNextStep",function(um) + SendFileToServer(um:ReadShort(), um:ReadShort()) +end) + +usermessage.Hook("AdvDupe2_UploadRejected",function(um) + AdvDupe2.File = nil + AdvDupe2.LastPos = nil + AdvDupe2.Length = nil + if(um:ReadBool())then AdvDupe2.RemoveProgressBar() end +end) \ No newline at end of file diff --git a/lua/advdupe2/nullesc.lua b/lua/advdupe2/nullesc.lua new file mode 100644 index 0000000..826e031 --- /dev/null +++ b/lua/advdupe2/nullesc.lua @@ -0,0 +1,54 @@ +--[[ + Title: Null Escaper + + Desc: Escapes null characters. + + Author: AD2 Team + + Version: 1.0 +]] + +local char = string.char +local find = string.find +local gsub = string.gsub +local match = string.match + +local Null = {} + +local escseq = { --no palindromes + "bbq", + "wtf", + "cat", + "car", + "bro", + "moo", + "sky", +} + +function Null.esc(str) + local genseq + for i=1,#escseq do + if not find(str, escseq[i]) then + local genseq = escseq[i] + return genseq.."\n"..gsub(str,"%z",genseq) + end + end + for i=30,200 do + genseq = char(i, i-1, i+1) + if not find(str, genseq) then + return genseq.."\n"..gsub(str,"%z",genseq) + end + genseq = char(i, i, i+1) + if not find(str, genseq) then + return genseq.."\n"..gsub(str,"%z",genseq) + end + end + error("nullesc could not escape the string") +end + +function Null.invesc(str) + local delim,huff = match(str,"^(.-)\n(.-)$") + return gsub(huff,delim,"\0") +end + +AdvDupe2.Null = Null diff --git a/lua/advdupe2/sv_clipboard.lua b/lua/advdupe2/sv_clipboard.lua new file mode 100644 index 0000000..6fd9434 --- /dev/null +++ b/lua/advdupe2/sv_clipboard.lua @@ -0,0 +1,1283 @@ +--[[ + Title: Adv. Duplicator 2 Module + + Desc: Provides advanced duplication functionality for the Adv. Dupe 2 tool. + + Author: TB + + Version: 1.0 +]] + +require "duplicator" + +AdvDupe2.duplicator = {} +//AdvDupe2.AllowNPCs = false --Allow to paste NPCs +AdvDupe2.JobManager = {} +AdvDupe2.JobManager.PastingHook = false +AdvDupe2.JobManager.Queue = {} + +local constraints = {Weld=true, Axis=true, Ballsocket=true, Elastic=true, Hydraulic=true, Motor=true, Muscle=true, Pulley=true, Rope=true, Slider=true, Winch=true} + + +--[[ + Name: CopyEntTable + Desc: Returns a copy of the passed entity's table + Params: Ent + Returns: enttable +]] + +/*--------------------------------------------------------- + Returns a copy of the passed entity's table +---------------------------------------------------------*/ +local function CopyEntTable( Ent, Offset ) + + local Tab = {} + + if Ent.PreEntityCopy then + Ent:PreEntityCopy() + end + + local EntityClass = duplicator.FindEntityClass( Ent:GetClass() ) + + if EntityClass then + + local EntTable = table.Copy(Ent:GetTable()) + local Arg + for iNumber, Key in pairs( EntityClass.Args ) do + -- Translate keys from old system + if ( Key == "pos" or Key == "position" ) then Key = "Pos" end + if ( Key == "ang" or Key == "Ang" or Key == "angle" ) then Key = "Angle" end + if ( Key == "model" ) then Key = "Model" end + + Arg = EntTable[ Key ] + + -- Special keys + --if ( Key == "Data" ) then Arg = EntTable end + + -- If there's a missing argument then unpack will stop sending at that argument + if Arg == nil then Arg = false end + + Tab[ Key ] = Arg + + end + + end + + Tab.BoneMods = table.Copy( Ent.BoneMods ) + if(Ent.EntityMods)then + Tab.EntityMods = Ent.EntityMods + end + + if Ent.PostEntityCopy then + Ent:PostEntityCopy() + end + + Tab.Pos = Ent:GetPos() + Tab.Angle = nil + Tab.Class = Ent:GetClass() + Tab.Model = Ent:GetModel() + Tab.Skin = Ent:GetSkin() + if(Tab.Skin==0)then Tab.Skin = nil end + Tab.CollisionGroup = Ent:GetCollisionGroup() + if(Tab.CollisionGroup==0)then Tab.CollisionGroup = nil end + + if(Tab.Class == "gmod_cameraprop")then + Tab.key = Ent:GetNetworkedInt("key") + end + -- Allow the entity to override the class + -- This is a hack for the jeep, since it's real class is different from the one it reports as + -- (It reports a different class to avoid compatibility problems) + if Ent.ClassOverride then Tab.Class = Ent.ClassOverride end + + Tab.PhysicsObjects = {} + + -- Physics Objects + local iNumPhysObjects = Ent:GetPhysicsObjectCount() + local PhysObj + + for Bone = 0, iNumPhysObjects-1 do + PhysObj = Ent:GetPhysicsObjectNum( Bone ) + if PhysObj!=nil then + Tab.PhysicsObjects[ Bone ] = Tab.PhysicsObjects[ Bone ] or {} + if(PhysObj:IsMoveable())then Tab.PhysicsObjects[ Bone ].Frozen = true end + PhysObj:EnableMotion(false) + Tab.PhysicsObjects[ Bone ].Pos = PhysObj:GetPos() - Tab.Pos + Tab.PhysicsObjects[ Bone ].Angle = PhysObj:GetAngle() + end + end + + Tab.PhysicsObjects[0].Pos = Tab.Pos - Offset + + Tab.Pos = nil + if(Tab.Class!="prop_physics")then + if(!Tab.BuildDupeInfo)then Tab.BuildDupeInfo = {} end + Tab.BuildDupeInfo.IsNPC = Ent:IsNPC() + Tab.BuildDupeInfo.IsVehicle = Ent:IsVehicle() + end + if( IsValid(Ent:GetParent()) ) then + if(!Tab.BuildDupeInfo)then Tab.BuildDupeInfo = {} end + Tab.PhysicsObjects[ 0 ].Angle = Ent:GetAngles() + Tab.BuildDupeInfo.DupeParentID = Ent.Entity:GetParent():EntIndex() + end + + -- Flexes + local FlexNum = Ent:GetFlexNum() + Tab.Flex = Tab.Flex or {} + local weight + local flexes = false + for i = 0, FlexNum do + weight = Ent:GetFlexWeight( i ) + if(weight!=0)then + Tab.Flex[ i ] = Ent:GetFlexWeight( i ) + flexes = true + end + end + + if(flexes)then + Tab.FlexScale = Ent:GetFlexScale() + else + Tab.Flex = nil + end + + // Make this function on your SENT if you want to modify the + // returned table specifically for your entity. + if Ent.OnEntityCopyTableFinish then + Ent:OnEntityCopyTableFinish( Tab ) + end + + return Tab + +end + + +--[[ + Name: CopyConstraintTable + Desc: Create a table for constraints + Params:
Constraints + Returns:
Constraints,
Entities +]] + +/*Still not finished: + +*/ +local function CopyConstraintTable( Const, Offset ) + + local Constraint = {} + local Entities = {} + + if(Const!=nil)then + Const.Constraint = nil + Const.OnDieFunctions=nil + Constraint.Entity={} + local Type = duplicator.ConstraintType[ Const.Type ] + + if ( Type ) then + for k, key in pairs( Type.Args ) do + if(!string.find(key, "Ent") and !string.find(key, "Bone"))then + Constraint[key] = Const[ key ] + end + end + + if((Const["Ent"] && Const["Ent"]:IsWorld()) || IsValid(Const["Ent"]))then + Constraint.Entity[ 1 ] = {} + Constraint.Entity[ 1 ].Index = Const["Ent"]:EntIndex() + if(!Const["Ent"]:IsWorld())then table.insert( Entities, Const["Ent"] ) end + else + local ent + for i=1,4 do + ent = "Ent"..i + + if((Const[ent] && Const[ent]:IsWorld()) || IsValid(Const[ent]))then + Constraint.Entity[ i ] = {} + Constraint.Entity[ i ].Index = Const[ent]:EntIndex() + Constraint.Entity[ i ].Bone = Const[ "Bone"..i ] + Constraint.Entity[ i ].Length = Const[ "Length"..i ] + Constraint.Entity[ i ].World = Const[ "World"..i ] + + if Const[ ent ]:IsWorld() then + Constraint.Entity[ i ].World = true + if ( Const[ "LPos"..i ] ) then + if(i!= 4 and i!=2)then + if(Const["Ent2"])then + Constraint.Entity[ i ].LPos = Const[ "LPos"..i ] - Const["Ent2"]:GetPos() + Constraint[ "LPos"..i ] = Const[ "LPos"..i ] - Const["Ent2"]:GetPos() + elseif(Const["Ent4"])then + Constraint.Entity[ i ].LPos = Const[ "LPos"..i ] - Const["Ent4"]:GetPos() + Constraint[ "LPos"..i ] = Const[ "LPos"..i ] - Const["Ent4"]:GetPos() + end + elseif(Const["Ent1"])then + Constraint.Entity[ i ].LPos = Const[ "LPos"..i ] - Const["Ent1"]:GetPos() + Constraint[ "LPos"..i ] = Const[ "LPos"..i ] - Const["Ent1"]:GetPos() + end + else + Constraint.Entity[ i ].LPos = Offset + Constraint[ "LPos"..i ] = Offset + end + else + Constraint.Entity[ i ].LPos = Const[ "LPos"..i ] + Constraint.Entity[ i ].WPos = Const[ "WPos"..i ] + end + + if(!Const[ent]:IsWorld())then table.insert( Entities, Const[ent] ) end + end + + if(Const["WPos"..i])then + if(!Const["Ent1"]:IsWorld())then + Constraint["WPos"..i] = Const[ "WPos"..i ] - Const["Ent1"]:GetPos() + else + Constraint["WPos"..i] = Const[ "WPos"..i ] - Const["Ent4"]:GetPos() + end + end + end + end + + Constraint.Type = Const.Type + Constraint.Identity = Const.Identity + if(Const.BuildDupeInfo)then Constraint.BuildDupeInfo = table.Copy(Const.BuildDupeInfo) end + end + + end + + return Constraint, Entities + +end + +--[[ + Name: Copy + Desc: Copy an entity and all entities constrained + Params: Entity + Returns:
Entities,
Constraints +]] +local function Copy( Ent, EntTable, ConstraintTable, Offset ) + + + local phys + + local index = Ent:EntIndex() + + EntTable[index] = CopyEntTable(Ent, Offset) + + if ( !constraint.HasConstraints( Ent ) ) then + local PhysObjs = EntTable[Ent:EntIndex()].PhysicsObjects + for i=0, Ent:GetPhysicsObjectCount() do + phys = Ent:GetPhysicsObjectNum(i) + if(IsValid(phys))then + phys:EnableMotion(PhysObjs[i].Frozen) + end + end + return EntTable, ConstraintTable + end + + local index + for k, Constraint in pairs( Ent.Constraints ) do + + /*if(!Constraint.BuildDupeInfo)then + Constraint.BuildDupeInfo = {} + end*/ + + if(!Constraint.Identity)then + index = Constraint:GetCreationID() + Constraint.Identity = Constraint:GetCreationID() + else + index = Constraint.Identity + end + + if ( index and !ConstraintTable[ index ] ) then + local ConstTable, ents = CopyConstraintTable( table.Copy(Constraint:GetTable()), Offset ) + ConstraintTable[ index ] = ConstTable + + for j,e in pairs(ents) do + if ( e and ( e:IsWorld() or e:IsValid() ) ) and ( !EntTable[ e:EntIndex() ] ) then + Copy( e, EntTable, ConstraintTable, Offset ) + end + end + end + + end + + local PhysObjs = EntTable[Ent:EntIndex()].PhysicsObjects + for i=0, Ent:GetPhysicsObjectCount() do + phys = Ent:GetPhysicsObjectNum(i) + if(IsValid(phys))then + phys:EnableMotion(PhysObjs[i].Frozen) + end + end + + return EntTable, ConstraintTable +end +AdvDupe2.duplicator.Copy = Copy + +--[[ + Name: LoadSents + Desc: Loads the entities list and the whitelist for spawning props + Params: + Returns: +]] +local function LoadSents() + AdvDupe2.duplicator.EntityList = {prop_physics=true, prop_ragdoll=true, prop_vehicle_prisoner_pod=true, prop_vehicle_airboat=true, prop_vehicle_jeep=true, prop_vehicle_jeep_old=true, phys_magnet=true, prop_effect=true} + AdvDupe2.duplicator.WhiteList = {prop_physics=true, prop_ragdoll=true, prop_vehicle_prisoner_pod=true, prop_vehicle_airboat=true, prop_vehicle_jeep=true, prop_vehicle_jeep_old=true, phys_magnet=true, prop_effect=true} + local exclusion = {prop_effect= true, gmod_player_start=true, gmod_ghost=true, lua_run=true}//, gmod_wire_hologram=true} + for _,v in pairs(scripted_ents.GetList( )) do + if _:sub(1,4) == "base" then continue end + if _:sub(1,4) == "info" then continue end + if _:sub(1,4) == "func" then continue end + if exclusion[_] then continue end + if v.t.AdminSpawnable and !v.t.Spawnable then + AdvDupe2.duplicator.EntityList[_] = false + else + AdvDupe2.duplicator.EntityList[_] = true + end + AdvDupe2.duplicator.WhiteList[_] = true + end +end +concommand.Add("advdupe2_reloadwhitelist", LoadSents) +hook.Add( "InitPostEntity", "LoadDuplicatingEntities", LoadSents) + +--[[ + Name: AreaCopy + Desc: Copy based on a box + Params: Entity + Returns:
Entities,
Constraints +]] +//Ghosts are a problem and entities need to be returned by there index, not seqeuntial +//Also need to make a get entities function for constraints +function AdvDupe2.duplicator.AreaCopy( Entities, Offset, CopyOutside ) + local EntTable = {} + local ConstraintTable = {} + + for _,Ent in pairs(Entities)do + + local phys + + local index = Ent:EntIndex() + EntTable[index] = CopyEntTable(Ent, Offset) + + if ( !constraint.HasConstraints( Ent ) ) then + local PhysObjs = EntTable[Ent:EntIndex()].PhysicsObjects + for i=0, Ent:GetPhysicsObjectCount() do + phys = Ent:GetPhysicsObjectNum(i) + if(IsValid(phys))then + phys:EnableMotion(PhysObjs[i].Frozen) //Restore the frozen state of the entity and all of its objects + end + end + continue + end + + local index + local add + for k, Constraint in pairs( Ent.Constraints ) do + + /*if(!Constraint.BuildDupeInfo)then + Constraint.BuildDupeInfo = {} + end*/ + + if(!Constraint.Identity)then + index = Constraint:GetCreationID() + Constraint.Identity = Constraint:GetCreationID() + else + index = Constraint.Identity + end + + if ( index and !ConstraintTable[ index ] ) then + local ConstTable, ents = CopyConstraintTable( table.Copy(Constraint:GetTable()), Offset ) + //If the entity is constrained to an entity outside of the area box, don't copy the constraint. + if(!CopyOutside)then + add = true + for j,e in pairs(ents)do + if(!Entities[e:EntIndex()])then add=false end + end + if(add)then ConstraintTable[ index ] = ConstTable end + else //Copy entities and constraints outside of the box that are constrained to entities inside the box + for k,v in pairs(ents)do + ConstraintTable[ index ] = ConstTable + if(v:EntIndex()!=_)then + local AddEnts, AddConstrs = Copy(v, {}, {}, Offset) + for j,e in pairs(AddEnts)do + if(!EntTable[j])then EntTable[j] = e end + end + + for j,e in pairs(AddConstrs)do + if(!ConstraintTable[j])then ConstraintTable[j] = e end + end + end + end + end + end + end + + local PhysObjs = EntTable[Ent:EntIndex()].PhysicsObjects + for i=0, Ent:GetPhysicsObjectCount() do + phys = Ent:GetPhysicsObjectNum(i) + if(IsValid(phys))then + phys:EnableMotion(PhysObjs[i].Frozen) //Restore the frozen state of the entity and all of its objects + end + end + + end + + return EntTable, ConstraintTable +end + +--[[ + Name: CreateConstraintFromTable + Desc: Creates a constraint from a given table + Params:
Constraint,
EntityList,
EntityTable + Returns: CreatedConstraint +]] +local function CreateConstraintFromTable(Constraint, EntityList, EntityTable, Player) + + local Factory = duplicator.ConstraintType[ Constraint.Type ] + + if not Factory then return end + + local first --Ent1 or Ent in the constraint's table + local second --Any other Ent that is not Ent1 or Ent + + -- Build the argument list for the Constraint's spawn function + local Args = {} + local Val + for k, Key in pairs( Factory.Args ) do + + Val = Constraint[ Key ] + + if Key == "pl" or Key == "ply" then + Val = Player + end + + for i=1, 4 do + -- if(!ValidEntity(Constraint.Entity[i])) then Player:ChatPrint("DUPLICATOR: ERROR, Invalid constraints, maybe wrong file version.")return end + if ( Constraint.Entity and Constraint.Entity[ i ] ) then + if Key == "Ent"..i or Key == "Ent" then + if ( Constraint.Entity[ i ].World ) then + Val = GetWorldEntity() + else + Val = EntityList[ Constraint.Entity[ i ].Index ] + + if not ValidEntity(Val) then + if(Player)then + Player:ChatPrint("DUPLICATOR: ERROR, "..Constraint.Type.." Constraint could not find an entity!") + else + print("DUPLICATOR: ERROR, "..Constraint.Type.." Constraint could not find an entity!") + end + return + else + --Important for perfect duplication + --Get which entity is which so we can reposition them before constraining + if(Key== "Ent" || Key == "Ent1")then + first=Val + firstindex = Constraint.Entity[ i ].Index + else + second=Val + secondindex = Constraint.Entity[ i ].Index + end + + end + end + + end + + if Key == "Bone"..i or Key == "Bone" then Val = Constraint.Entity[ i ].Bone end + + if Key == "LPos"..i then + if (Constraint.Entity[i].World && Constraint.Entity[i].LPos)then + if(i==2 || i==4)then + Val = Constraint.Entity[i].LPos + EntityList[Constraint.Entity[1].Index]:GetPos() + elseif(i==1)then + if(Constraint.Entity[2])then + Val = Constraint.Entity[i].LPos + EntityList[Constraint.Entity[2].Index]:GetPos() + else + Val = Constraint.Entity[i].LPos + EntityList[Constraint.Entity[4].Index]:GetPos() + end + end + elseif( Constraint.Entity[i].LPos ) then + Val = Constraint.Entity[ i ].LPos + end + end + + if Key == "Length"..i then Val = Constraint.Entity[ i ].Length end + end + if Key == "WPos"..i then + if(!Constraint.Entity[1].World)then + Val = Constraint["WPos"..i] + EntityList[Constraint.Entity[1].Index]:GetPos() + else + Val = Constraint["WPos"..i] + EntityList[Constraint.Entity[4].Index]:GetPos() + end + end + + end + -- If there's a missing argument then unpack will stop sending at that argument + Val = Val or false + table.insert( Args, Val ) + + end + + local Bone1 + local Bone1Index + local ReEnableFirst + local Bone2 + local Bone2Index + local ReEnableSecond + if(Constraint.BuildDupeInfo)then + + if second ~= nil and not second:IsWorld() and Constraint.BuildDupeInfo.EntityPos ~= nil then + ReEnableSecond = second:GetPhysicsObject():IsMoveable() + second:GetPhysicsObject():EnableMotion(false) + second:SetPos(first:GetPos()-Constraint.BuildDupeInfo.EntityPos) + if(Constraint.BuildDupeInfo.Bone2) then + Bone2Index = Constraint.BuildDupeInfo.Bone2 + Bone2 = second:GetPhysicsObjectNum(Bone2Index) + Bone2:EnableMotion(false) + Bone2:SetPos(second:GetPos() + Constraint.BuildDupeInfo.Bone2Pos) + Bone2:SetAngle(Constraint.BuildDupeInfo.Bone2Angle) + end + end + + if first ~= nil and Constraint.BuildDupeInfo.Ent1Ang ~= nil then + ReEnableFirst = first:GetPhysicsObject():IsMoveable() + first:GetPhysicsObject():EnableMotion(false) + first:SetAngles(Constraint.BuildDupeInfo.Ent1Ang) + if(Constraint.BuildDupeInfo.Bone1) then + Bone1Index = Constraint.BuildDupeInfo.Bone1 + Bone1 = first:GetPhysicsObjectNum(Bone1Index) + Bone1:EnableMotion(false) + Bone1:SetPos(first:GetPos() + Constraint.BuildDupeInfo.Bone1Pos) + Bone1:SetAngle(Constraint.BuildDupeInfo.Bone1Angle) + end + end + + if second ~= nil and Constraint.BuildDupeInfo.Ent2Ang ~= nil then + second:SetAngles(Constraint.BuildDupeInfo.Ent2Ang) + end + + if second ~= nil and Constraint.BuildDupeInfo.Ent4Ang ~= nil then + second:SetAngles(Constraint.BuildDupeInfo.Ent4Ang) + end + end + + local Ent + local status = pcall( function() Ent = Factory.Func( unpack(Args) ) end ) + if not status then + if(Player)then + AdvDupe2.Notify(ply, "ERROR, Failed to create "..Constraint.Type.." Constraint!", NOTIFY_ERROR) + else + print("DUPLICATOR: ERROR, Failed to create "..Constraint.Type.." Constraint!") + end + return + end + + Ent.BuildDupeInfo = table.Copy(Constraint.BuildDupeInfo) + + //Move the entities back after constraining them + if(EntityTable)then + if(first!=nil)then + first:SetPos(EntityTable[firstindex].BuildDupeInfo.PosReset) + first:SetAngles(EntityTable[firstindex].BuildDupeInfo.AngleReset) + if(ReEnableFirst)then first:GetPhysicsObject():EnableMotion(true) end + if(Bone1)then + Bone1:SetPos(EntityTable[firstindex].BuildDupeInfo.PosReset + EntityTable[firstindex].BuildDupeInfo.PhysicsObjects[Bone1Index].Pos) + Bone1:SetAngle(EntityTable[firstindex].PhysicsObjects[Bone1Index].Angle) + end + end + if(second!=nil)then + second:SetPos(EntityTable[secondindex].BuildDupeInfo.PosReset) + second:SetAngles(EntityTable[secondindex].BuildDupeInfo.AngleReset) + if(ReEnableSecond)then second:GetPhysicsObject():EnableMotion(true) end + if(Bone2)then + Bone2:SetPos(EntityTable[secondindex].BuildDupeInfo.PosReset + EntityTable[secondindex].BuildDupeInfo.PhysicsObjects[Bone2Index].Pos) + Bone2:SetAngle(EntityTable[secondindex].PhysicsObjects[Bone2Index].Angle) + end + end + end + + if(Ent and Ent.length)then Ent.length = Constraint["length"] end //Fix for weird bug with ropes + + return Ent +end + +--[[ + Name: DoGenericPhysics + Desc: Applies bone data, generically. + Params: Player,
data + Returns: Entity,
data +]] +local function DoGenericPhysics( Entity, data, Player ) + + if (!data) then return end + if (!data.PhysicsObjects) then return end + local Phys + for Bone, Args in pairs( data.PhysicsObjects ) do + Phys = Entity:GetPhysicsObjectNum(Bone) + if ( IsValid(Phys) ) then + Phys:SetPos( Args.Pos ) + Phys:SetAngle( Args.Angle ) + if ( Args.Frozen == true ) then + Phys:EnableMotion( false ) + if(Player)then Player:AddFrozenPhysicsObject( Entity, Phys ) end + end + end + end +end + +local function reportclass(ply,class) + umsg.Start("AdvDupe2_ReportClass", ply) + umsg.String(class) + umsg.End() +end + +local function reportmodel(ply,model) + umsg.Start("AdvDupe2_ReportModel", ply) + umsg.String(model) + umsg.End() +end + +--[[ + Name: GenericDuplicatorFunction + Desc: Override the default duplicator's GenericDuplicatorFunction function + Params: Player,
data + Returns: Entity +]] +local function GenericDuplicatorFunction( data, Player ) + + local Entity = ents.Create( data.Class ) + if ( !ValidEntity(Entity) ) then + if(Player)then + reportclass(Player,data.Class) + else + print("Advanced Duplicator 2 Invalid Class: "..data.Class) + end + return nil + end + + if( !util.IsValidModel(data.Model) )then + if(Player)then + reportmodel(Player,data.Model) + else + print("Advanced Duplicator 2 Invalid Model: "..data.Model) + end + return nil + end + + duplicator.DoGeneric( Entity, data ) + Entity:Spawn() + Entity:Activate() + DoGenericPhysics( Entity, data, Player ) + + table.Add( Entity:GetTable(), data ) + + return Entity + +end + +--[[ + Name: MakeProp + Desc: Make prop without spawn effects + Params: Player, Pos, Ang, Model,
PhysicsObject,
Data + Returns: Prop +]] +local function MakeProp(Player, Pos, Ang, Model, PhysicsObject, Data) + + if( !util.IsValidModel(Model) )then + if(Player)then + reportmodel(Player,Data.Model) + else + print("Advanced Duplicator 2 Invalid Model: "..Model) + end + return nil + end + + Data.Pos = Pos + Data.Angle = Ang + Data.Model = Model + + // Make sure this is allowed + if( Player )then + if ( !gamemode.Call( "PlayerSpawnProp", Player, Model ) ) then return false end + end + + local Prop = ents.Create( "prop_physics" ) + if !IsValid(Prop) then return false end + + duplicator.DoGeneric( Prop, Data ) + Prop:Spawn() + Prop:Activate() + DoGenericPhysics( Prop, Data, Player ) + duplicator.DoFlex( Prop, Data.Flex, Data.FlexScale ) + + return Prop +end + +--[[ + Name: CreateEntityFromTable + Desc: Creates an entity from a given table + Params:
EntTable, Player + Returns: nil +]] +local function CreateEntityFromTable(EntTable, Player) + + local EntityClass = duplicator.FindEntityClass( EntTable.Class ) + if ( !Player:IsAdmin( ) && !Player:IsSuperAdmin() && !SinglePlayer())then + if(!AdvDupe2.duplicator.EntityList[EntTable.Class])then + Player:ChatPrint([[Entity Class Black listed, "]]..EntTable.Class..[["]]) + return nil + end + end + + local sent = false + local status, valid + local GENERIC = false + // This class is unregistered. Instead of failing try using a generic + // Duplication function to make a new copy. + if (!EntityClass) then + GENERIC = true + sent = true + + if(EntTable.Class=="prop_effect")then + sent = gamemode.Call( "PlayerSpawnEffect", Player, EntTable.Model) + else + sent = gamemode.Call( "PlayerSpawnSENT", Player, EntTable.Class) + end + + if(!sent)then + print("Advanced Duplicator 2: Creation rejected for class, : "..EntTable.Class) + return nil + end + + if(AdvDupe2.duplicator.WhiteList[EntTable.Class] || (EntTable.BuildDupeInfo.IsNPC && ((tobool(GetConVarString("AdvDupe2_AllowNPCPasting")) && string.sub(EntTable.Class, 1, 4)=="npc_") || SinglePlayer())))then + status, valid = pcall(GenericDuplicatorFunction, EntTable, Player ) + else + print("Advanced Duplicator 2: ENTITY CLASS IS BLACKLISTED, CLASS NAME: "..EntTable.Class) + return nil + end + end + + if(!GENERIC)then + + // Build the argument list for the Entitie's spawn function + local ArgList = {} + local Arg + for iNumber, Key in pairs( EntityClass.Args ) do + + Arg = nil + + // Translate keys from old system + if ( Key == "pos" || Key == "position" ) then Key = "Pos" end + if ( Key == "ang" || Key == "Ang" || Key == "angle" ) then Key = "Angle" end + if ( Key == "model" ) then Key = "Model" end + if ( Key == "VehicleTable" )then //Exploit fix, not sure if its still an exploit + EntTable[Key] = {vehiclescript=EntTable[Key].vehiclescript, limitview=EntTable[Key].limitview} + end + Arg = EntTable[ Key ] + + // Special keys + if ( Key == "Data" ) then Arg = EntTable end + + // If there's a missing argument then unpack will stop sending at that argument + if ( Arg == nil ) then Arg = false end + + ArgList[ iNumber ] = Arg + + end + // Create and return the entity + if(EntTable.Class=="prop_physics")then + valid = MakeProp(Player, unpack(ArgList)) //Create prop_physics like this because if the model doesn't exist it will cause + elseif(AdvDupe2.duplicator.WhiteList[EntTable.Class] || (EntTable.BuildDupeInfo.IsNPC && ((tobool(GetConVarString("AdvDupe2_AllowNPCPasting")) && string.sub(EntTable.Class, 1, 4)=="npc_") || SinglePlayer())))then + //Create sents using their spawn function with the arguments we stored earlier + sent = true + + if(!EntTable.BuildDupeInfo.IsVehicle || !EntTable.BuildDupeInfo.IsNPC || EntTable.Class!="prop_ragdoll")then //These three are auto done + sent = gamemode.Call( "PlayerSpawnSENT", Player, EntTable.Class) + end + + if(!sent)then + print("Advanced Duplicator 2: Creation rejected for class, : "..EntTable.Class) + return nil + end + + status,valid = pcall( EntityClass.Func, Player, unpack(ArgList) ) + else + print("Advanced Duplicator 2: ENTITY CLASS IS BLACKLISTED, CLASS NAME: "..EntTable.Class) + return nil + end + end + + //If its a valid entity send it back to the entities list so we can constrain it + if( status!=false and IsValid(valid) )then + if(sent)then + local iNumPhysObjects = valid:GetPhysicsObjectCount() + local PhysObj + for Bone = 0, iNumPhysObjects-1 do + PhysObj = valid:GetPhysicsObjectNum( Bone ) + if IsValid(PhysObj) then + PhysObj:EnableMotion(false) + end + end + if(Player)then + if(!valid:IsVehicle() && EntTable.Class!="prop_ragdoll" && !valid:IsNPC())then //These three get called automatically + if(EntTable.Class=="prop_effect")then + gamemode.Call("PlayerSpawnedEffect", Player, valid:GetModel(), valid) + else + gamemode.Call("PlayerSpawnedSENT", Player, valid) + end + end + end + else + gamemode.Call( "PlayerSpawnedProp", Player, valid:GetModel(), valid ) + end + + return valid + else + if(valid==false)then + return false + else + return nil + end + end + +end + +--[[ + Name: Paste + Desc: Override the default duplicator's paste function + Params: Player,
Entities,
Constraints + Returns:
Entities,
Constraints +]] + +function AdvDupe2.duplicator.Paste( Player, EntityList, ConstraintList, Position, AngleOffset, OrigPos, Parenting ) + + table.SortByMember(ConstraintList, "Identity", function(a, b) return a > b end) + + local CreatedEntities = {} + -- + -- Create entities + -- + local proppos + for k, v in pairs( EntityList ) do + if(!v.BuildDupeInfo)then v.BuildDupeInfo={} end + v.BuildDupeInfo.PhysicsObjects = table.Copy(v.PhysicsObjects) + proppos = v.PhysicsObjects[0].Pos + v.BuildDupeInfo.PhysicsObjects[0].Pos = Vector(0,0,0) + if( OrigPos )then + for i,p in pairs(v.BuildDupeInfo.PhysicsObjects) do + v.PhysicsObjects[i].Pos = p.Pos + proppos + OrigPos + v.PhysicsObjects[i].Frozen = true + end + v.Pos = v.PhysicsObjects[0].Pos + v.Angle = v.PhysicsObjects[0].Angle + v.BuildDupeInfo.PosReset = v.Pos + v.BuildDupeInfo.AngleReset = v.Angle + else + for i,p in pairs(v.BuildDupeInfo.PhysicsObjects)do + v.PhysicsObjects[i].Pos, v.PhysicsObjects[i].Angle = LocalToWorld(p.Pos + proppos, p.Angle, Position, AngleOffset) + v.PhysicsObjects[i].Frozen = true + end + v.Pos = v.PhysicsObjects[0].Pos + v.BuildDupeInfo.PosReset = v.Pos + v.Angle = v.PhysicsObjects[0].Angle + v.BuildDupeInfo.AngleReset = v.Angle + end + + CreatedEntities[k] = CreateEntityFromTable(v, Player) + + if CreatedEntities[ k ] then + if(Player)then Player:AddCleanup( "AdvDupe2", CreatedEntities[ k ] ) end + CreatedEntities[ k ].BoneMods = table.Copy( v.BoneMods ) + CreatedEntities[ k ].EntityMods = table.Copy( v.EntityMods ) + CreatedEntities[ k ].PhysicsObjects = table.Copy( v.PhysicsObjects ) + if(v.CollisionGroup)then CreatedEntities[ k ]:SetCollisionGroup(v.CollisionGroup) end + duplicator.ApplyEntityModifiers ( Player, CreatedEntities[ k ] ) + duplicator.ApplyBoneModifiers ( Player, CreatedEntities[ k ] ) + CreatedEntities[k]:SetNotSolid(true) + elseif(CreatedEntities[ k ]==false)then + CreatedEntities[ k ] = nil + ConstraintList = {} + break + else + CreatedEntities[ k ] = nil + end + + end + + local CreatedConstraints = {} + local Entity + -- + -- Create constraints + -- + + for k, Constraint in pairs( ConstraintList ) do + Entity = CreateConstraintFromTable( Constraint, CreatedEntities, EntityList, Player ) + if(IsValid(Entity))then + table.insert( CreatedConstraints, Entity ) + end + end + + if(Player)then + + undo.Create "AdvDupe2_Paste" + + for _,v in pairs( CreatedEntities ) do + --If the entity has a PostEntityPaste function tell it to use it now + if v.PostEntityPaste then + v:PostEntityPaste( Player, v, CreatedEntities ) + end + v:GetPhysicsObject():EnableMotion(false) + + if(EntityList[_].BuildDupeInfo.DupeParentID!=nil)then + v:SetParent(CreatedEntities[EntityList[_].BuildDupeInfo.DupeParentID]) + end + + v:SetNotSolid(false) + undo.AddEntity( v ) + end + + undo.SetPlayer( Player ) + + undo.Finish() + + //if(Tool)then AdvDupe2.FinishPasting(Player, true) end + + else + + for _,v in pairs( CreatedEntities ) do + --If the entity has a PostEntityPaste function tell it to use it now + if v.PostEntityPaste then + v:PostEntityPaste( Player, v, CreatedEntities ) + end + v:GetPhysicsObject():EnableMotion(false) + + if(EntityList[_].BuildDupeInfo.DupeParentID!=nil && Parenting)then + v:SetParent(CreatedEntities[EntityList[_].BuildDupeInfo.DupeParentID]) + end + + v:SetNotSolid(false) + end + end + + return CreatedEntities, CreatedConstraints + +end + + +local function AdvDupe2_Spawn() + + local Queue = AdvDupe2.JobManager.Queue[AdvDupe2.JobManager.CurrentPlayer] + + if(IsValid(Queue.Player))then + if(Queue.Entity)then + if(Queue.Current==1)then + + AdvDupe2.InitProgressBar(Queue.Player,"Pasting:") + Queue.Player.AdvDupe2.Queued = false + end + local newpos + if(Queue.Current>#Queue.SortedEntities)then + Queue.Entity = false + Queue.Constraint = true + Queue.Current = 1 + return + end + if(!Queue.SortedEntities[Queue.Current])then Queue.Current = Queue.Current+1 return end + + local k = Queue.SortedEntities[Queue.Current] + local v = Queue.EntityList[k] + + if(!v.BuildDupeInfo)then v.BuildDupeInfo={} end + if(v.LocalPos)then + for i,p in pairs(v.PhysicsObjects) do + v.PhysicsObjects[i] = {Pos=v.LocalPos, Angle=v.LocalAngle} + end + end + + v.BuildDupeInfo.PhysicsObjects = table.Copy(v.PhysicsObjects) + proppos = v.PhysicsObjects[0].Pos + v.BuildDupeInfo.PhysicsObjects[0].Pos = Vector(0,0,0) + if( Queue.OrigPos )then + for i,p in pairs(v.BuildDupeInfo.PhysicsObjects) do + v.PhysicsObjects[i].Pos = p.Pos + proppos + Queue.OrigPos + v.PhysicsObjects[i].Frozen = true + end + v.Pos = v.PhysicsObjects[0].Pos + v.Angle = v.PhysicsObjects[0].Angle + v.BuildDupeInfo.PosReset = v.Pos + v.BuildDupeInfo.AngleReset = v.Angle + else + for i,p in pairs(v.BuildDupeInfo.PhysicsObjects)do + + v.PhysicsObjects[i].Pos, v.PhysicsObjects[i].Angle = LocalToWorld(p.Pos + proppos, p.Angle, Queue.PositionOffset, Queue.AngleOffset) + v.PhysicsObjects[i].Frozen = true + end + v.Pos = v.PhysicsObjects[0].Pos + v.BuildDupeInfo.PosReset = v.Pos + v.Angle = v.PhysicsObjects[0].Angle + v.BuildDupeInfo.AngleReset = v.Angle + end + + //if(v.SavedParentIdx)then v.BuildDupeInfo.DupeParentID = v.SavedParentIdx end + Queue.CreatedEntities[k] = CreateEntityFromTable(v, Queue.Player) + + if Queue.CreatedEntities[ k ] then + Queue.Player:AddCleanup( "AdvDupe2", Queue.CreatedEntities[ k ] ) + Queue.CreatedEntities[ k ].BoneMods = table.Copy( v.BoneMods ) + Queue.CreatedEntities[ k ].EntityMods = table.Copy( v.EntityMods ) + Queue.CreatedEntities[ k ].PhysicsObjects = table.Copy( v.PhysicsObjects ) + if(v.CollisionGroup)then Queue.CreatedEntities[ k ]:SetCollisionGroup(v.CollisionGroup) end + duplicator.ApplyEntityModifiers ( Queue.Player, Queue.CreatedEntities[ k ] ) + duplicator.ApplyBoneModifiers ( Queue.Player, Queue.CreatedEntities[ k ] ) + Queue.CreatedEntities[k]:SetNotSolid(true) + elseif(Queue.CreatedEntities[ k ]==false)then + Queue.CreatedEntities[ k ] = nil + Queue.Entity = false + Queue.Constraint = true + Queue.Current = 1 + Queue.ConstraintList = {} + else + Queue.CreatedEntities[ k ] = nil + end + + local perc = math.floor((Queue.Percent*Queue.Current)*100) + AdvDupe2.UpdateProgressBar(Queue.Player,perc) + Queue.Current = Queue.Current+1 + if(Queue.Current>#Queue.SortedEntities)then + Queue.Entity = false + Queue.Constraint = true + Queue.Current = 1 + end + + if(#AdvDupe2.JobManager.Queue>=AdvDupe2.JobManager.CurrentPlayer+1)then + AdvDupe2.JobManager.CurrentPlayer = AdvDupe2.JobManager.CurrentPlayer+1 + else + AdvDupe2.JobManager.CurrentPlayer = 1 + end + else + if(#Queue.ConstraintList>0)then + + if(#AdvDupe2.JobManager.Queue==0)then + hook.Remove("Tick", "AdvDupe2_Spawning") + AdvDupe2.JobManager.PastingHook = false + end + if(!Queue.ConstraintList[Queue.Current])then Queue.Current = Queue.Current+1 return end + + local Entity = CreateConstraintFromTable( Queue.ConstraintList[Queue.Current], Queue.CreatedEntities, Queue.EntityList, Queue.Player ) + + if IsValid(Entity) then + table.insert( Queue.CreatedConstraints, Entity ) + end + elseif(Queue.ConstraintList && Queue.ConstraintList!={})then + local tbl = {} + for k,v in pairs(Queue.ConstraintList)do + table.insert(tbl, v) + end + Queue.ConstraintList = tbl + Queue.Current=0 + end + local perc = math.floor((Queue.Percent*(Queue.Current+Queue.Plus))*100) + AdvDupe2.UpdateProgressBar(Queue.Player,perc) + Queue.Current = Queue.Current+1 + + + if(Queue.Current>#Queue.ConstraintList)then + + local unfreeze = tobool(Queue.Player:GetInfo("advdupe2_paste_unfreeze")) or false + local preservefrozenstate = tobool(Queue.Player:GetInfo("advdupe2_preserve_freeze")) or false + + //Remove the undo for stopping pasting + local undos = undo.GetTable()[Queue.Player:UniqueID()] + local str = "AdvDupe2_"..Queue.Player:UniqueID() + for i=#undos, 1, -1 do + if(undos[i] && undos[i].Name == str)then + undos[i] = nil + umsg.Start( "Undone", Queue.Player ) + umsg.Long( i ) + umsg.End() + break + end + end + + undo.Create "AdvDupe2" + local phys + local edit + for _,v in pairs( Queue.CreatedEntities ) do + + edit = true + if(Queue.EntityList[_].BuildDupeInfo.DupeParentID!=nil && Queue.Parenting)then + v:SetParent(Queue.CreatedEntities[Queue.EntityList[_].BuildDupeInfo.DupeParentID]) + if(v.Constraints!=nil)then + for i,c in pairs(v.Constraints)do + if(c && constraints[c.Type])then + edit=false + break + end + end + end + if(edit)then + v:SetCollisionGroup(COLLISION_GROUP_WORLD) + v:GetPhysicsObject():EnableMotion(false) + v:GetPhysicsObject():Sleep() + end + else + edit=false + end + + --If the entity has a PostEntityPaste function tell it to use it now + if v.PostEntityPaste then + v:PostEntityPaste( Queue.Player, v, Queue.CreatedEntities ) + end + + if(unfreeze)then + for i=0, v:GetPhysicsObjectCount() do + phys = v:GetPhysicsObjectNum(i) + if(IsValid(phys))then + phys:EnableMotion(true) //Unfreeze the entitiy and all of its objects + end + end + elseif(preservefrozenstate)then + for i=0, v:GetPhysicsObjectCount() do + phys = v:GetPhysicsObjectNum(i) + if(IsValid(phys))then + phys:EnableMotion(Queue.EntityList[_].BuildDupeInfo.PhysicsObjects[i].Frozen) //Restore the entity and all of its objects to their original frozen state + end + end + else + for i=0, v:GetPhysicsObjectCount() do + phys = v:GetPhysicsObjectNum(i) + if(IsValid(phys) && phys:IsMoveable())then + phys:EnableMotion(false) //Freeze the entitiy and all of its objects + Queue.Player:AddFrozenPhysicsObject( v, phys ) + end + end + end + + if(!edit || !Queue.DisableParents)then + v:SetNotSolid(false) + end + undo.AddEntity( v ) + end + undo.SetPlayer( Queue.Player ) + undo.Finish() + + hook.Call("AdvDupe_FinishPasting", nil, {{EntityList=Queue.EntityList, CreatedEntities=Queue.CreatedEntities, ConstraintList=Queue.ConstraintList, CreatedConstraints=Queue.CreatedConstraints, HitPos=Queue.PositionOffset}}, 1) + AdvDupe2.FinishPasting(Queue.Player, true) + + table.remove(AdvDupe2.JobManager.Queue, AdvDupe2.JobManager.CurrentPlayer) + if(#AdvDupe2.JobManager.Queue==0)then + hook.Remove("Tick", "AdvDupe2_Spawning") + AdvDupe2.JobManager.PastingHook = false + end + end + if(#AdvDupe2.JobManager.Queue>=AdvDupe2.JobManager.CurrentPlayer+1)then + AdvDupe2.JobManager.CurrentPlayer = AdvDupe2.JobManager.CurrentPlayer+1 + else + AdvDupe2.JobManager.CurrentPlayer = 1 + end + + end + else + table.remove(AdvDupe2.JobManager.Queue, AdvDupe2.JobManager.CurrentPlayer) + if(#AdvDupe2.JobManager.Queue==0)then + hook.Remove("Tick", "AdvDupe2_Spawning") + AdvDupe2.JobManager.PastingHook = false + end + end +end + +local function ErrorCatchSpawning() + + local status, error = pcall(AdvDupe2_Spawn) + if(!status)then + //PUT ERROR LOGGING HERE + + local Queue = AdvDupe2.JobManager.Queue[AdvDupe2.JobManager.CurrentPlayer] + + local undos = undo.GetTable()[Queue.Player:UniqueID()] + local str = "AdvDupe2_"..Queue.Player:UniqueID() + for i=#undos, 1, -1 do + if(undos[i] && undos[i].Name == str)then + undos[i] = nil + umsg.Start( "Undone", Queue.Player ) + umsg.Long( i ) + umsg.End() + break + end + end + + for k,v in pairs(Queue.CreatedEntities)do + v:Remove() + end + Queue.Player:ChatPrint([[Error spawning your contraptions, "]]..error..[["]]) + AdvDupe2.FinishPasting(Queue.Player, true) + + table.remove(AdvDupe2.JobManager.Queue, AdvDupe2.JobManager.CurrentPlayer) + + + if(#AdvDupe2.JobManager.Queue==0)then + hook.Remove("Tick", "AdvDupe2_Spawning") + AdvDupe2.JobManager.PastingHook = false + else + if(#Queue ply + Return:
stamp +]] +function AdvDupe2.GenerateDupeStamp(ply) + local stamp = {} + stamp.name = ply:GetName() + stamp.time = os.date("%I:%M %p") + stamp.date = os.date("%d %B %Y") + stamp.timezone = os.date("%z") + hook.Call("AdvDupe2_StampGenerated",GAMEMODE,stamp) + return stamp +end + +local function makeInfo(tbl) + local info = "" + for k,v in pairs(tbl) do + info = concat{info,k,"\1",v,"\1"} + end + return info.."\2" +end + +local AD2FF = "AD2F%s\n%s\n%s" + +local period = CreateConVar("advdupe2_codec_pipeperiod",1,"Every this many ticks, the codec pipeline processor will run.",FCVAR_ARCHIVE,FCVAR_DONTRECORD) +local clock = 1 +local pipelines = {} +local function addPipeline(pipeline) + insert(pipelines,pipeline) +end +local function pipeproc() + if clock % period:GetInt() == 0 then + done = {} + for idx,pipeline in pairs(pipelines) do + local i = pipeline.idx + 1 + pipeline.idx = i + if i == pipeline.cbk then + done[#done+1] = idx + pipeline.info.size = #pipeline.eax + local success, err = pcall(pipeline[i], AD2FF:format(char(pipeline.REVISION), makeInfo(pipeline.info), pipeline.eax), unpack(pipeline.args)) + if not success then ErrorNoHalt(err) end + else + local success, err = pcall(pipeline[i], pipeline.eax) + if success then + pipeline.eax = err + else + ErrorNoHalt(err) + done[#done+1] = idx + end + end + end + sort(done) + for i = #done, 1, -1 do + remove(pipelines,done[i]) + end + clock = 1 + else + clock = clock + 1 + end +end +hook.Add("Tick","AD2CodecPipelineProc",pipeproc) + +local encode_types, decode_types +local str,pos +local a,b,c,m,n,w + +local function write(data) + local t = encode_types[type(data)] + if t then + local data, id_override = t[2](data) + return char(id_override or t[1])..data + end +end + +encode_types = { + table = {2, function(o) + local is_array = true + m = 0 + for k in pairs(o) do + m = m + 1 + if k ~= m then + is_array = false + break + end + end + local u = {} + if is_array then + for g = 1,#o do + u[g] = write(o[g]) + end + return concat(u).."\1", 3 + else + local i = 0 + for k,v in pairs(o) do + w = write(v) + if w then + i = i + 2 + u[i-1] = write(k) + u[i] = w + end + end + return concat(u).."\1" + end + end}, + boolean = {4, function(o) + return "", o and 5 + end}, + number = {6, function(o) + return (o==0 and "" or o).."\1" + end}, + string = {7, function(o) + return o.."\1" + end}, + Vector = {8, function(o) + return format("%g\1%g\1%g\1",o.x,o.y,o.z) + end}, + Angle = {9, function(o) + return format("%g\1%g\1%g\1",o.p,o.y,o.r) + end} +} + +local function read() + local t = byte(str,pos+1) + if t then + local dt = decode_types[t] + if dt then + pos = pos + 1 + return dt() + else + error(format("encountered invalid data type (%u)",t)) + end + else + error("expected value, got EOF") + end +end + +decode_types = { + [1 ] = function() + error("expected value, got terminator") + end, + [2 ] = function() -- table + local t = {} + while true do + if byte(str,pos+1) == 1 then + pos = pos+1 + return t + else + t[read()] = read() + end + end + end, + [3 ] = function() -- array + local t, i = {}, 1 + while true do + if byte(str,pos+1) == 1 then + pos = pos+1 + return t + else + t[i] = read() + i = i + 1 + end + end + end, + [4 ] = function() -- false boolean + return false + end, + [5 ] = function() -- true boolean + return true + end, + [6 ] = function() -- number + m = find(str,"\1",pos) + if m then + a = tonumber(sub(str,pos+1,m-1)) or 0 + pos = m + return a + else + error("expected number, got EOF") + end + end, + [7 ] = function() -- string + m = find(str,"\1",pos) + if m then + w = sub(str,pos+1,m-1) + pos = m + return w + else + error("expected string, got EOF") + end + end, + [8 ] = function() -- Vector + m,n = find(str,".-\1.-\1.-\1",pos) + if m then + a,b,c = match(str,"^(.-)\1(.-)\1(.-)\1",pos+1) + pos = n + return Vector(tonumber(a), tonumber(b), tonumber(c)) + else + error("expected vector, got EOF") + end + end, + [9 ] = function() -- Angle + m,n = find(str,".-\1.-\1.-\1",pos) + if m then + a,b,c = match(str,"^(.-)\1(.-)\1(.-)\1",pos+1) + pos = n + return Angle(tonumber(a), tonumber(b), tonumber(c)) + else + error("expected angle, got EOF") + end + end +} +local function deserialize(data) + str = data + pos = 0 + return read() +end +local function serialize(data) + return write(data) +end + +local idxmem = {} +for i=0,252 do + idxmem[i] = char(i) +end + +local function encodeIndex(index) + local buffer = {} + local buffer_len = 0 + local temp + while index>0 do + temp = index>>8 + buffer_len = buffer_len + 1 + buffer[buffer_len] = index - (temp << 8) + index = temp + end + return char(256 - buffer_len, unpack(buffer)) +end +local function lzwEncode(raw) + local dictionary_length = 256 + local dictionary = {} + local compressed = {} + local word = "" + for i = 0, 255 do + dictionary[char(i)] = i + end + local curchar + local wordc + local compressed_length = 0 + local temp + for i = 1, #raw do + curchar = sub(raw,i,i) + wordc = word..curchar + if dictionary[wordc] then + word = wordc + else + dictionary[wordc] = dictionary_length + dictionary_length = dictionary_length + 1 + + temp = idxmem[dictionary[word]] + + compressed_length = compressed_length + 1 + if temp then + compressed[compressed_length] = temp + else + temp = encodeIndex(dictionary[word]) + compressed[compressed_length] = temp + idxmem[dictionary[word]] = temp + end + + word = curchar + end + end + temp = idxmem[dictionary[word]] + if temp then + compressed[compressed_length+1] = temp + else + temp = encodeIndex(dictionary[word]) + compressed[compressed_length+1] = temp + idxmem[dictionary[word]] = temp + end + return concat(compressed) +end + +local function lzwDecode(encoded) + local dictionary_length = 256 + local dictionary = {} + for i = 0, 255 do + dictionary[i] = char(i) + end + + local pos = 2 + local decompressed = {} + local decompressed_length = 1 + + local index = byte(encoded) + local word = dictionary[index] + + decompressed[decompressed_length] = dictionary[index] + + local entry + local encoded_length = #encoded + local firstbyte --of an index + while pos <= encoded_length do + firstbyte = byte(encoded,pos) + if firstbyte > 252 then --now we know it's a length indicator for a multibyte index + index = 0 + firstbyte = 256 - firstbyte + for i = pos+firstbyte, pos+1, -1 do + index = (index << 8) | byte(encoded,i) + end + pos = pos + firstbyte + 1 + else + index = firstbyte + pos = pos + 1 + end + entry = dictionary[index] or (word..sub(word,1,1)) + decompressed_length = decompressed_length + 1 + decompressed[decompressed_length] = entry + dictionary[dictionary_length] = word..sub(entry,1,1) + dictionary_length = dictionary_length + 1 + word = entry + end + return concat(decompressed) +end + +--http://en.wikipedia.org/wiki/Huffman_coding#Compression + +local codes = {{22,5},{11,5},{58,6},{57,6},{37,6},{35,6},{13,6},{31,6},{51,6},{55,6},{26,7},{10,7},{9,6},{1,7},{59,6},{15,7},{61,7},{33,7},{97,7},{5,8},{133,8},{130,8},{65,7},{41,7},{94,7},{62,7},{17,7},{7,7},{162,8},{89,7},{87,7},{3,7},{39,7},{2,8},{66,8},{142,8},{21,8},{47,7},{50,7},{82,7},{46,7},{25,7},{19,7},{170,8},{90,9},{305,9},{290,9},{437,9},{270,9},{254,9},{85,9},{369,9},{49,9},{42,9},{53,9},{238,9},{381,9},{29,9},{346,10},{245,10},{497,9},{226,10},{327,9},{207,9},{458,9},{301,9},{81,9},{490,9},{489,9},{283,9},{103,9},{626,10},{109,9},{429,9},{262,10},{509,9},{237,9},{390,9},{233,9},{413,9},{774,10},{181,9},{323,9},{177,9},{197,9},{45,9},{730,10},{91,9},{349,9},{882,10},{63,9},{646,10},{202,10},{718,10},{325,9},{402,10},{286,9},{414,9},{117,9},{366,9},{111,8},{105,9},{67,9},{361,9},{14,9},{242,10},{453,9},{510,9},{422,9},{70,9},{166,9},{38,9},{658,10},{337,9},{294,9},{102,9},{253,9},{27,9},{411,9},{110,9},{241,9},{255,9},{213,9},{733,10},{746,10},{198,10},{454,10},{786,10},{586,10},{157,9},{846,10},{486,10},{962,10},{78,10},{610,10},{590,10},{219,9},{625,10},{493,9},{474,10},{194,10},{842,10},{974,10},{285,9},{917,10},{83,9},{127,9},{370,10},{710,10},{1013,10},{134,10},{221,10},{511,9},{998,10},{191,9},{114,10},{467,9},{209,10},{447,9},{1006,10},{382,10},{319,9},{149,9},{462,10},{126,10},{330,10},{475,9},{309,10},{98,10},{69,9},{986,10},{742,10},{810,10},{383,9},{455,9},{407,9},{155,9},{199,9},{465,10},{354,10},{618,10},{469,9},{158,9},{365,9},{206,10},{106,10},{721,10},{714,10},{870,10},{894,10},{542,10},{74,10},{347,9},{146,10},{821,10},{279,9},{638,10},{373,9},{211,9},{866,10},{231,9},{501,10},{530,10},{450,10},{230,10},{487,9},{494,10},{195,9},{23,9},{173,9},{239,9},{966,10},{6,10},{234,10},{113,10},{274,10},{334,10},{30,10},{706,10},{34,10},{914,10},{341,9},{71,9},{151,9},{339,9},{93,9},{125,9},{451,9},{754,10},{482,10},{335,9},{218,10},{994,10},{874,10},{858,10},{518,10},{498,10},{738,10},{362,10},{757,10},{477,9},{405,10},{463,9},{326,10},{495,9},{838,10},{1010,10},{298,10},{358,10},{359,9},{79,9},{977,10},{546,10},{0,2},{18,10},[0]={433,9}} +local function huffmanEncode(raw) + + local rawlen = #raw + + --output is headed by the unencoded size as a 24-bit integer (65kB+ LZW encodings are easily possible here, 16MB not so much) + local encoded = { + char(rawlen & 0xff), + char((rawlen >> 8) & 0xff), + char((rawlen >> 16) & 0xff) + } + local encoded_length = 3 + local buffer = 0 + local buffer_length = 0 + + local code + --the encoding would be way faster in C (most of the execution time of this function is spent calling string.byte) + for i = 1, rawlen do + code = codes[byte(raw,i)] + buffer = buffer + (code[1] << buffer_length) + buffer_length = buffer_length + code[2] + while buffer_length>=8 do + encoded_length = encoded_length + 1 + encoded[encoded_length] = char(buffer & 0xff) + buffer = buffer >> 8 + buffer_length = buffer_length - 8 + end + end + + if buffer_length>0 then + encoded[encoded_length+1] = char(buffer) + end + + return concat(encoded) +end + +--http://en.wikipedia.org/wiki/Huffman_coding#Decompression + +local invcodes = {[2]={[0]="\254"},[5]={[22]="\1",[11]="\2"},[6]={[13]="\7",[35]="\6",[37]="\5",[58]="\3",[31]="\8",[9]="\13",[51]="\9",[55]="\10",[57]="\4",[59]="\15"},[7]={[1]="\14",[15]="\16",[87]="\31",[89]="\30",[62]="\26",[17]="\27",[97]="\19",[19]="\43",[10]="\12",[39]="\33",[41]="\24",[82]="\40",[3]="\32",[46]="\41",[47]="\38",[94]="\25",[65]="\23",[50]="\39",[26]="\11",[7]="\28",[33]="\18",[61]="\17",[25]="\42"},[8]={[111]="\101",[162]="\29",[2]="\34",[133]="\21",[142]="\36",[5]="\20",[21]="\37",[170]="\44",[130]="\22",[66]="\35"},[9]={[241]="\121",[361]="\104",[365]="\184",[125]="\227",[373]="\198",[253]="\117",[381]="\57",[270]="\49",[413]="\80",[290]="\47",[294]="\115",[38]="\112",[429]="\74",[433]="\0",[437]="\48",[158]="\183",[453]="\107",[166]="\111",[469]="\182",[477]="\241",[45]="\86",[489]="\69",[366]="\100",[497]="\61",[509]="\76",[49]="\53",[390]="\78",[279]="\196",[283]="\70",[414]="\98",[53]="\55",[422]="\109",[233]="\79",[349]="\89",[369]="\52",[14]="\105",[238]="\56",[319]="\162",[323]="\83",[327]="\63",[458]="\65",[335]="\231",[339]="\225",[337]="\114",[347]="\193",[493]="\139",[23]="\209",[359]="\250",[490]="\68",[42]="\54",[63]="\91",[286]="\97",[254]="\50",[510]="\108",[109]="\73",[67]="\103",[255]="\122",[69]="\170",[70]="\110",[407]="\176",[411]="\119",[110]="\120",[83]="\146",[149]="\163",[151]="\224",[85]="\51",[155]="\177",[79]="\251",[27]="\118",[447]="\159",[451]="\228",[455]="\175",[383]="\174",[463]="\243",[467]="\157",[173]="\210",[475]="\167",[177]="\84",[90]="\45",[487]="\206",[93]="\226",[495]="\245",[207]="\64",[127]="\147",[191]="\155",[511]="\153",[195]="\208",[197]="\85",[199]="\178",[181]="\82",[102]="\116",[103]="\71",[285]="\144",[105]="\102",[211]="\199",[213]="\123",[301]="\66",[305]="\46",[219]="\137",[81]="\67",[91]="\88",[157]="\130",[325]="\95",[29]="\58",[231]="\201",[117]="\99",[341]="\222",[237]="\77",[239]="\211",[71]="\223"},[10]={[710]="\149",[245]="\60",[742]="\172",[774]="\81",[134]="\151",[917]="\145",[274]="\216",[405]="\242",[146]="\194",[838]="\246",[298]="\248",[870]="\189",[1013]="\150",[894]="\190",[326]="\244",[330]="\166",[334]="\217",[465]="\179",[346]="\59",[354]="\180",[966]="\212",[974]="\143",[370]="\148",[998]="\154",[625]="\138",[382]="\161",[194]="\141",[198]="\126",[402]="\96",[206]="\185",[586]="\129",[721]="\187",[610]="\135",[618]="\181",[626]="\72",[226]="\62",[454]="\127",[658]="\113",[462]="\164",[234]="\214",[474]="\140",[242]="\106",[714]="\188",[730]="\87",[498]="\237",[746]="\125",[754]="\229",[786]="\128",[202]="\93",[18]="\255",[810]="\173",[846]="\131",[74]="\192",[842]="\142",[977]="\252",[858]="\235",[78]="\134",[874]="\234",[882]="\90",[646]="\92",[1006]="\160",[126]="\165",[914]="\221",[718]="\94",[738]="\238",[638]="\197",[482]="\230",[34]="\220",[962]="\133",[6]="\213",[706]="\219",[986]="\171",[994]="\233",[866]="\200",[1010]="\247",[98]="\169",[518]="\236",[494]="\207",[230]="\205",[542]="\191",[501]="\202",[530]="\203",[450]="\204",[209]="\158",[106]="\186",[590]="\136",[218]="\232",[733]="\124",[309]="\168",[221]="\152",[757]="\240",[113]="\215",[114]="\156",[362]="\239",[486]="\132",[358]="\249",[262]="\75",[30]="\218",[821]="\195",[546]="\253"}} + +local function huffmanDecode(encoded) + + local encoded_length = #encoded+1 + local h1,h2,h3 = byte(encoded, 1, 3) + local original_length = (h3<<16) | (h2<<8) | h1 + local decoded = {} + local decoded_length = 0 + local buffer = 0 + local buffer_length = 0 + local code + local code_len = 2 + local temp + local pos = 4 + + while decoded_length < original_length do + if code_len <= buffer_length then + temp = invcodes[code_len] + code = buffer & (1 << code_len)-1 + if temp and temp[code] then --most of the time temp is nil + decoded_length = decoded_length + 1 + decoded[decoded_length] = temp[code] + buffer = buffer >> code_len + buffer_length = buffer_length - code_len + code_len = 2 + else + code_len = code_len + 1 + if code_len > 10 then + error("malformed code") + end + end + else + buffer = buffer | (byte(encoded, pos) << buffer_length) + buffer_length = buffer_length + 8 + pos = pos + 1 + if pos > encoded_length then + error("malformed code") + end + end + end + + return concat(decoded) +end + +--escape sequences can't be palindromes +local escseq = { + "bbq", + "wtf", + "cat", + "car", + "bro", + "moo", + "sky", +} + +local function escapeSub(str) + local genseq + for i=1,#escseq do + if not find(str, escseq[i]) then + genseq = escseq[i] + return genseq.."\n"..gsub(str,"\26",genseq) + end + end + for i=30,200 do + genseq = char(i, i-1, i+1) + if not find(str, genseq) then + return genseq.."\n"..gsub(str,"\26",genseq) + end + genseq = char(i, i, i+1) + if not find(str, genseq) then + return genseq.."\n"..gsub(str,"\26",genseq) + end + end + error(" could not be escaped") +end + +local function invEscapeSub(str) + local escseq,body = match(str,"^(.-)\n(.-)$") + return gsub(body,escseq,"\26") +end + +local dictionary +local subtables + +local function deserializeChunk(chunk) + + local ctype,val = byte(chunk),sub(chunk,3) + + if ctype == 89 then return dictionary[ val ] + elseif ctype == 86 then + local a,b,c = match(val,"^(.-),(.-),(.+)$") + return Vector( tonumber(a), tonumber(b), tonumber(c) ) + elseif ctype == 65 then + local a,b,c = match(val,"^(.-),(.-),(.+)$") + return Angle( tonumber(a), tonumber(b), tonumber(c) ) + elseif ctype == 84 then + local t = {} + local tv = subtables[val] + if not tv then + tv = {} + subtables[ val ] = tv + end + tv[#tv+1] = t + return t + elseif ctype == 78 then return tonumber(val) + elseif ctype == 83 then return gsub(sub(val,2,-2),"»",";") + elseif ctype == 66 then return val == "t" + elseif ctype == 80 then return 1 + end + + error(format("AD1 deserialization failed: invalid chunk (%u:%s)",ctype,val)) + +end + +local function deserializeAD1(dupestring) + + local header, extraHeader, dupeBlock, dictBlock = dupestring:match("%[Info%]\n(.+)\n%[More Information%]\n(.+)\n%[Save%]\n(.+)\n%[Dict%]\n(.+)") + + if not header then + error("unknown duplication format") + end + + local info = {} + for k,v in header:gmatch("([^\n:]+):([^\n]+)") do + info[k] = v + end + + local moreinfo = {} + for k,v in extraHeader:gmatch("([^\n:]+):([^\n]+)") do + moreinfo[k] = v + end + + dictionary = {} + for k,v in dictBlock:gmatch("([^\n]+):\"(.-)\"") do + dictionary[k] = v + end + + local dupe = {} + for key,block in dupeBlock:gmatch("([^\n:]+):([^\n]+)") do + + local tables = {} + subtables = {} + local head + + for id,chunk in block:gmatch('([A-H0-9]+){(.-)}') do + + --check if this table is the trunk + if byte(id) == 72 then + id = sub(id,2) + head = id + end + + tables[id] = {} + + for kv in gmatch(chunk,'[^;]+') do + + local k,v = match(kv,'(.-)=(.+)') + + if k then + k = deserializeChunk( k ) + v = deserializeChunk( v ) + + tables[id][k] = v + else + v = deserializeChunk( kv ) + local tid = tables[id] + tid[#tid+1]=v + end + + end + end + + --Restore table references + for id,tbls in pairs( subtables ) do + for _,tbl in pairs( tbls ) do + merge( tbl, tables[id] ) + end + end + + dupe[key] = tables[ head ] + + end + + return dupe, info, moreinfo + +end + +--[[ + Name: Encode + Desc: Generates the string for a dupe file with the given data. + Params:
dupe,
info, callback, <...> args + Return: runs callback( encoded_dupe, <...> args) +]] +function AdvDupe2.Encode(dupe, info, callback, ...) + + info.check = "\r\n\t\n" + + addPipeline{ + serialize, + lzwEncode, + huffmanEncode, + escapeSub, + callback, + eax = dupe, + REVISION = REVISION, + info = info, + args = {...}, + idx = 0, + cbk = 5 + } + +end + +--seperates the header and body and converts the header to a table +local function getInfo(str) + local last = str:find("\2") + if not last then + error("attempt to read AD2 file with malformed info block") + end + local info = {} + local ss = str:sub(1,last-1) + for k,v in ss:gmatch("(.-)\1(.-)\1") do + info[k] = v + end + if not info.check or info.check ~= "\r\n\t\n" then + error("attempt to read AD2 file with malformed info block") + end + return info, str:sub(last+2) +end + +--decoders for individual versions go here +versions = {} + +versions[1] = function(encodedDupe) + local info, dupestring = getInfo(encodedDupe:sub(7)) + return deserialize( + lzwDecode( + huffmanDecode( + invEscapeSub(dupestring) + ) + ) + ), info +end + +--[[ + Name: Decode + Desc: Generates the table for a dupe from the given string. Inverse of Encode + Params: encodedDupe, callback, <...> args + Return: runs callback( success,
tbl,
info) +]] +function AdvDupe2.Decode(encodedDupe, callback, ...) + + local sig, rev = encodedDupe:match("^(....)(.)") + + if not rev then + error("malformed dupe (wtf <5 chars long?!)") + end + + rev = rev:byte() + + if sig ~= "AD2F" then + if sig == "[Inf" then --legacy support, ENGAGE (AD1 dupe detected) + local success, tbl, info, moreinfo = pcall(deserializeAD1, encodedDupe) + + if success then + info.size = #encodedDupe + info.revision = 0 + info.ad1 = true + else + ErrorNoHalt(tbl) + end + + callback(success, tbl, info, moreinfo, ...) + else + error("unknown duplication format") + end + elseif rev > REVISION then + error(format("this install lacks the codec version to parse the dupe (have rev %u, need rev %u)",REVISION,rev)) + elseif rev == 0 then + error("attempt to use an invalid format revision (rev 0)") + else + local success, tbl, info = pcall(versions[rev], encodedDupe) + + if success then + info.revision = rev + else + ErrorNoHalt(tbl) + end + + callback(success, tbl, info, ...) + end + +end \ No newline at end of file diff --git a/lua/advdupe2/sv_file.lua b/lua/advdupe2/sv_file.lua new file mode 100644 index 0000000..c8f2301 --- /dev/null +++ b/lua/advdupe2/sv_file.lua @@ -0,0 +1,92 @@ +--[[ + Title: Adv. Dupe 2 Filing Clerk (Serverside) + + Desc: Reads/writes AdvDupe2 files. + + Author: AD2 Team + + Version: 1.0 +]] + +function _R.Player:SteamIDSafe() + return self:SteamID():gsub(":","_") +end + +--[[ + Name: WriteAdvDupe2File + Desc: Writes a dupe file to the dupe folder. + Params: dupe, name + Return: success/ path +]] +function AdvDupe2.WriteFile(ply, name, dupe) + + name = name:lower() + + if name:find("[<>:\\\"|%?%*%.]") then return false end + + name = name:gsub("//","/") + + local path + if SinglePlayer() then + path = string.format("%s/%s", AdvDupe2.DataFolder, name) + else + path = string.format("%s/%s/%s", AdvDupe2.DataFolder, ply and ply:SteamIDSafe() or "=Public=", name) + end + + --if a file with this name already exists, we have to come up with a different name + if file.Exists(path..".txt") then + for i = 1, AdvDupe2.FileRenameTryLimit do + --check if theres already a file with the name we came up with, and retry if there is + --otherwise, we can exit the loop and write the file + if not file.Exists(path.."_"..i..".txt") then + path = path.."_"..i + break + end + end + --if we still can't find a unique name we give up + if file.Exists(path..".txt") then return false end + end + + --write the file + file.Write(path..".txt", dupe) + + --returns if the write was successful and the name the path ended up being saved under + return path..".txt", path:match("[^/]-$") + +end + +--[[ + Name: ReadAdvDupe2File + Desc: Reads a dupe file from the dupe folder. + Params: name + Return: contents +]] +function AdvDupe2.ReadFile(ply, name, dirOverride) + if SinglePlayer() then + return file.Read(string.format("%s/%s.txt", dirOverride or AdvDupe2.DataFolder, name)) + else + local path = string.format("%s/%s/%s.txt", dirOverride or AdvDupe2.DataFolder, ply and ply:SteamIDSafe() or "=Public=", name) + if(file.Size(path)/1024>tonumber(GetConVarString("AdvDupe2_MaxFileSize")))then + return false + else + return file.Read(path) + end + end + +end + +function _R.Player:WriteAdvDupe2File(name, dupe) + return AdvDupe2.WriteFile(self, name, dupe) +end + +function _R.Player:ReadAdvDupe2File(name) + return AdvDupe2.ReadFile(self, name) +end + +function _R.Player:GetAdvDupe2Folder() + if SinglePlayer() then + return AdvDupe2.DataFolder + else + return string.format("%s/%s", AdvDupe2.DataFolder, self:SteamIDSafe()) + end +end diff --git a/lua/advdupe2/sv_misc.lua b/lua/advdupe2/sv_misc.lua new file mode 100644 index 0000000..b29aa29 --- /dev/null +++ b/lua/advdupe2/sv_misc.lua @@ -0,0 +1,131 @@ +--[[ + Title: Miscellaneous + + Desc: Contains miscellaneous (serverside) things AD2 needs to function that don't fit anywhere else. + + Author: TB + + Version: 1.0 +]] + +--[[ + Name: SavePositions + Desc: Save the position of the entities to prevent sagging on dupe. + Params: Constraint + Returns: nil +]] + +local function SavePositions( Constraint ) + + if IsValid(Constraint) then + + if Constraint.BuildDupeInfo then return end + + if not Constraint.BuildDupeInfo then Constraint.BuildDupeInfo = {} end + + Constraint.Identity = Constraint:GetCreationID() + local Ent1 + local Ent2 + if IsValid(Constraint.Ent) then + Constraint.BuildDupeInfo.Ent1Ang = Constraint.Ent:GetAngles() + end + + if IsValid(Constraint.Ent1) then + Constraint.BuildDupeInfo.Ent1Ang = Constraint.Ent1:GetAngles() + if(Constraint.Ent1:GetPhysicsObjectCount()>1)then + Constraint.BuildDupeInfo.Bone1 = Constraint["Bone1"] + Constraint.BuildDupeInfo.Bone1Pos = Constraint.Ent1:GetPhysicsObjectNum(Constraint["Bone1"]):GetPos() - Constraint.Ent1:GetPos() + Constraint.BuildDupeInfo.Bone1Angle = Constraint.Ent1:GetPhysicsObjectNum(Constraint["Bone1"]):GetAngle() + end + if IsValid(Constraint.Ent2) then + Constraint.BuildDupeInfo.EntityPos = Constraint.Ent1:GetPos() - Constraint.Ent2:GetPos() + Constraint.BuildDupeInfo.Ent2Ang = Constraint.Ent2:GetAngles() + if(Constraint.Ent2:GetPhysicsObjectCount()>1)then + Constraint.BuildDupeInfo.Bone2 = Constraint["Bone2"] + Constraint.BuildDupeInfo.Bone2Pos = Constraint.Ent2:GetPhysicsObjectNum(Constraint["Bone2"]):GetPos() - Constraint.Ent2:GetPos() + Constraint.BuildDupeInfo.Bone2Angle = Constraint.Ent2:GetPhysicsObjectNum(Constraint["Bone2"]):GetAngle() + end + elseif IsValid(Constraint.Ent4) then + Constraint.BuildDupeInfo.EntityPos = Constraint.Ent1:GetPos() - Constraint.Ent4:GetPos() + Constraint.BuildDupeInfo.Ent4Ang = Constraint.Ent4:GetAngles() + if(Constraint.Ent4:GetPhysicsObjectCount()>1)then + Constraint.BuildDupeInfo.Bone2 = Constraint["Bone4"] + Constraint.BuildDupeInfo.Bone2Pos = Constraint.Ent4:GetPhysicsObjectNum(Constraint["Bone4"]):GetPos() - Constraint.Ent4:GetPos() + Constraint.BuildDupeInfo.Bone2Angle = Constraint.Ent4:GetPhysicsObjectNum(Constraint["Bone4"]):GetAngle() + end + end + + end + + end + +end + + +local function FixMagnet(Magnet) + Magnet.Entity = Magnet +end + +//Find out when a Constraint is created +hook.Add( "OnEntityCreated", "AdvDupe2_SavePositions", function(entity) + + if not ValidEntity( entity ) then return end + + local class = string.Explode("_",entity:GetClass()) + if class[2] == "magnet" then + timer.Simple( 0, FixMagnet, entity ) + end + + if class[1] == "phys" then + timer.Simple( 0, SavePositions, entity ) + end + +end ) + +-- Register camera entity class +-- fixes key not being saved (Conna) +local function CamRegister(Player, Pos, Ang, Key, Locked, Toggle, Vel, aVel, Frozen, Nocollide) + if (!Key) then return end + + local Camera = ents.Create("gmod_cameraprop") + Camera:SetAngles(Ang) + Camera:SetPos(Pos) + Camera:Spawn() + Camera:SetKey(Key) + Camera:SetPlayer(Player) + Camera:SetLocked(Locked) + Camera.toggle = Toggle + Camera:SetTracking(NULL, Vector(0)) + + if (Toggle == 1) then + numpad.OnDown(Player, Key, "Camera_Toggle", Camera) + else + numpad.OnDown(Player, Key, "Camera_On", Camera) + numpad.OnUp(Player, Key, "Camera_Off", Camera) + end + + if (Nocollide) then Camera:GetPhysicsObject():EnableCollisions(false) end + + -- Merge table + local Table = { + key = Key, + toggle = Toggle, + locked = Locked, + pl = Player, + nocollide = nocollide + } + table.Merge(Camera:GetTable(), Table) + + -- remove any camera that has the same key defined for this player then add the new one + local ID = Player:UniqueID() + GAMEMODE.CameraList[ID] = GAMEMODE.CameraList[ID] or {} + local List = GAMEMODE.CameraList[ID] + if (List[Key] and List[Key] != NULL ) then + local Entity = List[Key] + Entity:Remove() + end + List[Key] = Camera + return Camera + +end +duplicator.RegisterEntityClass("gmod_cameraprop", CamRegister, "Pos", "Ang", "key", "locked", "toggle", "Vel", "aVel", "frozen", "nocollide") \ No newline at end of file diff --git a/lua/advdupe2/sv_networking.lua b/lua/advdupe2/sv_networking.lua new file mode 100644 index 0000000..2ea03ec --- /dev/null +++ b/lua/advdupe2/sv_networking.lua @@ -0,0 +1,310 @@ +--[[ + Title: Adv. Dupe 2 Networking (Serverside) + + Desc: Handles file transfers and all that jazz. + + Author: TB + + Version: 1.0 +]] + +include "nullesc.lua" +AddCSLuaFile "nullesc.lua" + +AdvDupe2.Network = {} + +AdvDupe2.Network.Networks = {} +AdvDupe2.Network.ClientNetworks = {} +AdvDupe2.Network.SvStaggerSendRate = 0 +AdvDupe2.Network.ClStaggerSendRate = 0 + +local function CheckFileNameSv(path) + if file.Exists(path..".txt") then + for i = 1, AdvDupe2.FileRenameTryLimit do + if not file.Exists(path.."_"..i..".txt") then + return path.."_"..i..".txt" + end + end + end + + return path..".txt" +end + +function AdvDupe2.UpdateProgressBar(ply,percent) + umsg.Start("AdvDupe2_UpdateProgressBar",ply) + umsg.Char(percent) + umsg.End() +end + +function AdvDupe2.RemoveProgressBar(ply) + umsg.Start("AdvDupe2_RemoveProgressBar",ply) + umsg.End() +end + +//=========================================== +//========= Server To Client ========= +//=========================================== + + +--[[ + Name: AdvDupe2_SendFile + Desc: Client has responded and is ready for the next chunk of data + Params: Network table, Network ID + Returns: +]] +function AdvDupe2_SendFile(ID) + + local Net = AdvDupe2.Network.Networks[ID] + local Network = AdvDupe2.Network + + if(!IsValid(Net.Player))then + AdvDupe2.Network.Networks[ID] = nil + return + end + + local status = 0 + + local data = "" + for i=1,tonumber(GetConVarString("AdvDupe2_ServerDataChunks")) do + status = 0 + if(Net.LastPos==1)then status = 1 AdvDupe2.InitProgressBar(Net.Player,"Downloading:") end + data = string.sub(Net.File, Net.LastPos, Net.LastPos+tonumber(GetConVarString("AdvDupe2_MaxDownloadBytes"))) + + Net.LastPos=Net.LastPos+tonumber(GetConVarString("AdvDupe2_MaxDownloadBytes"))+1 + if(Net.LastPos>=Net.Length)then status = 2 end + + umsg.Start("AdvDupe2_RecieveFile", Net.Player) + umsg.Short(status) + umsg.String(data) + umsg.End() + + if(status==2)then break end + end + + AdvDupe2.UpdateProgressBar(Net.Player, math.floor((Net.LastPos/Net.Length)*100)) + + if(Net.LastPos>=Net.Length)then + Net.Player.AdvDupe2.Downloading = false + AdvDupe2.RemoveProgressBar(Net.Player) + if(Net.Player.AdvDupe2.Entities && !Net.Player.AdvDupe2.GhostEntities)then + AdvDupe2.StartGhosting(Net.Player) + end + + AdvDupe2.Network.Networks[ID] = nil + return + end + + local Cur_Time = CurTime() + local time = Network.SvStaggerSendRate - Cur_Time + + timer.Simple(time, AdvDupe2_SendFile, ID) + + if(time > 0)then + AdvDupe2.Network.SvStaggerSendRate = Cur_Time + tonumber(GetConVarString("AdvDupe2_ServerSendRate")) + time + else + AdvDupe2.Network.SvStaggerSendRate = Cur_Time + tonumber(GetConVarString("AdvDupe2_ServerSendRate")) + end + +end + + +--[[ + Name: EstablishNetwork + Desc: Add user to the queue and set up to begin data sending + Params: Player, File data + Returns: +]] +function AdvDupe2.EstablishNetwork(ply, file) + if(!IsValid(ply))then return end + + if(!tobool(GetConVarString("AdvDupe2_AllowDownloading")))then + AdvDupe2.Notify(ply,"Downloading is not allowed!",NOTIFY_ERROR,5) + return + end + + file = AdvDupe2.Null.esc(file) + + local id = ply:UniqueID() + ply.AdvDupe2.Downloading = true + AdvDupe2.Network.Networks[id] = {Player = ply, File=file, Length = #file, LastPos=1} + + local Cur_Time = CurTime() + local time = AdvDupe2.Network.SvStaggerSendRate - Cur_Time + + if(time > 0)then + AdvDupe2.Network.SvStaggerSendRate = Cur_Time + tonumber(GetConVarString("AdvDupe2_ServerSendRate")) + time + timer.Simple(time, AdvDupe2_SendFile, id) + else + AdvDupe2.Network.SvStaggerSendRate = Cur_Time + tonumber(GetConVarString("AdvDupe2_ServerSendRate")) + AdvDupe2_SendFile(id) + end + +end + +function AdvDupe2.RecieveNextStep(id) + if(!IsValid(AdvDupe2.Network.ClientNetworks[id].Player))then AdvDupe2.Network.ClientNetworks[id] = nil return end + umsg.Start("AdvDupe2_RecieveNextStep", AdvDupe2.Network.ClientNetworks[id].Player) + umsg.Short(tonumber(GetConVarString("AdvDupe2_MaxUploadBytes"))) + umsg.Short(tonumber(GetConVarString("AdvDupe2_ClientDataChunks"))) + umsg.End() +end + +//=========================================== +//========= Client To Server ========= +//=========================================== + + +local function GetPlayersFolder(ply) + local path + if SinglePlayer() then + path = string.format("%s", AdvDupe2.DataFolder) + else + path = string.format("%s/%s", AdvDupe2.DataFolder, ply:SteamID():gsub(":","_")) + end + return path +end + +--[[ + Name: AdvDupe2_InitRecieveFile + Desc: Start the file recieving process and send the servers settings to the client + Params: concommand + Returns: +]] +local function AdvDupe2_InitRecieveFile( ply, cmd, args ) + if(!IsValid(ply))then return end + if(!tobool(GetConVarString("AdvDupe2_AllowUploading")))then + umsg.Start("AdvDupe2_UploadRejected", ply) + umsg.Bool(true) + umsg.End() + AdvDupe2.Notify(ply, "Uploading is not allowed!",NOTIFY_ERROR,5) + return + elseif(ply.AdvDupe2.Pasting || ply.AdvDupe2.Downloading)then + umsg.Start("AdvDupe2_UploadRejected", ply) + umsg.Bool(false) + umsg.End() + AdvDupe2.Notify(ply, "Duplicator is Busy!",NOTIFY_ERROR,5) + return + end + + local path = args[1] + local area = tonumber(args[2]) + + if(area==0)then + path = GetPlayersFolder(ply).."/"..path + elseif(area==1)then + if(!tobool(GetConVarString("AdvDupe2_AllowPublicFolder")))then + umsg.Start("AdvDupe2_UploadRejected", ply) + umsg.Bool(true) + umsg.End() + AdvDupe2.Notify(ply,"Public Folder is disabled."..dupe,NOTIFY_ERROR) + return + end + path = AdvDupe2.DataFolder.."/=Public=/"..path + else + path = "adv_duplicator/"..ply:SteamIDSafe().."/"..path + end + + local id = ply:UniqueID() + if(AdvDupe2.Network.ClientNetworks[id])then return false end + ply.AdvDupe2.Downloading = true + ply.AdvDupe2.Uploading = true + + AdvDupe2.Network.ClientNetworks[id] = {Player = ply, Data = "", Size = 0, Name = path, SubN = args[3], SubQ = args[4], ParentID = tonumber(args[5]), Parts = 0} + + local Cur_Time = CurTime() + local time = AdvDupe2.Network.ClStaggerSendRate - Cur_Time + if(time > 0)then + AdvDupe2.Network.ClStaggerSendRate = Cur_Time + tonumber(GetConVarString("AdvDupe2_ClientSendRate")) + time + AdvDupe2.Network.ClientNetworks[id].NextSend = time + Cur_Time + timer.Simple(time, AdvDupe2.RecieveNextStep, id) + else + AdvDupe2.Network.ClStaggerSendRate = Cur_Time + tonumber(GetConVarString("AdvDupe2_ClientSendRate")) + AdvDupe2.Network.ClientNetworks[id].NextSend = Cur_Time + AdvDupe2.RecieveNextStep(id) + end + +end +concommand.Add("AdvDupe2_InitRecieveFile", AdvDupe2_InitRecieveFile) + + +local function AdvDupe2_SetNextResponse(id) + + local Cur_Time = CurTime() + local time = AdvDupe2.Network.ClStaggerSendRate - Cur_Time + if(time > 0)then + AdvDupe2.Network.ClStaggerSendRate = Cur_Time + tonumber(GetConVarString("AdvDupe2_ClientSendRate")) + time + AdvDupe2.Network.ClientNetworks[id].NextSend = time + Cur_Time + timer.Simple(time, AdvDupe2.RecieveNextStep, id) + else + AdvDupe2.Network.ClStaggerSendRate = Cur_Time + tonumber(GetConVarString("AdvDupe2_ClientSendRate")) + AdvDupe2.Network.ClientNetworks[id].NextSend = Cur_Time + AdvDupe2.RecieveNextStep(id) + end + +end + +--[[ + Name: AdvDupe2_RecieveFile + Desc: Recieve file data from the client to save on the server + Params: concommand + Returns: +]] +local function AdvDupe2_RecieveFile(ply, cmd, args) + if(!IsValid(ply))then return end + + local id = ply:UniqueID() + if(!AdvDupe2.Network.ClientNetworks[id])then return end + local Net = AdvDupe2.Network.ClientNetworks[id] + + //Someone tried to mess with upload concommands + if(Net.NextSend - CurTime()>0)then + AdvDupe2.Network.ClientNetworks[id]=nil + ply.AdvDupe2.Downloading = false + ply.AdvDupe2.Uploading = false + + umsg.Start("AdvDupe2_UploadRejected") + umsg.Bool(true) + umsg.End() + AdvDupe2.Notify(ply,"Upload Rejected!",NOTIFY_GENERIC,5) + end + + local data = args[2] + + Net.Data = Net.Data..data + Net.Parts = Net.Parts + 1 + + if(tonumber(args[1])!=0)then + Net.Data = string.gsub(Net.Data, Net.SubN, "\10") + Net.Data = string.gsub(Net.Data, Net.SubQ, [["]]) + Net.Name = CheckFileNameSv(Net.Name) + local filename = string.Explode("/", Net.Name) + Net.FileName = string.sub(filename[#filename], 1, -5) + + file.Write(Net.Name, AdvDupe2.Null.invesc(Net.Data)) + + umsg.Start("AdvDupe2_AddFile",ply) + umsg.String(Net.FileName) + umsg.Short(Net.ParentID) + umsg.Bool(true) + umsg.End() + + AdvDupe2.Network.ClientNetworks[id]=nil + ply.AdvDupe2.Downloading = false + ply.AdvDupe2.Uploading = false + if(ply.AdvDupe2.Entities && !ply.AdvDupe2.GhostEntities)then + AdvDupe2.StartGhosting(ply) + end + + umsg.Start("AdvDupe2_UploadRejected") + umsg.Bool(false) + umsg.End() + AdvDupe2.Notify(ply,"File successfully uploaded!",NOTIFY_GENERIC,5) + return + end + + if(Net.Parts == tonumber(GetConVarString("AdvDupe2_ClientDataChunks")))then + Net.Parts = 0 + AdvDupe2_SetNextResponse(id) + end +end +concommand.Add("AdvDupe2_RecieveFile", AdvDupe2_RecieveFile) diff --git a/lua/autorun/client/advdupe2_cl_init.lua b/lua/autorun/client/advdupe2_cl_init.lua new file mode 100644 index 0000000..c2bf11f --- /dev/null +++ b/lua/autorun/client/advdupe2_cl_init.lua @@ -0,0 +1,69 @@ +AdvDupe2 = { + Version = "1.0.0", + Revision = 1 +} + +AdvDupe2.DataFolder = "advdupe2" --name of the folder in data where dupes will be saved + +AdvDupe2.FileRenameTryLimit = 256 + +include "advdupe2/cl_browser.lua" +include "advdupe2/cl_file.lua" +include "advdupe2/cl_networking.lua" + +function AdvDupe2.Notify(msg,typ,dur) + surface.PlaySound(typ == 1 and "buttons/button10.wav" or "ambient/water/drip1.wav") + GAMEMODE:AddNotify(msg, typ or NOTIFY_GENERIC, dur or 5) + if not SinglePlayer() then + print("[AdvDupe2Notify]\t"..msg) + end +end + +function AdvDupe2.ShowSplash() + local ad2folder + for k,v in pairs(GetAddonList()) do + if GetAddonInfo(v).Name == "Adv. Duplicator 2" then + ad2folder = v + break + end + end + + local splash = vgui.Create("DFrame") + splash:SetSize(512, 316) // Make it 1/4 the users screen size + splash:SetPos((ScrW()/2) - splash:GetWide()/2, (ScrH()/2) - splash:GetTall()/2) + splash:SetVisible( true ) + splash:SetTitle("") + splash:SetDraggable( true ) + splash:ShowCloseButton( true ) + splash.Paint = function( self ) + surface.SetDrawColor(255, 255, 255, 255) + surface.DrawRect(0, 0, self:GetWide(), self:GetTall()) + end + splash:MakePopup() + + local logo = vgui.Create("TGAImage", splash) + logo:SetPos(0, 24) + logo:SetSize(512, 128) + logo:LoadTGAImage(("addons/%s/materials/gui/ad2logo.tga"):format(ad2folder),"LOL it doesn't actually have to be 'MOD'") + + local version = vgui.Create("DLabel", splash) + version:SetPos(512 - (512-446)/2 - 85,140) // Position + version:SetColor(Color(0,0,0,255)) // Color + version:SetText(("v%s (rev. %u)"):format(AdvDupe2.Version, AdvDupe2.Revision)) // Text + version:SizeToContents() + + local credit = vgui.Create("DLabel", splash) + credit:SetPos((512-446)/2 + 16,190) + credit:SetColor(Color(64,32,16,255)) + credit:SetFont("Trebuchet24") + credit:SetText("Developed by: TB and emspike\n\nHosted by: Google Code") + credit:SizeToContents() +end + +usermessage.Hook("AdvDupe2Notify",function(um) + AdvDupe2.Notify(um:ReadString(),um:ReadChar(),um:ReadChar()) +end) + +timer.Simple(0, function() + AdvDupe2.ProgressBar={} +end) \ No newline at end of file diff --git a/lua/autorun/server/advdupe2_sv_init.lua b/lua/autorun/server/advdupe2_sv_init.lua new file mode 100644 index 0000000..d9f7d4d --- /dev/null +++ b/lua/autorun/server/advdupe2_sv_init.lua @@ -0,0 +1,88 @@ +AdvDupe2 = { + Version = "1.0.0", + Revision = 1 +} + +AdvDupe2.DataFolder = "advdupe2" --name of the folder in data where dupes will be saved + +AdvDupe2.FileRenameTryLimit = 256 + +include "advdupe2/sv_clipboard.lua" +include "advdupe2/sv_codec.lua" +include "advdupe2/sv_file.lua" +include "advdupe2/sv_networking.lua" +include "advdupe2/sv_misc.lua" + +AddCSLuaFile "autorun/client/advdupe2_cl_init.lua" +AddCSLuaFile "advdupe2/cl_browser.lua" +AddCSLuaFile "advdupe2/cl_networking.lua" +AddCSLuaFile "advdupe2/cl_file.lua" + +resource.AddFile("materials/gui/ad2logo.tga") +resource.AddFile("materials/gui/silkicons/help.vtf") +resource.AddFile("materials/gui/silkicons/help.vmt") + +function AdvDupe2.Notify(ply,msg,typ,dur) + umsg.Start("AdvDupe2Notify",ply) + umsg.String(msg) + umsg.Char(typ or NOTIFY_GENERIC) + umsg.Char(dur or 5) + umsg.End() + print("[AdvDupe2Notify]",msg) +end + +local function RemovePlayersFiles(ply) + + if(SinglePlayer() || !tobool(GetConVarString("AdvDupe2_RemoveFilesOnDisconnect")))then return end + + local function TFind(Search, Folders, Files) + Search = string.sub(Search, 6, -2) + for k,v in pairs(Files)do + file.Delete(Search..v) + end + + for k,v in pairs(Folders)do + file.TFind("Data/"..Search..v.."/*", TFind) + end + end + file.TFind("Data/"..ply:GetAdvDupe2Folder().."/*", TFind) +end + +CreateConVar("AdvDupe2_MaxFileSize", "200", {FCVAR_ARCHIVE}) +CreateConVar("AdvDupe2_MaxEntities", "0", {FCVAR_ARCHIVE}) +CreateConVar("AdvDupe2_MaxConstraints", "300", {FCVAR_ARCHIVE}) +CreateConVar("AdvDupe2_AllowUploading", "true", {FCVAR_ARCHIVE}) +CreateConVar("AdvDupe2_AllowDownloading", "true", {FCVAR_ARCHIVE}) +CreateConVar("AdvDupe2_AllowPublicFolder", "true", {FCVAR_ARCHIVE}) + +CreateConVar("AdvDupe2_MaxContraptionEntities", "10", {FCVAR_ARCHIVE}) +CreateConVar("AdvDupe2_MaxContraptionConstraints", "15", {FCVAR_ARCHIVE}) + +CreateConVar("AdvDupe2_MaxAreaCopySize", "2500", {FCVAR_ARCHIVE}) +CreateConVar("AdvDupe2_RemoveFilesOnDisconnect", "false", {FCVAR_ARCHIVE}) + +CreateConVar("AdvDupe2_FileModificationDelay", "5", {FCVAR_ARCHIVE}) +CreateConVar("AdvDupe2_UpdateFilesDelay", "10", {FCVAR_ARCHIVE}) +CreateConVar("AdvDupe2_AllowNPCPasting", "false", {FCVAR_ARCHIVE}) + +CreateConVar("AdvDupe2_MaxDownloadBytes", "200", {FCVAR_ARCHIVE}) +CreateConVar("AdvDupe2_MaxUploadBytes", "180", {FCVAR_ARCHIVE}) +CreateConVar("AdvDupe2_ServerSendRate", "0.15", {FCVAR_ARCHIVE}) +CreateConVar("AdvDupe2_ClientSendRate", "0.15", {FCVAR_ARCHIVE}) +CreateConVar("AdvDupe2_ServerDataChunks", "4", {FCVAR_ARCHIVE}) +CreateConVar("AdvDupe2_ClientDataChunks", "4", {FCVAR_ARCHIVE}) + +cvars.AddChangeCallback("AdvDupe2_RemoveFilesOnDisconnect", + function(cvar, preval, newval) + if(tobool(newval))then + hook.Add("PlayerDisconnected", "AdvDupe2_RemovePlayersFiles", RemovePlayersFiles) + else + hook.Remove("PlayerDisconnected", "AdvDupe2_RemovePlayersFiles") + end + end) +hook.Add("Initialize", "AdvDupe2_CheckServerSettings", + function() + if(tobool(GetConVarString("AdvDupe2_RemoveFilesOnDisconnect")))then + hook.Add("PlayerDisconnected", "AdvDupe2_RemovePlayersFiles", RemovePlayersFiles) + end + end) diff --git a/lua/entities/gmod_contr_spawner/cl_init.lua b/lua/entities/gmod_contr_spawner/cl_init.lua new file mode 100644 index 0000000..a2eab73 --- /dev/null +++ b/lua/entities/gmod_contr_spawner/cl_init.lua @@ -0,0 +1,6 @@ +include("shared.lua") + +function ENT:Draw() + self.BaseClass.Draw(self) + self.Entity:DrawModel() +end diff --git a/lua/entities/gmod_contr_spawner/init.lua b/lua/entities/gmod_contr_spawner/init.lua new file mode 100644 index 0000000..c484460 --- /dev/null +++ b/lua/entities/gmod_contr_spawner/init.lua @@ -0,0 +1,293 @@ +--[[ + Title: Adv. Dupe 2 Contraption Spawner + + Desc: A mobile duplicator + + Author: TB + + Version: 1.0 +]] + + +AddCSLuaFile( "cl_init.lua" ) +AddCSLuaFile( "shared.lua" ) +if(WireLib)then + include('entities/base_wire_entity/init.lua') +end +include('shared.lua') + + +function ENT:Initialize() + + self.Entity:SetMoveType( MOVETYPE_NONE ) + self.Entity:PhysicsInit( SOLID_VPHYSICS ) + self.Entity:SetCollisionGroup( COLLISION_GROUP_WORLD ) + self.Entity:DrawShadow( false ) + + local phys = self.Entity:GetPhysicsObject() + if phys:IsValid() then + phys:Wake() + end + + self.UndoList = {} + self.Ghosts = {} + + self.SpawnLastValue = 0 + self.UndoLastValue = 0 + + self.LastSpawnTime = 0 + + self.CurrentPropCount = 0 + + if WireLib then + self.Inputs = Wire_CreateInputs(self.Entity, {"Spawn", "Undo"}) + self.Outputs = WireLib.CreateSpecialOutputs(self.Entity, {"Out", "LastSpawned"}, { "NORMAL", "ENTITY" }) + end +end + + + +/*-----------------------------------------------------------------------* + * Sets options for this spawner + *-----------------------------------------------------------------------*/ +function ENT:SetOptions(ply, delay, undo_delay, key, undo_key, disgrav, disdrag, addvel ) + + self.delay = delay + self.undo_delay = undo_delay + + --Key bindings + self.key = key + self.undo_key = undo_key + + numpad.Remove( self.CreateKey ) + numpad.Remove( self.UndoKey ) + self.CreateKey = numpad.OnDown( ply, self.key, "ContrSpawnerCreate", self.Entity, true ) + self.UndoKey = numpad.OnDown( ply, self.undo_key, "ContrSpawnerUndo", self.Entity, true ) + self.DisableGravity = disgrav + self.DisableDrag = disdrag + self.AddVelocity = addvel + + self:ShowOutput() +end + +function ENT:UpdateOptions( options ) + self:SetOptions( options["delay"], options["undo_delay"], options["key"], options["undo_key"]) +end + + +function ENT:AddGhosts() + local moveable = self:GetPhysicsObject():IsMoveable() + self:GetPhysicsObject():EnableMotion(false) + + local EntTable + local GhostEntity + local Offset = self.DupeAngle - self.EntAngle + local Phys + for EntIndex,v in pairs(self.EntityTable)do + if(EntIndex!=self.HeadEnt)then + if(self.EntityTable[EntIndex].Class=="gmod_contr_spawner")then self.EntityTable[EntIndex] = nil continue end + EntTable = table.Copy(self.EntityTable[EntIndex]) + if(EntTable.BuildDupeInfo && EntTable.BuildDupeInfo.PhysicsObjects)then + Phys = EntTable.BuildDupeInfo.PhysicsObjects[0] + else + v.BuildDupeInfo = {} + v.BuildDupeInfo.PhysicsObjects = table.Copy(v.PhysicsObjects) + Phys = EntTable.PhysicsObjects[0] + end + + GhostEntity = nil + + if(EntTable.Model==nil || !util.IsValidModel(EntTable.Model)) then EntTable.Model="models/error.mdl" end + + if ( EntTable.Model:sub( 1, 1 ) == "*" ) then + GhostEntity = ents.Create( "func_physbox" ) + else + GhostEntity = ents.Create( "gmod_ghost" ) + end + + // If there are too many entities we might not spawn.. + if ( !GhostEntity || GhostEntity == NULL ) then return end + + duplicator.DoGeneric( GhostEntity, EntTable ) + + GhostEntity:Spawn() + + GhostEntity:DrawShadow( false ) + GhostEntity:SetMoveType( MOVETYPE_NONE ) + GhostEntity:SetSolid( SOLID_VPHYSICS ); + GhostEntity:SetNotSolid( true ) + GhostEntity:SetRenderMode( RENDERMODE_TRANSALPHA ) + GhostEntity:SetColor( 255, 255, 255, 150 ) + + GhostEntity:SetAngles(Phys.Angle) + GhostEntity:SetPos(self:GetPos() + Phys.Pos - self.Offset) + self:SetAngles(self.EntAngle) + GhostEntity:SetParent( self ) + self:SetAngles(self.DupeAngle) + self.Ghosts[EntIndex] = GhostEntity + end + end + self:SetAngles(self.DupeAngle) + self:GetPhysicsObject():EnableMotion(moveable) +end + +function ENT:GetCreationDelay() return self.delay end +function ENT:GetDeletionDelay() return self.undo_delay end + +function ENT:OnTakeDamage( dmginfo ) self.Entity:TakePhysicsDamage( dmginfo ) end + +function ENT:SetDupeInfo( HeadEnt, EntityTable, ConstraintTable ) + + self.HeadEnt = HeadEnt + self.EntityTable = EntityTable + self.ConstraintTable = ConstraintTable + if(!self.DupeAngle)then self.DupeAngle = self:GetAngles() end + if(!self.EntAngle)then self.EntAngle = EntityTable[HeadEnt].PhysicsObjects[0].Angle end + if(!self.Offset)then self.Offset = self.EntityTable[HeadEnt].PhysicsObjects[0].Pos end + self.EntityTable[HeadEnt].PhysicsObjects[0].Pos = Vector(0,0,0) +end + + + + +function ENT:DoSpawn( ply ) + + self.EntityTable[self.HeadEnt].PhysicsObjects[0].Pos = self:GetPos() + self.EntityTable[self.HeadEnt].PhysicsObjects[0].Angle = self:GetAngles() + for k,v in pairs(self.Ghosts)do + self.EntityTable[k].PhysicsObjects[0].Pos = v:GetPos() + self.EntityTable[k].PhysicsObjects[0].Angle = v:GetAngles() + end + + /*local AngleOffset = self.EntAngle + AngleOffset = self:GetAngles() - AngleOffset + local AngleOffset2 = Angle(0,0,0) + //AngleOffset2.y = AngleOffset.y + AngleOffset2:RotateAroundAxis(self:GetUp(), AngleOffset.y) + AngleOffset2:RotateAroundAxis(self:GetRight(),AngleOffset.p) + AngleOffset2:RotateAroundAxis(self:GetForward(),AngleOffset.r)*/ + + local Ents, Constrs = AdvDupe2.duplicator.Paste(ply, table.Copy(self.EntityTable), table.Copy(self.ConstraintTable), nil, nil, Vector(0,0,0), true) + local i = #self.UndoList+1 + self.UndoList[i] = Ents + + undo.Create("contraption_spawns") + local phys + for k,ent in pairs(Ents)do + phys = ent:GetPhysicsObject() + if IsValid(phys) then + phys:Wake() + if(self.DisableGravity==1)then phys:EnableGravity(false) end + if(self.DisableDrag==1)then phys:EnableDrag(false) end + phys:EnableMotion(true) + if(self.AddVelocity==1)then + phys:SetVelocity( self:GetVelocity() ) + phys:AddAngleVelocity( self:GetPhysicsObject():GetAngleVelocity() ) + end + end + + undo.AddEntity(ent) + end + + undo.SetPlayer(ply) + undo.Finish() + + if(self.undo_delay>0)then + timer.Simple(self.undo_delay, function() + if(self.UndoList && self.UndoList[i])then + for k,ent in pairs(self.UndoList[i]) do + if(IsValid(ent)) then + ent:Remove() + end + end + end + end) + end + +end + + + +function ENT:DoUndo( ply ) + + if(!self.UndoList || #self.UndoList == 0)then return end + + local entities = self.UndoList[ #self.UndoList ] + self.UndoList[ #self.UndoList ] = nil + for _,ent in pairs(entities) do + if (IsValid(ent)) then + ent:Remove() + end + end +end + +function ENT:TriggerInput(iname, value) + local ply = self:GetPlayer() + + if(iname == "Spawn")then + if ((value > 0) == self.SpawnLastValue) then return end + self.SpawnLastValue = (value > 0) + + if(self.SpawnLastValue)then + local delay = self:GetCreationDelay() + if (delay == 0) then self:DoSpawn( ply ) return end + if(CurTime() < self.LastSpawnTime)then return end + self:DoSpawn( ply ) + self.LastSpawnTime=CurTime()+delay + end + elseif (iname == "Undo") then + // Same here + if((value > 0) == self.UndoLastValue)then return end + self.UndoLastValue = (value > 0) + + if(self.UndoLastValue)then self:DoUndo(ply) end + end +end + +local text2 = {"Enabled", "Disabled"} +function ENT:ShowOutput() + local text = "\nGravity: " + if(self.DisableGravity==1)then text=text.."Enabled" else text=text.."Disabled" end + text=text.."\nDrag: " + if(self.DisableDrag==1)then text=text.."Enabled" else text=text.."Disabled" end + text=text.."\nVelocity: " + if(self.AddVelocity==1)then text=text.."Enabled" else text=text.."Disabled" end + + self.Entity:SetOverlayText( + "Spawn Delay: " .. tostring(self:GetCreationDelay()) .. + "\nUndo Delay: ".. tostring(self:GetDeletionDelay()) .. + text + ) + +end + + +/*-----------------------------------------------------------------------* + * Handler for spawn keypad input + *-----------------------------------------------------------------------*/ +function SpawnContrSpawner( ply, ent ) + + if (!ent || !ent:IsValid()) then return end + + local delay = ent:GetTable():GetCreationDelay() + + if(delay == 0) then + ent:DoSpawn( ply ) + return + end + + if(CurTime() < ent.LastSpawnTime)then return end + ent:DoSpawn( ply ) + ent.LastSpawnTime=CurTime()+delay +end + +/*-----------------------------------------------------------------------* + * Handler for undo keypad input + *-----------------------------------------------------------------------*/ +function UndoContrSpawner( ply, ent ) + if (!ent || !ent:IsValid()) then return end + ent:DoUndo( ply, true ) +end + +numpad.Register( "ContrSpawnerCreate", SpawnContrSpawner ) +numpad.Register( "ContrSpawnerUndo", UndoContrSpawner ) \ No newline at end of file diff --git a/lua/entities/gmod_contr_spawner/shared.lua b/lua/entities/gmod_contr_spawner/shared.lua new file mode 100644 index 0000000..bfc1947 --- /dev/null +++ b/lua/entities/gmod_contr_spawner/shared.lua @@ -0,0 +1,10 @@ +ENT.Type = "anim" +ENT.Base = WireLib and "base_wire_entity" or "base_gmodentity" +ENT.PrintName = "Contraption Spawner" +ENT.Author = "TB" +ENT.Contact = "" +ENT.Purpose = "" +ENT.Instructions = "" + +ENT.Spawnable = false +ENT.AdminSpawnable = false diff --git a/lua/weapons/gmod_tool/stools/advdupe2.lua b/lua/weapons/gmod_tool/stools/advdupe2.lua new file mode 100644 index 0000000..18ca461 --- /dev/null +++ b/lua/weapons/gmod_tool/stools/advdupe2.lua @@ -0,0 +1,2051 @@ +--[[ + Title: Adv. Dupe 2 Tool + + Desc: Defines the AD2 tool and assorted functionalities. + + Author: TB + + Version: 1.0 +]] + + +TOOL.Category = "Construction" +TOOL.Name = "#Advanced Duplicator 2" +cleanup.Register( "AdvDupe2" ) + + + +--[[ + Name: LeftClick + Desc: Defines the tool's behavior when the player left-clicks. + Params: trace + Returns: success +]] +function TOOL:LeftClick( trace ) + + if(!trace)then return false end + if(CLIENT)then return true end + + local ply = self:GetOwner() + + if(!ply.AdvDupe2 || !ply.AdvDupe2.Entities)then return false end + + if(ply.AdvDupe2.Pasting || ply.AdvDupe2.Downloading)then + AdvDupe2.Notify(ply,"Advanced Duplicator 2 is busy.",NOTIFY_ERROR) + return false + end + + local z = tonumber(ply:GetInfo("advdupe2_offset_z")) + ply.AdvDupe2.HeadEnt.Z + ply.AdvDupe2.Position = trace.HitPos + Vector(0, 0, z) + ply.AdvDupe2.Angle = Angle(tonumber(ply:GetInfo("advdupe2_offset_pitch")), tonumber(ply:GetInfo("advdupe2_offset_yaw")), tonumber(ply:GetInfo("advdupe2_offset_roll"))) + + if(tobool(ply:GetInfo("advdupe2_offset_world")))then ply.AdvDupe2.Angle = ply.AdvDupe2.Angle - ply.AdvDupe2.Entities[ply.AdvDupe2.HeadEnt.Index].PhysicsObjects[0].Angle end + ply.AdvDupe2.Pasting = true + umsg.Start("AdvDupe2_NotGhosting", ply) + umsg.End() + AdvDupe2.Notify(ply,"Pasting...") + local origin + if(tobool(ply:GetInfo("advdupe2_original_origin")))then + origin = ply.AdvDupe2.HeadEnt.Pos + end + AdvDupe2.InitPastingQueue(ply, ply.AdvDupe2.Position, ply.AdvDupe2.Angle, origin, tobool(ply:GetInfo("advdupe2_paste_constraints")), tobool(ply:GetInfo("advdupe2_paste_parents")), tobool(ply:GetInfo("advdupe2_paste_disparents"))) + //AdvDupe2.duplicator.Paste(ply, table.Copy(ply.AdvDupe2.Entities), table.Copy(ply.AdvDupe2.Constraints), ply.AdvDupe2.Position, ply.AdvDupe2.Angle, nil, true) + return true +end + + +//Thanks to Donovan for fixing the table +//Turns a table into a numerically indexed table +local function CollapseTableToArray( t ) + + local array = {} + local q = {} + local min, max = 0, 0 + --get the bounds + for k in pairs(t) do + if not min and not max then min,max = k,k end + min = (k < min) and k or min + max = (k > max) and k or max + end + for i=min, max do + if t[i] then + array[#array+1] = t[i] + end + end + return array +end + +//Find all the entities in a box, given the adjacent corners and the player +local function FindInBox(min, max, ply) + + local Entities = ents.GetAll() + local EntTable = {} + local pos + for _,ent in pairs(Entities) do + pos = ent:GetPos() + if (pos.X>=min.X) and (pos.X<=max.X) and (pos.Y>=min.Y) and (pos.Y<=max.Y) and (pos.Z>=min.Z) and (pos.Z<=max.Z) and (AdvDupe2.duplicator.EntityList[ent:GetClass()] ~= nil) then + if CPPI then + if ent:CPPICanTool(ply, "advdupe2") then + EntTable[ent:EntIndex()] = ent + end + else + EntTable[ent:EntIndex()] = ent + end + end + end + + return EntTable +end + +//Start drawing the area copy box +function AdvDupe2.DrawSelectBox(ply) + umsg.Start("AdvDupe2_DrawSelectBox", ply) + umsg.End() +end + +//Removes the area copy box +function AdvDupe2.RemoveSelectBox(ply) + umsg.Start("AdvDupe2_RemoveSelectBox", ply) + umsg.End() +end + +//Reset the offsets of height, pitch, yaw, and roll back to default +function AdvDupe2.ResetOffsets(ply) + umsg.Start("AdvDupe2_ResetOffsets", ply) + umsg.End() +end + +//Remove player's ghosts and tell the client to stop updating ghosts +local function RemoveGhosts(ply) + ply.AdvDupe2.Ghosting = false + if(ply.AdvDupe2.GhostEntities)then + for k,v in pairs(ply.AdvDupe2.GhostEntities)do + if(IsValid(v))then + v:Remove() + end + end + end + ply.AdvDupe2.GhostEntities = nil + ply.AdvDupe2.GhostToSpawn = nil + ply.AdvDupe2.CurrentGhost = 1 + umsg.Start("AdvDupe2_NotGhosting", ply) + umsg.End() +end + +--[[ + Name: RightClick + Desc: Defines the tool's behavior when the player right-clicks. + Params: trace + Returns: success +]] +function TOOL:RightClick( trace ) + + if CLIENT then return true end + + local ply = self:GetOwner() + + if(!ply.AdvDupe2)then ply.AdvDupe2 = {} end + if(ply.AdvDupe2.Pasting || ply.AdvDupe2.Downloading)then + AdvDupe2.Notify(ply,"Advanced Duplicator 2 is busy.",NOTIFY_ERROR) + return false + end + + //Set Area Copy on or off + if( ply:KeyDown(IN_SPEED) && !ply:KeyDown(IN_WALK) )then + if( self:GetStage()==0)then + AdvDupe2.DrawSelectBox(ply) + self:SetStage(1) + return false + elseif(self:GetStage()==1)then + AdvDupe2.RemoveSelectBox(ply) + self:SetStage(0) + return false + end + end + + if(!trace or !trace.Hit)then return false end + //If area copy is on and an ent was not right clicked, do an area copy and pick an ent + if( self:GetStage()==1 and !IsValid(trace.Entity) )then + if( !SinglePlayer() && (tonumber(ply:GetInfo("advdupe2_area_copy_size"))or 50) > tonumber(GetConVarString("AdvDupe2_MaxAreaCopySize")))then + AdvDupe2.Notify(ply,"Area copy size exceeds limit of "..GetConVarString("AdvDupe2_MaxAreaCopySize")..".",NOTIFY_ERROR) + return false + end + local i = tonumber(ply:GetInfo("advdupe2_area_copy_size")) or 50 + local Pos = trace.HitPos + local T = (Vector(i,i,i)+Pos) + local B = (Vector(-i,-i,-i)+Pos) + local timecheck = SysTime() + local Entities = FindInBox(B,T, ply) + + if(table.Count(Entities)==0)then + self:SetStage(0) + AdvDupe2.RemoveSelectBox(ply) + return true + end + + RemoveGhosts(ply) + ply.AdvDupe2.HeadEnt = {} + ply.AdvDupe2.Entities = {} + ply.AdvDupe2.Constraints = {} + + ply.AdvDupe2.HeadEnt.Index = table.GetFirstKey(Entities) + ply.AdvDupe2.HeadEnt.Pos = Entities[ply.AdvDupe2.HeadEnt.Index]:GetPos() + + local Outside = false + if((tonumber(ply:GetInfo("advdupe2_copy_outside")) or 0)==1)then + Outside = true + end + + ply.AdvDupe2.Entities, ply.AdvDupe2.Constraints = AdvDupe2.duplicator.AreaCopy(Entities, ply.AdvDupe2.HeadEnt.Pos, Outside) + + local tracedata = {} + tracedata.mask = MASK_NPCWORLDSTATIC + tracedata.start = ply.AdvDupe2.HeadEnt.Pos+Vector(0,0,1) + tracedata.endpos = ply.AdvDupe2.HeadEnt.Pos-Vector(0,0,50000) + local WorldTrace = util.TraceLine( tracedata ) + if(WorldTrace.Hit)then ply.AdvDupe2.HeadEnt.Z = math.abs(ply.AdvDupe2.HeadEnt.Pos.Z-WorldTrace.HitPos.Z) else ply.AdvDupe2.HeadEnt.Z = 0 end + + AdvDupe2.RemoveSelectBox(ply) + else //Area Copy is off or the ent is valid + + //Non valid entity or clicked the world + if(!IsValid(trace.Entity))then + + //If shift and alt are being held, clear the dupe + if(ply:KeyDown(IN_WALK) && ply:KeyDown(IN_SPEED))then + + RemoveGhosts(ply) + ply.AdvDupe2.Entities = nil + ply.AdvDupe2.Constraints = nil + umsg.Start("AdvDupe2_ResetDupeInfo", ply) + umsg.End() + AdvDupe2.ResetOffsets(ply) + end + return false + end + + //If Alt is being held, add a prop to the dupe + if(self:GetStage()==0 and ply:KeyDown(IN_WALK) and ply.AdvDupe2.Entities!=nil and table.Count(ply.AdvDupe2.Entities)>0)then + AdvDupe2.duplicator.Copy( trace.Entity, ply.AdvDupe2.Entities, ply.AdvDupe2.Constraints, ply.AdvDupe2.HeadEnt.Pos) + + ply.AdvDupe2.Constraints = CollapseTableToArray(ply.AdvDupe2.Constraints) + + umsg.Start("AdvDupe2_SetDupeInfo", ply) + umsg.String("") + umsg.String("") + umsg.String("") + umsg.String(os.date("%I:%M %p")) + umsg.String("") + umsg.String("") + umsg.String(table.Count(ply.AdvDupe2.Entities)) + umsg.String(#ply.AdvDupe2.Constraints) + umsg.End() + + //Only add the one ghost + local index = trace.Entity:EntIndex() + if(ply.AdvDupe2.Entities[index] && !ply.AdvDupe2.GhostEntities[index])then + if(!ply.AdvDupe2.GhostToSpawn)then ply.AdvDupe2.GhostToSpawn={} end + ply.AdvDupe2.GhostToSpawn[#ply.AdvDupe2.GhostToSpawn] = index + ply.AdvDupe2.LastGhost = CurTime()+0.02 + ply.AdvDupe2.Ghosting = true + end + + else + RemoveGhosts(ply) + + ply.AdvDupe2.HeadEnt = {} + ply.AdvDupe2.HeadEnt.Index = trace.Entity:EntIndex() + ply.AdvDupe2.Entities = {} + ply.AdvDupe2.Constraints = {} + ply.AdvDupe2.HeadEnt.Pos = trace.HitPos //trace.Entity:GetPos() + + local tracedata = {} + tracedata.mask = MASK_NPCWORLDSTATIC + tracedata.start = ply.AdvDupe2.HeadEnt.Pos + tracedata.endpos = ply.AdvDupe2.HeadEnt.Pos-Vector(0,0,50000) + local WorldTrace = util.TraceLine( tracedata ) + if WorldTrace.Hit then ply.AdvDupe2.HeadEnt.Z = math.abs(ply.AdvDupe2.HeadEnt.Pos.Z-WorldTrace.HitPos.Z) else ply.AdvDupe2.HeadEnt.Z=0 end + + //Area Copy is off, do a regular copy + if(self:GetStage()==0)then + AdvDupe2.duplicator.Copy( trace.Entity, ply.AdvDupe2.Entities, ply.AdvDupe2.Constraints, trace.HitPos ) //ply.AdvDupe2.HeadEnt.Pos ) + else //Area copy is on and an ent was clicked, do an area copy + if( !SinglePlayer() && (tonumber(ply:GetInfo("advdupe2_area_copy_size"))or 50) > tonumber(GetConVarString("AdvDupe2_MaxAreaCopySize")))then + AdvDupe2.Notify(ply,"Area copy size exceeds limit of "..GetConVarString("AdvDupe2_MaxAreaCopySize")..".",NOTIFY_ERROR) + return false + end + local i = tonumber(ply:GetInfo("advdupe2_area_copy_size")) or 50 + local Pos = ply.AdvDupe2.HeadEnt.Pos + local T = (Vector(i,i,i)+Pos) + local B = (Vector(-i,-i,-i)+Pos) + + local Outside = false + if((tonumber(ply:GetInfo("advdupe2_copy_outside")) or 0)==1)then + Outside = true + end + + local Entities = FindInBox(B,T, ply) + + ply.AdvDupe2.Entities, ply.AdvDupe2.Constraints = AdvDupe2.duplicator.AreaCopy(Entities, Pos, Outside) + + self:SetStage(0) + AdvDupe2.RemoveSelectBox(ply) + end + end + end + + ply.AdvDupe2.Constraints = CollapseTableToArray(ply.AdvDupe2.Constraints) + + umsg.Start("AdvDupe2_SetDupeInfo", ply) + umsg.String("") + umsg.String(ply:Nick()) + umsg.String(os.date("%d %B %Y")) + umsg.String(os.date("%I:%M %p")) + umsg.String("") + umsg.String("") + umsg.String(table.Count(ply.AdvDupe2.Entities)) + umsg.String(#ply.AdvDupe2.Constraints) + umsg.End() + + AdvDupe2.StartGhosting(ply) + + if(self:GetStage()==1)then + self:SetStage(0) + AdvDupe2.RemoveSelectBox(ply) + end + + AdvDupe2.ResetOffsets(ply) + + return true +end + +//Called to clean up the tool when pasting is finished or undo during pasting +function AdvDupe2.FinishPasting(Player, Paste) + Player.AdvDupe2.Pasting=false + AdvDupe2.RemoveProgressBar(Player) + + if(Paste)then AdvDupe2.Notify(Player,"Finished Pasting!") end + + local tool = Player:GetTool() + if(tool && Player:GetActiveWeapon():GetClass()=="gmod_tool" && tool.Mode=="advdupe2")then + if(Player.AdvDupe2.Ghosting)then AdvDupe2.InitProgressBar(Player, "Ghosting: ") end + umsg.Start("AdvDupe2_Ghosting", Player) + umsg.End() + return + else + RemoveGhosts(Player) + end + +end + +//Update the ghost's postion and angles based on where the player is looking and the offsets +local function UpdateGhost(ply, toolWep) + + local trace = util.TraceLine(util.GetPlayerTrace(ply, ply:GetCursorAimVector())) + if (!trace.Hit) then return end + + local GhostEnt = toolWep:GetNetworkedEntity("GhostEntity", nil) + + if(!IsValid(GhostEnt))then + if SERVER then RemoveGhosts(ply) end + return + end + + GhostEnt:SetMoveType(MOVETYPE_VPHYSICS) + GhostEnt:SetNotSolid(true) + + local PhysObj = GhostEnt:GetPhysicsObject() + if ( IsValid(PhysObj) ) then + PhysObj:EnableMotion( false ) + if(tobool(ply:GetInfo("advdupe2_original_origin")))then + PhysObj:SetPos(toolWep:GetNetworkedVector("HeadPos", Vector(0,0,0)) + toolWep:GetNetworkedVector( "HeadOffset", Vector(0,0,0) )) + PhysObj:SetAngle(toolWep:GetNetworkedAngle("HeadAngle", Angle(0,0,0))) + else + local EntAngle = toolWep:GetNetworkedAngle("HeadAngle", Angle(0,0,0)) + if(tobool(ply:GetInfo("advdupe2_offset_world")))then EntAngle = Angle(0,0,0) end + trace.HitPos.Z = trace.HitPos.Z + toolWep:GetNetworkedFloat("HeadZPos", 0) + tonumber(ply:GetInfo("advdupe2_offset_z")) or 0 + local Pos, Angle = LocalToWorld(toolWep:GetNetworkedVector("HeadOffset", Vector(0,0,0)), EntAngle, trace.HitPos, Angle(math.Clamp(tonumber(ply:GetInfo("advdupe2_offset_pitch")) or 0,-180,180), math.Clamp(tonumber(ply:GetInfo("advdupe2_offset_yaw")) or 0,-180,180), math.Clamp(tonumber(ply:GetInfo("advdupe2_offset_roll")) or 0,-180,180))) + PhysObj:SetPos(Pos) + PhysObj:SetAngle(Angle) + end + PhysObj:Wake() + else + // Give the head ghost entity a physics object + // This way the movement will be predicted on the client + if(CLIENT)then + GhostEnt:PhysicsInit(SOLID_VPHYSICS) + end + end +end + +//Add a folder to the clients file browser +local function AddFolder(ply,name,id,parent,new) + umsg.Start("AdvDupe2_AddFolder",ply) + umsg.String(name) + umsg.Short(id) + umsg.Short(parent) + umsg.Bool(new) + umsg.End() +end + +//Add a file to the clients file browser +local function AddFile(ply,name,parent,new) + umsg.Start("AdvDupe2_AddFile",ply) + umsg.String(name) + umsg.Short(parent) + umsg.Bool(new) + umsg.End() +end + +//Take control of the mouse wheel bind so the player can modify the height of the dupe +local function MouseWheelScrolled(ply, bind, pressed) + + if(bind=="invprev")then + local Z = tonumber(ply:GetInfo("advdupe2_offset_z")) + 5 + RunConsoleCommand("advdupe2_offset_z",Z) + return true + elseif(bind=="invnext")then + local Z = tonumber(ply:GetInfo("advdupe2_offset_z")) - 5 + RunConsoleCommand("advdupe2_offset_z",Z) + return true + end + + GAMEMODE:PlayerBindPress(ply, bind, pressed) +end + + +//Creates a ghost from the given entity's table +local function MakeGhostsFromTable( toolWep, gParent, EntTable, Player) + + if(!EntTable)then return end + + if(!EntTable.Model || !util.IsValidModel(EntTable.Model)) then EntTable.Model="models/error.mdl" end + + local GhostEntity + if ( EntTable.Model:sub( 1, 1 ) == "*" ) then + GhostEntity = ents.Create( "func_physbox" ) + else + GhostEntity = ents.Create( "gmod_ghost" ) + end + // If there are too many entities we might not spawn.. + if !IsValid(GhostEntity) then + RemoveGhosts(Player) + AdvDupe2.RemoveProgressBar(Player) + AdvDupe2.Notify(Player, "To many entities to spawn ghosts", NOTIFY_ERROR) + return + end + + local Phys = EntTable.PhysicsObjects[0] + EntTable.Pos = Phys.Pos + EntTable.Angle = Phys.Angle + duplicator.DoGeneric( GhostEntity, EntTable ) + + GhostEntity:Spawn() + GhostEntity:DrawShadow( false ) + GhostEntity:SetMoveType( MOVETYPE_NONE ) + GhostEntity:SetSolid( SOLID_VPHYSICS ); + GhostEntity:SetNotSolid( true ) + GhostEntity:SetRenderMode( RENDERMODE_TRANSALPHA ) + + GhostEntity:SetColor( 255, 255, 255, 150 ) + + // If we're a ragdoll send our bone positions + if ( EntTable.Class == "prop_ragdoll" ) then + for k, v in pairs( EntTable.PhysicsObjects ) do + if(k==0)then + GhostEntity:SetNetworkedBonePosition( k, Vector(0,0,0), v.Angle ) + else + GhostEntity:SetNetworkedBonePosition( k, v.Pos, v.Angle ) + end + end + Phys.Angle = Angle(0,0,0) + end + + if ( gParent ) then + local Parent = toolWep:GetNetworkedEntity("GhostEntity", nil) + local temp = Parent:GetAngles() + GhostEntity:SetPos(Parent:GetPos() + Phys.Pos - toolWep:GetNetworkedAngle("HeadOffset", Angle(0,0,0))) + GhostEntity:SetAngles(Phys.Angle) + Parent:SetAngles(toolWep:GetNetworkedAngle("HeadAngle", Angle(0,0,0))) + GhostEntity:SetParent(Parent) + Parent:SetAngles(temp) + else + GhostEntity:SetAngles(Phys.Angle) + toolWep:SetNetworkedEntity("GhostEntity", GhostEntity) + toolWep:SetNetworkedVector("HeadPos", Player.AdvDupe2.HeadEnt.Pos) + toolWep:SetNetworkedVector("HeadOffset", EntTable.Pos) + toolWep:SetNetworkedFloat("HeadZPos", Player.AdvDupe2.HeadEnt.Z) + toolWep:SetNetworkedAngle("HeadAngle", Phys.Angle) + + umsg.Start("AdvDupe2_Ghosting", Player) + umsg.End() + end + + return GhostEntity +end + + +local XTotal = 0 +local YTotal = 0 +local LastXDegree = 0 +//Retrieves the players files for the file browser, creates and updates ghosts, checks binds to modify dupes position and angles +function TOOL:Think() + + local ply = self:GetOwner() + + if(SERVER && ply.AdvDupe2)then + + if(ply.AdvDupe2.GhostEntities && !ply.AdvDupe2.Pasting)then + UpdateGhost(ply, self.Weapon) + end + + if(ply.AdvDupe2.Ghosting && CurTime()>=ply.AdvDupe2.LastGhost && !ply.AdvDupe2.Pasting)then + + local i = ply.AdvDupe2.GhostToSpawn[ply.AdvDupe2.CurrentGhost] + if(i!=nil)then + + local total = math.Round((math.Clamp( tonumber(ply:GetInfo("advdupe2_limit_ghost")) or 100, 1, 100 )/100)*#ply.AdvDupe2.GhostToSpawn) + if(ply.AdvDupe2.CurrentGhost >= total)then + + AdvDupe2.RemoveProgressBar(ply) + ply.AdvDupe2.Ghosting = false + ply.AdvDupe2.CurrentGhost=1 + end + + ply.AdvDupe2.GhostEntities[i] = MakeGhostsFromTable( self.Weapon, ply.AdvDupe2.HeadEnt.Index, table.Copy(ply.AdvDupe2.Entities[i]), ply) + ply.AdvDupe2.CurrentGhost = ply.AdvDupe2.CurrentGhost+1 + local barperc = math.floor((ply.AdvDupe2.CurrentGhost/total)*100) + if(!ply.AdvDupe2.Downloading)then + AdvDupe2.UpdateProgressBar(ply, barperc) + end + ply.AdvDupe2.LastGhost=CurTime()+0.02 + else + AdvDupe2.RemoveProgressBar(ply) + ply.AdvDupe2.Ghosting = false + ply.AdvDupe2.CurrentGhost=1 + end + + end + + if(ply.AdvDupe2.SendFiles && CurTime()>= ply.AdvDupe2.LastFile)then + if(ply.AdvDupe2.Folders[1])then + local Folder = ply.AdvDupe2.Folders[1] + AddFolder(ply, Folder.Name, Folder.ID, Folder.Parent, false) + table.remove(ply.AdvDupe2.Folders, 1) + elseif(ply.AdvDupe2.Files[1])then + local File = ply.AdvDupe2.Files[1] + AddFile(ply, File.Name, File.Parent, false) + table.remove(ply.AdvDupe2.Files, 1) + else + ply.AdvDupe2.SendFiles = false + ply.AdvDupe2.LastFile = 0 + end + + ply.AdvDupe2.LastFile = CurTime()+0.02 + end + + else + if(!AdvDupe2.GhostEntity)then return end + + UpdateGhost(ply, self.Weapon) + + local cmd = ply:GetCurrentCommand() + + if(ply:KeyDown(IN_USE))then + if(!AdvDupe2.Rotation)then + hook.Add("PlayerBindPress", "AdvDupe2_BindPress", MouseWheelScrolled) + AdvDupe2.Rotation = true + end + else + if(AdvDupe2.Rotation)then + AdvDupe2.Rotation = false + hook.Remove("PlayerBindPress", "AdvDupe2_BindPress") + end + + XTotal = 0 + YTotal = 0 + LastXDegree = 0 + + return + end + + local X = -cmd:GetMouseX()/-20 + local Y = cmd:GetMouseY()/-20 + + local X2 = 0 + local Y2 = 0 + + if(X!=0)then + + X2 = tonumber(ply:GetInfo("advdupe2_offset_yaw")) + + if(ply:KeyDown(IN_SPEED))then + XTotal = XTotal + X + local temp = XTotal + X2 + + local degree = math.Round(temp/45)*45 + if(degree>=225)then + degree = -135 + elseif(degree<=-225)then + degree = 135 + end + if(degree!=LastXDegree)then + XTotal = 0 + LastXDegree = degree + end + + X2 = degree + + else + + X2 = X2 + X + + if(X2<-180)then + X2 = X2+360 + elseif(X2>180)then + X2 = X2-360 + end + + end + + RunConsoleCommand("advdupe2_offset_yaw", X2) + end + + /*if(Y!=0)then + Y2 = tonumber(ply:GetInfo("advdupe2_offset_pitch")) + local Y3 = tonumber(ply:GetInfo("advdupe2_offset_roll")) + if(ply:KeyDown(IN_SPEED))then + YTotal = YTotal + Y + local temp = YTotal + Y2 + + local degree = math.Round(temp/45)*45 + if(degree>=225)then + degree = -135 + elseif(degree<=-225)then + degree = 135 + end + if(degree!=LastYDegree)then + YTotal = 0 + LastYDegree = degree + end + + Y2 = degree + else + local dir = LocalPlayer():GetForward() + + Y2 = Y2 + Y*dir.X + Y3 = Y3 + Y*dir.Y + + if(Y2<-180)then + Y2 = Y2+360 + elseif(Y2>180)then + Y2 = Y2-360 + end + end + + + + RunConsoleCommand("advdupe2_offset_pitch",Y2) + RunConsoleCommand("advdupe2_offset_roll",Y3) + end*/ + + cmd:SetMouseX(0) + cmd:SetMouseY(0) + end + +end + +//Hinder the player from looking to modify offsets with the mouse +function TOOL:FreezeMovement() + return AdvDupe2.Rotation +end + +//Checks table, re-draws loading bar, and recreates ghosts when tool is pulled out +function TOOL:Deploy() + if ( CLIENT ) then return end + local ply = self:GetOwner() + if ( !ply.AdvDupe2 ) then ply.AdvDupe2={} end + + if(!ply.AdvDupe2.Entities)then return end + if(ply.AdvDupe2.Queued)then + AdvDupe2.InitProgressBar(ply, "Queued: ") + return + end + + if(ply.AdvDupe2.Pasting)then + AdvDupe2.InitProgressBar(ply, "Pasting: ") + return + elseif(ply.AdvDupe2.Uploading)then + AdvDupe2.InitProgressBar(ply, "Uploading: ") + return + elseif(ply.AdvDupe2.Downloading)then + AdvDupe2.InitProgressBar(ply, "Downloading: ") + return + end + + AdvDupe2.StartGhosting(ply) +end + +//Removes progress bar and removes ghosts when tool is put away +function TOOL:Holster() + if( CLIENT ) then return end + + local ply = self:GetOwner() + if(self:GetStage()==1)then + AdvDupe2.RemoveSelectBox(ply) + end + + AdvDupe2.RemoveProgressBar(ply) + + if ( !ply.AdvDupe2 || !ply.AdvDupe2.GhostEntities || ply.AdvDupe2.Pasting ) then return end + + RemoveGhosts(ply) + +end + +//function for creating a contraption spawner +function MakeContraptionSpawner( ply, Pos, Ang, HeadEnt, EntityTable, ConstraintTable, delay, undo_delay, model, key, undo_key, disgrav, disdrag, addvel) + + if !ply:CheckLimit("gmod_contr_spawners") then return nil end + + if(!SinglePlayer())then + if(table.Count(EntityTable)>tonumber(GetConVarString("AdvDupe2_MaxContraptionEntities")))then + AdvDupe2.Notify(ply,"Contraption Spawner exceeds the maximum amount of "..GetConVarString("AdvDupe2_MaxContraptionEntities").." entities for a spawner!",NOTIFY_ERROR) + return false + end + if(#ConstraintTable>tonumber(GetConVarString("AdvDupe2_MaxContraptionConstraints")))then + AdvDupe2.Notify(ply,"Contraption Spawner exceeds the maximum amount of "..GetConVarString("AdvDupe2_MaxContraptionConstraints").." constraints for a spawner!",NOTIFY_ERROR) + return false + end + end + + local spawner = ents.Create("gmod_contr_spawner") + if !IsValid(spawner) then return end + + spawner:SetPos(Pos) + spawner:SetAngles(Ang) + spawner:SetModel(model) + spawner:SetRenderMode(RENDERMODE_TRANSALPHA) + spawner:Spawn() + + duplicator.ApplyEntityModifiers(ply, spawner) + + if IsValid(spawner:GetPhysicsObject()) then + spawner:GetPhysicsObject():EnableMotion(false) + end + + if !SinglePlayer() and delay < 0.2 then + delay = 0.33 + end + + if !SinglePlayer() and (undo_delay <=0 or undo_delay>=60) then + undo_delay = 15 + end + + // Set options + spawner:SetPlayer(ply) + spawner:GetTable():SetOptions(ply, delay, undo_delay, key, undo_key, disgrav, disdrag, addvel) + + local tbl = { + ply = ply, + delay = delay, + undo_delay = undo_delay, + disgrav = disgrav, + disdrag = disdrag, + addvel = addvel; + } + table.Merge(spawner:GetTable(), tbl) + + spawner:SetDupeInfo(HeadEnt, EntityTable, ConstraintTable) + spawner:AddGhosts() + + ply:AddCount("gmod_contr_spawners", spawner) + ply:AddCleanup("gmod_contr_spawner", spawner) + return spawner +end +duplicator.RegisterEntityClass("gmod_contr_spawner", MakeContraptionSpawner, "Pos", "Ang", "HeadEnt", "EntityTable", "ConstraintTable", "delay", "undo_delay", "model", "key", "undo_key", "disgrav", "disdrag", "addvel") + + +--[[ + Name: Reload + Desc: Creates an Advance Contraption Spawner. + Params: trace + Returns: success +]] +function TOOL:Reload( trace ) + if CLIENT then return true end + local ply = self:GetOwner() + + //If a contraption spawner was clicked then update it with the current settings + if(trace.Entity:GetClass()=="gmod_contr_spawner")then + trace.Entity:GetTable():SetOptions(ply, tonumber(ply:GetInfo("advdupe2_contr_spawner_delay")) or .33,tonumber(ply:GetInfo("advdupe2_contr_spawner_undo_delay")) or 0, tonumber(ply:GetInfo("advdupe2_contr_spawner_key")), tonumber(ply:GetInfo("advdupe2_contr_spawner_undo_key")), tonumber(ply:GetInfo("advdupe2_contr_spawner_disgrav")) or 0, tonumber(ply:GetInfo("advdupe2_contr_spawner_disdrag")) or 0, tonumber(ply:GetInfo("advdupe2_contr_spawner_addvel")) or 1 ) + return true + end + + //Create a contration spawner + if ply.AdvDupe2 and ply.AdvDupe2.Entities then + + local headent = ply.AdvDupe2.Entities[ply.AdvDupe2.HeadEnt.Index] + local ghostent = ply.AdvDupe2.GhostEntities[ply.AdvDupe2.HeadEnt.Index] + if(headent.Class=="gmod_contr_spawner") then return false end + local spawner = MakeContraptionSpawner( ply, ghostent:GetPos(), ghostent:GetAngles(), ply.AdvDupe2.HeadEnt.Index, table.Copy(ply.AdvDupe2.Entities), table.Copy(ply.AdvDupe2.Constraints), tonumber(ply:GetInfo("advdupe2_contr_spawner_delay")) or .33,tonumber(ply:GetInfo("advdupe2_contr_spawner_undo_delay")) or 0, headent.Model, tonumber(ply:GetInfo("advdupe2_contr_spawner_key")), tonumber(ply:GetInfo("advdupe2_contr_spawner_undo_key")), tonumber(ply:GetInfo("advdupe2_contr_spawner_disgrav")) or 0, tonumber(ply:GetInfo("advdupe2_contr_spawner_disdrag")) or 0, tonumber(ply:GetInfo("advdupe2_contr_spawner_addvel")) or 1 ) + ply:AddCleanup( "AdvDupe2", spawner ) + undo.Create("gmod_contr_spawner") + undo.AddEntity( spawner ) + undo.SetPlayer( ply ) + undo.Finish() + + return true + end +end + + + +if SERVER then + + CreateConVar("sbox_maxgmod_contr_spawners",5) + + function AdvDupe2.StartGhosting(ply) + RemoveGhosts(ply) + + if(!ply.AdvDupe2.Entities)then return end + + local index = ply.AdvDupe2.HeadEnt.Index + + local tool = ply:GetTool() + if(!tool || ply:GetActiveWeapon():GetClass()!="gmod_tool" || tool.Mode!="advdupe2")then return end + + ply.AdvDupe2.GhostEntities = {} + ply.AdvDupe2.GhostEntities[index] = MakeGhostsFromTable( tool.Weapon, nil, table.Copy(ply.AdvDupe2.Entities[index]), ply) + + if !IsValid(ply.AdvDupe2.GhostEntities[index]) then + ply.AdvDupe2.GhostEntities = nil + AdvDupe2.Notify(ply, "Parent ghost is invalid, not creating ghosts", NOTIFY_ERROR) + return + end + + ply.AdvDupe2.GhostToSpawn = {} + local total = 1 + for k,v in pairs(ply.AdvDupe2.Entities)do + if(k!=index)then + ply.AdvDupe2.GhostToSpawn[total] = k + total = total + 1 + end + end + ply.AdvDupe2.LastGhost = CurTime()+0.02 + AdvDupe2.InitProgressBar(ply, "Ghosting: ") + ply.AdvDupe2.Ghosting = true + end + + local function RenameNode(ply, newname) + umsg.Start("AdvDupe2_RenameNode", ply) + umsg.String(newname) + umsg.End() + end + + --[[==============]]-- + --[[FILE FUNCTIONS]]-- + --[[==============]]-- + + //Download a file from the server + local function DownloadFile(ply, cmd, args) + + if(ply.AdvDupe2.Pasting || ply.AdvDupe2.Downloading)then + AdvDupe2.Notify(ply,"Advanced Duplicator 2 is busy.",NOTIFY_ERROR) + return false + end + if(!tobool(GetConVarString("AdvDupe2_AllowDownloading")))then + AdvDupe2.Notify(ply,"Downloading is not allowed.",NOTIFY_ERROR) + return false + end + + local path = args[1] + local area = tonumber(args[2]) + + local newfile + if(area==0)then //AD2 folder in client's folder + newfile = ply:GetAdvDupe2Folder().."/"..path..".txt" + elseif(area==1)then //Public folder + if(!tobool(GetConVarString("AdvDupe2_AllowPublicFolder")))then + AdvDupe2.Notify(ply,"Public Folder is disabled.",NOTIFY_ERROR) + return + end + newfile = AdvDupe2.DataFolder.."/=Public=/"..path..".txt" + else //AD1 folder in client's folder + newfile = "adv_duplicator/"..ply:GetAdvDupe2Folder().."/"..path..".txt" + end + + if(!file.Exists(newfile))then return end + + AdvDupe2.EstablishNetwork(ply, file.Read(newfile)) + end + concommand.Add("AdvDupe2_DownloadFile", DownloadFile) + + //Open a file on the server + local function OpenFile(ply, cmd, args) + if(args[1]=="" || args[1]==nil || args[2]=="" || args[2]==nil)then return end + + if(ply.AdvDupe2.Pasting || ply.AdvDupe2.Downloading)then + AdvDupe2.Notify(ply,"Advanced Duplicator 2 is busy.",NOTIFY_ERROR) + return false + end + + if(!SinglePlayer() && CurTime()-(ply.AdvDupe2.FileMod or 0) < 0)then + AdvDupe2.Notify(ply,"Cannot open at the moment.", NOTIFY_ERROR) + return + end + ply.AdvDupe2.FileMod = CurTime()+tonumber(GetConVarString("AdvDupe2_FileModificationDelay")) + + local path, area = args[1], tonumber(args[2]) + local name = string.Explode("/", args[1]) + name = name[#name] + + if(area==0)then + data = ply:ReadAdvDupe2File(path) + elseif(area==1)then + if(SinglePlayer())then path = "=Public=/"..path end + data = AdvDupe2.ReadFile(nil, path) + else + data = AdvDupe2.ReadFile(ply, path, "adv_duplicator") + end + if(data==false)then + AdvDupe2.Notify(ply,"File size is greater than "..GetConVarString("AdvDupe2_MaxFileSize"), NOTIFY_ERROR) + return + end + + AdvDupe2.Decode(data, function(success,dupe,info,moreinfo) + + if(!IsValid(ply))then return end + + if(!SinglePlayer())then + if(#dupe["Constraints"]>tonumber(GetConVarString("AdvDupe2_MaxConstraints")))then + AdvDupe2.Notify(ply,"Amount of constraints is greater than "..GetConVarString("AdvDupe2_MaxConstraints"),NOTIFY_ERROR) + return false + end + + if(tonumber(GetConVarString("AdvDupe2_MaxEntities"))>0)then + if(table.Count(dupe["Entities"])>tonumber(GetConVarString("AdvDupe2_MaxEntities")))then + AdvDupe2.Notify(ply,"Amount of entities is greater than "..GetConVarString("AdvDupe2_MaxEntities"),NOTIFY_ERROR) + return false + end + else + if(table.Count(dupe["Entities"])>tonumber(GetConVarString("sbox_maxprops")))then + AdvDupe2.Notify(ply,"Amount of entities is greater than "..GetConVarString("sbox_maxprops"),NOTIFY_ERROR) + return false + end + end + end + + if not success then + AdvDupe2.Notify(ply,"Could not open "..dupe,NOTIFY_ERROR) + return + end + + ply.AdvDupe2.Entities = {} + ply.AdvDupe2.Constraints = {} + ply.AdvDupe2.HeadEnt={} + local time = "" + local desc = "" + local date = "" + local creator = "" + + if(info.ad1)then + time = moreinfo["Time"] + desc = info["Description"] + date = info["Date"] + creator = info["Creator"] + + ply.AdvDupe2.HeadEnt.Index = tonumber(moreinfo.Head) + local startpos = string.Explode(",", moreinfo.StartPos) + ply.AdvDupe2.HeadEnt.Pos = Vector(tonumber(startpos[1]), tonumber(startpos[2]), tonumber(startpos[3])) + ply.AdvDupe2.HeadEnt.Z = tonumber(string.Explode(",", moreinfo.HoldPos)[3])*-1 + local z = ply.AdvDupe2.HeadEnt.Z + local Pos = nil + local Ang = nil + for k,v in pairs(dupe["Entities"])do + Pos = nil + Ang = nil + if(v.SavedParentIdx)then + if(!v.BuildDupeInfo)then v.BuildDupeInfo = {} end + v.BuildDupeInfo.DupeParentID = v.SavedParentIdx + Pos = v.LocalPos*1 + Ang = v.LocalAngle*1 + end + for i,p in pairs(v.PhysicsObjects)do + p.Pos = Pos or (p.LocalPos*1) + p.Pos.Z = p.Pos.Z - z + p.Angle = Ang or (p.LocalAngle*1) + p.LocalPos = nil + p.LocalAngle = nil + end + v.LocalPos = nil + v.LocalAngle = nil + end + + ply.AdvDupe2.Entities = dupe["Entities"] + ply.AdvDupe2.Constraints = dupe["Constraints"] + + else + time = info["time"] + desc = dupe["Description"] + date = info["date"] + creator = info["name"] + + ply.AdvDupe2.Entities = dupe["Entities"] + ply.AdvDupe2.Constraints = dupe["Constraints"] + ply.AdvDupe2.HeadEnt = dupe["HeadEnt"] + end + + //ply.AdvDupe2.Name = name[#name] + + umsg.Start("AdvDupe2_SetDupeInfo", ply) + umsg.String(name) + umsg.String(creator) + umsg.String(date) + umsg.String(time) + umsg.String(string.NiceSize(tonumber(info.size) or 0)) + umsg.String(desc) + umsg.String(table.Count(ply.AdvDupe2.Entities)) + umsg.String(#ply.AdvDupe2.Constraints) + umsg.End() + + AdvDupe2.ResetOffsets(ply) + AdvDupe2.StartGhosting(ply) + end) + end + concommand.Add("AdvDupe2_OpenFile", OpenFile) + + //Save a file to the server + local function SaveFile(ply, cmd, args) + if(!ply.AdvDupe2 || !ply.AdvDupe2.Entities || ply.AdvDupe2.Entities == {})then return end + if(args[1]=="" || args[1]==nil || args[3]=="" || args[3]==nil)then return end + + if(!SinglePlayer() && CurTime()-(ply.AdvDupe2.FileMod or 0) < 0)then + AdvDupe2.Notify(ply,"Cannot save at the moment.", NOTIFY_ERROR) + return + end + ply.AdvDupe2.FileMod = CurTime()+tonumber(GetConVarString("AdvDupe2_FileModificationDelay")) + + local path, area = args[1], tonumber(args[3]) + local public = false + + if(args[2]!="")then + path = args[2].."/"..path + end + + if(area==1)then + if(!tobool(GetConVarString("AdvDupe2_AllowPublicFolder")))then + AdvDupe2.Notify(ply,"Public Folder is disabled.",NOTIFY_ERROR) + return + end + if(SinglePlayer())then path = "=Public=/"..path end + public = true + elseif(area==2)then + AdvDupe2.Notify(ply,"Cannot save into this directory.",NOTIFY_ERROR) + return + end + + umsg.Start("AdvDupe2_SetDupeInfo", ply) + umsg.String(args[1]) + umsg.String(ply:Nick()) + umsg.String(os.date("%d %B %Y")) + umsg.String(os.date("%I:%M %p")) + umsg.String("") + umsg.String(args[4]) + umsg.String(table.Count(ply.AdvDupe2.Entities)) + umsg.String(#ply.AdvDupe2.Constraints) + umsg.End() + + local Tab = {Entities = ply.AdvDupe2.Entities, Constraints = ply.AdvDupe2.Constraints, HeadEnt = ply.AdvDupe2.HeadEnt, Description=args[4]} + + AdvDupe2.Encode( + Tab, + AdvDupe2.GenerateDupeStamp(ply), + function(data) + local dir, name = "", "" + if(!public)then + dir, name = ply:WriteAdvDupe2File(path, data) + else + dir, name = AdvDupe2.WriteFile(nil, path, data) + end + AddFile(ply,name,args[5],true) + end) + + if(!SinglePlayer() && tobool(GetConVarString("AdvDupe2_RemoveFilesOnDisconnect")))then + AdvDupe2.Notify(ply, "Your saved files will be deleted when you disconnect!", NOTIFY_CLEANUP, 10) + end + end + concommand.Add("AdvDupe2_SaveFile", SaveFile) + + //Add a new folder to the server + local function NewFolder(ply, cmd, args) + + if(!SinglePlayer() && CurTime()-(ply.AdvDupe2.FileMod or 0) < 0)then + AdvDupe2.Notify(ply,"Cannot create a new folder at the moment.", NOTIFY_ERROR) + return + end + ply.AdvDupe2.FileMod = CurTime()+tonumber(GetConVarString("AdvDupe2_FileModificationDelay")) + + local path, area = args[1], tonumber(args[3]) + local public = false + if path:find("%W") then AdvDupe2.Notify(ply,"Invalid folder name.",NOTIFY_ERROR) return false end + + if(args[2]!="")then + path = args[2].."/"..path + end + + if(area==0)then + path = ply:GetAdvDupe2Folder().."/"..path + elseif(area==1)then + if(!tobool(GetConVarString("AdvDupe2_AllowPublicFolder")))then + AdvDupe2.Notify(ply,"Public Folder is disabled.",NOTIFY_ERROR) + return + end + path = AdvDupe2.DataFolder.."/=Public=/"..path + else + path = "adv_duplicator/"..ply:SteamIDSafe().."/"..path + end + + + if(file.IsDir(path))then + AdvDupe2.Notify(ply,"Folder name already exists.",NOTIFY_ERROR) + return + end + file.CreateDir(path) + ply.AdvDupe2.FolderID = ply.AdvDupe2.FolderID+1 + AddFolder(ply, args[1], ply.AdvDupe2.FolderID, args[4], true) + end + concommand.Add("AdvDupe2_NewFolder", NewFolder) + + local function TFindDelete(Search, Folders, Files) + Search = string.sub(Search, 6, -2) + + for k,v in pairs(Files)do + file.Delete(Search..v) + end + + for k,v in pairs(Folders)do + file.TFind("Data/"..Search..v.."/*", + function(Search2, Folders2, Files2) + TFindDelete(Search2, Folders2, Files2) + end) + end + end + + //Delete a file on the server + local function DeleteFile(ply, cmd, args) + + if(!SinglePlayer() && CurTime()-(ply.AdvDupe2.FileMod or 0) < 0)then + AdvDupe2.Notify(ply,"Cannot delete at the moment.", NOTIFY_ERROR) + return + end + ply.AdvDupe2.FileMod = CurTime()+tonumber(GetConVarString("AdvDupe2_FileModificationDelay")) + + local path, area = args[1], tonumber(args[2]) + local folder = tobool(args[3]) + + + if(area==0)then + if(folder)then + path = ply:GetAdvDupe2Folder().."/"..path + else + path = ply:GetAdvDupe2Folder().."/"..path..".txt" + end + elseif(area==1)then + if(!ply:IsAdmin())then + AdvDupe2.Notify(ply,"You are not an admin.",NOTIFY_ERROR) + return + end + if(folder)then + path = AdvDupe2.DataFolder.."/=Public=/"..path + else + path = AdvDupe2.DataFolder.."/=Public=/"..path..".txt" + end + else + if(folder)then + path = "adv_duplicator/"..ply:SteamIDSafe().."/"..path + else + path = "adv_duplicator/"..ply:SteamIDSafe().."/"..path..".txt" + end + end + if(!folder && file.Exists(path))then + file.Delete(path) + end + + if(folder && file.IsDir(path))then + file.TFind("Data/"..path.."/*", + function(Search, Folders, Files) + TFindDelete(Search, Folders, Files) + end) + end + umsg.Start("AdvDupe2_DeleteNode") + umsg.End() + + end + concommand.Add("AdvDupe2_DeleteFile", DeleteFile) + + local function RenameFile(ply, cmd, args) + + if(!SinglePlayer() && CurTime()-(ply.AdvDupe2.FileMod or 0) < 0)then + AdvDupe2.Notify(ply,"Cannot rename at the moment.", NOTIFY_ERROR) + return + end + ply.AdvDupe2.FileMod = CurTime()+tonumber(GetConVarString("AdvDupe2_FileModificationDelay")) + + local Alt = tonumber(args[1]) or nil + if(Alt==nil)then return end + local NewName = args[2] + local Path = args[3] + + if(Alt==0)then + Path = ply:GetAdvDupe2Folder().."/"..Path + elseif(Alt==1)then + AdvDupe2.Notify(ply, "Public folder modification not allowed", NOTIFY_ERROR) + //Path = AdvDupe2.DataFolder.."/"..Path + else + Path = "adv_duplicator/"..ply:SteamIDSafe().."/"..Path + end + + local OldName = string.Explode("/", Path) + OldName = OldName[#OldName] + + local NewPath = string.sub(Path, 1, -#OldName-1)..NewName + + if file.Exists(NewPath..".txt") then + local found = false + for i = 1, AdvDupe2.FileRenameTryLimit do + if not file.Exists(NewPath.."_"..i..".txt") then + NewPath = NewPath.."_"..i + found = true + break + end + end + if(!found)then AdvDupe2.Notify(ply, "File could not be renamed.", NOTIFY_ERROR) return end + end + local File = file.Read(Path..".txt") + file.Write(NewPath..".txt", File) + + if(file.Exists(NewPath..".txt"))then + file.Delete(Path..".txt") + NewName = string.Explode("/", NewPath) + RenameNode(ply, NewName[#NewName]) + else + AdvDupe2.Notify(ply, "File rename failed.", NOTIFY_ERROR) + end + + end + concommand.Add("AdvDupe2_RenameFile", RenameFile) + + local function MoveFile(ply, cmd, args) + + if(!SinglePlayer() && CurTime()-(ply.AdvDupe2.FileMod or 0) < 0)then + AdvDupe2.Notify(ply,"Cannot move file at the moment.", NOTIFY_ERROR) + return + end + ply.AdvDupe2.FileMod = CurTime()+tonumber(GetConVarString("AdvDupe2_FileModificationDelay")) + + local area1, area2 = tonumber(args[1]) or nil, tonumber(args[2]) or nil + local path1, path2 = args[3], args[4] + + if(area1==nil || area2==nil)then return end + if((area1==2 && area2!=2) || (area2==2 && area1!=2))then return end + + local name = string.Explode("/", path1) + name = name[#name] + + path1 = ply:SteamIDSafe().."/"..path1 + path2 = ply:SteamIDSafe().."/"..path2.."/"..name + + if(area1==0)then + path1 = AdvDupe2.DataFolder.."/"..path1 + elseif(area1==1)then + AdvDupe2.Notify(ply, "Public folder modification not allowed", NOTIFY_ERROR) + //path1 = AdvDupe2.DataFolder.."/".."=Public=/"..path1 + return + else + path1 = "adv_duplicator/"..path1 + end + + if(area2==0)then + path2 = AdvDupe2.DataFolder.."/"..path2 + elseif(area2==1)then + AdvDupe2.Notify(ply, "Public folder modification not allowed", NOTIFY_ERROR) + //path2 = AdvDupe2.DataFolder.."/".."=Public=/"..path2 + return + else + path2 = "adv_duplicator/"..path2 + end + + local File = file.Read(path1..".txt") + if(!File)then return end + + if file.Exists(path2..".txt") then + local found = false + for i = 1, AdvDupe2.FileRenameTryLimit do + if not file.Exists(path2.."_"..i..".txt") then + path2 = path2.."_"..i + found = true + break + end + end + if(!found)then AdvDupe2.Notify(ply, "File could not be renamed.", NOTIFY_ERROR) return end + end + + file.Write(path2..".txt", File) + if(file.Exists(path2..".txt"))then + file.Delete(path1..".txt") + local name = string.Explode("/", path2) + + umsg.Start("AdvDupe2_MoveNode", ply) + umsg.String(name[#name]) + umsg.End() + else + AdvDupe2.Notify(ply, "File could not be moved.", NOTIFY_ERROR) + end + + end + concommand.Add("AdvDupe2_MoveFile", MoveFile) + + //TFind files and folders on the server + local function TFind(ply, Search, Folders, Files, parent) + + for k,v in pairs(Files)do + local File = {} + File.Name = string.Left(v, #v-4) + File.IsFolder = 0 + File.Parent = parent + table.insert(ply.AdvDupe2.Files, File) + end + for k,v in pairs(Folders)do + ply.AdvDupe2.FolderID=ply.AdvDupe2.FolderID+1 + local Folder = {} + Folder.Name = v + Folder.Parent = parent + Folder.ID = ply.AdvDupe2.FolderID + table.insert(ply.AdvDupe2.Folders, Folder) + file.TFind(string.Left(Search,#Search-1)..v.."/*", function(Search2, Folders2, Files2) TFind(ply, Search2, Folders2, Files2, Folder.ID) end) + end + ply.AdvDupe2.SendFiles = true + end + + concommand.Add("AdvDupe2_SendFiles", function(ply, cmd, args) + + if(ply.AdvDupe2 && !SinglePlayer() && CurTime()-(ply.AdvDupe2.NextSend or 0) < 0)then + AdvDupe2.Notify(ply,"Cannot update at the moment.",NOTIFY_ERROR) + return + elseif(tonumber(args[1])==0)then + umsg.Start("AdvDupe2_ClearBrowser", ply) + umsg.End() + return + end + + if(!ply.AdvDupe2)then ply.AdvDupe2 = {} end + if(ply.AdvDupe2.SendFiles)then return end + + file.TFind("Data/"..ply:GetAdvDupe2Folder().."/*", + function(Search, Folders, Files) + if(!ply.AdvDupe2)then ply.AdvDupe2 = {} end + ply.AdvDupe2.Folders = {} + ply.AdvDupe2.Files = {} + ply.AdvDupe2.FolderID = 0 + ply.AdvDupe2.NextSend = CurTime() + tonumber(GetConVarString("AdvDupe2_UpdateFilesDelay")) + + local AD1 = "adv_duplicator" + if(!SinglePlayer())then + AD1 = AD1.."/"..ply:SteamIDSafe() + end + ply.AdvDupe2.FolderID=ply.AdvDupe2.FolderID+1 + local AD1Folder = {} + AD1Folder.Name = "=Adv Duplicator=" + AD1Folder.Parent = 0 + AD1Folder.ID = ply.AdvDupe2.FolderID + table.insert(ply.AdvDupe2.Folders, AD1Folder) + + if(!SinglePlayer() && tobool(GetConVarString("AdvDupe2_AllowPublicFolder")))then + ply.AdvDupe2.FolderID=ply.AdvDupe2.FolderID+1 + local Folder = {} + Folder.Name = "=Public=" + Folder.Parent = 0 + Folder.ID = ply.AdvDupe2.FolderID + Folder.Public = true + table.insert(ply.AdvDupe2.Folders, Folder) + file.TFind("Data/advdupe2/=Public=/*", function(Search, Folders, Files) TFind(ply, Search, Folders, Files, Folder.ID) end) + end + + file.TFind("Data/"..AD1.."/*", + function(Search2, Folders2, Files2) + TFind(ply, Search2, Folders2, Files2, AD1Folder.ID) + end) + + TFind(ply, Search, Folders, Files, 0) + ply.AdvDupe2.LastFile = 0 + ply.AdvDupe2.SendFiles = true + end) + + + end) + + --[[=====================]]-- + --[[END OF FILE FUNCTIONS]]-- + --[[=====================]]-- + + function AdvDupe2.InitProgressBar(ply,label) + umsg.Start("AdvDupe2_InitProgressBar",ply) + umsg.String(label) + umsg.End() + end + + concommand.Add("AdvDupe2_RemakeGhosts", function(ply, cmd, args) + RemoveGhosts(ply) + AdvDupe2.StartGhosting(ply) + AdvDupe2.ResetOffsets(ply) + end) +end + + +concommand.Add( "SaveDupe", SaveDupe ) +concommand.Add( "ReadDupe", ReadDupe ) +if CLIENT then + + language.Add( "Tool_advdupe2_name", "Advanced Duplicator 2" ) + language.Add( "Tool_advdupe2_desc", "Duplicate things." ) + language.Add( "Tool_advdupe2_0", "Primary: Paste, Secondary: Copy." ) + language.Add( "Tool_advdupe2_1", "Primary: Paste, Secondary: Copy an area." ) + language.Add( "Undone_AdvDupe2", "Undone AdvDupe2 paste" ) + language.Add( "Cleanup_AdvDupe2", "Adv. Duplications" ) + language.Add( "Cleaned_AdvDupe2", "Cleaned up all Adv. Duplications" ) + language.Add( "SBoxLimit_AdvDupe2", "You've reached the Adv. Duplicator limit!" ) + + CreateClientConVar("advdupe2_offset_world", 0, false, true) + CreateClientConVar("advdupe2_offset_z", 0, false, true) + CreateClientConVar("advdupe2_offset_pitch", 0, false, true) + CreateClientConVar("advdupe2_offset_yaw", 0, false, true) + CreateClientConVar("advdupe2_offset_roll", 0, false, true) + CreateClientConVar("advdupe2_original_origin", 0, false, true) + CreateClientConVar("advdupe2_paste_constraints", 1, false, true) + CreateClientConVar("advdupe2_paste_parents", 1, false, true) + CreateClientConVar("advdupe2_paste_unfreeze", 0, false, true) + CreateClientConVar("advdupe2_preserve_freeze", 0, false, true) + CreateClientConVar("advdupe2_copy_outside", 0, false, true) + CreateClientConVar("advdupe2_limit_ghost", 100, false, true) + CreateClientConVar("advdupe2_area_copy_size", 300, false, true) + + //Contraption Spawner + CreateClientConVar("advdupe2_contr_spawner_key", -1, false, true) + CreateClientConVar("advdupe2_contr_spawner_undo_key", -1, false, true) + CreateClientConVar("advdupe2_contr_spawner_delay", 0, false, true) + CreateClientConVar("advdupe2_contr_spawner_undo_delay", 0, false, true) + CreateClientConVar("advdupe2_contr_spawner_disgrav", 0, false, true) + CreateClientConVar("advdupe2_contr_spawner_disdrag", 0, false, true) + CreateClientConVar("advdupe2_contr_spawner_addvel", 1, false, true) + + //Experimental + CreateClientConVar("advdupe2_paste_disparents", 0, false, true) + + local function BuildCPanel() + local CPanel = GetControlPanel("advdupe2") + + if not CPanel then return end + CPanel:ClearControls() + local Fill = vgui.Create( "DPanel" ) + CPanel:AddPanel(Fill) + Fill:SetTall(CPanel:GetParent():GetParent():GetTall()-45) + local List = vgui.Create( "DPanelList", CPanel ) + List:EnableVerticalScrollbar( true ) + List:Dock( FILL ) + List:SetSpacing( 2 ) + List:SetPadding( 2 ) + + local FileBrowser = vgui.Create("advdupe2_browser") + AdvDupe2.FileBrowser = FileBrowser + List:AddItem(FileBrowser) + FileBrowser:SetSize(235,450) + FileBrowser.Filler = Fill + FileBrowser.Initialized = true + RunConsoleCommand("AdvDupe2_SendFiles") + + local Check = vgui.Create("DCheckBoxLabel") + Check:SetText( "Paste at original position" ) + Check:SetConVar( "advdupe2_original_origin" ) + Check:SetValue( 0 ) + Check:SetToolTip("Paste at the coords originally copied") + List:AddItem(Check) + + Check = vgui.Create("DCheckBoxLabel") + Check:SetText( "Paste with constraints" ) + Check:SetConVar( "advdupe2_paste_constraints" ) + Check:SetValue( 1 ) + Check:SetToolTip("Paste with or without constraints") + List:AddItem(Check) + + Check = vgui.Create("DCheckBoxLabel") + Check:SetText( "Paste with parenting" ) + Check:SetConVar( "advdupe2_paste_parents" ) + Check:SetValue( 1 ) + Check:SetToolTip("Paste with or without parenting") + List:AddItem(Check) + + local Check_1 = vgui.Create("DCheckBoxLabel") + local Check_2 = vgui.Create("DCheckBoxLabel") + + Check_1:SetText( "Unfreeze all after paste" ) + Check_1:SetConVar( "advdupe2_paste_unfreeze" ) + Check_1:SetValue( 0 ) + Check_1.OnChange = function() + if(Check_1:GetChecked() and Check_2:GetChecked())then + Check_2:SetValue(0) + end + end + Check_1:SetToolTip("Unfreeze all props after pasting") + List:AddItem(Check_1) + + Check_2:SetText( "Preserve frozen state after paste" ) + Check_2:SetConVar( "advdupe2_preserve_freeze" ) + Check_2:SetValue( 0 ) + Check_2.OnChange = function() + if(Check_2:GetChecked() and Check_1:GetChecked())then + Check_1:SetValue(0) + end + end + Check_2:SetToolTip("Makes props have the same frozen state as when they were copied") + List:AddItem(Check_2) + + Check = vgui.Create("DCheckBoxLabel") + Check:SetText( "Area copy constrained props outside of box" ) + Check:SetConVar( "advdupe2_copy_outside" ) + Check:SetValue( 0 ) + Check:SetToolTip("Copy entities outside of the area copy that are constrained to entities insde") + List:AddItem(Check) + + local NumSlider = vgui.Create( "DNumSlider" ) + NumSlider:SetText( "Percent of ghost to create" ) + NumSlider:SetMin( 0 ) + NumSlider:SetMax( 100 ) + NumSlider:SetDecimals( 0 ) + NumSlider:SetConVar( "advdupe2_limit_ghost" ) + NumSlider:SetToolTip("Change the percent of ghosts to spawn") + //If these funcs are not here, problems occur for each + local func = NumSlider.Slider.OnMouseReleased + NumSlider.Slider.OnMouseReleased = function(mcode) func(mcode) RunConsoleCommand("AdvDupe2_RemakeGhosts") end + local func2 = NumSlider.Wang.OnMouseReleased //Hacky way to make it work + NumSlider.Wang.OnMouseReleased = function(mousecode) func2(mousecode) RunConsoleCommand("AdvDupe2_RemakeGhosts") end + local func3 = NumSlider.Wang.TextEntry.OnLoseFocus + NumSlider.Wang.TextEntry.OnLoseFocus = function(txtBox) func3(txtBox) RunConsoleCommand("AdvDupe2_RemakeGhosts") end + List:AddItem(NumSlider) + + NumSlider = vgui.Create( "DNumSlider" ) + NumSlider:SetText( "Area Copy Size" ) + NumSlider:SetMin( 0 ) + NumSlider:SetMax( 2500 ) + NumSlider:SetDecimals( 0 ) + NumSlider:SetConVar( "advdupe2_area_copy_size" ) + NumSlider:SetToolTip("Change the size of the area copy") + List:AddItem(NumSlider) + + local Category1 = vgui.Create("DCollapsibleCategory") + List:AddItem(Category1) + Category1:SetLabel("Offsets") + Category1:SetExpanded(0) + + --[[Offsets]]-- + local CategoryContent1 = vgui.Create( "DPanelList" ) + CategoryContent1:SetAutoSize( true ) + CategoryContent1:SetDrawBackground( false ) + CategoryContent1:SetSpacing( 1 ) + CategoryContent1:SetPadding( 2 ) + + Category1:SetContents( CategoryContent1 ) + + NumSlider = vgui.Create( "DNumSlider" ) + NumSlider:SetText( "Height Offset" ) + NumSlider:SetMin( 0 ) + NumSlider:SetMax( 2500 ) + NumSlider:SetDecimals( 0 ) + NumSlider:SetConVar("advdupe2_offset_z") + NumSlider:SetToolTip("Change the Z offset of the dupe") + CategoryContent1:AddItem(NumSlider) + + Check = vgui.Create("DCheckBoxLabel") + Check:SetText( "Use World Angles" ) + Check:SetConVar( "advdupe2_offset_world" ) + Check:SetValue( 0 ) + Check:SetToolTip("Use world angles for the offset instead of the main entity") + CategoryContent1:AddItem(Check) + + NumSlider = vgui.Create( "DNumSlider" ) + NumSlider:SetText( "Pitch Offset" ) + NumSlider:SetMin( -180 ) + NumSlider:SetMax( 180 ) + NumSlider:SetDecimals( 0 ) + NumSlider:SetConVar("advdupe2_offset_pitch") + CategoryContent1:AddItem(NumSlider) + + NumSlider = vgui.Create( "DNumSlider" ) + NumSlider:SetText( "Yaw Offset" ) + NumSlider:SetMin( -180 ) + NumSlider:SetMax( 180 ) + NumSlider:SetDecimals( 0 ) + NumSlider:SetConVar("advdupe2_offset_yaw") + CategoryContent1:AddItem(NumSlider) + + NumSlider = vgui.Create( "DNumSlider" ) + NumSlider:SetText( "Roll Offset" ) + NumSlider:SetMin( -180 ) + NumSlider:SetMax( 180 ) + NumSlider:SetDecimals( 0 ) + NumSlider:SetConVar("advdupe2_offset_roll") + CategoryContent1:AddItem(NumSlider) + + + --[[Dupe Information]]-- + local Category2 = vgui.Create("DCollapsibleCategory") + List:AddItem(Category2) + Category2:SetLabel("Dupe Information") + Category2:SetExpanded(0) + + local CategoryContent2 = vgui.Create( "DPanelList" ) + CategoryContent2:SetAutoSize( true ) + CategoryContent2:SetDrawBackground( false ) + CategoryContent2:SetSpacing( 3 ) + CategoryContent2:SetPadding( 2 ) + Category2:SetContents( CategoryContent2 ) + + AdvDupe2.Info = {} + + local lbl = vgui.Create( "DLabel" ) + lbl:SetText("File: ") + CategoryContent2:AddItem(lbl) + AdvDupe2.Info.File = lbl + + lbl = vgui.Create( "DLabel" ) + lbl:SetText("Creator:") + CategoryContent2:AddItem(lbl) + AdvDupe2.Info.Creator = lbl + + lbl = vgui.Create( "DLabel" ) + lbl:SetText("Date:") + CategoryContent2:AddItem(lbl) + AdvDupe2.Info.Date = lbl + + lbl = vgui.Create( "DLabel" ) + lbl:SetText("Time:") + CategoryContent2:AddItem(lbl) + AdvDupe2.Info.Time = lbl + + lbl = vgui.Create( "DLabel" ) + lbl:SetText("Size:") + CategoryContent2:AddItem(lbl) + AdvDupe2.Info.Size = lbl + + lbl = vgui.Create( "DLabel" ) + lbl:SetText("Desc:") + CategoryContent2:AddItem(lbl) + AdvDupe2.Info.Desc = lbl + + lbl = vgui.Create( "DLabel" ) + lbl:SetText("Entities:") + CategoryContent2:AddItem(lbl) + AdvDupe2.Info.Entities = lbl + + lbl = vgui.Create( "DLabel" ) + lbl:SetText("Constraints:") + CategoryContent2:AddItem(lbl) + AdvDupe2.Info.Constraints = lbl + + --[[Contraption Spawner]]-- + local Category3 = vgui.Create("DCollapsibleCategory") + List:AddItem(Category3) + Category3:SetLabel("Contraption Spawner") + Category3:SetExpanded(0) + + local CategoryContent3 = vgui.Create( "DPanelList" ) + CategoryContent3:SetAutoSize( true ) + CategoryContent3:SetDrawBackground( false ) + CategoryContent3:SetSpacing( 3 ) + CategoryContent3:SetPadding( 2 ) + Category3:SetContents( CategoryContent3 ) + + local ctrl = vgui.Create( "CtrlNumPad" ) + ctrl:SetConVar1( "advdupe2_contr_spawner_key" ) + ctrl:SetConVar2( "advdupe2_contr_spawner_undo_key" ) + ctrl:SetLabel1( "Spawn Key") + ctrl:SetLabel2( "Undo Key" ) + CategoryContent3:AddItem(ctrl) + + NumSlider = vgui.Create( "DNumSlider" ) + NumSlider:SetText( "Spawn Delay" ) + NumSlider:SetMin( 0 ) + NumSlider:SetMax( 256 ) + NumSlider:SetDecimals( 0 ) + NumSlider:SetConVar("advdupe2_contr_spawner_delay") + CategoryContent3:AddItem(NumSlider) + + NumSlider = vgui.Create( "DNumSlider" ) + NumSlider:SetText( "Undo Delay" ) + NumSlider:SetMin( 0 ) + NumSlider:SetMax( 256 ) + NumSlider:SetDecimals( 0 ) + NumSlider:SetConVar("advdupe2_contr_spawner_undo_delay") + CategoryContent3:AddItem(NumSlider) + + Check = vgui.Create("DCheckBoxLabel") + Check:SetText( "Disable gravity for all spawned props" ) + Check:SetConVar( "advdupe2_contr_spawner_disgrav" ) + Check:SetValue( 0 ) + CategoryContent3:AddItem(Check) + + Check = vgui.Create("DCheckBoxLabel") + Check:SetText( "Disable drag for all spawned props" ) + Check:SetConVar( "advdupe2_contr_spawner_disdrag" ) + Check:SetValue( 0 ) + CategoryContent3:AddItem(Check) + + Check = vgui.Create("DCheckBoxLabel") + Check:SetText( "Add spawner's velocity to contraption" ) + Check:SetConVar( "advdupe2_contr_spawner_addvel" ) + Check:SetValue( 1 ) + CategoryContent3:AddItem(Check) + + --[[Experimental Section]]-- + local Category4 = vgui.Create("DCollapsibleCategory") + List:AddItem(Category4) + Category4:SetLabel("Experimental Section") + Category4:SetExpanded(0) + + local CategoryContent4 = vgui.Create( "DPanelList" ) + CategoryContent4:SetAutoSize( true ) + CategoryContent4:SetDrawBackground( false ) + CategoryContent4:SetSpacing( 3 ) + CategoryContent4:SetPadding( 2 ) + Category4:SetContents( CategoryContent4 ) + + Check = vgui.Create("DCheckBoxLabel") + Check:SetText( "Disable parented props physics interaction" ) + Check:SetConVar( "advdupe2_paste_disparents" ) + Check:SetValue( 0 ) + CategoryContent4:AddItem(Check) + end + + function TOOL.BuildCPanel(panel) + panel:AddControl("Header", { + Text = "Advanced Duplicator 2", + Description = "Duplicate stuff." + }) + timer.Simple(0, BuildCPanel) + end + + local state = 0 + local ToColor = {r=25, g=100, b=40, a=255} + local CurColor = {r=25, g=100, b=40, a=255} + local rate + surface.CreateFont ("Arial", 40, 1000, true, false, "AD2Font") ---Remember to use gm_clearfonts + surface.CreateFont ("Arial", 24, 1000, true, false, "AD2TitleFont") + //local spacing = {" "," "," "," "," "," "} + function TOOL:RenderToolScreen() + if(!AdvDupe2)then return true end + + local text = "Ready" + state=0 + if(AdvDupe2.ProgressBar.Text)then + state=1 + text = AdvDupe2.ProgressBar.Text + end + + cam.Start2D() + + surface.SetDrawColor(32, 32, 32, 255) + surface.DrawRect(0, 0, 256, 256) + + if(state==0)then + ToColor = {r=25, g=100, b=40, a=255} + else + ToColor = {r=130, g=25, b=40, a=255} + end + + rate = FrameTime()*160 + CurColor.r = math.Approach( CurColor.r, ToColor.r, rate ) + CurColor.g = math.Approach( CurColor.g, ToColor.g, rate ) + + surface.SetDrawColor(CurColor) + surface.DrawRect(13, 13, 230, 230) + + surface.SetTextColor( 255, 255, 255, 255 ) + + draw.SimpleText("Advanced Duplicator 2", "AD2TitleFont", 128, 50, Color(255,255,255,255), TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP) + draw.SimpleText(text, "AD2Font", 128, 128, Color(255,255,255,255), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) + if(state!=0)then + draw.RoundedBox( 6, 32, 178, 192, 28, Color( 255, 255, 255, 150 ) ) + draw.RoundedBox( 6, 36, 182, 188*(AdvDupe2.ProgressBar.Percent/100), 24, Color( 0, 255, 0, 255 ) ) + elseif(LocalPlayer():KeyDown(IN_USE))then + //draw.SimpleText("Height: Pitch: Roll:", "AD2TitleFont", 128, 206, Color(255,255,255,255), TEXT_ALIGN_CENTER, TEXT_ALIGN_BOTTOM) + //local str_space1 = spacing[7-string.len(height)] or "" + //local str_space2 = spacing[7-string.len(pitch)] or "" + //draw.SimpleText(height..str_space1..pitch..str_space2..LocalPlayer():GetInfo("advdupe2_offset_roll"), "AD2TitleFont", 25, 226, Color(255,255,255,255), TEXT_ALIGN_LEFT, TEXT_ALIGN_BOTTOM) + draw.SimpleText("Height: "..LocalPlayer():GetInfo("advdupe2_offset_z"), "AD2TitleFont", 25, 180, Color(255,255,255,255), TEXT_ALIGN_LEFT, TEXT_ALIGN_BOTTOM) + draw.SimpleText("Pitch: "..LocalPlayer():GetInfo("advdupe2_offset_pitch"), "AD2TitleFont", 25, 210, Color(255,255,255,255), TEXT_ALIGN_LEFT, TEXT_ALIGN_BOTTOM) + draw.SimpleText("Yaw: "..LocalPlayer():GetInfo("advdupe2_offset_yaw"), "AD2TitleFont", 25, 240, Color(255,255,255,255), TEXT_ALIGN_LEFT, TEXT_ALIGN_BOTTOM) + end + + cam.End2D() + end + + + local function FindInBox(min, max, ply) + + local Entities = ents.GetAll() + local EntTable = {} + for _,ent in pairs(Entities) do + local pos = ent:GetPos() + if (pos.X>=min.X) and (pos.X<=max.X) and (pos.Y>=min.Y) and (pos.Y<=max.Y) and (pos.Z>=min.Z) and (pos.Z<=max.Z) then + if(ent:GetClass()!="gmod_ghost")then + EntTable[ent:EntIndex()] = ent + end + end + end + + return EntTable + end + + local function DrawSelectionBox() + + local trace = util.GetPlayerTrace(LocalPlayer()) + local TraceRes = util.TraceLine(trace) + local i = tonumber(LocalPlayer():GetInfo("advdupe2_area_copy_size")) or 50 + + //Bottom Points + local B1 = (Vector(-i,-i,-i)+TraceRes.HitPos) + local B2 = (Vector(-i,i,-i)+TraceRes.HitPos) + local B3 = (Vector(i,i,-i)+TraceRes.HitPos) + local B4 = (Vector(i,-i,-i)+TraceRes.HitPos) + + //Top Points + local T1 = (Vector(-i,-i,i)+TraceRes.HitPos):ToScreen() + local T2 = (Vector(-i,i,i)+TraceRes.HitPos):ToScreen() + local T3 = (Vector(i,i,i)+TraceRes.HitPos):ToScreen() + local T4 = (Vector(i,-i,i)+TraceRes.HitPos):ToScreen() + + //Version 1 Constantly resets the color of all the props that have entered the box and changes all the props color in the it. + //Version 2 Only Colors if the prop is new or has left the box, but if a prop is moved it will change back...gmod bug. + + //Version 1 of prop coloring + if(!AdvDupe2.LastUpdate || CurTime()>=AdvDupe2.LastUpdate)then + + if AdvDupe2.ColorEntities then + for k,v in pairs(AdvDupe2.EntityColors)do + local ent = AdvDupe2.ColorEntities[k] + if(IsValid(ent))then + AdvDupe2.ColorEntities[k]:SetColor(v.r,v.g,v.b,v.a) + end + end + end + + local Entities = FindInBox(B1, (Vector(i,i,i)+TraceRes.HitPos), LocalPlayer()) + AdvDupe2.ColorEntities = Entities + AdvDupe2.EntityColors = {} + for k,v in pairs(Entities)do + local r,g,b,a = v:GetColor() + AdvDupe2.EntityColors[k] = {r = r, g = g,b = b,a = a} + v:SetColor(0,255,0,255) + end + AdvDupe2.LastUpdate = CurTime()+0.25 + + end + + /* Version 2 of prop coloring(this version needs some stuff uncommented in the hook) + if(!AdvDupe2.LastUpdate || CurTime()<=AdvDupe2.LastUpdate)then + + AdvDupe2.TempEntities = {} + local Entities = ents.FindInBox(B1, (Vector(i,i,i)+TraceRes.HitPos)) + + for k,v in pairs(Entities)do + local i = v:EntIndex() + if(!AdvDupe2.ColorEntities[i])then + local r,g,b,a = v:GetColor() + AdvDupe2.EntityColors[i] = {r = r, g = g,b = b,a = a} + v:SetColor(0,255,0,255) + AdvDupe2.ColorEntities[i] = v + end + AdvDupe2.TempEntities[i] = v + end + + if AdvDupe2.ColorEntities then + for k,v in pairs(AdvDupe2.EntityColors)do + if(!AdvDupe2.TempEntities[k])then + local ent = AdvDupe2.ColorEntities[k] + if(ent:IsValid())then + AdvDupe2.ColorEntities[k]:SetColor(v.r,v.g,v.b,v.a) + AdvDupe2.ColorEntities[k] = nil + AdvDupe2.EntityColors[k] = nil + + end + end + end + end + + AdvDupe2.LastUpdate = CurTime()+0.5 + end + */ + + local tracedata = {} + tracedata.mask = MASK_NPCWORLDSTATIC + local WorldTrace + + tracedata.start = B1+Vector(0,0,i*2) + tracedata.endpos = B1 + WorldTrace = util.TraceLine( tracedata ) + B1 = WorldTrace.HitPos:ToScreen() + tracedata.start = B2+Vector(0,0,i*2) + tracedata.endpos = B2 + WorldTrace = util.TraceLine( tracedata ) + B2 = WorldTrace.HitPos:ToScreen() + tracedata.start = B3+Vector(0,0,i*2) + tracedata.endpos = B3 + WorldTrace = util.TraceLine( tracedata ) + B3 = WorldTrace.HitPos:ToScreen() + tracedata.start = B4+Vector(0,0,i*2) + tracedata.endpos = B4 + WorldTrace = util.TraceLine( tracedata ) + B4 = WorldTrace.HitPos:ToScreen() + + surface.SetDrawColor( 0, 255, 0, 255 ) + + //Draw Sides + surface.DrawLine(B1.x, B1.y, T1.x, T1.y) + surface.DrawLine(B2.x, B2.y, T2.x, T2.y) + surface.DrawLine(B3.x, B3.y, T3.x, T3.y) + surface.DrawLine(B4.x, B4.y, T4.x, T4.y) + + //Draw Bottom + surface.DrawLine(B1.x, B1.y, B2.x, B2.y) + surface.DrawLine(B2.x, B2.y, B3.x, B3.y) + surface.DrawLine(B3.x, B3.y, B4.x, B4.y) + surface.DrawLine(B4.x, B4.y, B1.x, B1.y) + + //Draw Top + surface.DrawLine(T1.x, T1.y, T2.x, T2.y) + surface.DrawLine(T2.x, T2.y, T3.x, T3.y) + surface.DrawLine(T3.x, T3.y, T4.x, T4.y) + surface.DrawLine(T4.x, T4.y, T1.x, T1.y) + + end + + usermessage.Hook("AdvDupe2_DrawSelectBox",function() + hook.Add("HUDPaint", "AdvDupe2_DrawSelectionBox", DrawSelectionBox) + if !AdvDupe2 then AdvDupe2={} AdvDupe2.ProgressBar={} end + /*Version 2 Prop coloring + AdvDupe2.ColorEntities = {} + AdvDupe2.EntityColors = {} + */ + end) + + usermessage.Hook("AdvDupe2_RemoveSelectBox",function() + hook.Remove("HUDPaint", "AdvDupe2_DrawSelectionBox") + if AdvDupe2.ColorEntities then + for k,v in pairs(AdvDupe2.EntityColors)do + if(!IsValid(AdvDupe2.ColorEntities[k]))then AdvDupe2.ColorEntities[k]=nil continue end + local r,g,b,a = v.r, v.g, v.b, v.a + AdvDupe2.ColorEntities[k]:SetColor(r,g,b,a) + end + AdvDupe2.ColorEntities={} + AdvDupe2.EntityColors={} + end + end) + + function AdvDupe2.InitProgressBar(label) + if !AdvDupe2 then AdvDupe2={} end + AdvDupe2.ProgressBar = {} + AdvDupe2.ProgressBar.Text = label + AdvDupe2.ProgressBar.Percent = 0 + end + + usermessage.Hook("AdvDupe2_InitProgressBar",function(um) + AdvDupe2.InitProgressBar(um:ReadString()) + end) + + usermessage.Hook("AdvDupe2_UpdateProgressBar",function(um) + AdvDupe2.ProgressBar.Percent = um:ReadChar() + end) + + usermessage.Hook("AdvDupe2_RemoveProgressBar",function(um) + if !AdvDupe2 then AdvDupe2={} end + AdvDupe2.ProgressBar = {} + end) + + usermessage.Hook("AdvDupe2_ResetOffsets",function(um) + RunConsoleCommand("advdupe2_original_origin", "0") + RunConsoleCommand("advdupe2_paste_constraints","1") + RunConsoleCommand("advdupe2_offset_z","0") + RunConsoleCommand("advdupe2_offset_pitch","0") + RunConsoleCommand("advdupe2_offset_yaw","0") + RunConsoleCommand("advdupe2_offset_roll","0") + RunConsoleCommand("advdupe2_paste_parents","1") + RunConsoleCommand("advdupe2_paste_disparents","0") + end) + + usermessage.Hook("AdvDupe2_ReportModel",function(um) + print("Advanced Duplicator 2: Invalid Model: "..um:ReadString()) + end) + + usermessage.Hook("AdvDupe2_ReportClass",function(um) + print("Advanced Duplicator 2: Invalid Class: "..um:ReadString()) + end) + + usermessage.Hook("AdvDupe2_AddFile",function(um) + AdvDupe2.FileBrowser:AddFile(um:ReadString(), um:ReadShort(), um:ReadBool()) + end) + + usermessage.Hook("AdvDupe2_AddFolder",function(um) + AdvDupe2.FileBrowser:AddFolder(um:ReadString(), um:ReadShort(), um:ReadShort(), um:ReadBool()) + end) + + usermessage.Hook("AdvDupe2_ClearBrowser",function(um) + AdvDupe2.FileBrowser:ClearBrowser() + end) + + usermessage.Hook("AdvDupe2_SetDupeInfo",function(um) + if(!AdvDupe2.Info)then return end + + AdvDupe2.Info.File:SetText('File: "'..um:ReadString()..'"') + AdvDupe2.Info.Creator:SetText("Creator: "..um:ReadString()) + AdvDupe2.Info.Date:SetText("Date: "..um:ReadString()) + AdvDupe2.Info.Time:SetText("Time: "..um:ReadString()) + AdvDupe2.Info.Size:SetText("Size : "..um:ReadString()) + AdvDupe2.Info.Desc:SetText("Desc: "..um:ReadString()) + AdvDupe2.Info.Entities:SetText("Entities: "..um:ReadString()) + AdvDupe2.Info.Constraints:SetText("Constraints: "..um:ReadString()) + end) + + usermessage.Hook("AdvDupe2_ResetDupeInfo",function(um) + if(!AdvDupe2.Info)then return end + AdvDupe2.Info.File:SetText("File:") + AdvDupe2.Info.Creator:SetText("Creator:") + AdvDupe2.Info.Date:SetText("Date:") + AdvDupe2.Info.Time:SetText("Time:") + AdvDupe2.Info.Size:SetText("Size:") + AdvDupe2.Info.Desc:SetText("Desc:") + AdvDupe2.Info.Entities:SetText("Entities:") + AdvDupe2.Info.Constraints:SetText("Constraints:") + end) + + usermessage.Hook("AdvDupe2_Ghosting", function(um) + AdvDupe2.GhostEntity = true + end) + + usermessage.Hook("AdvDupe2_NotGhosting", function(um) + AdvDupe2.GhostEntity = nil + AdvDupe2.Rotation = false + end) + + usermessage.Hook("AdvDupe2_RenameNode", function(um) + AdvDupe2.FileBrowser:RenameNode(um:ReadString()) + end) + + usermessage.Hook("AdvDupe2_MoveNode", function(um) + AdvDupe2.FileBrowser:MoveNode(um:ReadString()) + end) + + usermessage.Hook("AdvDupe2_DeleteNode", function(um) + AdvDupe2.FileBrowser:DeleteNode() + end) + +end \ No newline at end of file diff --git a/materials/gui/ad2logo.tga b/materials/gui/ad2logo.tga new file mode 100644 index 0000000000000000000000000000000000000000..0b5b4674627345009363197593ae09cc5833e22b GIT binary patch literal 144241 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 P?48LOGKTP!3Gp8QzuqpW literal 0 HcmV?d00001