--Cracker64's Autorun Script Manager --The autorun to end all autoruns --Version 3.14 --TODO: --manual file addition (that can be anywhere and any extension) --Moving window (because why not) --some more API functions --prettier, organize code --CHANGES: --Version 3.14: Fix extra newlines being inserted into scripts on Windows --Version 3.13: Better support for upcoming versions of TPT, all script downloads now async, settings now stored separately per scripts directory, fix another rare failure on startup --Version 3.12: Use https for all requests, online view loads async, add FILTER button to online, fix rare failure on startup if downloaded scripts list is corrupted --Version 3.11: Fix icons in 94.0, fix "view script in browser" --Version 3.10: Fix HTTP requests, without this update the online section may break --Version 3.9: Minor icon fix for latest version of jacob1's mod --Version 3.8: Fix being unable to download scripts with / in the name, make sure tooltips don't go offscreen --Version 3.7: Account for extra menu in TPT 91.4 --Version 3.6: Fix bug where it might delete your scripts after updating on windows --Version 3.5: Lua5.2 support, TPT 91.0 platform API support, [] can be used to scroll, misc fixes --Version 3.4: some new buttons, better tooltips, fix 'Change dir' button, fix broken buttons on OS X --Version 3.3: fix apostophes in filenames, allow authors to rename their scripts on the server --Version 3.2: put MANAGER stuff in table, fix displaying changelogs --Version 3.1: Organize scripts less randomly, fix scripts being run twice, fix other bugs --central script / update server at starcatcher.us / delete local scripts / lots of other things by jacob1 v3.0 --Scan all subdirectories in scripts folder! v2.25 --Remove step hooks, v87 fixes them --Socket is now default in v87+ , horray, everyone can now use update features without extra downloads. --Handles up to 50 extra step functions, up from the default 5 (not including the manager's step) v2.1 --Other various nice API functions --Scripts can store/retrieve settings through the manager, see comments below v2.0 --Small fillrect change for v85, boxes can have backgrounds v1.92 --Support spaces in exe names v1.91 --Auto-update for OTHER scripts now works, is a bit messy, will fix later, but nothing should change for users to use this -- Place a line '--VER num UPDATE link' in one of the first four lines of the file, see my above example -- The link at top downloads a file that contains ONLY version,full link,and prints the rest(changelog). See my link for example local jacobsmod = tpt.version.jacob1s_mod local icons = { ["delete1"] = "\xEE\x80\x85", ["delete2"] = "\xEE\x80\x86", ["folder"] = "\xEE\x80\x93" } if jacobsmod then icons = { ["delete1"] = "\133", ["delete2"] = "\134", ["folder"] = "\147" } end if not socket then error("TPT version not supported") end if MANAGER then error("manager is already running") end local scriptversion = 16 MANAGER = {["version"] = "3.14", ["scriptversion"] = scriptversion, ["hidden"] = true} local type = type -- people like to overwrite this function with a global a lot local TPT_LUA_PATH = 'scripts' local PATH_SEP = '\\' local OS = "WIN32" local CHECKUPDATE = false local EXE_NAME if platform then OS = platform.platform() if OS ~= "WIN32" and OS ~= "WIN64" then PATH_SEP = '/' end EXE_NAME = platform.exeName() local temp = EXE_NAME:reverse():find(PATH_SEP) EXE_NAME = EXE_NAME:sub(#EXE_NAME-temp+2) else if os.getenv('HOME') then PATH_SEP = '/' if fs.exists("/Applications") then OS = "MACOSX" else OS = "LIN64" end end if OS == "WIN32" or OS == "WIN64" then EXE_NAME = jacobsmod and "Jacob1\'s Mod.exe" or "Powder.exe" elseif OS == "MACOSX" then EXE_NAME = "powder-x" --can't restart on OS X (if using < 91.0) else EXE_NAME = jacobsmod and "Jacob1\'s Mod" or "powder" end end local beginInput, beginConfirm = ui.beginInput, ui.beginConfirm if not beginInput then beginInput = function(...) local args = {...} local cb = table.remove(args) local input = tpt.input(unpack(args)) cb(input) end end if not beginConfirm then beginConfirm = function(...) local args = {...} local cb = table.remove(args) local confirmed = tpt.confirm(unpack(args)) cb(confirmed) end end local filenames = {} local num_files = 0 --downloaded scripts aren't stored in filenames local localscripts = {} local onlinescripts = {} local running = {} local requiresrestart=false local online = false local first_online = true local online_req = nil local script_manager_update_req = nil local updatetable --temporarily holds info on script manager updates local gen_buttons local count_local_scripts local check_req_status local sidebutton local download_file local settings = {} local search_terms = {} math.randomseed(os.time()) math.random() math.random() math.random() --some filler randoms --get line that can be saved into scriptinfo file local function scriptInfoString(info) --Write table into data format if type(info)~="table" then return end local t = {} for k,v in pairs(info) do table.insert(t,k..":\""..v.."\"") end local rstr = table.concat(t,","):gsub("\r",""):gsub("\n","\\n") return rstr end --read a scriptinfo line local function readScriptInfo(list) if not list then return {} end local scriptlist = {} for i in list:gmatch("[^\n]+") do local t = {} local ID = 0 for k,v in i:gmatch("(%w+):\"([^\"]*)\"") do t[k]= tonumber(v) or v:gsub("\r",""):gsub("\\n","\n") end if not t.ID then print("Skipping invalid script in script list") else scriptlist[t.ID] = t end end return scriptlist end --save settings local function save_dir() -- Older versions of script manager stored settings here when TPT_LUA_PATH was changed away from scripts/ -- But now, only the "DIR" argument is kept here fs.removeFile("autorunsettings.txt") if TPT_LUA_PATH ~= "scripts" then f = io.open("autorunsettings.txt", "wb") if f then f:write("DIR "..TPT_LUA_PATH) f:close() end end end local function save_last() local savestring="" for script,v in pairs(running) do savestring = savestring.." \""..script.."\"" end savestring = "SAV "..savestring.."\nDIR "..TPT_LUA_PATH for k,t in pairs(settings) do for n,v in pairs(t) do savestring = savestring.."\nSET "..k.." "..n..":\""..v.."\"" end end local f = io.open(TPT_LUA_PATH..PATH_SEP.."autorunsettings.txt", "wb") if f then f:write(savestring) f:close() else MANAGER.print("Couldn't save autorunsettings.txt") end save_dir() f = io.open(TPT_LUA_PATH..PATH_SEP.."downloaded"..PATH_SEP.."scriptinfo", "wb") if f then for k,v in pairs(localscripts) do f:write(scriptInfoString(v).."\n") end f:close() end end local function load_downloaded() localscripts = {} local f = io.open(TPT_LUA_PATH..PATH_SEP.."downloaded"..PATH_SEP.."scriptinfo","r") if f then local lines = f:read("*a") f:close() localscripts = readScriptInfo(lines) for k,v in pairs(localscripts) do if k ~= 1 then if not v["ID"] or not v["name"] or not v["description"] or not v["path"] or not v["version"] then localscripts[k] = nil elseif not fs.exists(TPT_LUA_PATH.."/"..v["path"]:gsub("\\","/")) then localscripts[k] = nil end end end end end --load settings before anything else local function load_settings(settings_file) local f = io.open(settings_file, "r") if f then local lines = {} local line = f:read("*l") while line do table.insert(lines,(line:gsub("\r",""))) line = f:read("*l") end f:close() for i=1, #lines do local tok=lines[i]:sub(1,3) local str=lines[i]:sub(5) if tok=="SAV" then for word in string.gmatch(str, "\"(.-)\"") do running[word] = true end elseif tok=="EXE" then EXE_NAME=str elseif tok=="DIR" then TPT_LUA_PATH=str elseif tok=="SET" then local ident,name,val = string.match(str,"(.-) (.-):\"(.-)\"") if ident and name then if settings[ident] then settings[ident][name]=val else settings[ident]={[name]=val} end end end end end end local function load_last() -- Load settings from both places. -- Older versions of script manager may keep all settings in base-dir autorunsettings.txt -- Modern versions only keep the DIR arg there, and put everything else into the scripts subfolder load_settings("autorunsettings.txt") load_settings(TPT_LUA_PATH .. PATH_SEP .. "autorunsettings.txt") load_downloaded() end load_last() --get list of files in scripts folder local function load_filenames() filenames = {} local function searchRecursive(directory) local dirlist = fs.list(directory) if not dirlist then return end for i,v in ipairs(dirlist) do local file = directory.."/"..v if fs.isDirectory(file) and v ~= "downloaded" then searchRecursive(file) elseif fs.isFile(file) then if file:find("%.lua$") then local toinsert = file:sub(#TPT_LUA_PATH+2) if OS == "WIN32" or OS == "WIN64" then toinsert = toinsert:gsub("/", "\\") --not actually required end table.insert(filenames, toinsert) end end end end searchRecursive(TPT_LUA_PATH) table.sort(filenames, function(first,second) return first:lower() < second:lower() end) end --ui object stuff local ui_base local ui_box local ui_line local ui_text local ui_button local ui_scrollbar local ui_tooltip local ui_checkbox local ui_console local ui_window local tooltip ui_base = { new = function() local b={} b.drawlist = {} function b:drawadd(f) table.insert(self.drawlist,f) end function b:draw(...) for _,f in ipairs(self.drawlist) do if type(f)=="function" then f(self,...) end end end b.movelist = {} function b:moveadd(f) table.insert(self.movelist,f) end function b:onmove(x,y) for _,f in ipairs(self.movelist) do if type(f)=="function" then f(self,x,y) end end end return b end } ui_box = { new = function(x,y,w,h,r,g,b) local box=ui_base.new() box.x=x box.y=y box.w=w box.h=h box.x2=x+w box.y2=y+h box.r=r or 255 box.g=g or 255 box.b=b or 255 function box:setcolor(r,g,b) self.r=r self.g=g self.b=b end function box:setbackground(r,g,b,a) self.br=r self.bg=g self.bb=b self.ba=a end box.drawbox=true box.drawbackground=false box:drawadd(function(self) if self.drawbackground then tpt.fillrect(self.x,self.y,self.w+1,self.h+1,self.br,self.bg,self.bb,self.ba) end if self.drawbox then tpt.drawrect(self.x,self.y,self.w,self.h,self.r,self.g,self.b) end end) box:moveadd(function(self,x,y) if x then self.x=self.x+x self.x2=self.x2+x end if y then self.y=self.y+y self.y2=self.y2+y end end) return box end } ui_line = { new=function(x,y,x2,y2,r,g,b) local line=ui_box.new(x,y,x2-x,y2-y,r,g,b) --Line is essentially a box, but with a different draw line.drawlist={} line:drawadd(function(self) tpt.drawline(self.x,self.y,self.x2,self.y2,self.r,self.g,self.b) end) return line end } ui_text = { new = function(text,x,y,r,g,b) local txt = ui_base.new() txt.text = text txt.x=x or 0 txt.y=y or 0 txt.r=r or 255 txt.g=g or 255 txt.b=b or 255 function txt:setcolor(r,g,b) self.r=r self.g=g self.b=b end txt:drawadd(function(self,x,y) tpt.drawtext(x or self.x,y or self.y,self.text,self.r,self.g,self.b) end) txt:moveadd(function(self,x,y) if x then self.x=self.x+x end if y then self.y=self.y+y end end) function txt:process() return false end return txt end, --Scrolls while holding mouse over newscroll = function(text,x,y,vis,r,g,b) local txt = ui_text.new(text,x,y,r,g,b) if tpt.textwidth(text) self.timer then if self.last >= self.length then self.start = 1 self:cuttext(self) self.timer = socket.gettime()+3 else self.start = self.start + 1 self:cuttext(self) if self.last >= self.length then self.timer = socket.gettime()+3 else self.timer = socket.gettime()+.15 end end end tpt.drawtext(x or self.x,y or self.y, self.text:sub(self.start,self.last) ,self.r,self.g,self.b) end) function txt:process(mx,my,button,event,wheel) if event==3 then local newlast = math.floor((mx-self.x)/self.ppl)+self.minlast if newlast0 and newlast~=self.last then local newstart=1 while tpt.textwidth(self.text:sub(newstart,newlast))>= self.visible do newstart=newstart+1 end self.start=newstart self.last=newlast self.timer = socket.gettime()+3 end end end return txt end } ui_scrollbar = { new = function(x,y,h,t,m) local bar = ui_base.new() --use line object as base? bar.x=x bar.y=y bar.h=h bar.total=t bar.numshown=m bar.pos=0 bar.length=math.floor((1/math.ceil(bar.total-bar.numshown+1))*bar.h) bar.soffset=math.floor(bar.pos*((bar.h-bar.length)/(bar.total-bar.numshown))) function bar:update(total,shown,pos) self.pos=pos or 0 if self.pos<0 then self.pos=0 end self.total=total self.numshown=shown self.length= math.floor((1/math.ceil(self.total-self.numshown+1))*self.h) self.soffset= math.floor(self.pos*((self.h-self.length)/(self.total-self.numshown))) end function bar:move(wheel) self.pos = self.pos-wheel if self.pos < 0 then self.pos=0 end if self.pos > (self.total-self.numshown) then self.pos=(self.total-self.numshown) end self.soffset= math.floor(self.pos*((self.h-self.length)/(self.total-self.numshown))) end bar:drawadd(function(self) if self.total > self.numshown then tpt.drawline(self.x,self.y+self.soffset,self.x,self.y+self.soffset+self.length) end end) bar:moveadd(function(self,x,y) if x then self.x=self.x+x end if y then self.y=self.y+y end end) function bar:process(mx,my,button,event,wheel) if wheel~=0 and not MANAGER.hidden then if self.total > self.numshown then local previous = self.pos self:move(wheel) if self.pos~=previous then return previous-self.pos end end end --possibly click the bar and drag? return false end return bar end } ui_button = { new = function(x,y,w,h,f,text) local b = ui_box.new(x,y,w,h) b.f=f b.t=ui_text.new(text,x+2,y+2) b.drawbox=false b.clicked=false b.almostselected=false b.invert=true b:setbackground(127,127,127,125) b:drawadd(function(self) if self.invert and self.almostselected then self.almostselected=false tpt.fillrect(self.x,self.y,self.w,self.h) local tr=self.t.r local tg=self.t.g local tb=self.t.b b.t:setcolor(0,0,0) b.t:draw() b.t:setcolor(tr,tg,tb) else if tpt.mousex>=self.x and tpt.mousex<=self.x2 and tpt.mousey>=self.y and tpt.mousey<=self.y2 then self.drawbackground=true else self.drawbackground=false end b.t:draw() end end) b:moveadd(function(self,x,y) self.t:onmove(x,y) end) function b:process(mx,my,button,event,wheel) local clicked = self.clicked if event==2 then self.clicked = false end if mxself.x2 or myself.y2 then return false end if event==1 then self.clicked=true elseif clicked then if event==3 then self.almostselected=true end if event==2 then self:f() end return true end end return b end } ui_tooltip = { new = function(x,y,w,text) local b = ui_box.new(x,y-1,w,0) function b:updatetooltip(tooltip) self.tooltip = tooltip self.length = #tooltip self.lines = 1 local linebreak,lastspace = 0,nil for i=0,#self.tooltip do local width = tpt.textwidth(tooltip:sub(linebreak,i+1)) if width > self.w/2 and tooltip:sub(i,i):match("[%s,_%.%-?!]") then lastspace = i end local isnewline = (self.tooltip:sub(i,i) == '\n') if width > self.w or isnewline then local pos = (i==#tooltip or not lastspace) and i or lastspace self.lines = self.lines + 1 if self.tooltip:sub(pos,pos) == ' ' then self.tooltip = self.tooltip:sub(1,pos-1).."\n"..self.tooltip:sub(pos+1) elseif not isnewline then self.length = self.length + 1 self.tooltip = self.tooltip:sub(1,pos-1).."\n"..self.tooltip:sub(pos) i = i + 1 pos = pos + 1 end linebreak = pos+1 lastspace = nil end end self.h = self.lines*12+2 if self.y + self.h > gfx.HEIGHT then local movement = (gfx.HEIGHT-self.h-1)-self.y if self.y+movement < 0 then movement = -self.y end self:onmove(0, movement) end --self.w = tpt.textwidth(self.tooltip)+3 self.drawbox = tooltip ~= "" self.drawbackground = tooltip ~= "" end function b:settooltip(tooltip_) tooltip:onmove(tpt.mousex+5-tooltip.x, tpt.mousey+5-tooltip.y) tooltip:updatetooltip(tooltip_) end b:updatetooltip(text) b:setbackground(0,0,0,255) b.drawbackground = true b:drawadd(function(self) if self.tooltip ~= "" then tpt.drawtext(self.x+1,self.y+2,self.tooltip) end self:updatetooltip("") end) function b:process(mx,my,button,event,wheel) end return b end } ui_checkbox = { up_button = function(x,y,w,h,f,text) local b=ui_button.new(x,y,w,h,f,text) b.canupdate=false return b end, new_button = function(x,y,w,h,splitx,f,f2,text,localscript) local b = ui_box.new(x,y,splitx,h) b.f=f b.f2=f2 b.localscript=localscript b.splitx = splitx b.t=ui_text.newscroll(text,x+24,y+2,splitx-24) b.clicked=false b.selected=false b.checkbut=ui_checkbox.up_button(x+splitx+9,y,33,9,ui_button.scriptcheck,"Update") b.drawbox=false b:setbackground(127,127,127,100) b:drawadd(function(self) if self.t.text == "" then return end self.drawbackground = false if tpt.mousey >= self.y and tpt.mousey < self.y2 then if tpt.mousex >= self.x and tpt.mousex < self.x+8 then if self.localscript then tooltip:settooltip("delete this script") else tooltip:settooltip("view script in browser") end elseif tpt.mousex>=self.x and tpt.mousex= self.x2 then if tpt.mousex < self.x2+9 and self.running then tooltip:settooltip(online and "downloaded" or "running") elseif tpt.mousex >= self.x2+9 and tpt.mousex < self.x2+43 and self.checkbut.canupdate and onlinescripts[self.ID] and onlinescripts[self.ID]["changelog"] then tooltip:settooltip(onlinescripts[self.ID]["changelog"]) end end end self.t:draw() if self.localscript then local swapicon = tpt.version.jacob1s_mod_build and tpt.version.jacob1s_mod_build > 76 local offsetX = swapicon and 1 or 0 local offsetY = swapicon and 2 or 0 local innericon = swapicon and icons["delete1"] or icons["delete2"] local outericon = swapicon and icons["delete2"] or icons["delete1"] if self.deletealmostselected then self.deletealmostselected = false tpt.drawtext(self.x+1+offsetX, self.y+1+offsetY, innericon, 255, 48, 32, 255) else tpt.drawtext(self.x+1+offsetX, self.y+1+offsetY, innericon, 160, 48, 32, 255) end tpt.drawtext(self.x+1+offsetX, self.y+1+offsetY, outericon, 255, 255, 255, 255) else tpt.drawtext(self.x+1, self.y+1, icons["folder"], 255, 200, 80, 255) end tpt.drawrect(self.x+12,self.y+1,8,8) if self.almostselected then self.almostselected=false tpt.fillrect(self.x+12,self.y+1,8,8,150,150,150) elseif self.selected then tpt.fillrect(self.x+12,self.y+1,8,8) end local filepath = self.ID and localscripts[self.ID] and localscripts[self.ID]["path"] or self.t.text if self.running then tpt.drawtext(self.x+self.splitx+2,self.y+2,online and "D" or "R") end if self.checkbut.canupdate then self.checkbut:draw() end end) b:moveadd(function(self,x,y) self.t:onmove(x,y) self.checkbut:onmove(x,y) end) function b:process(mx,my,button,event,wheel) if self.f2 and mx <= self.x+8 then if event==1 then self.clicked = 1 elseif self.clicked == 1 then if event==3 then self.deletealmostselected = true end if event==2 then self:f2() end end elseif self.f and mx<=self.x+self.splitx then if event==1 then self.clicked = 2 elseif self.clicked == 2 then if event==3 then self.almostselected=true end if event==2 then self:f() end self.t:process(mx,my,button,event,wheel) end else if self.checkbut.canupdate then self.checkbut:process(mx,my,button,event,wheel) end end return true end return b end, new = function(x,y,w,h) local box = ui_box.new(x,y,w,h) box.list={} box.numlist = 0 box.max_lines = math.floor(box.h/10)-1 box.max_text_width = math.floor(box.w*0.8) box.splitx=x+box.max_text_width box.scrollbar = ui_scrollbar.new(box.x2-2,box.y+11,box.h-12,0,box.max_lines) box.lines={ ui_line.new(box.x+1,box.y+10,box.x2-1,box.y+10,170,170,170), ui_line.new(box.x+22,box.y+10,box.x+22,box.y2-1,170,170,170), ui_line.new(box.splitx,box.y+10,box.splitx,box.y2-1,170,170,170), ui_line.new(box.splitx+9,box.y+10,box.splitx+9,box.y2-1,170,170,170), } function box:updatescroll() self.scrollbar:update(self.numlist,self.max_lines) end function box:clear() self.list={} self.numlist=0 end function box:add(f,f2,text,localscript) local but = ui_checkbox.new_button(self.x,self.y+1+((self.numlist+1)*10),tpt.textwidth(text)+4,10,self.max_text_width,f,f2,text,localscript) table.insert(self.list,but) self.numlist = #self.list return but end box:drawadd(function (self) tpt.drawtext(self.x+24,self.y+2,"Files in "..TPT_LUA_PATH.." folder") tpt.drawtext(self.splitx+11,self.y+2,"Update") for i,line in ipairs(self.lines) do line:draw() end self.scrollbar:draw() local restart = false for i,check in ipairs(self.list) do local filepath = check.ID and localscripts[check.ID] and localscripts[check.ID]["path"] or check.t.text if not check.selected and running[filepath] then restart = true end if i>self.scrollbar.pos and i<=self.scrollbar.pos+self.max_lines then check:draw() end end requiresrestart = restart and not online end) box:moveadd(function(self,x,y) for i,line in ipairs(self.lines) do line:onmove(x,y) end for i,check in ipairs(self.list) do check:onmove(x,y) end end) function box:scroll(amount) local move = amount*10 if move==0 then return end for i,check in ipairs(self.list) do check:onmove(0,move) end end function box:process(mx,my,button,event,wheel) if mxself.x2 or myself.y2-7 then return false end local scrolled = self.scrollbar:process(mx,my,button,event,wheel) if scrolled then self:scroll(scrolled) end local which = math.floor((my-self.y-11)/10)+1 if which>0 and which<=self.numlist then self.list[which+self.scrollbar.pos]:process(mx,my,button,event,wheel) end if event == 2 then for i,v in ipairs(self.list) do v.clicked = false end end return true end return box end } ui_console = { new = function(x,y,w,h) local con = ui_box.new(x,y,w,h) con.shown_lines = math.floor(con.h/10) con.max_lines = 300 con.max_width = con.w-4 con.lines = {} con.scrollbar = ui_scrollbar.new(con.x2-2,con.y+1,con.h-2,0,con.shown_lines) con:drawadd(function(self) self.scrollbar:draw() local count=0 for i,line in ipairs(self.lines) do if i>self.scrollbar.pos and i<= self.scrollbar.pos+self.shown_lines then line:draw(self.x+3,self.y+3+(count*10)) count = count+1 end end end) con:moveadd(function(self,x,y) self.scrollbar:onmove(x,y) end) function con:clear() self.lines = {} self.scrollbar:update(0,con.shown_lines) end function con:addstr(str,r,g,b) str = tostring(str) local nextl = str:find('\n') while nextl do local line = str:sub(1,nextl-1) self:addline(line,r,g,b) str = str:sub(nextl+1) nextl = str:find('\n') end self:addline(str,r,g,b) --anything leftover end function con:addline(line,r,g,b) if not line or line=="" then return end --No blank lines table.insert(self.lines,ui_text.newscroll(line,self.x,0,self.max_width,r,g,b)) if #self.lines>self.max_lines then table.remove(self.lines,1) end self.scrollbar:update(#self.lines,self.shown_lines,#self.lines-self.shown_lines) end function con:process(mx,my,button,event,wheel) if mxself.x2 or myself.y2 then return false end self.scrollbar:process(mx,my,button,event,wheel) local which = math.floor((my-self.y-1)/10)+1 if which>0 and which<=self.shown_lines and self.lines[which+self.scrollbar.pos] then self.lines[which+self.scrollbar.pos]:process(mx,my,button,event,wheel) end return true end return con end } ui_window = { new = function(x,y,w,h) local w=ui_box.new(x,y,w,h) w.sub={} function w:add(m,name) if name then w[name]=m end table.insert(self.sub,m) return m end w:drawadd(function(self) for i,sub in ipairs(self.sub) do sub:draw() end end) w:moveadd(function(self,x,y) for i,sub in ipairs(self.sub) do sub:onmove(x,y) end end) function w:process(mx,my,button,event,wheel) if mxself.x2 or myself.y2 then if button == 0 then return end ui_button.sidepressed() return true end local ret for i,sub in ipairs(self.sub) do if sub:process(mx,my,button,event,wheel) then ret = true end end return ret end return w end } --Main window with everything! local mainwindow = ui_window.new(50,50,525,300) mainwindow:setbackground(10,10,10,235) mainwindow.drawbackground=true mainwindow:add(ui_console.new(275,148,300,189),"menuconsole") mainwindow:add(ui_checkbox.new(50,80,225,257),"checkbox") tooltip = ui_tooltip.new(0,1,250,"") --Some API functions you can call from other scripts --put 'using_manager=MANAGER ~= nil' or similar in your scripts, using_manager will be true if the manager is active --Print a message to the manager console, can be colored function MANAGER.print(msg,...) mainwindow.menuconsole:addstr(msg,...) end -- Gets script info table for a script, or all scripts if nil is used as id. Data is fetched from the server. -- Returns table as argument to callback function once info download finishes, or nil and http status code if download / parsing failed function MANAGER.scriptinfo(id, callback) if not callback then error("Callback function argument is required") end local url = "https://starcatcher.us/scripts/main.lua" if id then url = url.."?info="..id end download_file(url, function(info, status_code) if status_code == 200 then local infotable = readScriptInfo(info) callback(id and infotable[id] or infotable) else callback(nil, status_code) end end) end --Get various info about the system (operating system, script directory, path seperator, if socket is loaded) function MANAGER.sysinfo() return {["OS"]=OS, ["scriptDir"]=TPT_LUA_PATH, ["pathSep"]=PATH_SEP, ["exeName"] = EXE_NAME} end --Save a setting in the autorun settings file, ident should be your script name no one else would use. --Name is variable name, val is the value which will be saved/returned as a string function MANAGER.savesetting(ident,name,val) ident = tostring(ident) name = tostring(name) val = tostring(val) if settings[ident] then settings[ident][name]=val else settings[ident]={[name]=val} end save_last() end --Get a previously saved value, if it has one function MANAGER.getsetting(ident,name) if settings[ident] then return settings[ident][name] end return nil end --delete a setting, leave name nil to delete all of ident function MANAGER.delsetting(ident,name) if settings[ident] then if name then settings[ident][name]=nil else settings[ident]=nil end save_last() end end local active_downloads = {} function download_file(url, cb) if not http then MANAGER.print("TPT 95.0 or greater required to use http api", 255, 0, 0) return false end if not cb then MANAGER.print("Callback function required for async download", 255, 0, 0) return false end local req = http.get(url) local timeout_after = socket.gettime() + 3 table.insert(active_downloads, {req=req, timeout_after=timeout_after, cb=cb}) end local function process_downloads() for k,v in pairs(active_downloads) do local req = v["req"] local cb = v["cb"] local timeout_after = v["timeout_after"] local status = req:status() if status ~= "running" then active_downloads[k] = nil local body, status_code = req:finish() if status_code and status_code ~= 200 then MANAGER.print("http download failed with status code " .. status_code, 255, 0, 0) end cb(body, status_code) end if socket.gettime() > timeout_after then active_downloads[k] = nil MANAGER.print("http download timed out ", 255, 0, 0) req:cancel() cb(nil, 408) end end end --Downloads script to a location, runs callback function with either true or false argument indicading success local function download_script(ID, location, cb) download_file("https://starcatcher.us/scripts/main.lua?get=" .. ID, function(file, status_code) if file and status_code == 200 then f = io.open(location, "wb") f:write(file) f:close() cb(true, status_code) else MANAGER.print("Got http status " .. status_code .. " while downloading script", 255, 0, 0) cb(false, status_code) end end) end --Restart exe (if named correctly) local function do_restart(skip_save) if not skip_save then save_last() end if platform then platform.restart() end if OS == "WIN32" or OS == "WIN64" then os.execute("TASKKILL /IM \""..EXE_NAME.."\" /F &&START .\\\""..EXE_NAME.."\"") elseif OS == "OSX" then MANAGER.print("Can't restart on OS X when using game versions less than 91.0, please manually close and reopen The Powder Toy") return else os.execute("killall -s KILL \""..EXE_NAME.."\" && ./\""..EXE_NAME.."\"") end MANAGER.print("Restart failed, do you have the exe name right?",255,0,0) end local function open_link(url) if platform then platform.openLink(url) else local command = (OS == "WIN32" or OS == "WIN64") and "start" or (OS == "MACOSX" and "open" or "xdg-open") os.execute(command.." "..url) end end --TPT interface local function step() if jacobsmod then tpt.fillrect(0,0,gfx.WIDTH,gfx.HEIGHT,0,0,0,150) else tpt.fillrect(-1,-1,gfx.WIDTH,gfx.HEIGHT,0,0,0,150) end mainwindow:draw() tpt.drawtext(280,140,"Console Output:") if requiresrestart then tpt.drawtext(280,88,"Disabling a script requires a restart for effect!",255,50,50) end tpt.drawtext(55,55,"Click a script to toggle, hit DONE when finished") tpt.drawtext(474,55,"Script Manager v"..MANAGER.version)--479 for simple versions tooltip:draw() if online_req and online then local textwidth = tpt.textwidth("Loading ...") tpt.drawtext(mainwindow.checkbox.x + (mainwindow.checkbox.w - textwidth) / 2, mainwindow.checkbox.y + (mainwindow.checkbox.h - 6) / 2, "Loading ...") end end local function mouseclick(mousex,mousey,button,event,wheel) sidebutton:process(mousex,mousey,button,event,wheel) if MANAGER.hidden then return true end if mousex>612 or mousey>384 then return false end mainwindow:process(mousex,mousey,button,event,wheel) return false end local jacobsmod_old_menu_check = false local function keypress(key,nkey,modifier,event) if jacobsmod and (key == 'o' or nkey == 96) and event == 1 then jacobsmod_old_menu_check = true end if nkey==27 and not MANAGER.hidden then MANAGER.hidden=true return false end if MANAGER.hidden then return end if event == 1 then if key == "[" then mainwindow:process(mainwindow.x+30, mainwindow.y+30, 0, 2, 1) elseif key == "]" then mainwindow:process(mainwindow.x+30, mainwindow.y+30, 0, 2, -1) end end return false end --small button on right to bring up main menu local WHITE = {255,255,255,255} local BLACK = {0,0,0,255} local ICON = math.random(2) --pick a random icon local lua_letters= {{{2,2,2,7},{2,7,4,7},{6,7,6,11},{6,11,8,11},{8,7,8,11},{10,11,12,11},{10,11,10,15},{11,13,11,13},{12,11,12,15},}, {{2,3,2,13},{2,14,7,14},{4,3,4,12},{4,12,7,12},{7,3,7,12},{9,3,12,3},{9,3,9,14},{10,8,11,8},{12,3,12,14},}} local function smallstep() gfx.drawRect(sidebutton.x, sidebutton.y+1, sidebutton.w+1, sidebutton.h+1,200,200,200) local color=WHITE if not MANAGER.hidden then step() gfx.fillRect(sidebutton.x, sidebutton.y+1, sidebutton.w+1, sidebutton.h+1) color=BLACK end for i,dline in ipairs(lua_letters[ICON]) do tpt.drawline(dline[1]+sidebutton.x,dline[2]+sidebutton.y,dline[3]+sidebutton.x,dline[4]+sidebutton.y,color[1],color[2],color[3]) end if jacobsmod_old_menu_check then local ypos = 134 if jacobsmod and tpt.oldmenu and tpt.oldmenu()==1 then ypos = 390 elseif tpt.num_menus then ypos = 390-16*tpt.num_menus()-(not jacobsmod and 16 or 0) end sidebutton:onmove(0, ypos-sidebutton.y) jacobsmod_old_menu_check = false end check_req_status() end local function reload_action() load_filenames() load_downloaded() if not online then gen_buttons() mainwindow.checkbox:updatescroll() else count_local_scripts() end if num_files == 0 then MANAGER.print("No scripts found in '"..TPT_LUA_PATH.."' folder",255,255,0) fs.makeDirectory(TPT_LUA_PATH) else MANAGER.print("Reloaded file list, found "..num_files.." scripts") end end --button functions on click function ui_button.reloadpressed(self) if not online then reload_action() else beginInput("Script filtering", "Enter search terms to filter by", function(filter) search_terms = {} for match in filter:gmatch("%w+") do table.insert(search_terms, match) end gen_buttons() end) end end function ui_button.selectnone(self) for i,but in ipairs(mainwindow.checkbox.list) do but.selected = false end end function ui_button.consoleclear(self) mainwindow.menuconsole:clear() end function ui_button.changedir(self) beginInput("Change search directory", "Enter the folder where your scripts and settings are (requires restart)", TPT_LUA_PATH, TPT_LUA_PATH, function(new) local last = TPT_LUA_PATH if new and new~=last and new~="" then save_last() MANAGER.print("Directory changed to "..new,255,255,0) TPT_LUA_PATH = new save_dir() do_restart(true) end end) end function ui_button.uploadscript(self) if not online then local command = (OS == "WIN32" or OS == "WIN64") and "start" or (OS == "MACOSX" and "open" or "xdg-open") os.execute(command.." "..TPT_LUA_PATH) else open_link("https://starcatcher.us/scripts/paste.lua") end end local lastpaused function ui_button.sidepressed(self) if TPTMP and TPTMP.chatHidden == false then print("minimize TPTMP before opening the manager") return end MANAGER.hidden = not MANAGER.hidden ui_button.localview() if not MANAGER.hidden then lastpaused = tpt.set_pause() tpt.set_pause(1) ui_button.reloadpressed() else tpt.set_pause(lastpaused) end end local donebutton function ui_button.donepressed(self) MANAGER.hidden = true for i,but in ipairs(mainwindow.checkbox.list) do local filepath = but.ID and localscripts[but.ID]["path"] or but.t.text if but.selected then if requiresrestart then running[filepath] = true else if not running[filepath] then local status,err = pcall(dofile,TPT_LUA_PATH..PATH_SEP..filepath) if not status then MANAGER.print(err,255,0,0) print(err) but.selected = false else MANAGER.print("Started "..filepath) running[filepath] = true end end end elseif running[filepath] then running[filepath] = nil end end if requiresrestart then do_restart() return end save_last() end function ui_button.downloadpressed(self) local remaining_downloads = 0 local any_failed = false for i,but in ipairs(mainwindow.checkbox.list) do if but.selected then local displayName = "downloaded"..PATH_SEP..but.ID.." "..onlinescripts[but.ID].author:gsub("[^%w _-]", "_").."-"..onlinescripts[but.ID].name:gsub("[^%w _-]", "_")..".lua" local name = TPT_LUA_PATH..PATH_SEP..displayName if not fs.exists(TPT_LUA_PATH..PATH_SEP.."downloaded") then fs.makeDirectory(TPT_LUA_PATH..PATH_SEP.."downloaded") end remaining_downloads = remaining_downloads + 1 download_script(but.ID, name, function(success, status_code) if success then local status, err = pcall(function() if localscripts[but.ID] and localscripts[but.ID]["path"] ~= displayName then local oldpath = localscripts[but.ID]["path"] fs.removeFile(TPT_LUA_PATH.."/"..oldpath:gsub("\\","/")) running[oldpath] = nil end localscripts[but.ID] = onlinescripts[but.ID] localscripts[but.ID]["path"] = displayName dofile(name) MANAGER.print("Downloaded and started "..but.t.text) running[displayName] = true end) if not status then MANAGER.print(err) any_failed = true end else any_failed = true end -- All remaining downloads finished, close the manager and save data remaining_downloads = remaining_downloads - 1 if remaining_downloads == 0 then save_last() print("Finished downloading and installing scripts") if not any_failed then MANAGER.hidden = true ui_button.localview() else print("Some scripts failed, see manager log") end end end) end end end function ui_button.pressed(self) self.selected = not self.selected end function ui_button.delete(self) local cb = function(confirmed) if confirmed ~= true then return end local filepath = self.ID and localscripts[self.ID]["path"] or self.t.text fs.removeFile(TPT_LUA_PATH.."/"..filepath:gsub("\\","/")) if running[filepath] then running[filepath] = nil end if localscripts[self.ID] then localscripts[self.ID] = nil end save_last() ui_button.localview() load_filenames() gen_buttons() end beginConfirm("Delete File", "Delete "..self.t.text.."?", cb) end function ui_button.viewonline(self) open_link("https://starcatcher.us/scripts?view="..self.ID) end function ui_button.scriptcheck(self) local oldpath = localscripts[self.ID]["path"] local newpath = "downloaded"..PATH_SEP..self.ID.." "..onlinescripts[self.ID].author:gsub("[^%w _-]", "_").."-"..onlinescripts[self.ID].name:gsub("[^%w _-]", "_")..".lua" download_script(self.ID, TPT_LUA_PATH..PATH_SEP..newpath, function(success, status_code) if not success then return end self.canupdate = false localscripts[self.ID] = onlinescripts[self.ID] localscripts[self.ID]["path"] = newpath if oldpath:gsub("\\","/") ~= newpath:gsub("\\","/") then fs.removeFile(TPT_LUA_PATH.."/"..oldpath:gsub("\\","/")) if running[oldpath] then running[newpath],running[oldpath] = running[oldpath],nil end end if running[newpath] then do_restart() else save_last() MANAGER.print("Updated "..onlinescripts[self.ID]["name"]) end end) end function ui_button.doupdate(self) local scriptname, scriptbackup = "autorun.lua", "autorunold.lua" if jacobsmod and jacobsmod >= 30 then scriptname, scriptbackup = "scriptmanager.lua", "scriptmanagerold.lua" end fileSystem.move(scriptname, scriptbackup) download_script(1, scriptname, function() localscripts[1] = updatetable[1] do_restart() end) end local uploadscriptbutton, reloadbutton function ui_button.localview(self) if online then online = false gen_buttons() donebutton.t.text = "DONE" donebutton.w = 29 donebutton.x2 = donebutton.x + donebutton.w donebutton.f = ui_button.donepressed uploadscriptbutton.t.text = icons["folder"].." Script Folder" reloadbutton.t.text = "RELOAD" end end function ui_button.onlineview(self) if not online then online = true gen_buttons() donebutton.t.text = "DOWNLOAD" donebutton.w = 55 donebutton.x2 = donebutton.x + donebutton.w donebutton.f = ui_button.downloadpressed uploadscriptbutton.t.text = "Upload Script" reloadbutton.t.text = "FILTER" search_terms = {} end end --add buttons to window donebutton = ui_button.new(55,339,29,10,ui_button.donepressed,"DONE") mainwindow:add(donebutton) mainwindow:add(ui_button.new(134,339,40,10,ui_button.sidepressed,"CANCEL")) --mainwindow:add(ui_button.new(152,339,29,10,ui_button.selectnone,"NONE")) local nonebutton = ui_button.new(62,81,8,8,ui_button.selectnone,"") nonebutton.drawbox = true mainwindow:add(nonebutton) mainwindow:add(ui_button.new(538,339,33,10,ui_button.consoleclear,"CLEAR")) reloadbutton = mainwindow:add(ui_button.new(278,67,39,10,ui_button.reloadpressed,"RELOAD"), "reload") mainwindow:add(ui_button.new(378,67,51,10,ui_button.changedir,"Change dir")) uploadscriptbutton = mainwindow:add(ui_button.new(478,67,79,10,ui_button.uploadscript, icons["folder"].." Script Folder")) local tempbutton = ui_button.new(60, 65, 30, 10, ui_button.localview, "Local") tempbutton.drawbox = true mainwindow:add(tempbutton) tempbutton = ui_button.new(100, 65, 35, 10, ui_button.onlineview, "Online") tempbutton.drawbox = true mainwindow:add(tempbutton) local ypos = 134 if jacobsmod and tpt.oldmenu and tpt.oldmenu()==1 then ypos = 390 elseif tpt.num_menus then ypos = 390-16*tpt.num_menus()-(not jacobsmod and 16 or 0) end sidebutton = ui_button.new(gfx.WIDTH-16,ypos,14,15,ui_button.sidepressed,'') local function gen_buttons_local() local count = 0 local sorted = {} for k,v in pairs(localscripts) do if v.ID ~= 1 then table.insert(sorted, v) end end table.sort(sorted, function(first,second) return first.name:lower() < second.name:lower() end) for i,v in ipairs(sorted) do local check = mainwindow.checkbox:add(ui_button.pressed,ui_button.delete,v.name,true) check.ID = v.ID if running[v.path] then check.running = true check.selected = true end count = count + 1 end if #sorted >= 5 and #filenames >= 5 then mainwindow.checkbox:add(nil, nil, "", false) --empty space to separate things end for i=1,#filenames do local check = mainwindow.checkbox:add(ui_button.pressed,ui_button.delete,filenames[i],true) if running[filenames[i]] then check.running = true check.selected = true end end num_files = count + #filenames end function count_local_scripts() local total = #filenames for k in pairs(localscripts) do total = total + 1 end num_files = total end local function gen_buttons_online() if not http then MANAGER.print("TPT 95.0 or greater required to use the online tab", 255, 0, 0) return end if online_req then online_req:cancel() end online_req = http.get("https://starcatcher.us/scripts/main.lua") if first_online then first_online = false script_manager_update_req = http.get("https://starcatcher.us/scripts/main.lua?info=1") end end local function check_search_term(script, search_term) search_term = search_term:lower() return script.name and script.name:lower():match(search_term) or script.description and script.description:lower():match(search_term) or script.author and script.author:lower():match(search_term) end -- Check status of "Online" tab request local function check_online_req_status() if online_req and online_req:status() ~= "running" then local list, status_code = online_req:finish() online_req = nil if status_code ~= 200 then MANAGER.print("script list download failed with status code " .. status_code, 255, 0, 0) return end if not online then return end onlinescripts = readScriptInfo(list) local sorted = {} for k,v in pairs(onlinescripts) do table.insert(sorted, v) end table.sort(sorted, function(first,second) return first.ID < second.ID end) for k,v in pairs(sorted) do local matches = true for i,term in ipairs(search_terms) do matches = matches and check_search_term(v, term) end if matches then local check = mainwindow.checkbox:add(ui_button.pressed, ui_button.viewonline, v.name, false) check.ID = v.ID check.checkbut.ID = v.ID if localscripts[v.ID] then check.running = true if tonumber(v.version) > tonumber(localscripts[check.ID].version) then check.checkbut.canupdate = true end end end end mainwindow.checkbox:updatescroll() end end -- Check status of self update check local function check_update_req_status() if script_manager_update_req and script_manager_update_req:status() ~= "running" then local updateinfo, status_code = script_manager_update_req:finish() script_manager_update_req = nil if status_code ~= 200 then MANAGER.print("self update check failed with status code " .. status_code, 255, 0, 0) return end updatetable = readScriptInfo(updateinfo) if not updatetable[1] then return end if tonumber(updatetable[1].version) > scriptversion then local updatebutton = ui_button.new(278,127,40,10,ui_button.doupdate,"UPDATE") updatebutton.t:setcolor(25,255,25) mainwindow:add(updatebutton) MANAGER.print("A script manager update is available! Click UPDATE",25,255,55) MANAGER.print(updatetable[1].changelog,25,255,55) end end end -- Check status of pending requests function check_req_status() check_online_req_status() check_update_req_status() -- Check other misc downloads (script or update downloads) process_downloads() end gen_buttons = function() mainwindow.checkbox:clear() if online then gen_buttons_online() else gen_buttons_local() end mainwindow.checkbox:updatescroll() end gen_buttons() --register manager first tpt.register_step(smallstep) --load previously running scripts local started = "" for prev,v in pairs(running) do local status,err = pcall(dofile,TPT_LUA_PATH..PATH_SEP..prev) if not status then MANAGER.print(err,255,0,0) running[prev] = nil else started=started.." "..prev local newbut = mainwindow.checkbox:add(ui_button.pressed,prev,nil,false) newbut.selected=true end end save_last() if started~="" then MANAGER.print("Auto started"..started) end tpt.register_mouseevent(mouseclick) tpt.register_keypress(keypress)