-- Path of Building -- -- Module: Main -- Main module of program. -- local ipairs = ipairs local t_insert = table.insert local t_remove = table.remove local m_ceil = math.ceil local m_floor = math.floor local m_max = math.max local m_min = math.min local m_sin = math.sin local m_cos = math.cos local m_pi = math.pi LoadModule("GameVersions") LoadModule("Modules/Common") LoadModule("Modules/Data") LoadModule("Modules/ModTools") LoadModule("Modules/ItemTools") LoadModule("Modules/CalcTools") LoadModule("Modules/PantheonTools") --[[if launch.devMode then for skillName, skill in pairs(data.enchantments.Helmet) do for _, mod in ipairs(skill.ENDGAME) do local modList, extra = modLib.parseMod(mod) if not modList or extra then ConPrintf("%s: '%s' '%s'", skillName, mod, extra or "") end end end end]] local tempTable1 = { } local tempTable2 = { } main = new("ControlHost") function main:Init() self.modes = { } self.modes["LIST"] = LoadModule("Modules/BuildList") self.modes["BUILD"] = LoadModule("Modules/Build") if launch.devMode or (GetScriptPath() == GetRuntimePath() and not launch.installedMode) then -- If running in dev mode or standalone mode, put user data in the script path self.userPath = GetScriptPath().."/" else self.userPath = GetUserPath().."/Path of Building/" MakeDir(self.userPath) end self.defaultBuildPath = self.userPath.."Builds/" self.buildPath = self.defaultBuildPath MakeDir(self.buildPath) if launch.devMode and IsKeyDown("CTRL") then self.rebuildModCache = true else -- Load mod cache LoadModule("Data/ModCache", modLib.parseModCache) end if launch.devMode and IsKeyDown("CTRL") and IsKeyDown("SHIFT") then self.allowTreeDownload = true end self.tree = { } for _, treeVersion in ipairs(treeVersionList) do self.tree[treeVersion] = new("PassiveTree", treeVersion) end ConPrintf("Loading item databases...") self.uniqueDB = { list = { } } for type, typeList in pairs(data.uniques) do for _, raw in pairs(typeList) do local newItem = new("Item", "Rarity: Unique\n"..raw) if newItem.base then newItem:NormaliseQuality() self.uniqueDB.list[newItem.name] = newItem elseif launch.devMode then ConPrintf("Unique DB unrecognised item of type '%s':\n%s", type, raw) end end end self.rareDB = { list = { } } for _, raw in pairs(data.rares) do local newItem = new("Item", "Rarity: Rare\n"..raw) if newItem.base then newItem:NormaliseQuality() if newItem.crafted then if newItem.base.implicit and #newItem.implicitModLines == 0 then -- Automatically add implicit for line in newItem.base.implicit:gmatch("[^\n]+") do t_insert(newItem.implicitModLines, { line = line }) end end newItem:Craft() end self.rareDB.list[newItem.name] = newItem elseif launch.devMode then ConPrintf("Rare DB unrecognised item:\n%s", raw) end end if self.rebuildModCache then -- Update mod cache local out = io.open("Data/ModCache.lua", "w") out:write('local c=...') for line, dat in pairs(modLib.parseModCache) do if not dat[1] or not dat[1][1] or dat[1][1].name ~= "JewelFunc" then out:write('c["', line:gsub("\n","\\n"), '"]={') if dat[1] then writeLuaTable(out, dat[1]) else out:write('nil') end if dat[2] then out:write(',"', dat[2]:gsub("\n","\\n"), '"}\n') else out:write(',nil}\n') end end end out:close() end self.sharedItemList = { } self.sharedItemSetList = { } self.anchorMain = new("Control", nil, 4, 0, 0, 0) self.anchorMain.y = function() return self.screenH - 4 end self.controls.options = new("ButtonControl", {"BOTTOMLEFT",self.anchorMain,"BOTTOMLEFT"}, 0, 0, 68, 20, "Options", function() self:OpenOptionsPopup() end) self.controls.about = new("ButtonControl", {"BOTTOMLEFT",self.anchorMain,"BOTTOMLEFT"}, 72, 0, 68, 20, "About", function() self:OpenAboutPopup() end) self.controls.applyUpdate = new("ButtonControl", {"BOTTOMLEFT",self.anchorMain,"BOTTOMLEFT"}, 0, -24, 140, 20, "^x50E050Update Ready", function() self:OpenUpdatePopup() end) self.controls.applyUpdate.shown = function() return launch.updateAvailable and launch.updateAvailable ~= "none" end self.controls.checkUpdate = new("ButtonControl", {"BOTTOMLEFT",self.anchorMain,"BOTTOMLEFT"}, 0, -24, 140, 20, "", function() launch:CheckForUpdate() end) self.controls.checkUpdate.shown = function() return not launch.devMode and (not launch.updateAvailable or launch.updateAvailable == "none") end self.controls.checkUpdate.label = function() return launch.updateCheckRunning and launch.updateProgress or "Check for Update" end self.controls.checkUpdate.enabled = function() return not launch.updateCheckRunning end self.controls.forkLabel = new("LabelControl", {"BOTTOMLEFT",self.anchorMain,"BOTTOMLEFT"}, 148, -26, 0, 16, "") self.controls.forkLabel.label = function() return "^8PoB Community Fork" end self.controls.versionLabel = new("LabelControl", {"BOTTOMLEFT",self.anchorMain,"BOTTOMLEFT"}, 148, -2, 0, 16, "") self.controls.versionLabel.label = function() return "^8Version: "..launch.versionNumber..(launch.versionBranch == "dev" and " (Dev)" or "") end self.controls.devMode = new("LabelControl", {"BOTTOMLEFT",self.anchorMain,"BOTTOMLEFT"}, 0, -26, 0, 20, "^1Dev Mode") self.controls.devMode.shown = function() return launch.devMode end self.controls.dismissToast = new("ButtonControl", {"BOTTOMLEFT",self.anchorMain,"BOTTOMLEFT"}, 0, function() return -self.mainBarHeight + self.toastHeight end, 80, 20, "Dismiss", function() self.toastMode = "HIDING" self.toastStart = GetTime() end) self.controls.dismissToast.shown = function() return self.toastMode == "SHOWN" end self.mainBarHeight = 58 self.toastMessages = { } if launch.devMode and GetTime() >= 0 and GetTime() < 15000 then t_insert(self.toastMessages, [[ ^xFF7700Warning: ^7Developer Mode active! The program is currently running in developer mode, which is not intended for normal use. If you are not expecting this, then you may have set up the program from the source .zip instead of using one of the installers. If that is the case, please reinstall using one of the installers from the "Releases" section of the GitHub page.]]) end self.inputEvents = { } self.popups = { } self.tooltipLines = { } self.gameAccounts = { } self.buildSortMode = "NAME" self.nodePowerTheme = "RED/BLUE" self.showThousandsSeparators = true self.thousandsSeparator = "," self.decimalSeparator = "." self.showTitlebarName = true local ignoreBuild = self:LoadPastebinBuild() if not ignoreBuild then self:SetMode("BUILD", false, "Unnamed build") end self:LoadSettings(ignoreBuild) end function main:CanExit() local ret = self:CallMode("CanExit", "EXIT") if ret ~= nil then return ret else return true end end function main:Shutdown() self:CallMode("Shutdown") self:SaveSettings() end function main:OnFrame() self.screenW, self.screenH = GetScreenSize() while self.newMode do if self.mode then self:CallMode("Shutdown") end self.mode = self.newMode self.newMode = nil self:CallMode("Init", unpack(self.newModeArgs)) if self.newModeChangeToTree then self.modes[self.mode].viewMode = "TREE" end self.newModeChangeToTree = false end self.viewPort = { x = 0, y = 0, width = self.screenW, height = self.screenH } if self.popups[1] then self.popups[1]:ProcessInput(self.inputEvents, self.viewPort) wipeTable(self.inputEvents) else self:ProcessControlsInput(self.inputEvents, self.viewPort) end self:CallMode("OnFrame", self.inputEvents, self.viewPort) if launch.updateErrMsg then t_insert(self.toastMessages, string.format("Update check failed!\n%s", launch.updateErrMsg)) launch.updateErrMsg = nil end if launch.updateAvailable then if launch.updateAvailable == "none" then t_insert(self.toastMessages, "No update available\nYou are running the latest version.") launch.updateAvailable = nil elseif not self.updateAvailableShown then t_insert(self.toastMessages, "Update Available\nAn update has been downloaded and is ready\nto be applied.") self.updateAvailableShown = true end end -- Run toasts if self.toastMessages[1] then if not self.toastMode then self.toastMode = "SHOWING" self.toastStart = GetTime() self.toastHeight = #self.toastMessages[1]:gsub("[^\n]","") * 16 + 20 + 40 end if self.toastMode == "SHOWING" then local now = GetTime() if now >= self.toastStart + 250 then self.toastMode = "SHOWN" else self.mainBarHeight = 58 + self.toastHeight * (now - self.toastStart) / 250 end end if self.toastMode == "SHOWN" then self.mainBarHeight = 58 + self.toastHeight elseif self.toastMode == "HIDING" then local now = GetTime() if now >= self.toastStart + 75 then self.toastMode = nil self.mainBarHeight = 58 t_remove(self.toastMessages, 1) else self.mainBarHeight = 58 + self.toastHeight * (1 - (now - self.toastStart) / 75) end end if self.toastMode then SetDrawColor(0.85, 0.85, 0.85) DrawImage(nil, 0, self.screenH - self.mainBarHeight, 312, self.mainBarHeight) SetDrawColor(0.1, 0.1, 0.1) DrawImage(nil, 0, self.screenH - self.mainBarHeight + 4, 308, self.mainBarHeight - 4) SetDrawColor(1, 1, 1) DrawString(4, self.screenH - self.mainBarHeight + 8, "LEFT", 20, "VAR", self.toastMessages[1]:gsub("\n.*","")) DrawString(4, self.screenH - self.mainBarHeight + 28, "LEFT", 16, "VAR", self.toastMessages[1]:gsub("^[^\n]*\n?","")) end end -- Draw main controls SetDrawColor(0.85, 0.85, 0.85) DrawImage(nil, 0, self.screenH - 58, 312, 58) SetDrawColor(0.1, 0.1, 0.1) DrawImage(nil, 0, self.screenH - 54, 308, 54) self:DrawControls(self.viewPort) if self.popups[1] then SetDrawLayer(10) SetDrawColor(0, 0, 0, 0.5) DrawImage(nil, 0, 0, self.screenW, self.screenH) self.popups[1]:Draw(self.viewPort) SetDrawLayer(0) end if self.showDragText then local cursorX, cursorY = GetCursorPos() local strWidth = DrawStringWidth(16, "VAR", self.showDragText) SetDrawLayer(20, 0) SetDrawColor(0.15, 0.15, 0.15, 0.75) DrawImage(nil, cursorX, cursorY - 8, strWidth + 2, 18) SetDrawColor(1, 1, 1) DrawString(cursorX + 1, cursorY - 7, "LEFT", 16, "VAR", self.showDragText) self.showDragText = nil end --[[local par = 300 for x = 0, 750 do for y = 0, 750 do local dpsCol = (x / par * 1.5) ^ 0.5 local defCol = (y / par * 1.5) ^ 0.5 local mixCol = (m_max(dpsCol - 0.5, 0) + m_max(defCol - 0.5, 0)) / 2 if main.nodePowerTheme == "RED/BLUE" then SetDrawColor(dpsCol, mixCol, defCol) elseif main.nodePowerTheme == "RED/GREEN" then SetDrawColor(dpsCol, defCol, mixCol) elseif main.nodePowerTheme == "GREEN/BLUE" then SetDrawColor(mixCol, dpsCol, defCol) end DrawImage(nil, x + 500, y + 200, 1, 1) end end SetDrawColor(0, 0, 0) DrawImage(nil, par + 500, 200, 2, 750) DrawImage(nil, 500, par + 200, 759, 2)]] wipeTable(self.inputEvents) end function main:OnKeyDown(key, doubleClick) t_insert(self.inputEvents, { type = "KeyDown", key = key, doubleClick = doubleClick }) end function main:OnKeyUp(key) t_insert(self.inputEvents, { type = "KeyUp", key = key }) end function main:OnChar(key) t_insert(self.inputEvents, { type = "Char", key = key }) end function main:SetMode(newMode, ...) self.newMode = newMode self.newModeArgs = {...} end function main:CallMode(func, ...) local modeTbl = self.modes[self.mode] if modeTbl and modeTbl[func] then return modeTbl[func](modeTbl, ...) end end function main:LoadPastebinBuild() local fullUri = arg[1] if not fullUri then return false end arg[1] = nil -- Protect against downloading again this session. local pastebinCode = string.match(fullUri, "^pob:[/\\]*pastebin[/\\]+(%w+)[/\\]*") if pastebinCode then launch:DownloadPage("https://pastebin.com/raw/" .. pastebinCode, function(page, errMsg) if errMsg then self:SetMode("BUILD", false, "Failed Build Import (Download failed " .. pastebinCode .. ")") else local xmlText = Inflate(common.base64.decode(page:gsub("-","+"):gsub("_","/"))) if xmlText then self:SetMode("BUILD", false, "Imported Build", xmlText) self.newModeChangeToTree = true else self:SetMode("BUILD", false, "Failed Build Import (Decompress failed)") end end end) return true end return false end function main:LoadSettings(ignoreBuild) local setXML, errMsg = common.xml.LoadXMLFile(self.userPath.."Settings.xml") if not setXML then return true elseif setXML[1].elem ~= "PathOfBuilding" then launch:ShowErrMsg("^1Error parsing 'Settings.xml': 'PathOfBuilding' root element missing") return true end for _, node in ipairs(setXML[1]) do if type(node) == "table" then if not ignoreBuild and node.elem == "Mode" then if not node.attrib.mode or not self.modes[node.attrib.mode] then launch:ShowErrMsg("^1Error parsing 'Settings.xml': Invalid mode attribute in 'Mode' element") return true end local args = { } for _, child in ipairs(node) do if type(child) == "table" then if child.elem == "Arg" then if child.attrib.number then t_insert(args, tonumber(child.attrib.number)) elseif child.attrib.string then t_insert(args, child.attrib.string) elseif child.attrib.boolean then t_insert(args, child.attrib.boolean == "true") end end end end self:SetMode(node.attrib.mode, unpack(args)) elseif node.elem == "Accounts" then self.lastAccountName = node.attrib.lastAccountName self.lastRealm = node.attrib.lastRealm for _, child in ipairs(node) do if child.elem == "Account" then self.gameAccounts[child.attrib.accountName] = { sessionID = child.attrib.sessionID, } end end elseif node.elem == "SharedItems" then for _, child in ipairs(node) do if child.elem == "Item" then local rawItem = { raw = "" } for _, subChild in ipairs(child) do if type(subChild) == "string" then rawItem.raw = subChild end end local newItem = new("Item", rawItem.raw) t_insert(self.sharedItemList, newItem) elseif child.elem == "ItemSet" then local sharedItemSet = { title = child.attrib.title, slots = { } } for _, grandChild in ipairs(child) do if grandChild.elem == "Item" then local rawItem = { raw = "" } for _, subChild in ipairs(grandChild) do if type(subChild) == "string" then rawItem.raw = subChild end end local newItem = new("Item", rawItem.raw) sharedItemSet.slots[grandChild.attrib.slotName] = newItem end end t_insert(self.sharedItemSetList, sharedItemSet) end end elseif node.elem == "Misc" then if node.attrib.buildSortMode then self.buildSortMode = node.attrib.buildSortMode end launch.proxyURL = node.attrib.proxyURL if node.attrib.buildPath then self.buildPath = node.attrib.buildPath end if node.attrib.nodePowerTheme then self.nodePowerTheme = node.attrib.nodePowerTheme end -- In order to preserve users' settings through renameing/merging this variable, we have this if statement to use the first found setting -- Once the user has closed PoB once, they will be using the new `showThousandsSeparator` variable name, so after some time, this statement may be removed if node.attrib.showThousandsCalcs then self.showThousandsSeparators = node.attrib.showThousandsCalcs == "true" elseif node.attrib.showThousandsSidebar then self.showThousandsSeparators = node.attrib.showThousandsSidebar == "true" end if node.attrib.showThousandsSeparators then self.showThousandsSeparators = node.attrib.showThousandsSeparators == "true" end if node.attrib.thousandsSeparator then self.thousandsSeparator = node.attrib.thousandsSeparator end if node.attrib.decimalSeparator then self.decimalSeparator = node.attrib.decimalSeparator end if node.attrib.showTitlebarName then self.showTitlebarName = node.attrib.showTitlebarName == "true" end end end end end function main:SaveSettings() local setXML = { elem = "PathOfBuilding" } local mode = { elem = "Mode", attrib = { mode = self.mode } } for _, val in ipairs({ self:CallMode("GetArgs") }) do local child = { elem = "Arg", attrib = { } } if type(val) == "number" then child.attrib.number = tostring(val) elseif type(val) == "boolean" then child.attrib.boolean = tostring(val) else child.attrib.string = tostring(val) end t_insert(mode, child) end t_insert(setXML, mode) local accounts = { elem = "Accounts", attrib = { lastAccountName = self.lastAccountName, lastRealm = self.lastRealm } } for accountName, account in pairs(self.gameAccounts) do t_insert(accounts, { elem = "Account", attrib = { accountName = accountName, sessionID = account.sessionID } }) end t_insert(setXML, accounts) local sharedItemList = { elem = "SharedItems" } for _, verItem in ipairs(self.sharedItemList) do t_insert(sharedItemList, { elem = "Item", [1] = verItem.raw }) end for _, sharedItemSet in ipairs(self.sharedItemSetList) do local set = { elem = "ItemSet", attrib = { title = sharedItemSet.title } } for slotName, verItem in pairs(sharedItemSet.slots) do t_insert(set, { elem = "Item", attrib = { slotName = slotName }, [1] = verItem.raw }) end t_insert(sharedItemList, set) end t_insert(setXML, sharedItemList) t_insert(setXML, { elem = "Misc", attrib = { buildSortMode = self.buildSortMode, proxyURL = launch.proxyURL, buildPath = (self.buildPath ~= self.defaultBuildPath and self.buildPath or nil), nodePowerTheme = self.nodePowerTheme, showThousandsSeparators = tostring(self.showThousandsSeparators), thousandsSeparator = self.thousandsSeparator, decimalSeparator = self.decimalSeparator, showTitlebarName = tostring(self.showTitlebarName), } }) local res, errMsg = common.xml.SaveXMLFile(setXML, self.userPath.."Settings.xml") if not res then launch:ShowErrMsg("Error saving 'Settings.xml': %s", errMsg) return true end end function main:OpenOptionsPopup() local controls = { } controls.proxyType = new("DropDownControl", {"TOPLEFT",nil,"TOPLEFT"}, 150, 20, 80, 18, { { label = "HTTP", scheme = "http" }, { label = "SOCKS", scheme = "socks5" }, { label = "SOCKS5H", scheme = "socks5h" }, }) controls.proxyLabel = new("LabelControl", {"RIGHT",controls.proxyType,"LEFT"}, -4, 0, 0, 16, "^7Proxy server:") controls.proxyURL = new("EditControl", {"LEFT",controls.proxyType,"RIGHT"}, 4, 0, 206, 18) if launch.proxyURL then local scheme, url = launch.proxyURL:match("(%w+)://(.+)") controls.proxyType:SelByValue(scheme, "scheme") controls.proxyURL:SetText(url) end controls.buildPath = new("EditControl", {"TOPLEFT",nil,"TOPLEFT"}, 150, 44, 290, 18) controls.buildPathLabel = new("LabelControl", {"RIGHT",controls.buildPath,"LEFT"}, -4, 0, 0, 16, "^7Build save path:") if self.buildPath ~= self.defaultBuildPath then controls.buildPath:SetText(self.buildPath) end controls.buildPath.tooltipText = "Overrides the default save location for builds.\nThe default location is: '"..self.defaultBuildPath.."'" controls.nodePowerTheme = new("DropDownControl", {"TOPLEFT",nil,"TOPLEFT"}, 150, 68, 100, 18, { { label = "Red & Blue", theme = "RED/BLUE" }, { label = "Red & Green", theme = "RED/GREEN" }, { label = "Green & Blue", theme = "GREEN/BLUE" }, }, function(index, value) self.nodePowerTheme = value.theme end) controls.nodePowerThemeLabel = new("LabelControl", {"RIGHT",controls.nodePowerTheme,"LEFT"}, -4, 0, 0, 16, "^7Node Power colours:") controls.nodePowerTheme.tooltipText = "Changes the colour scheme used for the node power display on the passive tree." controls.nodePowerTheme:SelByValue(self.nodePowerTheme, "theme") controls.separatorLabel = new("LabelControl", {"TOPRIGHT",nil,"TOPLEFT"}, 210, 94, 0, 16, "^7Show thousands separators:") controls.thousandsSeparators = new("CheckBoxControl", {"TOPLEFT",nil,"TOPLEFT"}, 280, 92, 20, nil, function(state) self.showThousandsSeparators = state end) controls.thousandsSeparators.state = self.showThousandsSeparators controls.thousandsSeparator = new("EditControl", {"TOPLEFT",nil,"TOPLEFT"}, 280, 116, 20, 20, self.thousandsSeparator, nil, "%%^", 1, function(buf) self.thousandsSeparator = buf end) controls.thousandsSeparatorLabel = new("LabelControl", {"TOPRIGHT",nil,"TOPLEFT"}, 210, 116, 92, 16, "Thousands Separator:") controls.decimalSeparator = new("EditControl", {"TOPLEFT",nil,"TOPLEFT"}, 280, 138, 20, 20, self.decimalSeparator, nil, "%%^", 1, function(buf) self.decimalSeparator = buf end) controls.decimalSeparatorLabel = new("LabelControl", {"TOPRIGHT",nil,"TOPLEFT"}, 210, 138, 92, 16, "Decimal Separator:") controls.titlebarName = new("CheckBoxControl", {"TOPLEFT",nil,"TOPLEFT"}, 230, 160, 20, "Show build name in window title:", function(state) self.showTitlebarName = state end) controls.titlebarName.state = self.showTitlebarName local initialNodePowerTheme = self.nodePowerTheme local initialThousandsSeparatorDisplay = self.showThousandsSeparators local initialTitlebarName = self.showTitlebarName local initialThousandsSeparator = self.thousandsSeparator local initialDecimalSeparator = self.decimalSeparator controls.save = new("ButtonControl", nil, -45, 182, 80, 20, "Save", function() if controls.proxyURL.buf:match("%w") then launch.proxyURL = controls.proxyType.list[controls.proxyType.selIndex].scheme .. "://" .. controls.proxyURL.buf else launch.proxyURL = nil end if controls.buildPath.buf:match("%S") then self.buildPath = controls.buildPath.buf if not self.buildPath:match("[\\/]$") then self.buildPath = self.buildPath .. "/" end else self.buildPath = self.defaultBuildPath end if self.mode == "LIST" then self.modes.LIST:BuildList() end main:ClosePopup() end) controls.cancel = new("ButtonControl", nil, 45, 182, 80, 20, "Cancel", function() self.nodePowerTheme = initialNodePowerTheme self.showThousandsSeparators = initialThousandsSeparatorDisplay self.thousandsSeparator = initialThousandsSeparator self.decimalSeparator = initialDecimalSeparator self.showTitlebarName = initialTitlebarName main:ClosePopup() end) self:OpenPopup(450, 218, "Options", controls, "save", nil, "cancel") end function main:OpenUpdatePopup() local changeList = { } for line in io.lines("changelog.txt") do local ver, date = line:match("^VERSION%[(.+)%]%[(.+)%]$") if ver then if ver == launch.versionNumber then break end if #changeList > 0 then t_insert(changeList, { height = 12 }) end t_insert(changeList, { height = 20, "^7Version "..ver.." ("..date..")" }) else t_insert(changeList, { height = 14, "^7"..line }) end end local controls = { } controls.changeLog = new("TextListControl", nil, 0, 20, 780, 192, nil, changeList) controls.update = new("ButtonControl", nil, -45, 220, 80, 20, "Update", function() self:ClosePopup() local ret = self:CallMode("CanExit", "UPDATE") if ret == nil or ret == true then launch:ApplyUpdate(launch.updateAvailable) end end) controls.cancel = new("ButtonControl", nil, 45, 220, 80, 20, "Cancel", function() self:ClosePopup() end) self:OpenPopup(800, 250, "Update Available", controls) end function main:OpenAboutPopup() local changeList = { } for line in io.lines("changelog.txt") do local ver, date = line:match("^VERSION%[(.+)%]%[(.+)%]$") if ver then if #changeList > 0 then t_insert(changeList, { height = 10 }) end t_insert(changeList, { height = 18, "^7Version "..ver.." ("..date..")" }) else t_insert(changeList, { height = 12, "^7"..line }) end end local controls = { } controls.close = new("ButtonControl", {"TOPRIGHT",nil,"TOPRIGHT"}, -10, 10, 50, 20, "Close", function() self:ClosePopup() end) controls.version = new("LabelControl", nil, 0, 18, 0, 18, "Path of Building Community Fork v"..launch.versionNumber) controls.forum = new("LabelControl", nil, 0, 36, 0, 18, "Based on Openarl's Path of Building") controls.github = new("ButtonControl", nil, 0, 62, 438, 18, "GitHub page: ^x4040FFhttps://github.com/PathOfBuildingCommunity/PathOfBuilding", function(control) OpenURL("https://github.com/PathOfBuildingCommunity/PathOfBuilding") end) controls.verLabel = new("LabelControl", {"TOPLEFT",nil,"TOPLEFT"}, 10, 82, 0, 18, "^7Version history:") controls.changelog = new("TextListControl", nil, 0, 100, 630, 390, nil, changeList) self:OpenPopup(650, 500, "About", controls) end function main:DrawBackground(viewPort) SetDrawLayer(nil, -100) SetDrawColor(0.5, 0.5, 0.5) DrawImage(self.tree["2_6"].assets.Background1.handle, viewPort.x, viewPort.y, viewPort.width, viewPort.height, 0, 0, viewPort.width / 100, viewPort.height / 100) SetDrawLayer(nil, 0) end function main:DrawArrow(x, y, width, height, dir) local x1 = x - width / 2 local x2 = x + width / 2 local xMid = (x1 + x2) / 2 local y1 = y - height / 2 local y2 = y + height / 2 local yMid = (y1 + y2) / 2 if dir == "UP" then DrawImageQuad(nil, xMid, y1, xMid, y1, x2, y2, x1, y2) elseif dir == "RIGHT" then DrawImageQuad(nil, x1, y1, x2, yMid, x2, yMid, x1, y2) elseif dir == "DOWN" then DrawImageQuad(nil, x1, y1, x2, y1, xMid, y2, xMid, y2) elseif dir == "LEFT" then DrawImageQuad(nil, x1, yMid, x2, y1, x2, y2, x1, yMid) end end function main:DrawCheckMark(x, y, size) size = size / 0.8 x = x - size / 2 y = y - size / 2 DrawImageQuad(nil, x + size * 0.15, y + size * 0.50, x + size * 0.30, y + size * 0.45, x + size * 0.50, y + size * 0.80, x + size * 0.40, y + size * 0.90) DrawImageQuad(nil, x + size * 0.40, y + size * 0.90, x + size * 0.35, y + size * 0.75, x + size * 0.80, y + size * 0.10, x + size * 0.90, y + size * 0.20) end do local cos45 = m_cos(m_pi / 4) local cos35 = m_cos(m_pi * 0.195) local sin35 = m_sin(m_pi * 0.195) function main:WorldToScreen(x, y, z, width, height) -- World -> camera local cx = (x - y) * cos45 local cy = -5.33 - (y + x) * cos45 * cos35 - z * sin35 local cz = 122 + (y + x) * cos45 * sin35 - z * cos35 -- Camera -> screen local sx = width * 0.5 + cx / cz * 1.27 * height local sy = height * 0.5 + cy / cz * 1.27 * height return round(sx), round(sy) end end function main:RenderCircle(x, y, width, height, oX, oY, radius) local minX = wipeTable(tempTable1) local maxX = wipeTable(tempTable2) local minY = height local maxY = 0 for d = 0, 360, 0.15 do local r = d / 180 * m_pi local px, py = main:WorldToScreen(oX + m_sin(r) * radius, oY + m_cos(r) * radius, 0, width, height) if py >= 0 and py < height then px = m_min(width, m_max(0, px)) minY = m_min(minY, py) maxY = m_max(maxY, py) minX[py] = m_min(minX[py] or px, px) maxX[py] = m_max(maxX[py] or px, px) end end for ly = minY, maxY do if minX[ly] then DrawImage(nil, x + minX[ly], y + ly, maxX[ly] - minX[ly] + 1, 1) end end end function main:RenderRing(x, y, width, height, oX, oY, radius, size) local lastX, lastY for d = 0, 360, 0.2 do local r = d / 180 * m_pi local px, py = main:WorldToScreen(oX + m_sin(r) * radius, oY + m_cos(r) * radius, 0, width, height) if px >= -size/2 and px < width + size/2 and py >= -size/2 and py < height + size/2 and (px ~= lastX or py ~= lastY) then DrawImage(nil, x + px - size/2, y + py, size, size) lastX, lastY = px, py end end end function main:StatColor(stat, base, limit) if limit and stat > limit then return colorCodes.NEGATIVE elseif base and stat ~= base then return colorCodes.MAGIC else return "^7" end end function main:MoveFolder(name, srcPath, dstPath) -- Create destination folder local res, msg = MakeDir(dstPath..name) if not res then self:OpenMessagePopup("Error", "Couldn't move '"..name.."' to '"..dstPath.."' : "..msg) return end -- Move subfolders local handle = NewFileSearch(srcPath..name.."/*", true) while handle do self:MoveFolder(handle:GetFileName(), srcPath..name.."/", dstPath..name.."/") if not handle:NextFile() then break end end -- Move files handle = NewFileSearch(srcPath..name.."/*") while handle do local fileName = handle:GetFileName() local srcName = srcPath..name.."/"..fileName local dstName = dstPath..name.."/"..fileName local res, msg = os.rename(srcName, dstName) if not res then self:OpenMessagePopup("Error", "Couldn't move '"..srcName.."' to '"..dstName.."': "..msg) return end if not handle:NextFile() then break end end -- Remove source folder local res, msg = RemoveDir(srcPath..name) if not res then self:OpenMessagePopup("Error", "Couldn't delete '"..dstPath..name.."' : "..msg) return end end function main:CopyFolder(srcName, dstName) -- Create destination folder local res, msg = MakeDir(dstName) if not res then self:OpenMessagePopup("Error", "Couldn't copy '"..srcName.."' to '"..dstName.."' : "..msg) return end -- Copy subfolders local handle = NewFileSearch(srcName.."/*", true) while handle do local fileName = handle:GetFileName() self:CopyFolder(srcName.."/"..fileName, dstName.."/"..fileName) if not handle:NextFile() then break end end -- Copy files handle = NewFileSearch(srcName.."/*") while handle do local fileName = handle:GetFileName() local srcName = srcName.."/"..fileName local dstName = dstName.."/"..fileName local res, msg = copyFile(srcName, dstName) if not res then self:OpenMessagePopup("Error", "Couldn't copy '"..srcName.."' to '"..dstName.."': "..msg) return end if not handle:NextFile() then break end end end function main:OpenPopup(width, height, title, controls, enterControl, defaultControl, escapeControl) local popup = new("PopupDialog", width, height, title, controls, enterControl, defaultControl, escapeControl) t_insert(self.popups, 1, popup) return popup end function main:ClosePopup() t_remove(self.popups, 1) end function main:OpenMessagePopup(title, msg) local controls = { } local numMsgLines = 0 for line in string.gmatch(msg .. "\n", "([^\n]*)\n") do t_insert(controls, new("LabelControl", nil, 0, 20 + numMsgLines * 16, 0, 16, line)) numMsgLines = numMsgLines + 1 end controls.close = new("ButtonControl", nil, 0, 40 + numMsgLines * 16, 80, 20, "Ok", function() main:ClosePopup() end) return self:OpenPopup(m_max(DrawStringWidth(16, "VAR", msg) + 30, 190), 70 + numMsgLines * 16, title, controls, "close") end function main:OpenConfirmPopup(title, msg, confirmLabel, onConfirm) local controls = { } local numMsgLines = 0 for line in string.gmatch(msg .. "\n", "([^\n]*)\n") do t_insert(controls, new("LabelControl", nil, 0, 20 + numMsgLines * 16, 0, 16, line)) numMsgLines = numMsgLines + 1 end local confirmWidth = m_max(80, DrawStringWidth(16, "VAR", confirmLabel) + 10) controls.confirm = new("ButtonControl", nil, -5 - m_ceil(confirmWidth/2), 40 + numMsgLines * 16, confirmWidth, 20, confirmLabel, function() main:ClosePopup() onConfirm() end) t_insert(controls, new("ButtonControl", nil, 5 + m_ceil(confirmWidth/2), 40 + numMsgLines * 16, confirmWidth, 20, "Cancel", function() main:ClosePopup() end)) return self:OpenPopup(m_max(DrawStringWidth(16, "VAR", msg) + 30, 190), 70 + numMsgLines * 16, title, controls, "confirm") end function main:OpenNewFolderPopup(path, onClose) local controls = { } controls.label = new("LabelControl", nil, 0, 20, 0, 16, "^7Enter folder name:") controls.edit = new("EditControl", nil, 0, 40, 350, 20, nil, nil, "\\/:%*%?\"<>|%c", 100, function(buf) controls.create.enabled = buf:match("%S") end) controls.create = new("ButtonControl", nil, -45, 70, 80, 20, "Create", function() local newFolderName = controls.edit.buf local res, msg = MakeDir(path..newFolderName) if not res then main:OpenMessagePopup("Error", "Couldn't create '"..newFolderName.."': "..msg) return end if onClose then onClose(newFolderName) end main:ClosePopup() end) controls.create.enabled = false controls.cancel = new("ButtonControl", nil, 45, 70, 80, 20, "Cancel", function() if onClose then onClose() end main:ClosePopup() end) main:OpenPopup(370, 100, "New Folder", controls, "create", "edit", "cancel") end function main:SetWindowTitleSubtext(subtext) if not subtext or not self.showTitlebarName then SetWindowTitle("Path of Building") else SetWindowTitle("Path of Building - "..subtext) end end do local wrapTable = { } function main:WrapString(str, height, width) wipeTable(wrapTable) local lineStart = 1 local lastSpace, lastBreak while true do local s, e = str:find("%s+", lastSpace) if not s then s = #str + 1 e = #str + 1 end if DrawStringWidth(height, "VAR", str:sub(lineStart, s - 1)) > width then t_insert(wrapTable, str:sub(lineStart, lastBreak)) lineStart = lastSpace end if s > #str then t_insert(wrapTable, str:sub(lineStart, -1)) break end lastBreak = s - 1 lastSpace = e + 1 end return wrapTable end end return main