├── .editorconfig ├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── ItemTester ├── SearchDPS.lua ├── TestItem.lua ├── UpdateBuild.lua ├── inspect.lua ├── json.lua ├── mockui.lua ├── mods.json ├── pobinterface.lua └── testercore.lua ├── LICENSE ├── README.md ├── TestItem.ahk ├── bin ├── lua51.dll └── luajit.exe ├── imgs ├── sshot-dps.png └── sshot-tester.png └── test ├── README.md ├── _testsetup.bat ├── testdps.bat ├── testitem.bat ├── testitems ├── jewel.txt └── ring.txt ├── testupdate.bat ├── updatemods-curl.bat └── updatemods-powershell.bat /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = crlf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [coldino,xanthics] 2 | ko_fi: volatilepulse 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.ini 2 | test/testitems/*.html 3 | *.code-workspace 4 | *.version 5 | -------------------------------------------------------------------------------- /ItemTester/SearchDPS.lua: -------------------------------------------------------------------------------- 1 | HELP = [[ 2 | Use the AHK script with hotkey Ctrl-Windows-D. 3 | 4 | See testdps.bat for an example of running it directly. 5 | ]] 6 | 7 | 8 | local BUILD_XML = arg[1] 9 | if BUILD_XML == nil or BUILD_XML == "-h" or BUILD_XML == "--help" or BUILD_XML == "/?" then 10 | print("Usage: SearchDPS.lua |CURRENT [|OPTIONS]") 11 | os.exit(1) 12 | end 13 | 14 | local _,_,SCRIPT_PATH=string.find(arg[0], "(.+[/\\]).-") 15 | dofile(SCRIPT_PATH.."mockui.lua") 16 | 17 | xml = require("xml") 18 | json = require("json") 19 | inspect = require("inspect") 20 | local testercore = require("testercore") 21 | local pobinterface = require("pobinterface") 22 | 23 | debug = false 24 | 25 | function findRelevantStat(activeEffect, chosenField) 26 | local calcFunc, stats = calcs.getMiscCalculator(build) 27 | 28 | local actorType = nil 29 | if stats.FullDPS == nil or stats.FullDPS == 0 then 30 | if stats['Minion'] then 31 | actorType = 'Minion' 32 | end 33 | 34 | if actorType then 35 | stats = stats[actorType] 36 | end 37 | end 38 | 39 | if chosenField and chosenField == "OPTIONS" then -- show stat list 40 | print("\nAvailable stats:") 41 | print(inspect(stats)) 42 | os.exit(1) 43 | elseif chosenField and stats[chosenField] ~= nil then -- user-specified stat 44 | return actorType,chosenField 45 | elseif chosenField then -- bad user-specified stat 46 | print("ERROR: Stat '"..chosenField.."' is not found (case sensitive)") 47 | os.exit(1) 48 | end 49 | 50 | if stats['FullDPS'] ~= 0 then return actorType,'FullDPS' end 51 | if stats['CombinedDPS'] ~= 0 then return actorType,'CombinedDPS' end 52 | if stats['AverageHit'] ~= 0 then return actorType,'AverageHit' end 53 | if stats['TotalDotDPS'] ~= 0 then return actorType,'TotalDotDPS' end 54 | print("ERROR: Don't know how to deal with this build's damage output type") 55 | os.exit(1) 56 | end 57 | 58 | function findModEffect(modLine, statField, actorType) 59 | -- Construct an empty passive socket node to test in 60 | local testNode = {id="temporary-test-node", type="Socket", alloc=false, sd={"Temp Test Socket"}, modList={}} 61 | 62 | -- Construct jewel with the mod just to use its mods in the passive node 63 | local itemText = "Test Jewel\nCobalt Jewel\n"..modLine 64 | local item 65 | if build.targetVersionData then -- handle code changes in 1.4.170.17 66 | item = new("Item", build.targetVersion, itemText) 67 | else 68 | item = new("Item", itemText) 69 | end 70 | testNode.modList = item.modList 71 | 72 | -- Calculate stat differences 73 | local calcFunc, baseStats = calcs.getMiscCalculator(build) 74 | local newStats = calcFunc({ addNodes={ [testNode]=true } }) 75 | 76 | -- Switch to minion/totem stats if needed 77 | if actorType then 78 | newStats = newStats[actorType] 79 | baseStats = baseStats[actorType] 80 | end 81 | 82 | -- Pull out the difference in DPS 83 | local statVal1 = newStats[statField] or 0 84 | local statVal2 = baseStats[statField] or 0 85 | local diff = statVal1 - statVal2 86 | 87 | return diff 88 | end 89 | 90 | function char_to_hex(c) 91 | return string.format("%%%02X", string.byte(c)) 92 | end 93 | 94 | function urlencode(url) 95 | if url == nil then 96 | return 97 | end 98 | url = url:gsub("([^%w])", char_to_hex) 99 | return url 100 | end 101 | 102 | function extractWeaponFlags(env, weapon, flags) 103 | if not weapon or not weapon.type then return end 104 | local info = env.data.weaponTypeInfo[weapon.type] 105 | flags[info.flag] = true 106 | if info.melee then flags['Melee'] = true end 107 | if info.oneHand then 108 | flags['One Handed Weapon'] = true 109 | else 110 | flags['Two Handed Weapon'] = true 111 | end 112 | end 113 | 114 | function getCharges(name, modDB) 115 | local value = modDB:Sum("BASE", nil, name.."ChargesMax") 116 | if modDB:Flag(nil, "Use"..name.."Charges") then 117 | value = modDB:Override(nil, name.."Charges") or value 118 | else 119 | value = 0 120 | end 121 | return value 122 | end 123 | 124 | function encodeValue(value) 125 | if value == true then 126 | return "True" 127 | else 128 | return string.format("%s", value) 129 | end 130 | end 131 | 132 | local modDataText = loadText(SCRIPT_PATH.."mods.json") 133 | if modDataText then 134 | modData = json.decode(modDataText) 135 | else 136 | print("ERROR: Failed to load mods.json") 137 | os.exit(1) 138 | end 139 | 140 | -- Load a specific build file or use the default 141 | testercore.loadBuild(BUILD_XML) 142 | 143 | -- Check for bad PoB installs 144 | if not build.calcsTab or not calcs then 145 | error("ERROR: Unexpected error - you might need to re-install a freshly downloaded copy of Path of Building") 146 | end 147 | 148 | -- Work out which field to use to report damage: Full DPS / CombinedDPS / AverageHit 149 | local actorType,statField = findRelevantStat(activeEffect, arg[2]) 150 | 151 | -- Setup the main actor for gathering data 152 | local calcFunc, baseStats = calcs.getMiscCalculator(build) 153 | local env = build.calcsTab.calcs.initEnv(build, "MAIN") 154 | local actor = env.player 155 | 156 | if actorType and statField ~= "FullDPS" then 157 | baseStats = baseStats[actorType] 158 | end 159 | 160 | -- Work out a reasonable skill name 161 | local skillName = "" 162 | if statField == "FullDPS" then 163 | -- List all skills included in Full DPS 164 | skillName = "" 165 | for i,skill in pairs(baseStats.SkillDPS) do 166 | skillName = skillName .. " + " .. skill.name 167 | end 168 | skillName = skillName:sub(4) 169 | else 170 | -- Gather currently selected skill and part 171 | local parts = pobinterface.readSkillSelection() 172 | local pickedGroupName = parts.group 173 | local pickedActiveSkillName = parts.name 174 | skillName = pickedGroupName; 175 | if (pickedGroupName ~= pickedActiveSkillName) then 176 | skillName = skillName.." / "..pickedActiveSkillName 177 | end 178 | if (pickedPartName) then 179 | skillName = skillName.." / "..parts.part 180 | end 181 | end 182 | 183 | print() 184 | print("Using stat: " .. statField) 185 | print("Using actor: " .. (actorType or 'Player')) 186 | print("Using skill(s): " .. skillName) 187 | print() 188 | 189 | -- Get DPS difference for each mod 190 | url = 'https://xanthics.github.io/PoE_Weighted_Search/?' 191 | modsVersion = modData[1].version 192 | if modsVersion == nil then 193 | print("ERROR: mods.json needs updating") 194 | os.exit(2) 195 | end 196 | url = url .. "vals=" .. modsVersion .. "," 197 | for _,mod in ipairs(modData) do 198 | local dps = findModEffect(mod.desc, statField, actorType) 199 | if debug then print(' ' .. mod.desc .. ' = ' .. dps) end 200 | if dps >= 0.05 or dps <= -0.05 then 201 | url = url .. string.format("%.1f,", dps) 202 | else 203 | url = url .. "," 204 | end 205 | end 206 | url = url:match("(.-),*$") .. "&" 207 | 208 | if debug then 209 | -- print("Stats:") 210 | -- print(inspect(baseStats)) 211 | 212 | print("\nConditions:") 213 | print(inspect(env.modDB.conditions)) 214 | 215 | print("\nConfig:") 216 | print(inspect(env.configInput)) 217 | 218 | print("\nSkill flags:") 219 | print(inspect(actor.mainSkill.skillFlags)) 220 | 221 | print("\nWeapon 1: " .. (actor.weaponData1.type or '-') .. " (" .. (actor.weaponData1.type and env.data.weaponTypeInfo[actor.weaponData1.type].flag or '-') .. ")") 222 | print("Weapon 2: " .. (actor.weaponData2.type or '-') .. " (" .. (actor.weaponData2.type and env.data.weaponTypeInfo[actor.weaponData2.type].flag or '-') .. ")") 223 | print() 224 | end 225 | 226 | local flags = {} 227 | local values = {} 228 | 229 | -- Grab flags from main skill 230 | for skillType,_ in pairs(actor.mainSkill.skillTypes) do 231 | for name,type in pairs(SkillType) do 232 | if type == skillType then 233 | if name:match("Can[A-Z]") or name:match("Triggerable") or name:match(".+SingleTarget") or name:match("Type[0-9]+") then 234 | name = nil 235 | elseif name:match("ManaCost.+") or name:match("Aura.*") or name:match("Buff.*") then 236 | name = nil 237 | elseif name == "Instant" then 238 | name = nil 239 | elseif name == "SecondWindSupport" then 240 | name = nil 241 | elseif name:match(".+Skill") or name:match(".+Spell") then 242 | name = name:sub(0, #name-5) 243 | elseif name:match("Causes.+") then 244 | name = name:sub(7) 245 | elseif name:match(".+Attack") then 246 | name = name:sub(0, #name-6) 247 | end 248 | if name then flags[name] = true end 249 | end 250 | end 251 | end 252 | if actor.mainSkill.skillFlags.brand then flags['Brand'] = true end 253 | if actor.mainSkill.skillFlags.totem then flags['Totem'] = true end 254 | if actor.mainSkill.skillFlags.trap then flags['Trap'] = true end 255 | if actor.mainSkill.skillFlags.mine then flags['Mine'] = true end 256 | if actor.mainSkill.skillFlags.minion then flags['Minion'] = true end 257 | if actor.mainSkill.skillFlags.hit then flags['conditionHitRecently'] = true end 258 | 259 | -- Insert flags for weapon types 260 | flags["Shield"] = (actor.itemList["Weapon 2"] and actor.itemList["Weapon 2"].type == "Shield") or (actor == actor and env.aegisModList) 261 | flags["DualWielding"] = actor.weaponData1.type and actor.weaponData2.type and actor.weaponData1.type ~= "None" 262 | if actor.itemList["Weapon 1"] then extractWeaponFlags(env, actor.weaponData1, flags) end 263 | if actor.itemList["Weapon 2"] then extractWeaponFlags(env, actor.weaponData2, flags) end 264 | if flags["Spell"] then flags["Melee"] = nil end 265 | 266 | -- Grab config flags 267 | for flag,value in pairs(env.configInput) do 268 | if value == true and not flag:match("override") then 269 | flags[flag] = true 270 | end 271 | end 272 | if env.configInput['enemyIsBoss'] then flags['enemyIsBoss'] = true end 273 | if env.configInput['ImpaleStacks'] then flags['ImpaleStacks'] = env.configInput['ImpaleStacks'] end 274 | if baseStats["LifeUnreservedPercent"] and baseStats["LifeUnreservedPercent"] < 35 then flags["conditionLowLife"] = true end 275 | 276 | -- Work out how many charges we have 277 | for flag,value in pairs(flags) do 278 | name = flag:match('^use(.+)Charges$') 279 | if name then 280 | count = getCharges(name, actor.modDB) 281 | if count then values[name .. 'Count'] = count end 282 | flags[flag] = nil 283 | end 284 | end 285 | 286 | -- Infer some extra flags from what we already have 287 | if flags.Fire or flags.Cold or flags.Lightning then flags.Elemental = true end 288 | if baseStats.ChaosTakenHitMult == 0 then flags.conditionFullLife = true end -- CI 289 | if actorType == 'Minion' then flags.conditionUsedMinionSkillRecently = true end 290 | 291 | -- Add values to URL 292 | if debug then print('\nPost values:') end 293 | for name,value in pairs(values) do 294 | if value then url = url..urlencode(name:gsub(' ','')).."="..encodeValue(value).."&" end 295 | if debug then print(' '..name..string.format(" = %s", value)) end 296 | end 297 | 298 | -- Add flags to URL 299 | if debug then print('\nPost flags:') end 300 | local flagsString = 'Flags=' 301 | for flag,value in pairs(flags) do 302 | flag = flag:gsub('^condition', '') 303 | flag = flag:gsub(' ', '') 304 | if value then flagsString = flagsString..urlencode(flag).."," end 305 | if debug then print(' '..flag) end 306 | end 307 | url = url..flagsString.."&" 308 | 309 | -- Add skill name and character name 310 | name = build.buildName:gsub('.*[/\\]', ''):gsub('.xml','') 311 | url = url.."Skill="..urlencode(skillName).."&".."Character="..urlencode(name) 312 | 313 | if debug then 314 | print() 315 | print(url) 316 | else 317 | os.execute('start "" "' .. url .. '"') 318 | end 319 | -------------------------------------------------------------------------------- /ItemTester/TestItem.lua: -------------------------------------------------------------------------------- 1 | HELP = [[ 2 | Use the AHK script with hotkey Ctrl-Windows-C. 3 | 4 | See testitem.bat for an example of running it directly. 5 | Output HTML goes to .html 6 | 7 | Usage: lua TestItem.lua 8 | ]] 9 | 10 | local BUILD_XML = arg[1] 11 | local INPUT_FILE = arg[2] 12 | local OUTPUT_FILE = INPUT_FILE..".html" 13 | 14 | local _,_,SCRIPT_PATH=string.find(arg[0], "(.+[/\\]).-") 15 | dofile(SCRIPT_PATH.."mockui.lua") 16 | 17 | local fileHeader = [[ 18 | 25 | ]] 26 | 27 | 28 | local testercore = require("testercore") 29 | local pobinterface = require('pobinterface') 30 | 31 | -- Load a specific build file or use the default 32 | testercore.loadBuild(BUILD_XML) 33 | testercore.showSkills() 34 | 35 | -- Load an item from copy data 36 | local itemText = loadText(INPUT_FILE) 37 | local newItem 38 | if build.targetVersionData then -- handle code format changes in 1.4.170.17 39 | newItem = new("Item", build.targetVersion, itemText) 40 | else 41 | newItem = new("Item", itemText) 42 | end 43 | 44 | if newItem.base then 45 | newItem:NormaliseQuality() -- Set to top quality 46 | newItem:BuildModList() 47 | 48 | -- Extract new item's info to a fake tooltip 49 | local tooltip = FakeTooltip:new() 50 | build.itemsTab:AddItemTooltip(tooltip, newItem) 51 | 52 | -- Output tooltip as HTML 53 | local outFile = io.open(OUTPUT_FILE, "w") 54 | if outFile then 55 | outFile:write(fileHeader) 56 | for i, txt in pairs(tooltip.lines) do 57 | outFile:write(txt.."\n"); 58 | end 59 | outFile:close() 60 | end 61 | 62 | print("Results output to: "..OUTPUT_FILE) 63 | else 64 | print("ERROR: Unknown error calculating item") 65 | os.exit(1) 66 | end 67 | 68 | -------------------------------------------------------------------------------- /ItemTester/UpdateBuild.lua: -------------------------------------------------------------------------------- 1 | HELP = [[ 2 | Re-imports a build from pathofexile.com automatically. 3 | 4 | See testupdate.bat for an example of running it directly. 5 | 6 | Usage: lua UpdateBuild.lua |CURRENT 7 | ]] 8 | 9 | 10 | local BUILD_XML = arg[1]; 11 | if BUILD_XML == nil then 12 | print("Usage: UpdateBuild.lua |CURRENT") 13 | os.exit(1) 14 | end 15 | 16 | local _,_,SCRIPT_PATH=string.find(arg[0], "(.+[/\\]).-") 17 | dofile(SCRIPT_PATH.."mockui.lua") 18 | 19 | xml = require("xml") 20 | inspect = require("inspect") 21 | 22 | local testercore = require("testercore") 23 | local pobinterface = require('pobinterface') 24 | 25 | 26 | testercore.loadBuild(BUILD_XML) 27 | 28 | -- Remember previously selected skill 29 | local prevSkill = pobinterface.readSkillSelection() 30 | print("Skill group/gem/part: "..pobinterface.skillString(prevSkill)) 31 | 32 | -- Update 33 | print("Importing character changes...") 34 | pobinterface.updateBuild() 35 | 36 | -- Restore previously selected skill 37 | print("After group/gem/part: "..pobinterface.skillString()) 38 | pobinterface.selectSkill(prevSkill) 39 | print("Fixed group/gem/part: "..pobinterface.skillString()) 40 | 41 | testercore.saveBuild() 42 | print("Success") 43 | -------------------------------------------------------------------------------- /ItemTester/inspect.lua: -------------------------------------------------------------------------------- 1 | local inspect ={ 2 | _VERSION = 'inspect.lua 3.1.0', 3 | _URL = 'http://github.com/kikito/inspect.lua', 4 | _DESCRIPTION = 'human-readable representations of tables', 5 | _LICENSE = [[ 6 | MIT LICENSE 7 | 8 | Copyright (c) 2013 Enrique García Cota 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a 11 | copy of this software and associated documentation files (the 12 | "Software"), to deal in the Software without restriction, including 13 | without limitation the rights to use, copy, modify, merge, publish, 14 | distribute, sublicense, and/or sell copies of the Software, and to 15 | permit persons to whom the Software is furnished to do so, subject to 16 | the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included 19 | in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 24 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 26 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 27 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | ]] 29 | } 30 | 31 | local tostring = tostring 32 | 33 | inspect.KEY = setmetatable({}, {__tostring = function() return 'inspect.KEY' end}) 34 | inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end}) 35 | 36 | local function rawpairs(t) 37 | return next, t, nil 38 | end 39 | 40 | -- Apostrophizes the string if it has quotes, but not aphostrophes 41 | -- Otherwise, it returns a regular quoted string 42 | local function smartQuote(str) 43 | if str:match('"') and not str:match("'") then 44 | return "'" .. str .. "'" 45 | end 46 | return '"' .. str:gsub('"', '\\"') .. '"' 47 | end 48 | 49 | -- \a => '\\a', \0 => '\\0', 31 => '\31' 50 | local shortControlCharEscapes = { 51 | ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", 52 | ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v" 53 | } 54 | local longControlCharEscapes = {} -- \a => nil, \0 => \000, 31 => \031 55 | for i=0, 31 do 56 | local ch = string.char(i) 57 | if not shortControlCharEscapes[ch] then 58 | shortControlCharEscapes[ch] = "\\"..i 59 | longControlCharEscapes[ch] = string.format("\\%03d", i) 60 | end 61 | end 62 | 63 | local function escape(str) 64 | return (str:gsub("\\", "\\\\") 65 | :gsub("(%c)%f[0-9]", longControlCharEscapes) 66 | :gsub("%c", shortControlCharEscapes)) 67 | end 68 | 69 | local function isIdentifier(str) 70 | return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" ) 71 | end 72 | 73 | local function isSequenceKey(k, sequenceLength) 74 | return type(k) == 'number' 75 | and 1 <= k 76 | and k <= sequenceLength 77 | and math.floor(k) == k 78 | end 79 | 80 | local defaultTypeOrders = { 81 | ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4, 82 | ['function'] = 5, ['userdata'] = 6, ['thread'] = 7 83 | } 84 | 85 | local function sortKeys(a, b) 86 | local ta, tb = type(a), type(b) 87 | 88 | -- strings and numbers are sorted numerically/alphabetically 89 | if ta == tb and (ta == 'string' or ta == 'number') then return a < b end 90 | 91 | local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb] 92 | -- Two default types are compared according to the defaultTypeOrders table 93 | if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb] 94 | elseif dta then return true -- default types before custom ones 95 | elseif dtb then return false -- custom types after default ones 96 | end 97 | 98 | -- custom types are sorted out alphabetically 99 | return ta < tb 100 | end 101 | 102 | -- For implementation reasons, the behavior of rawlen & # is "undefined" when 103 | -- tables aren't pure sequences. So we implement our own # operator. 104 | local function getSequenceLength(t) 105 | local len = 1 106 | local v = rawget(t,len) 107 | while v ~= nil do 108 | len = len + 1 109 | v = rawget(t,len) 110 | end 111 | return len - 1 112 | end 113 | 114 | local function getNonSequentialKeys(t) 115 | local keys, keysLength = {}, 0 116 | local sequenceLength = getSequenceLength(t) 117 | for k,_ in rawpairs(t) do 118 | if not isSequenceKey(k, sequenceLength) then 119 | keysLength = keysLength + 1 120 | keys[keysLength] = k 121 | end 122 | end 123 | table.sort(keys, sortKeys) 124 | return keys, keysLength, sequenceLength 125 | end 126 | 127 | local function countTableAppearances(t, tableAppearances) 128 | tableAppearances = tableAppearances or {} 129 | 130 | if type(t) == 'table' then 131 | if not tableAppearances[t] then 132 | tableAppearances[t] = 1 133 | for k,v in rawpairs(t) do 134 | countTableAppearances(k, tableAppearances) 135 | countTableAppearances(v, tableAppearances) 136 | end 137 | countTableAppearances(getmetatable(t), tableAppearances) 138 | else 139 | tableAppearances[t] = tableAppearances[t] + 1 140 | end 141 | end 142 | 143 | return tableAppearances 144 | end 145 | 146 | local copySequence = function(s) 147 | local copy, len = {}, #s 148 | for i=1, len do copy[i] = s[i] end 149 | return copy, len 150 | end 151 | 152 | local function makePath(path, ...) 153 | local keys = {...} 154 | local newPath, len = copySequence(path) 155 | for i=1, #keys do 156 | newPath[len + i] = keys[i] 157 | end 158 | return newPath 159 | end 160 | 161 | local function processRecursive(process, item, path, visited) 162 | if item == nil then return nil end 163 | if visited[item] then return visited[item] end 164 | 165 | local processed = process(item, path) 166 | if type(processed) == 'table' then 167 | local processedCopy = {} 168 | visited[item] = processedCopy 169 | local processedKey 170 | 171 | for k,v in rawpairs(processed) do 172 | processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited) 173 | if processedKey ~= nil then 174 | processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited) 175 | end 176 | end 177 | 178 | local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited) 179 | if type(mt) ~= 'table' then mt = nil end -- ignore not nil/table __metatable field 180 | setmetatable(processedCopy, mt) 181 | processed = processedCopy 182 | end 183 | return processed 184 | end 185 | 186 | 187 | 188 | ------------------------------------------------------------------- 189 | 190 | local Inspector = {} 191 | local Inspector_mt = {__index = Inspector} 192 | 193 | function Inspector:puts(...) 194 | local args = {...} 195 | local buffer = self.buffer 196 | local len = #buffer 197 | for i=1, #args do 198 | len = len + 1 199 | buffer[len] = args[i] 200 | end 201 | end 202 | 203 | function Inspector:down(f) 204 | self.level = self.level + 1 205 | f() 206 | self.level = self.level - 1 207 | end 208 | 209 | function Inspector:tabify() 210 | self:puts(self.newline, string.rep(self.indent, self.level)) 211 | end 212 | 213 | function Inspector:alreadyVisited(v) 214 | return self.ids[v] ~= nil 215 | end 216 | 217 | function Inspector:getId(v) 218 | local id = self.ids[v] 219 | if not id then 220 | local tv = type(v) 221 | id = (self.maxIds[tv] or 0) + 1 222 | self.maxIds[tv] = id 223 | self.ids[v] = id 224 | end 225 | return tostring(id) 226 | end 227 | 228 | function Inspector:putKey(k) 229 | if isIdentifier(k) then return self:puts(k) end 230 | self:puts("[") 231 | self:putValue(k) 232 | self:puts("]") 233 | end 234 | 235 | function Inspector:putTable(t) 236 | if t == inspect.KEY or t == inspect.METATABLE then 237 | self:puts(tostring(t)) 238 | elseif self:alreadyVisited(t) then 239 | self:puts('') 240 | elseif self.level >= self.depth then 241 | self:puts('{...}') 242 | else 243 | if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end 244 | 245 | local nonSequentialKeys, nonSequentialKeysLength, sequenceLength = getNonSequentialKeys(t) 246 | local mt = getmetatable(t) 247 | 248 | self:puts('{') 249 | self:down(function() 250 | local count = 0 251 | for i=1, sequenceLength do 252 | if count > 0 then self:puts(',') end 253 | self:puts(' ') 254 | self:putValue(t[i]) 255 | count = count + 1 256 | end 257 | 258 | for i=1, nonSequentialKeysLength do 259 | local k = nonSequentialKeys[i] 260 | if count > 0 then self:puts(',') end 261 | self:tabify() 262 | self:putKey(k) 263 | self:puts(' = ') 264 | self:putValue(t[k]) 265 | count = count + 1 266 | end 267 | 268 | if type(mt) == 'table' then 269 | if count > 0 then self:puts(',') end 270 | self:tabify() 271 | self:puts(' = ') 272 | self:putValue(mt) 273 | end 274 | end) 275 | 276 | if nonSequentialKeysLength > 0 or type(mt) == 'table' then -- result is multi-lined. Justify closing } 277 | self:tabify() 278 | elseif sequenceLength > 0 then -- array tables have one extra space before closing } 279 | self:puts(' ') 280 | end 281 | 282 | self:puts('}') 283 | end 284 | end 285 | 286 | function Inspector:putValue(v) 287 | local tv = type(v) 288 | 289 | if tv == 'string' then 290 | self:puts(smartQuote(escape(v))) 291 | elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or 292 | tv == 'cdata' or tv == 'ctype' then 293 | self:puts(tostring(v)) 294 | elseif tv == 'table' then 295 | self:putTable(v) 296 | else 297 | self:puts('<', tv, ' ', self:getId(v), '>') 298 | end 299 | end 300 | 301 | ------------------------------------------------------------------- 302 | 303 | function inspect.inspect(root, options) 304 | options = options or {} 305 | 306 | local depth = options.depth or math.huge 307 | local newline = options.newline or '\n' 308 | local indent = options.indent or ' ' 309 | local process = options.process 310 | 311 | if process then 312 | root = processRecursive(process, root, {}, {}) 313 | end 314 | 315 | local inspector = setmetatable({ 316 | depth = depth, 317 | level = 0, 318 | buffer = {}, 319 | ids = {}, 320 | maxIds = {}, 321 | newline = newline, 322 | indent = indent, 323 | tableAppearances = countTableAppearances(root) 324 | }, Inspector_mt) 325 | 326 | inspector:putValue(root) 327 | 328 | return table.concat(inspector.buffer) 329 | end 330 | 331 | setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end }) 332 | 333 | return inspect 334 | -------------------------------------------------------------------------------- /ItemTester/json.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- json.lua 3 | -- 4 | -- Copyright (c) 2018 rxi 5 | -- 6 | -- Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | -- this software and associated documentation files (the "Software"), to deal in 8 | -- the Software without restriction, including without limitation the rights to 9 | -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | -- of the Software, and to permit persons to whom the Software is furnished to do 11 | -- so, subject to the following conditions: 12 | -- 13 | -- The above copyright notice and this permission notice shall be included in all 14 | -- copies or substantial portions of the Software. 15 | -- 16 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | -- SOFTWARE. 23 | -- 24 | 25 | local json = { _version = "0.1.1" } 26 | 27 | ------------------------------------------------------------------------------- 28 | -- Encode 29 | ------------------------------------------------------------------------------- 30 | 31 | local encode 32 | 33 | local escape_char_map = { 34 | [ "\\" ] = "\\\\", 35 | [ "\"" ] = "\\\"", 36 | [ "\b" ] = "\\b", 37 | [ "\f" ] = "\\f", 38 | [ "\n" ] = "\\n", 39 | [ "\r" ] = "\\r", 40 | [ "\t" ] = "\\t", 41 | } 42 | 43 | local escape_char_map_inv = { [ "\\/" ] = "/" } 44 | for k, v in pairs(escape_char_map) do 45 | escape_char_map_inv[v] = k 46 | end 47 | 48 | 49 | local function escape_char(c) 50 | return escape_char_map[c] or string.format("\\u%04x", c:byte()) 51 | end 52 | 53 | 54 | local function encode_nil(val) 55 | return "null" 56 | end 57 | 58 | 59 | local function encode_table(val, stack) 60 | local res = {} 61 | stack = stack or {} 62 | 63 | -- Circular reference? 64 | if stack[val] then error("circular reference") end 65 | 66 | stack[val] = true 67 | 68 | if val[1] ~= nil or next(val) == nil then 69 | -- Treat as array -- check keys are valid and it is not sparse 70 | local n = 0 71 | for k in pairs(val) do 72 | if type(k) ~= "number" then 73 | error("invalid table: mixed or invalid key types") 74 | end 75 | n = n + 1 76 | end 77 | if n ~= #val then 78 | error("invalid table: sparse array") 79 | end 80 | -- Encode 81 | for i, v in ipairs(val) do 82 | table.insert(res, encode(v, stack)) 83 | end 84 | stack[val] = nil 85 | return "[" .. table.concat(res, ",") .. "]" 86 | 87 | else 88 | -- Treat as an object 89 | for k, v in pairs(val) do 90 | if type(k) ~= "string" then 91 | error("invalid table: mixed or invalid key types") 92 | end 93 | table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) 94 | end 95 | stack[val] = nil 96 | return "{" .. table.concat(res, ",") .. "}" 97 | end 98 | end 99 | 100 | 101 | local function encode_string(val) 102 | return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' 103 | end 104 | 105 | 106 | local function encode_number(val) 107 | -- Check for NaN, -inf and inf 108 | if val ~= val or val <= -math.huge or val >= math.huge then 109 | error("unexpected number value '" .. tostring(val) .. "'") 110 | end 111 | return string.format("%.14g", val) 112 | end 113 | 114 | 115 | local type_func_map = { 116 | [ "nil" ] = encode_nil, 117 | [ "table" ] = encode_table, 118 | [ "string" ] = encode_string, 119 | [ "number" ] = encode_number, 120 | [ "boolean" ] = tostring, 121 | } 122 | 123 | 124 | encode = function(val, stack) 125 | local t = type(val) 126 | local f = type_func_map[t] 127 | if f then 128 | return f(val, stack) 129 | end 130 | error("unexpected type '" .. t .. "'") 131 | end 132 | 133 | 134 | function json.encode(val) 135 | return ( encode(val) ) 136 | end 137 | 138 | 139 | ------------------------------------------------------------------------------- 140 | -- Decode 141 | ------------------------------------------------------------------------------- 142 | 143 | local parse 144 | 145 | local function create_set(...) 146 | local res = {} 147 | for i = 1, select("#", ...) do 148 | res[ select(i, ...) ] = true 149 | end 150 | return res 151 | end 152 | 153 | local space_chars = create_set(" ", "\t", "\r", "\n") 154 | local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") 155 | local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") 156 | local literals = create_set("true", "false", "null") 157 | 158 | local literal_map = { 159 | [ "true" ] = true, 160 | [ "false" ] = false, 161 | [ "null" ] = nil, 162 | } 163 | 164 | 165 | local function next_char(str, idx, set, negate) 166 | for i = idx, #str do 167 | if set[str:sub(i, i)] ~= negate then 168 | return i 169 | end 170 | end 171 | return #str + 1 172 | end 173 | 174 | 175 | local function decode_error(str, idx, msg) 176 | local line_count = 1 177 | local col_count = 1 178 | for i = 1, idx - 1 do 179 | col_count = col_count + 1 180 | if str:sub(i, i) == "\n" then 181 | line_count = line_count + 1 182 | col_count = 1 183 | end 184 | end 185 | error( string.format("%s at line %d col %d", msg, line_count, col_count) ) 186 | end 187 | 188 | 189 | local function codepoint_to_utf8(n) 190 | -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa 191 | local f = math.floor 192 | if n <= 0x7f then 193 | return string.char(n) 194 | elseif n <= 0x7ff then 195 | return string.char(f(n / 64) + 192, n % 64 + 128) 196 | elseif n <= 0xffff then 197 | return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) 198 | elseif n <= 0x10ffff then 199 | return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, 200 | f(n % 4096 / 64) + 128, n % 64 + 128) 201 | end 202 | error( string.format("invalid unicode codepoint '%x'", n) ) 203 | end 204 | 205 | 206 | local function parse_unicode_escape(s) 207 | local n1 = tonumber( s:sub(3, 6), 16 ) 208 | local n2 = tonumber( s:sub(9, 12), 16 ) 209 | -- Surrogate pair? 210 | if n2 then 211 | return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) 212 | else 213 | return codepoint_to_utf8(n1) 214 | end 215 | end 216 | 217 | 218 | local function parse_string(str, i) 219 | local has_unicode_escape = false 220 | local has_surrogate_escape = false 221 | local has_escape = false 222 | local last 223 | for j = i + 1, #str do 224 | local x = str:byte(j) 225 | 226 | if x < 32 then 227 | decode_error(str, j, "control character in string") 228 | end 229 | 230 | if last == 92 then -- "\\" (escape char) 231 | if x == 117 then -- "u" (unicode escape sequence) 232 | local hex = str:sub(j + 1, j + 5) 233 | if not hex:find("%x%x%x%x") then 234 | decode_error(str, j, "invalid unicode escape in string") 235 | end 236 | if hex:find("^[dD][89aAbB]") then 237 | has_surrogate_escape = true 238 | else 239 | has_unicode_escape = true 240 | end 241 | else 242 | local c = string.char(x) 243 | if not escape_chars[c] then 244 | decode_error(str, j, "invalid escape char '" .. c .. "' in string") 245 | end 246 | has_escape = true 247 | end 248 | last = nil 249 | 250 | elseif x == 34 then -- '"' (end of string) 251 | local s = str:sub(i + 1, j - 1) 252 | if has_surrogate_escape then 253 | s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) 254 | end 255 | if has_unicode_escape then 256 | s = s:gsub("\\u....", parse_unicode_escape) 257 | end 258 | if has_escape then 259 | s = s:gsub("\\.", escape_char_map_inv) 260 | end 261 | return s, j + 1 262 | 263 | else 264 | last = x 265 | end 266 | end 267 | decode_error(str, i, "expected closing quote for string") 268 | end 269 | 270 | 271 | local function parse_number(str, i) 272 | local x = next_char(str, i, delim_chars) 273 | local s = str:sub(i, x - 1) 274 | local n = tonumber(s) 275 | if not n then 276 | decode_error(str, i, "invalid number '" .. s .. "'") 277 | end 278 | return n, x 279 | end 280 | 281 | 282 | local function parse_literal(str, i) 283 | local x = next_char(str, i, delim_chars) 284 | local word = str:sub(i, x - 1) 285 | if not literals[word] then 286 | decode_error(str, i, "invalid literal '" .. word .. "'") 287 | end 288 | return literal_map[word], x 289 | end 290 | 291 | 292 | local function parse_array(str, i) 293 | local res = {} 294 | local n = 1 295 | i = i + 1 296 | while 1 do 297 | local x 298 | i = next_char(str, i, space_chars, true) 299 | -- Empty / end of array? 300 | if str:sub(i, i) == "]" then 301 | i = i + 1 302 | break 303 | end 304 | -- Read token 305 | x, i = parse(str, i) 306 | res[n] = x 307 | n = n + 1 308 | -- Next token 309 | i = next_char(str, i, space_chars, true) 310 | local chr = str:sub(i, i) 311 | i = i + 1 312 | if chr == "]" then break end 313 | if chr ~= "," then decode_error(str, i, "expected ']' or ','") end 314 | end 315 | return res, i 316 | end 317 | 318 | 319 | local function parse_object(str, i) 320 | local res = {} 321 | i = i + 1 322 | while 1 do 323 | local key, val 324 | i = next_char(str, i, space_chars, true) 325 | -- Empty / end of object? 326 | if str:sub(i, i) == "}" then 327 | i = i + 1 328 | break 329 | end 330 | -- Read key 331 | if str:sub(i, i) ~= '"' then 332 | decode_error(str, i, "expected string for key") 333 | end 334 | key, i = parse(str, i) 335 | -- Read ':' delimiter 336 | i = next_char(str, i, space_chars, true) 337 | if str:sub(i, i) ~= ":" then 338 | decode_error(str, i, "expected ':' after key") 339 | end 340 | i = next_char(str, i + 1, space_chars, true) 341 | -- Read value 342 | val, i = parse(str, i) 343 | -- Set 344 | res[key] = val 345 | -- Next token 346 | i = next_char(str, i, space_chars, true) 347 | local chr = str:sub(i, i) 348 | i = i + 1 349 | if chr == "}" then break end 350 | if chr ~= "," then decode_error(str, i, "expected '}' or ','") end 351 | end 352 | return res, i 353 | end 354 | 355 | 356 | local char_func_map = { 357 | [ '"' ] = parse_string, 358 | [ "0" ] = parse_number, 359 | [ "1" ] = parse_number, 360 | [ "2" ] = parse_number, 361 | [ "3" ] = parse_number, 362 | [ "4" ] = parse_number, 363 | [ "5" ] = parse_number, 364 | [ "6" ] = parse_number, 365 | [ "7" ] = parse_number, 366 | [ "8" ] = parse_number, 367 | [ "9" ] = parse_number, 368 | [ "-" ] = parse_number, 369 | [ "t" ] = parse_literal, 370 | [ "f" ] = parse_literal, 371 | [ "n" ] = parse_literal, 372 | [ "[" ] = parse_array, 373 | [ "{" ] = parse_object, 374 | } 375 | 376 | 377 | parse = function(str, idx) 378 | local chr = str:sub(idx, idx) 379 | local f = char_func_map[chr] 380 | if f then 381 | return f(str, idx) 382 | end 383 | decode_error(str, idx, "unexpected character '" .. chr .. "'") 384 | end 385 | 386 | 387 | function json.decode(str) 388 | if type(str) ~= "string" then 389 | error("expected argument of type string, got " .. type(str)) 390 | end 391 | local res, idx = parse(str, next_char(str, 1, space_chars, true)) 392 | idx = next_char(str, idx, space_chars, true) 393 | if idx <= #str then 394 | decode_error(str, idx, "trailing garbage") 395 | end 396 | return res 397 | end 398 | 399 | 400 | return json 401 | -------------------------------------------------------------------------------- /ItemTester/mockui.lua: -------------------------------------------------------------------------------- 1 | #@ 2 | -- This wrapper allows the program to run headless on any OS (in theory) 3 | -- It can be run using a standard lua interpreter, although LuaJIT is preferable 4 | 5 | t_insert = table.insert 6 | t_remove = table.remove 7 | m_min = math.min 8 | m_max = math.max 9 | m_floor = math.floor 10 | m_abs = math.abs 11 | s_format = string.format 12 | 13 | -- Callbacks 14 | callbackTable = { } 15 | mainObject = nil 16 | function runCallback(name, ...) 17 | if callbackTable[name] then 18 | return callbackTable[name](...) 19 | elseif mainObject and mainObject[name] then 20 | return mainObject[name](mainObject, ...) 21 | end 22 | end 23 | function SetCallback(name, func) 24 | callbackTable[name] = func 25 | end 26 | function GetCallback(name) 27 | return callbackTable[name] 28 | end 29 | function SetMainObject(obj) 30 | mainObject = obj 31 | end 32 | 33 | -- Image Handles 34 | imageHandleClass = { } 35 | imageHandleClass.__index = imageHandleClass 36 | function NewImageHandle() 37 | return setmetatable({ }, imageHandleClass) 38 | end 39 | function imageHandleClass:Load(fileName, ...) 40 | self.valid = true 41 | end 42 | function imageHandleClass:Unload() 43 | self.valid = false 44 | end 45 | function imageHandleClass:IsValid() 46 | return self.valid 47 | end 48 | function imageHandleClass:SetLoadingPriority(pri) end 49 | function imageHandleClass:ImageSize() 50 | return 1, 1 51 | end 52 | 53 | -- Rendering 54 | function RenderInit() end 55 | function GetScreenSize() 56 | return 1920, 1080 57 | end 58 | function SetClearColor(r, g, b, a) end 59 | function SetDrawLayer(layer, subLayer) end 60 | function SetViewport(x, y, width, height) end 61 | function SetDrawColor(r, g, b, a) end 62 | function DrawImage(imgHandle, left, top, width, height, tcLeft, tcTop, tcRight, tcBottom) end 63 | function DrawImageQuad(imageHandle, x1, y1, x2, y2, x3, y3, x4, y4, s1, t1, s2, t2, s3, t3, s4, t4) end 64 | function DrawString(left, top, align, height, font, text) end 65 | function DrawStringWidth(height, font, text) 66 | return 1 67 | end 68 | function DrawStringCursorIndex(height, font, text, cursorX, cursorY) 69 | return 0 70 | end 71 | function StripEscapes(text) 72 | return text:gsub("^%d",""):gsub("^x%x%x%x%x%x%x","") 73 | end 74 | function GetAsyncCount() 75 | return 0 76 | end 77 | 78 | -- Search Handles 79 | function NewFileSearch() end 80 | 81 | -- General Functions 82 | function SetWindowTitle(title) end 83 | function GetCursorPos() 84 | return 0, 0 85 | end 86 | function SetCursorPos(x, y) end 87 | function ShowCursor(doShow) end 88 | function IsKeyDown(keyName) end 89 | function Copy(text) end 90 | function Paste() end 91 | function Deflate(data) 92 | -- TODO: Might need this 93 | return "" 94 | end 95 | function Inflate(data) 96 | -- TODO: And this 97 | return "" 98 | end 99 | function GetTime() 100 | return 0 101 | end 102 | function GetScriptPath() 103 | return os.getenv('POB_SCRIPTPATH') 104 | end 105 | function GetRuntimePath() 106 | return os.getenv('POB_RUNTIMEPATH') 107 | end 108 | function GetUserPath() 109 | return os.getenv('POB_USERPATH') 110 | end 111 | function MakeDir(path) end 112 | function RemoveDir(path) end 113 | function SetWorkDir(path) end 114 | function GetWorkDir() 115 | return "" 116 | end 117 | function LaunchSubScript(scriptText, funcList, subList, ...) end 118 | 119 | function DownloadPage(self, url, callback, cookies) 120 | -- Download the given page then calls the provided callback function when done: 121 | -- callback(pageText, errMsg) 122 | 123 | ConPrintf("Downloading page at: %s", url) 124 | local curl = require("lcurl.safe") 125 | local page = "" 126 | local easy = curl.easy() 127 | easy:setopt_url(url) 128 | easy:setopt(curl.OPT_ACCEPT_ENCODING, "") 129 | if cookies then 130 | easy:setopt(curl.OPT_COOKIE, cookies) 131 | end 132 | if proxyURL then 133 | easy:setopt(curl.OPT_PROXY, proxyURL) 134 | end 135 | easy:setopt_writefunction(function(data) 136 | page = page..data 137 | return true 138 | end) 139 | local _, error = easy:perform() 140 | local code = easy:getinfo(curl.INFO_RESPONSE_CODE) 141 | easy:close() 142 | local errMsg 143 | if error then 144 | errMsg = error:msg() 145 | elseif code ~= 200 then 146 | errMsg = "Response code: "..code 147 | elseif #page == 0 then 148 | errMsg = "No data returned" 149 | end 150 | ConPrintf("Download complete. Status: %s", errMsg or "OK") 151 | if errMsg then 152 | callback(nil, errMsg) 153 | else 154 | callback(page, nil) 155 | end 156 | end 157 | 158 | function AbortSubScript(ssID) end 159 | function IsSubScriptRunning(ssID) end 160 | function LoadModule(fileName, ...) 161 | if not fileName:match("%.lua") then 162 | fileName = fileName .. ".lua" 163 | end 164 | local func, err = loadfile(fileName) 165 | if func then 166 | return func(...) 167 | else 168 | error("LoadModule() error loading '"..fileName.."': "..err) 169 | end 170 | end 171 | function PLoadModule(fileName, ...) 172 | if not fileName:match("%.lua") then 173 | fileName = fileName .. ".lua" 174 | end 175 | local func, err = loadfile(fileName) 176 | if func then 177 | return PCall(func, ...) 178 | else 179 | error("PLoadModule() error loading '"..fileName.."': "..err) 180 | end 181 | end 182 | function PCall(func, ...) 183 | local ret = { pcall(func, ...) } 184 | if ret[1] then 185 | table.remove(ret, 1) 186 | return nil, unpack(ret) 187 | else 188 | return ret[2] 189 | end 190 | end 191 | function ConPrintf(fmt, ...) 192 | -- Optional 193 | -- print(string.format(fmt, ...)) 194 | end 195 | function ConPrintTable(tbl, noRecurse) end 196 | function ConExecute(cmd) end 197 | function ConClear() end 198 | function SpawnProcess(cmdName, args) end 199 | function OpenURL(url) end 200 | function SetProfiling(isEnabled) end 201 | function Restart() end 202 | function Exit() end 203 | 204 | function isValidString(s, expression) 205 | return s and s:match(expression or '%S') and true or false 206 | end 207 | 208 | l_require = require 209 | function require(name) 210 | return l_require(name) 211 | end 212 | 213 | 214 | dofile("Launch.lua") 215 | 216 | -- Patch some functions 217 | mainObject.DownloadPage = DownloadPage 218 | mainObject.CheckForUpdate = function () end 219 | 220 | runCallback("OnInit") 221 | runCallback("OnFrame") -- Need at least one frame for everything to initialise 222 | 223 | if mainObject.promptMsg then 224 | -- Something went wrong during startup 225 | error("ERROR: "..mainObject.promptMsg) 226 | return 227 | end 228 | 229 | -- The build module; once a build is loaded, you can find all the good stuff in here 230 | build = mainObject.main.modes["BUILD"] 231 | calcs = build.calcsTab.calcs 232 | 233 | -- Here's some helpful helper functions to help you get started 234 | function newBuild() 235 | mainObject.main:SetMode("BUILD", false, "Help, I'm stuck in Path of Building!") 236 | runCallback("OnFrame") 237 | end 238 | function loadBuildFromXML(xmlText) 239 | mainObject.main:SetMode("BUILD", false, "", xmlText) 240 | runCallback("OnFrame") 241 | end 242 | function loadBuildFromJSON(getItemsJSON, getPassiveSkillsJSON) 243 | mainObject.main:SetMode("BUILD", false, "") 244 | runCallback("OnFrame") 245 | local charData = build.importTab:ImportItemsAndSkills(getItemsJSON) 246 | build.importTab:ImportPassiveTreeAndJewels(getPassiveSkillsJSON, charData) 247 | -- You now have a build without a correct main skill selected, or any configuration options set 248 | -- Good luck! 249 | end 250 | 251 | function saveBuildToXml() 252 | local xmlText = build:SaveDB("dummy") 253 | if not xmlText then 254 | print("ERROR: Failed to prepare save XML") 255 | os.exit(1) 256 | end 257 | return xmlText 258 | end 259 | 260 | function saveText(filename, text) 261 | local file = io.open(filename, "w+") 262 | if not file then 263 | print("ERROR: Failed to write to output file") 264 | os.exit(1) 265 | end 266 | file:write(text) 267 | file:close() 268 | end 269 | 270 | function loadText(fileName) 271 | local fileHnd, errMsg = io.open(fileName, "r") 272 | if not fileHnd then 273 | print("ERROR: Failed to load file: "..fileName) 274 | os.exit(1) 275 | -- return nil, errMsg 276 | end 277 | local fileText = fileHnd:read("*a") 278 | fileHnd:close() 279 | return fileText 280 | end 281 | 282 | function loadTextLines(fileName) 283 | local fileHnd, errMsg = io.open(fileName, "r") 284 | if not fileHnd then 285 | print("ERROR: Failed to load file: "..fileName) 286 | os.exit(1) 287 | -- return nil, errMsg 288 | end 289 | local output = {} 290 | for line in fileHnd:lines() do 291 | output[#output + 1] = line 292 | end 293 | fileHnd:close() 294 | return output 295 | end 296 | 297 | 298 | FakeTooltip = { 299 | lines = {} 300 | } 301 | 302 | function FakeTooltip:new() 303 | o = {} 304 | setmetatable(o, self) 305 | self.__index = self 306 | return o 307 | end 308 | 309 | function FakeTooltip:AddLine(_, txt) 310 | local html = lineToHtml(txt) 311 | table.insert(self.lines, "

"..html.."

") 312 | end 313 | 314 | function FakeTooltip:AddSeparator(_, txt) 315 | -- Make sure we don't get two in a row 316 | if self.lines[#self.lines] ~= "
" then 317 | table.insert(self.lines, "
") 318 | end 319 | end 320 | 321 | function lineToHtml(txt) 322 | if txt == nil then return '' end 323 | return txt:gsub("^%^7", ""):gsub("%^x(......)", ""):gsub("%^7", ""):gsub("%^8", ""):gsub("%^1", "") 324 | end 325 | 326 | inspect = require('inspect') 327 | function _i(v, depth) 328 | if depth == nil then depth = 1 end 329 | print(inspect(v, {depth=depth})) 330 | end 331 | -------------------------------------------------------------------------------- /ItemTester/mods.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"name": "aspellsg", "desc": "+1 to Level of all Spell Skill Gems", "count": 1, "version": 8}, 3 | {"name": "asg", "desc": "+1 to Level of all Skill Gems", "count": 1}, 4 | {"name": "adexsg", "desc": "+1 to Level of all Dexterity Skill Gems", "count": 1}, 5 | {"name": "astrsg", "desc": "+1 to Level of all Strength Skill Gems", "count": 1}, 6 | {"name": "aintsg", "desc": "+1 to Level of all Intelligence Skill Gems", "count": 1}, 7 | {"name": "aminionsg", "desc": "+1 to Level of all Minion Skill Gems", "count": 1}, 8 | {"name": "achaossg", "desc": "+1 to Level of all Chaos Skill Gems", "count": 1}, 9 | {"name": "acoldsg", "desc": "+1 to Level of all Cold Skill Gems", "count": 1}, 10 | {"name": "afiresg", "desc": "+1 to Level of all Fire Skill Gems", "count": 1}, 11 | {"name": "alightningsg", "desc": "+1 to Level of all Lightning Skill Gems", "count": 1}, 12 | {"name": "aphysicalsg", "desc": "+1 to Level of all Physical Skill Gems", "count": 1}, 13 | {"name": "avaalsg", "desc": "+1 to Level of all Vaal Skill Gems", "count": 1}, 14 | {"name": "perfectcull", "desc": "Culling Strike", "count": 1}, 15 | {"name": "actionspeed", "desc": "4% increased Action Speed", "count": 4}, 16 | {"name": "pphysical", "desc": "12% increased Physical Damage", "count": 12}, 17 | {"name": "plightning", "desc": "12% increased Lightning Damage", "count": 12}, 18 | {"name": "pcold", "desc": "12% increased Cold Damage", "count": 12}, 19 | {"name": "pfire", "desc": "12% increased Fire Damage", "count": 12}, 20 | {"name": "pelemental", "desc": "12% increased Elemental Damage", "count": 12}, 21 | {"name": "pchaos", "desc": "12% increased Chaos Damage", "count": 12}, 22 | {"name": "pgeneric", "desc": "12% increased Damage", "count": 12}, 23 | {"name": "pspell", "desc": "12% increased Spell Damage", "count": 12}, 24 | {"name": "pattack", "desc": "12% increased Attack Damage", "count": 12}, 25 | {"name": "pmelee", "desc": "12% increased Melee Damage", "count": 12}, 26 | {"name": "flataccuracy", "desc": "+100 to Accuracy Rating", "count": 100}, 27 | {"name": "accuracypf", "desc": "+40 to Accuracy Rating per Frenzy Charge", "count": 40}, 28 | {"name": "paccuracy", "desc": "10% increased Global Accuracy Rating", "count": 10}, 29 | {"name": "critchance", "desc": "16% increased Global Critical Strike Chance", "count": 16}, 30 | {"name": "critmulti", "desc": "+16% to Global Critical Strike Multiplier", "count": 16}, 31 | {"name": "basecrit", "desc": "+1% Critical Strike Chance", "count": 1}, 32 | {"name": "attackspeed", "desc": "10% increased Attack Speed", "count": 10}, 33 | {"name": "castspeed", "desc": "10% increased Cast Speed", "count": 10}, 34 | {"name": "penall", "desc": "Damage Penetrates 8% Elemental Resistances", "count": 8}, 35 | {"name": "penlightning", "desc": "Damage Penetrates 8% Lightning Resistance", "count": 8}, 36 | {"name": "pencold", "desc": "Damage Penetrates 8% Cold Resistance", "count": 8}, 37 | {"name": "penfire", "desc": "Damage Penetrates 8% Fire Resistance", "count": 8}, 38 | {"name": "opdr", "desc": "Overwhelm 10% Physical Damage Reduction", "count": 10}, 39 | {"name": "flatphys", "desc": "Adds 5 to 7 Physical Damage", "count": 6}, 40 | {"name": "flatlightning", "desc": "Adds 6 to 68 Lightning Damage", "count": 37}, 41 | {"name": "flatcold", "desc": "Adds 14 to 31 Cold Damage", "count": 22.5}, 42 | {"name": "flatfire", "desc": "Adds 21 to 34 Fire Damage", "count": 27.5}, 43 | {"name": "flatchaos", "desc": "Adds 4 to 7 Chaos Damage", "count": 5.5}, 44 | {"name": "chancedoubledamage", "desc": "1% chance to deal Double Damage", "count": 1}, 45 | {"name": "extralightning", "desc": "15% of Physical Damage as Extra Lightning Damage", "count": 15}, 46 | {"name": "extracold", "desc": "15% of Physical Damage as Extra Cold Damage", "count": 15}, 47 | {"name": "extrafire", "desc": "15% of Physical Damage as Extra Fire Damage", "count": 15}, 48 | {"name": "extrachaos", "desc": "Gain 15% of Non-Chaos damage as Extra Chaos Damage", "count": 15}, 49 | {"name": "eleaschaos", "desc": "Gain 15% of Elemental Damage as Extra Chaos Damage", "count": 15}, 50 | {"name": "physicalasextrachaos", "desc": "15% of Physical Damage as Extra Chaos Damage", "count": 15}, 51 | {"name": "lightningasextrachaos", "desc": "15% of Lightning Damage as Extra Chaos Damage", "count": 15}, 52 | {"name": "coldasextrachaos", "desc": "15% of Cold Damage as Extra Chaos Damage", "count": 15}, 53 | {"name": "fireasextrachaos", "desc": "15% of Fire Damage as Extra Chaos Damage", "count": 15}, 54 | {"name": "1powercharge", "desc": "+1 to Maximum Power Charges", "count": 1}, 55 | {"name": "1frenzycharge", "desc": "+1 to Maximum Frenzy Charges", "count": 1}, 56 | {"name": "1endurancecharge", "desc": "+1 to Maximum Endurance Charges", "count": 1}, 57 | {"name": "20dex", "desc": "+20 to Dexterity", "count": 20}, 58 | {"name": "20int", "desc": "+20 to Intelligence", "count": 20}, 59 | {"name": "20str", "desc": "+20 to Strength", "count": 20}, 60 | {"name": "damageperdex", "desc": "1% increased Damage per 15 Dexterity", "count": 1}, 61 | {"name": "damageperint", "desc": "1% increased Damage per 15 Intelligence", "count": 1}, 62 | {"name": "damageperstr", "desc": "1% increased Damage per 15 Strength", "count": 1}, 63 | {"name": "pdex", "desc": "10% increased Dexterity", "count": 10}, 64 | {"name": "pint", "desc": "10% increased Intelligence", "count": 10}, 65 | {"name": "pstr", "desc": "10% increased Strength", "count": 10}, 66 | {"name": "ptotemlife", "desc": "7% increased Totem Life", "count": 7}, 67 | {"name": "plife", "desc": "7% increased maximum Life", "count": 7}, 68 | {"name": "flatlife", "desc": "+40 to maximum Life", "count": 40}, 69 | {"name": "pes", "desc": "7% increased maximum Energy Shield", "count": 7}, 70 | {"name": "flates", "desc": "+40 to maximum Energy Shield", "count": 40}, 71 | {"name": "pmana", "desc": "7% increased maximum Mana", "count": 7}, 72 | {"name": "flatmana", "desc": "+40 to maximum Mana", "count": 40}, 73 | {"name": "pmanaskillreduce", "desc": "7% reduced Mana Cost of Skills", "count": 7}, 74 | {"name": "nerchaos", "desc": "Nearby Enemies have -8% to Chaos Resistance", "count": 8}, 75 | {"name": "nercold", "desc": "Nearby Enemies have -8% to Cold Resistance", "count": 8}, 76 | {"name": "nerfire", "desc": "Nearby Enemies have -8% to Fire Resistance", "count": 8}, 77 | {"name": "nerlightning", "desc": "Nearby Enemies have -8% to Lightning Resistance", "count": 8}, 78 | {"name": "neiele", "desc": "Nearby Enemies take 8% increased Elemental Damage", "count": 8}, 79 | {"name": "neiphys", "desc": "Nearby Enemies take 8% increased Physical Damage", "count": 8}, 80 | {"name": "minionchancedoubledamage", "desc": "Minions have 1% chance to deal Double Damage", "count": 1}, 81 | {"name": "pminion", "desc": "Minions deal 12% increased Damage", "count": 12}, 82 | {"name": "minioncriticalstrike", "desc": "Minions have 30% increased Critical Strike Chance", "count": 30}, 83 | {"name": "minionattackspeed", "desc": "Minions have 10% increased Attack Speed", "count": 10}, 84 | {"name": "minioncastspeed", "desc": "Minions have 10% increased Cast Speed", "count": 10}, 85 | {"name": "minionflatphys", "desc": "Minions deal 5 to 7 additional Physical Damage", "count": 6}, 86 | {"name": "minionflatlightning", "desc": "Minions deal 6 to 68 additional Lightning Damage", "count": 37}, 87 | {"name": "minionflatcold", "desc": "Minions deal 14 to 31 additional Cold Damage", "count": 22.5}, 88 | {"name": "minionflatfire", "desc": "Minions deal 21 to 34 additional Fire Damage", "count": 27.5}, 89 | {"name": "minionflatchaos", "desc": "Minions deal 4 to 7 additional Chaos Damage", "count": 5.5}, 90 | {"name": "minionflataccuracy", "desc": "Minions have +100 to Accuracy Rating", "count": 100}, 91 | {"name": "minionpaccuracy", "desc": "10% increased Minion Accuracy Rating", "count": 10}, 92 | {"name": "pdot", "desc": "12% increased Damage over Time", "count": 12}, 93 | {"name": "pchaosdot", "desc": "12% increased Chaos Damage over Time", "count": 12}, 94 | {"name": "pphysdot", "desc": "12% increased Physical Damage over Time", "count": 12}, 95 | {"name": "pdotailment", "desc": "12% increased Damage with Ailments", "count": 12}, 96 | {"name": "pbleed", "desc": "12% increased Damage with Bleeding", "count": 12}, 97 | {"name": "ppoison", "desc": "12% increased Damage with Poison", "count": 12}, 98 | {"name": "pignite", "desc": "12% increased Burning Damage", "count": 12}, 99 | {"name": "pdotmulti", "desc": "+12% to Damage over Time Multiplier", "count": 12}, 100 | {"name": "physdotmulti", "desc": "+12% to Physical Damage over Time Multiplier", "count": 12}, 101 | {"name": "pfiredotmulti", "desc": "+12% to Fire Damage over Time Multiplier", "count": 12}, 102 | {"name": "pcolddotmulti", "desc": "+12% to Cold Damage over Time Multiplier", "count": 12}, 103 | {"name": "pchaosdotmulti", "desc": "+12% to Chaos Damage over Time Multiplier", "count": 12}, 104 | {"name": "fasterignite", "desc": "Ignites you inflict deal Damage 10% faster", "count": 10}, 105 | {"name": "fasterbleed", "desc": "Bleeding you inflict deal Damage 10% faster", "count": 10}, 106 | {"name": "fasterpoison", "desc": "Poisons you inflict deal Damage 10% faster", "count": 10}, 107 | {"name": "fasterdot", "desc": "Damaging Ailments deal damage 10% faster", "count": 10}, 108 | {"name": "longerdot", "desc": "10% increased Duration of Ailments on Enemies", "count": 10}, 109 | {"name": "iaeanger", "desc": "Anger has 34% increased Aura Effect", "count": 34}, 110 | {"name": "iaedeterminaton", "desc": "Determination has 34% increased Aura Effect", "count": 34}, 111 | {"name": "iaediscipline", "desc": "Discipline has 34% increased Aura Effect", "count": 34}, 112 | {"name": "iaegrace", "desc": "Grace has 34% increased Aura Effect", "count": 34}, 113 | {"name": "iaehaste", "desc": "Haste has 34% increased Aura Effect", "count": 34}, 114 | {"name": "iaehatred", "desc": "Hatred has 34% increased Aura Effect", "count": 34}, 115 | {"name": "iaemalevolence", "desc": "Malevolence has 34% increased Aura Effect", "count": 34}, 116 | {"name": "iaepride", "desc": "Pride has 34% increased Aura Effect", "count": 34}, 117 | {"name": "iaewrath", "desc": "Wrath has 34% increased Aura Effect", "count": 34}, 118 | {"name": "iaezealotry", "desc": "Zealotry has 34% increased Aura Effect", "count": 34}, 119 | {"name": "iaeenemy", "desc": "20% increased Effect of Non-Curse Auras from your Skills on Enemies", "count": 20}, 120 | {"name": "iaenc", "desc": "20% increased effect of Non-Curse Auras from your Skills", "count": 20}, 121 | {"name": "pdpas", "desc": "Auras from your Skills grant 4% increased Damage to you and Allies", "count": 4}, 122 | {"name": "iearcanesurge", "desc": "6% increased Effect of Arcane Surge on you", "count": 6}, 123 | {"name": "golemcarrion", "desc": "35% increased Effect of the Buff granted by your Carrion Golems", "count": 35}, 124 | {"name": "golemflame", "desc": "35% increased Effect of the Buff granted by your Flame Golems", "count": 35}, 125 | {"name": "golemice", "desc": "35% increased Effect of the Buff granted by your Ice Golems", "count": 35}, 126 | {"name": "golemlightning", "desc": "35% increased Effect of the Buff granted by your Lightning Golems", "count": 35}, 127 | {"name": "offeringbone", "desc": "Bone Offering has 6% increased Effect", "count": 6}, 128 | {"name": "offeringflesh", "desc": "Flesh Offering has 6% increased Effect", "count": 6}, 129 | {"name": "offeringspirit", "desc": "Spirit Offering has 6% increased Effect", "count": 6} 130 | ] 131 | -------------------------------------------------------------------------------- /ItemTester/pobinterface.lua: -------------------------------------------------------------------------------- 1 | --[==[ 2 | Methods that delve into the core of PoB and do useful things. 3 | 4 | This file is intended to be common across multiple projects. 5 | ]==]-- 6 | 7 | local pobinterface = {} 8 | 9 | if GlobalCache then GlobalCache.useFullDPS = true end 10 | 11 | function pobinterface.loadBuild(path) 12 | local buildXml = loadText(path) 13 | loadBuildFromXML(buildXml) 14 | build.buildName = getFilename(path) 15 | build.dbFileName = path 16 | build.dbFileSubPath = '' 17 | end 18 | 19 | 20 | function pobinterface.saveBuild() 21 | if not build.dbFileName then 22 | error("Unable to save - no build path set") 23 | end 24 | 25 | build.actionOnSave = nil -- Avoid post-save actions like app update, exit 26 | build:SaveDBFile() 27 | end 28 | 29 | 30 | function pobinterface.saveBuildAs(path) 31 | local saveXml = saveBuildToXml() 32 | saveText(path, saveXml) 33 | end 34 | 35 | 36 | function pobinterface.readSkillSelection() 37 | local pickedGroupIndex = build.mainSocketGroup 38 | local socketGroup = build.skillsTab.socketGroupList[pickedGroupIndex] 39 | if socketGroup == nil then 40 | error("ERROR: Do you have a skill selected? If so, you might need to update PoB using a recently downloaded installer.") 41 | end 42 | local pickedGroupName = socketGroup and socketGroup.displayLabel 43 | local pickedActiveSkillIndex = socketGroup and socketGroup.mainActiveSkill 44 | local displaySkill = socketGroup and socketGroup.displaySkillList[pickedActiveSkillIndex] 45 | local activeEffect = displaySkill and displaySkill.activeEffect 46 | local pickedActiveSkillName = activeEffect and activeEffect.grantedEffect.name 47 | local pickedPartIndex = activeEffect and activeEffect.grantedEffect.parts and activeEffect.srcInstance.skillPart 48 | local pickedPartName = activeEffect and activeEffect.grantedEffect.parts and activeEffect.grantedEffect.parts[pickedPartIndex].name 49 | 50 | return { 51 | group = pickedGroupName, 52 | name = pickedActiveSkillName, 53 | part = pickedPartName, 54 | } 55 | end 56 | 57 | 58 | function getFilename(path) 59 | local start, finish = path:find('[%w%s!-={-|]+[_%.].+') 60 | local name = path:sub(start,#path) 61 | if name:sub(-4) == '.xml' then 62 | name = name:sub(0, -5) 63 | end 64 | return name 65 | end 66 | 67 | 68 | function pobinterface.skillString(skill) 69 | local skill = skill or pobinterface.readSkillSelection() 70 | return ""..(skill.group or '-').." / "..(skill.name or '-').." / "..(skill.part or '-') 71 | end 72 | 73 | 74 | function pobinterface.updateBuild() 75 | -- Update a build from the PoE website automatically, ensuring skills are restored after import 76 | print("Pre-update checks...") 77 | 78 | -- Check importing is configured correctly in the build 79 | if not isValidString(build.importTab.lastAccountHash) or not isValidString(build.importTab.lastCharacterHash) then 80 | error("Update failed: Character must be imported in PoB before it can be automatically updated") 81 | end 82 | 83 | -- Check importing is configured correctly in PoB itself 84 | if not isValidString(build.importTab.controls.accountName.buf) then 85 | error("Update failed: Account name must be set within PoB before it can be automatically updated") 86 | end 87 | 88 | -- Check importer is in the right state 89 | if build.importTab.charImportMode ~= "GETACCOUNTNAME" then 90 | error("Update failed: Unknown import error - is PoB importing set up correctly?") 91 | end 92 | 93 | -- Check account name in the input box actually matches the one configured in the build 94 | if common.sha1(build.importTab.controls.accountName.buf) ~= build.importTab.lastAccountHash then 95 | error("Update failed: Build comes from an account that is not configired in PoB - character must be imported in PoB before it can be automatically updated") 96 | end 97 | 98 | -- Get character list 99 | print("Looking for matching character...") 100 | build.importTab:DownloadCharacterList() 101 | 102 | -- Get the character PoB selected and check it actually matches the last import hash 103 | local char = build.importTab.controls.charSelect.list[build.importTab.controls.charSelect.selIndex] 104 | print("Character selected: "..char.char.name) 105 | if common.sha1(char.char.name) ~= build.importTab.lastCharacterHash then 106 | error("Update failed: Selected character not found - was it deleted or renamed?") 107 | end 108 | 109 | -- Check importer is in the right state 110 | if build.importTab.charImportMode ~= "SELECTCHAR" then 111 | error("Update failed: Import not fully set up on this build") 112 | end 113 | 114 | -- Import tree and jewels 115 | print("Downloading passive tree...") 116 | build.importTab.controls.charImportTreeClearJewels.state = true 117 | build.importTab:DownloadPassiveTree() 118 | 119 | -- Check importer is in the right state 120 | if build.importTab.charImportMode ~= "SELECTCHAR" then 121 | error("Update failed: Unable to download the passive tree") 122 | end 123 | 124 | -- Import items and skills 125 | print("Downloading items and skills...") 126 | build.importTab.controls.charImportItemsClearItems.state = true 127 | build.importTab.controls.charImportItemsClearSkills.state = true 128 | build.importTab:DownloadItems() 129 | 130 | -- Check importer is in the right state 131 | if build.importTab.charImportMode ~= "SELECTCHAR" then 132 | error("Update failed: Unable to download items and skills") 133 | end 134 | 135 | -- Update skills 136 | print("Completing update...") 137 | build.outputRevision = build.outputRevision + 1 138 | build.buildFlag = false 139 | build.calcsTab:BuildOutput() 140 | build:RefreshStatList() 141 | build:RefreshSkillSelectControls(build.controls, build.mainSocketGroup, "") 142 | 143 | end 144 | 145 | 146 | function pobinterface.selectSkill(prevSkill) 147 | local newSkill = pobinterface.readSkillSelection() 148 | 149 | local newGroupIndex = build.mainSocketGroup 150 | socketGroup = build.skillsTab.socketGroupList[newGroupIndex] 151 | if socketGroup == nil then 152 | error("ERROR: Either no skill selected or PoB must be reinstalled with a recent installer") 153 | end 154 | local newGroupName = socketGroup.displayLabel 155 | 156 | if newGroupName ~= prevSkill.group then 157 | print("Socket group name '"..(newSkill.group).."' doesn't match... fixing") 158 | for i,grp in pairs(build.skillsTab.socketGroupList) do 159 | if grp.displayLabel == prevSkill.group then 160 | build.mainSocketGroup = i 161 | newGroupIndex = i 162 | socketGroup = build.skillsTab.socketGroupList[newGroupIndex] 163 | newGroupName = socketGroup.displayLabel 164 | break 165 | end 166 | end 167 | if newGroupName ~= prevSkill.group then 168 | error("Unable to update safely: Previous socket group not found (was '"..prevSkill.group.."')") 169 | end 170 | end 171 | 172 | local newActiveSkillIndex = socketGroup.mainActiveSkill 173 | local displaySkill = socketGroup.displaySkillList[newActiveSkillIndex] 174 | local activeEffect = displaySkill and displaySkill.activeEffect 175 | local newActiveSkillName = activeEffect and activeEffect.grantedEffect.name 176 | 177 | if newActiveSkillName ~= prevSkill.name then 178 | print("Active skill '"..(newSkill.name).."' doesn't match... fixing") 179 | for i,skill in pairs(socketGroup.displaySkillList) do 180 | if skill.activeEffect.grantedEffect.name == prevSkill.name then 181 | socketGroup.mainActiveSkill = i 182 | newActiveSkillIndex = i 183 | displaySkill = socketGroup.displaySkillList[newActiveSkillIndex] 184 | activeEffect = displaySkill.activeEffect 185 | newActiveSkillName = activeEffect.grantedEffect.name 186 | break 187 | end 188 | end 189 | if newGroupName ~= prevSkill.group then 190 | error("Unable to update safely: Previous active skill not found (was '"..prevSkill.name.."')") 191 | end 192 | end 193 | 194 | local newPartIndex = activeEffect.grantedEffect.parts and activeEffect.srcInstance.skillPart 195 | local newPartName = activeEffect.grantedEffect.parts and activeEffect.grantedEffect.parts[newPartIndex].name 196 | 197 | if pickedPartIndex and newPartName ~= prevSkill.part then 198 | print("Active sub-skill '"..(newSkill.part).."' doesn't match... fixing") 199 | for i,part in pairs(activeEffect.grantedEffect.parts) do 200 | if part.name == prevSkill.part then 201 | activeEffect.srcInstance.skillPart = i 202 | newPartIndex = i 203 | newPartName = part.name 204 | break 205 | end 206 | end 207 | if newPartName ~= prevSkill.part then 208 | error("Unable to update safely: Previous active skill-part not found (was '"..prevSkill.part.."')") 209 | end 210 | end 211 | end 212 | 213 | 214 | function pobinterface.findModEffect(modLine) 215 | -- Construct an empty passive socket node to test in 216 | local testNode = {id="temporary-test-node", type="Socket", alloc=false, sd={"Temp Test Socket"}, modList={}} 217 | 218 | -- Construct jewel with the mod just to use its mods in the passive node 219 | local itemText = "Test Jewel\nCobalt Jewel\n"..modLine 220 | local item 221 | if build.targetVersionData then -- handle code format changes in 1.4.170.17 222 | item = new("Item", build.targetVersion, itemText) 223 | else 224 | item = new("Item", itemText) 225 | end 226 | testNode.modList = item.modList 227 | 228 | -- Calculate stat differences 229 | local calcFunc, baseStats = build.calcsTab:GetMiscCalculator() 230 | local newStats = calcFunc({ addNodes={ [testNode]=true } }) 231 | 232 | return {base=baseStats, new=newStats} 233 | end 234 | 235 | 236 | return pobinterface 237 | -------------------------------------------------------------------------------- /ItemTester/testercore.lua: -------------------------------------------------------------------------------- 1 | --[==[ 2 | Shared methods used by multiple scripts within PoB Item Tester. 3 | ]==]-- 4 | 5 | local pobinterface = require('pobinterface') 6 | 7 | local testercore = {} 8 | 9 | 10 | function testercore.loadBuild(path) 11 | if path == "CURRENT" then 12 | -- Just check already-loaded build is viable 13 | if not build.buildName or not build.dbFileName then 14 | print("ERROR: Path of Building has no 'current' build selected!") 15 | print("PoB must be closed while the desired build is loaded for the 'current' option to work.") 16 | os.exit(1) 17 | end 18 | print("Using PoB's last loaded build") 19 | else 20 | pobinterface.loadBuild(path) 21 | end 22 | 23 | print("Path: "..build.dbFileName) 24 | print("Build: "..build.buildName) 25 | end 26 | 27 | 28 | function testercore.saveBuild() 29 | print("Saving: "..build.dbFileName) 30 | pobinterface.saveBuild() 31 | end 32 | 33 | 34 | function testercore.showSkills() 35 | local skill = pobinterface.readSkillSelection() 36 | print("Skill: "..pobinterface.skillString(skill)) 37 | end 38 | 39 | 40 | return testercore 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 VolatilePulse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ```diff 2 | ! Warning: OpenArl's Path of Building will no longer be supported due to difficulties supporting multiple file locations and lack of updates/community interest. 3 | ``` 4 | 5 | # Path of Building Item Tester ![Discord Banner 3](https://discordapp.com/api/guilds/520831275393613824/widget.png?style=shield) 6 | AHK and Lua scripts to automate comparing PoE items from in game or trade sites against your current build with the power of PoB - without needing to run it. 7 | 8 | #### Features: 9 | * Quickly see the impact of items from trade sites without launching PoB 10 | * Update your build from pathofexile.com in PoB with a single keypress 11 | * Generate a weighted DPS trade search to help find the strongest items for your build (utilising a 3rd-party website, see below) 12 | 13 | Simply run `TestItem.ahk` to get started. 14 | 15 | #### Requirements 16 | * [Autohotkey v1.1](https://www.autohotkey.com/) is required. Do not install v1.0 or v2. 17 | * [[Path of Building (Community Fork)](https://github.com/PathOfBuildingCommunity/PathOfBuilding) is required (supports both the portable and installer versions). 18 | * The original OpenArl version of Path of Building my still work but is no longer supported. Time to move on! 19 | 20 | **Note: There have been some issues reported if both the original PoB *and* the community fork are installed - uninstalling BOTH then reinstalling just the community fork should resolve it. Using portable versions eliminates this problem.** 21 | 22 | ## Item Testing 23 | With the AHK script running... 24 | * Copy the item to the clipboard. Use the official trade site's Copy button, or ingame simply press Ctrl-C over the item. 25 | * Press Ctrl-Windows-C. 26 | * Alternatively, press Ctrl-Windows-Alt-C to launch the build picker before performing the test. 27 | * A pop-up will show the item preview from inside Path of Building, including showing how your stats will be affected. 28 | 29 | Testing items on the official trade site: 30 | ![Screenshot of the item tester in action](https://raw.githubusercontent.com/VolatilePulse/PoB-Item-Tester/master/imgs/sshot-tester.png) 31 | 32 | ## Build Update 33 | With the AHK script running... 34 | * Press Ctrl-Windows-U and wait a moment. 35 | * Alteratively, invoking the build picker during item testing (with Ctrl-Windows-Alt-C) will allow you to select "Update build before continuing". 36 | * The script will re-import your build from pathofexile.com, using the existing import settings in Path of Building. 37 | * **Beware! This will overwrite local changes to your build.** 38 | 39 | ## DPS Search 40 | With the AHK script running... 41 | * Press Ctrl-Windows-D. 42 | * Alternatively, press Ctrl-Windows-Alt-D to launch the build picker before performing the test. 43 | * A browser will open `https://xanthics.github.io/PoE_Weighted_Search/`, including the results of various mod tests. The name of your build and current skill are also included only so you can verify the test was performed on the right skill. 44 | * Check the flags located further down the page and alter if desired. The script makes a guess from your skills and config but it's unlikely to get it 100% right. 45 | * Press the Generate button and a link to the official trade site will appear. 46 | * Sometimes this link will have to be opened twice due to an issue on the official trade site. 47 | 48 | Note: This webservice is created and maintained by [Xanthics](https://github.com/xanthics). See [its repository](https://github.com/xanthics/PoE_Weighted_Search) for more details. 49 | Without this service this functionality would not be possible. 50 | 51 | A generated DPS search: 52 | ![Screenshot of the DPS search result](https://raw.githubusercontent.com/VolatilePulse/PoB-Item-Tester/master/imgs/sshot-dps.png) 53 | 54 | ## Thanks 55 | This tool could not exist without: 56 | * The amazing Path of Building and the fresh life [@LocalIdentity](https://github.com/LocalIdentity) has given it 57 | * PoE_Weighted_Search and the continued support of [@xanthics](https://github.com/xanthics) 58 | * [@Nightblade](https://github.com/Nightblade) for AHK improvements 59 | * ... and of course Grinding Gear Games! 60 | -------------------------------------------------------------------------------- /TestItem.ahk: -------------------------------------------------------------------------------- 1 | #NoEnv 2 | #persistent 3 | #SingleInstance, force 4 | 5 | ;#Warn 6 | 7 | ;-------------------------------------------------- 8 | ; Initialization 9 | ;-------------------------------------------------- 10 | 11 | global _LuaDir := "\ItemTester" 12 | 13 | global _IniFile := A_ScriptDir "\TestItem.ini" 14 | global _LuaJIT := A_ScriptDir "\bin\luajit.exe" 15 | 16 | global _GUIOK := false 17 | global _PoBPath := "", _BuildDir = "", _CharacterFileName = "", _PoBInstall = "" 18 | 19 | ; Info Tooltip variables 20 | global _Info, _InfoHwnd, _InfoText 21 | ; Character Picker GUI variables 22 | global _CP, _CPHwnd, _CPCurrent, _CPDir, _CPTV, _CPUpdate, _CPChange, _CPOK 23 | ; Item Viewer variables 24 | global _Item, _ItemHwnd, _ItemText 25 | 26 | global MODSURL := "https://raw.githubusercontent.com/VolatilePulse/PoB-Item-Tester/master/ItemTester/mods.json" 27 | 28 | DetectHiddenWindows, On 29 | 30 | CreateGUI() 31 | SetVariablesAndFiles(_LuaDir, _PobInstall, _PoBPath, _BuildDir, _CharacterFileName) 32 | InsertTrayMenuItems() 33 | 34 | UpdateModData() 35 | 36 | Display("PoB Item Tester Loaded!") 37 | 38 | ; Register the function to call on script exit 39 | OnExit("ExitFunc") 40 | return 41 | 42 | ;-------------------------------------------------- 43 | ; Global Hooks 44 | ;-------------------------------------------------- 45 | 46 | CPCurrentCheck: 47 | GuiControlGet, isChecked, , _CPCurrent 48 | if (isChecked) { 49 | GuiControl, _CP: +Disabled, _CPTV 50 | GuiControl, _CP: -Disabled +Default, _CPOK 51 | } 52 | else { 53 | GuiControl, _CP: -Disabled, _CPTV 54 | if (TV_GetChild(TV_GetSelection())) 55 | GuiControl, _CP: -Default +Disabled, _CPOK 56 | } 57 | return 58 | 59 | CPTV: 60 | if ((A_GuiEvent != "S") || (!TV_GetText(_, A_EventInfo))) 61 | return 62 | 63 | ; A character file has been selected 64 | if (!TV_GetChild(A_EventInfo)) 65 | GuiControl, _CP: +Default -Disabled, _CPOK 66 | else 67 | GuiControl, _CP: -Default +Disabled, _CPOK 68 | return 69 | 70 | ChangeDir: 71 | CreateTV(GetBuildDir(_PoBPath, _BuildDir)) 72 | return 73 | 74 | Ok: 75 | _GUIOK := true 76 | Gui, Submit 77 | return 78 | 79 | ; Re-import build (update) 80 | TMenu_UpdateCharacterBuild: 81 | +^u:: 82 | UpdateCharacterBuild((_CharacterFileName == "CURRENT") ? "CURRENT" : _BuildDir "\" _CharacterFileName) 83 | return 84 | 85 | ; Test item from clipboard 86 | TMenu_TestItemFromClipboard: 87 | ^#c:: 88 | Item := GetItemFromClipboard() 89 | if (Item) 90 | TestItemFromClipboard(Item, (_CharacterFileName == "CURRENT") ? "CURRENT" : _BuildDir "\" _CharacterFileName) 91 | return 92 | 93 | ; Test item from clipboard with character picker 94 | TMenuWithPicker_TestItemFromClipboard: 95 | ^#!c:: 96 | Item := GetItemFromClipboard() 97 | if (Item) { 98 | filename := DisplayCharacterPicker() 99 | if (filename) 100 | TestItemFromClipboard(Item, filename) 101 | } 102 | return 103 | 104 | ; Generate DPS search 105 | TMenu_GenerateDPSSearch: 106 | ^#d:: 107 | GenerateDPSSearch((_CharacterFileName == "CURRENT") ? "CURRENT" : _BuildDir "\" _CharacterFileName) 108 | return 109 | 110 | ; Generate DPS search with character picker 111 | TMenuWithPicker_GenerateDPSSearch: 112 | ^#!d:: 113 | filename := DisplayCharacterPicker() 114 | if (filename) 115 | GenerateDPSSearch(filename) 116 | return 117 | 118 | TMenu_ShowCharacterPicker: 119 | if (!DisplayCharacterPicker(false)) { 120 | MsgBox, You didn't make a selection. The script will now exit. 121 | ExitApp, 1 122 | } 123 | return 124 | 125 | ;-------------------------------------------------- 126 | ; Functions 127 | ;-------------------------------------------------- 128 | 129 | ; Defines GUI layouts and forces Windows to render the GUI layouts 130 | CreateGUI() { 131 | ; Create an ImageList for the TreeView 132 | ImageListID := IL_Create(5) 133 | loop 5 134 | IL_Add(ImageListID, "shell32.dll", A_Index) 135 | 136 | ; Information Window 137 | Gui, _Info:New, +AlwaysOnTop -Border -MaximizeBox -MinimizeBox +LastFound +Disabled +Hwnd_InfoHwnd 138 | Gui, _Info:Add, Text, v_InfoText Center, Please select Character Build Directory ; Default control width 139 | Gui, _Info:Show, NoActivate Hide 140 | 141 | ; Character Picker 142 | Gui, _CP:New, +Hwnd_CPHwnd -MaximizeBox -MinimizeBox, Pick Your Character Build File 143 | Gui, _CP:Margin, 8, 8 144 | Gui, _CP:Add, Checkbox, v_CPCurrent gCPCurrentCheck, Use PoB's last used build (since it last closed) 145 | Gui, _CP:Add, Button, gChangeDir, Change 146 | Gui, _CP:Add, Text, v_CPDir x+5 ym+27 w300, Build Directory 147 | Gui, _CP:Add, TreeView, v_CPTV gCPTV w300 r20 xm ImageList%ImageListID% 148 | Gui, _CP:Add, Checkbox, v_CPUpdate, Update Build before continuing 149 | Gui, _CP:Add, Checkbox, v_CPChange Checked, Make this the default Build 150 | Gui, _CP:Add, Button, v_CPOK Default w50 gOK, OK 151 | Gui, _CP:Show, NoActivate Hide 152 | 153 | ; Item Viewer 154 | Gui, _Item:New, +AlwaysOnTop +Hwnd_ItemHwnd, PoB Item Tester 155 | Gui, _Item:Add, ActiveX, x0 y0 w400 h500 v_ItemText, Shell.Explorer 156 | _ItemText.silent := true 157 | Gui, _Item:Show, NoActivate Hide 158 | } 159 | 160 | SetVariablesAndFiles(byRef luaDir, byRef pobInstall, byRef pobPath, byRef buildDir, byRef fileName) { 161 | IniRead, pobInstall, %_IniFile%, General, PathToPoBInstall, %A_Space% 162 | IniRead, pobPath, %_IniFile%, General, PathToPoB, %A_Space% 163 | IniRead, buildDir, %_IniFile%, General, BuildDirectory, %A_Space% 164 | IniRead, fileName, %_IniFile%, General, CharacterBuildFileName, %A_Space% 165 | 166 | ; Make sure PoB hasn't moved 167 | GetPoBPath(pobInstall, pobPath) 168 | SaveBuildDirectory(buildDir, GetBuildDir(pobInstall, buildDir, false)) 169 | 170 | SetWorkingDir, %pobPath% 171 | 172 | luaDir := A_ScriptDir . luaDir 173 | userDocs := A_MyDocuments 174 | EnvGet, curPATH, PATH 175 | EnvSet, PATH, %pobInstall%;%curPATH% 176 | EnvSet, LUA_PATH, %luaDir%\?.lua;%pobPath%\lua\?.lua;%pobPath%\lua\?\init.lua;%pobInstall%\lua\?.lua;%pobInstall%\lua\?\init.lua 177 | EnvSet, LUA_CPATH, %pobInstall%\?.dll 178 | EnvSet, POB_SCRIPTPATH, %pobPath% 179 | EnvSet, POB_RUNTIMEPATH, %pobInstall% 180 | EnvSet, POB_USERPATH, %userDocs% 181 | 182 | ; Make sure the Character file still exists 183 | if (fileName <> "CURRENT" and !(fileName and FileExist(buildDir "\" fileName))) { 184 | if (!DisplayCharacterPicker(false)) { 185 | MsgBox, You didn't make a selection. The script will now exit. 186 | ExitApp, 1 187 | } 188 | } 189 | } 190 | 191 | GetPoBPath(byRef pobInstall, byRef pobPath) { 192 | exeFound := FileExist(pobInstall "\Path of Building.exe") 193 | launchFound := FileExist(pobPath "\Launch.lua") 194 | if ((!pobInstall) || (!pobPath) || (!exeFound) || (!launchFound)) { 195 | if (!WinExist("Path of Building ahk_class SimpleGraphic Class")) 196 | DisplayInformation("Please launch Path of Building") 197 | 198 | WinWait, Path of Building ahk_class SimpleGraphic Class, , 300 199 | WinGet, FullPath, ProcessPath, Path of Building ahk_class SimpleGraphic Class 200 | 201 | if !FullPath { 202 | MsgBox Path of Building not detected. Please relaunch this program and open Path of Building when requested. 203 | ExitApp, 1 204 | } 205 | 206 | ; Look at running PoB's command-line for paths 207 | GetPoBPathsFromCommandLine(pobInstall, pobPath) 208 | 209 | ; First verify we got a good pobInstall 210 | if (!pobInstall or !IsDir(pobInstall)) { 211 | MsgBox, % "Unable to locate Path of Building - please re-launch PoB and this script and try again" 212 | ExitApp, 1 213 | } 214 | 215 | ; Figure out a good pobPath 216 | CalculatePoBPath(pobInstall, pobPath) 217 | 218 | ; Check we got something usable 219 | if (!pobPath or !IsDir(pobPath) or !FileExist(pobPath "\Launch.lua")) { 220 | MsgBox, % "Unable to find Path of Building's data directory :( Please report this as a bug with details of your Path of Building setup." 221 | ExitApp, 1 222 | } 223 | 224 | IniWrite, %pobInstall%, %_IniFile%, General, PathToPoBInstall 225 | IniWrite, %pobPath%, %_IniFile%, General, PathToPoB 226 | 227 | DisplayInformation() 228 | } 229 | } 230 | 231 | CalculatePoBPath(ByRef pobInstall, ByRef pobPath) { 232 | ; Check for portable installs where pobPath can be set to pobInstall 233 | if (!pobPath and FileExist(pobInstall "\Launch.lua")) { 234 | pobPath := pobInstall 235 | Display("Detected PoB data as portable") 236 | return 237 | } 238 | 239 | ; Split installs are no longer supported due to not parsing command line arguments 240 | MsgBox, % "Please uninstall and reinstall the latest version of Path of Building Community" 241 | . " and relaunch this script." 242 | ExitApp, 1 243 | 244 | ; Path may already be good if the full command-line came from the shortcut 245 | if (pobPath and FileExist(pobPath "\Launch.lua")) { 246 | Display("Detected PoB data from shortcut") 247 | return 248 | } 249 | 250 | ; The old installer would put data in :\ProgramData\Path of Building [Community] 251 | SplitPath, pobInstall, , , , , drive 252 | 253 | pobPath := drive "\ProgramData\Path of Building Community" 254 | if (IsDir(pobPath) and FileExist(pobPath "\Launch.lua")) { 255 | Display("Detected PoB Community data") 256 | return 257 | } 258 | 259 | pobPath := drive "\ProgramData\Path of Building" 260 | if (IsDir(pobPath) and FileExist(pobPath "\Launch.lua")) { 261 | Display("Detected original PoB data") 262 | return 263 | } 264 | 265 | ; We failed :( 266 | pobPath := "" 267 | } 268 | 269 | GetPoBPathsFromCommandLine(ByRef pobInstall, ByRef pobPath) { 270 | ; Get the commandline that ran PoB 271 | cmdLine := "" 272 | pobPath := "" 273 | pobInstall := "" 274 | WinGet, pid, PID, Path of Building ahk_class SimpleGraphic Class 275 | for proc in ComObjGet("winmgmts:").ExecQuery("Select * from Win32_Process") { 276 | if (proc.Name == "Path of Building.exe") { 277 | if (exePath and exePath != proc.ExecutablePath) { 278 | MsgBox, % "Multiple versions of Path of Building are running - please run only the most recent and try agian" 279 | ExitApp, 1 280 | } 281 | exePath := proc.ExecutablePath 282 | } 283 | } 284 | 285 | SplitPath, exePath,, pobInstall 286 | } 287 | 288 | GetBuildDir(pobInstall, byRef buildDir, force = true) { 289 | if (!buildDir or !FileExist(buildDir)) 290 | { 291 | if (FileExist(pobInstall "\Builds")) 292 | buildDir := pobInstall "\Builds" 293 | else if (FileExist(A_MyDocuments "\Path of Building\Builds")) 294 | buildDir := A_MyDocuments "\Path of Building\Builds" 295 | } 296 | 297 | newDir := "" 298 | tempDir := buildDir 299 | 300 | if (force or !buildDir) 301 | FileSelectFolder, newDir, *%buildDir%, 2, Select Character Build Directory 302 | 303 | if (!newDir and !tempDir) { 304 | MsgBox A Character Build Directory wasn't selected. Please relaunch this program and select a Build Directory. 305 | ExitApp, 1 306 | } 307 | 308 | if (!newDir) 309 | newDir := tempDir 310 | 311 | GuiControl, _CP:Text, _CPDir, %newDir% 312 | return newDir 313 | } 314 | 315 | InsertTrayMenuItems() { 316 | Menu, Tray, NoStandard 317 | Menu, Tray, Add, Show Character Picker, TMenu_ShowCharacterPicker 318 | Menu, Tray, Add, Re-import build (update), TMenu_UpdateCharacterBuild 319 | Menu, Tray, Add, Test item from clipboard, TMenu_TestItemFromClipboard 320 | Menu, Tray, Add, Test item from clipboard (choose char), TMenuWithPicker_TestItemFromClipboard 321 | Menu, Tray, Add, Generate DPS search, TMenu_GenerateDPSSearch 322 | Menu, Tray, Add, Generate DPS search (choose char), TMenuWithPicker_GenerateDPSSearch 323 | Menu, Tray, Add ; Separator 324 | Menu, Tray, Standard 325 | } 326 | 327 | GetItemFromClipboard() { 328 | ; Verify the information is what we're looking for 329 | if RegExMatch(clipboard, "(Rarity|Item Class): .+?(\r.+?){3,}") = 0 { 330 | MsgBox % "Not a PoE item" 331 | return false 332 | } 333 | return clipboard 334 | } 335 | 336 | TestItemFromClipboard(item, fullPath) { 337 | ; Erase old content first 338 | FileDelete, %A_Temp%\PoBTestItem.txt 339 | FileDelete, %A_Temp%\PoBTestItem.txt.html 340 | FileAppend, %item%, %A_Temp%\PoBTestItem.txt 341 | 342 | DisplayInformation("Parsing Item Data...") 343 | cmd = "%_LuaJIT%" "%_LuaDir%\TestItem.lua" "%fullPath%" "%A_Temp%\PoBTestItem.txt" 344 | RunLua(cmd) 345 | DisplayInformation() 346 | DisplayOutput() 347 | } 348 | 349 | GenerateDPSSearch(fullPath) { 350 | DisplayInformation("Generating DPS search...") 351 | cmd = "%_LuaJIT%" "%_LuaDir%\SearchDPS.lua" "%fullPath%" 352 | RunLua(cmd) 353 | DisplayInformation() 354 | } 355 | 356 | UpdateCharacterBuild(fullPath) { 357 | DisplayInformation("Updating Character Build") 358 | cmd = "%_LuaJIT%" "%_LuaDir%\UpdateBuild.lua" "%fullPath%" 359 | RunLua(cmd) 360 | DisplayInformation() 361 | } 362 | 363 | SaveBuildDirectory(byRef buildDir, newDirectory) { 364 | buildDir := newDirectory 365 | IniWrite, %newDirectory%, %_IniFile%, General, BuildDirectory 366 | } 367 | 368 | SaveCharacterFile(byRef fileName, newFile) { 369 | fileName := newFile 370 | IniWrite, %newFile%, %_IniFile%, General, CharacterBuildFileName 371 | } 372 | 373 | SortArray(array, order := "A") { 374 | ; Order A: Ascending, D: Descending, R: Reverse 375 | maxIndex := ObjMaxIndex(array) 376 | if (order = "R") { 377 | count := 0 378 | loop, % maxIndex 379 | ObjInsert(array, ObjRemove(array, maxIndex - count ++)) 380 | return 381 | } 382 | partitions := "|" ObjMinIndex(array) "," maxIndex 383 | loop { 384 | comma := InStr(this_partition := SubStr(partitions, InStr(partitions, "|", false, 0) + 1), ",") 385 | spos := pivot := SubStr(this_partition, 1, comma - 1) , epos := SubStr(this_partition, comma + 1) 386 | if (order = "A") { 387 | loop, % epos - spos { 388 | if (array[pivot] > array[A_Index + spos]) 389 | ObjInsert(array, pivot ++, ObjRemove(array, A_Index + spos)) 390 | } 391 | } 392 | else { 393 | loop, % epos - spos { 394 | if (array[pivot] < array[A_Index + spos]) 395 | ObjInsert(array, pivot ++, ObjRemove(array, A_Index + spos)) 396 | } 397 | } 398 | partitions := SubStr(partitions, 1, InStr(partitions, "|", false, 0) - 1) 399 | if (pivot - spos) > 1 ;if more than one elements 400 | partitions .= "|" spos "," pivot - 1 ;the left partition 401 | if (epos - pivot) > 1 ;if more than one elements 402 | partitions .= "|" pivot + 1 "," epos ;the right partition 403 | } until !partitions 404 | } 405 | 406 | ;-------------------------------------------------- 407 | ; GUI Display Functions 408 | ;-------------------------------------------------- 409 | Display(byRef msg) { 410 | DisplayInformation(msg) 411 | Sleep, 3000 412 | DisplayInformation() 413 | } 414 | 415 | DisplayInformation(byRef string := "") { 416 | ; Hide the Information Window 417 | if (string = "") { 418 | Gui, _Info:Hide 419 | return 420 | } 421 | 422 | GuiControl, _Info:Text, _InfoText, %string% 423 | WinGetPos, winX, winY, winW, winH, A 424 | 425 | ; If no active window was found 426 | if (winX = "") { 427 | return 428 | } 429 | 430 | WinGetPos, , , guiW, guiH, ahk_id %_InfoHwnd% 431 | posX := winX + (winW - guiW) / 2 432 | posY := winY + 50 433 | Gui, _Info:Show, X%posX% Y%posY% NoActivate 434 | } 435 | 436 | CreateTV(folder, filePattern = "*.xml") { 437 | Gui, _CP:Default 438 | GuiControl, _CP:-Redraw, _CPTV 439 | TV_Delete() ; Clear the TreeView 440 | fileList := [] 441 | dirTree := ["" = 0] ; 0 for top directory in TV 442 | folder .= (SubStr(folder, 0) == "\" ? "" : "\") ; Directories aren't typically passed with trailing forward slash 443 | 444 | loop, Files, %folder%%filePattern%, FR 445 | { 446 | tempPath := SubStr(A_loopFileFullPath, StrLen(folder) + 1) 447 | fileList.push(tempPath) 448 | 449 | SplitPath, tempPath, tempFile, tempDir 450 | 451 | ; The directory has already been added 452 | if (dirTree[tempDir]) 453 | continue 454 | 455 | runningDir := "" 456 | loop, Parse, tempDir, "\" 457 | { 458 | if (runningDir) 459 | newPath := runningDir "\" A_LoopField 460 | else 461 | newPath := A_LoopField 462 | 463 | if (!dirTree[newPath]) 464 | dirTree[newPath] := TV_Add(A_LoopField, dirTree[runningDir], "Icon4") 465 | runningDir := newPath 466 | } 467 | } 468 | 469 | SortArray(fileList) 470 | for index, file in fileList { 471 | SplitPath, file, , tempDir, , tempName 472 | 473 | if ((folder . file) != (_BuildDir "\" _CharacterFileName)) 474 | TV_Add(tempName, dirTree[tempDir]) 475 | else 476 | TV_Add(tempName, dirTree[tempDir], "Select") 477 | } 478 | 479 | GuiControl, _CP:+Redraw, _CPTV 480 | } 481 | 482 | DisplayCharacterPicker(allowTemp = true) { 483 | _GUIOK := false 484 | rtnVal := "" 485 | GuiControl, _CP:Text, _CPDir, %_BuildDir% 486 | 487 | CreateTV(_BuildDir) 488 | if (TV_GetChild(TV_GetSelection())) 489 | GuiControl, _CP:-Default +Disabled, _CPOK 490 | 491 | if (allowTemp) 492 | GuiControl, _CP:-Disabled, _CPChange 493 | else 494 | GuiControl, _CP:+Disabled, _CPChange 495 | 496 | ; Move CharacterPicker to the center of the currently active window 497 | WinGetPos, winX, winY, winW, winH, A 498 | WinGetPos, , , guiW, guiH, ahk_id %_CPHwnd% 499 | posX := winX + (winW - guiW) / 2 500 | posY := winY + (winH - guiH) / 2 501 | Gui, _CP:Show, X%posX% Y%posY% 502 | 503 | DetectHiddenWindows, Off 504 | WinWait, ahk_id %_CPHwnd% 505 | WinWaitClose, ahk_id %_CPHwnd% 506 | DetectHiddenWindows, On 507 | 508 | if (!_GUIOK) 509 | return "" 510 | 511 | GuiControlGet, curDirectory, , _CPDir 512 | 513 | ; Set the Value to "CURRENT" instead of a specific path name 514 | if (_CPCurrent) 515 | rtnVal := "CURRENT" 516 | else { 517 | TV_GetText(rtnVal, TV_GetSelection()) 518 | parentID := TV_GetSelection() 519 | loop { 520 | parentID := TV_GetParent(parentID) 521 | if (!parentID) 522 | break 523 | TV_GetText(parentText, parentID) 524 | rtnVal := parentText "\" rtnVal 525 | } 526 | rtnVal := rtnVal ".xml" 527 | } 528 | 529 | ; Update the INI with the changes 530 | if (_CPChange) { 531 | SaveCharacterFile(_CharacterFileName, rtnVal) 532 | SaveBuildDirectory(_BuildDir, curDirectory) 533 | } 534 | 535 | if (rtnVal != "CURRENT") 536 | rtnVal := curDirectory "\" rtnVal 537 | 538 | ; Update the build before continuing 539 | if (_CPUpdate) 540 | UpdateCharacterBuild(rtnVal) 541 | 542 | return rtnVal 543 | } 544 | 545 | DisplayOutput() { 546 | if (!FileExist(A_Temp "\PoBTestItem.txt.html")) { 547 | MsgBox, Item type is not supported. 548 | return 549 | } 550 | 551 | _ItemText.Navigate("file://" A_Temp "\PoBTestItem.txt.html") 552 | while _ItemText.busy or _ItemText.ReadyState != 4 553 | Sleep 10 554 | 555 | WinGetPos, winX, winY, winW, winH, A 556 | Gui, _Item:+LastFound 557 | WinGetPos, , , guiW, guiH, ahk_id %_ItemHwnd% 558 | MouseGetPos, mouseX, mouseY 559 | posX := ((mouseX > (winX + winW / 2)) ? (winX + winW * 0.25 - guiW * 0.5) : (winX + winW * 0.75 - guiW * 0.5)) 560 | posY := ((mouseY > (winY + winH / 2)) ? (winY + winH * 0.25 - guiH * 0.5) : (winY + winH * 0.75 - guiH * 0.5)) 561 | Gui, _Item:Show, w400 h500 X%posX% Y%posY% NoActivate 562 | } 563 | 564 | UpdateModData() { 565 | OldEtag := "" 566 | FileRead, OldEtag, % "ItemTester\mods.json.version" ; ignores errors 567 | 568 | ; Begin a request for mods.json but only if the Etag doesn't match what we have 569 | global modsJsonReq 570 | modsJsonReq := ComObjCreate("Msxml2.XMLHTTP") 571 | modsJsonReq.open("GET", MODSURL, False) 572 | modsJsonReq.setRequestHeader("User-Agent", "VolatilePulse-PoBItemTester-updater") 573 | modsJsonReq.setRequestHeader("If-None-Match", OldEtag) 574 | modsJsonReq.onreadystatechange := Func("ReceiveModsJson") 575 | modsJsonReq.send() 576 | } 577 | 578 | ReceiveModsJson() { 579 | global modsJsonReq 580 | 581 | if (modsJsonReq.readyState != 4) ; Not done yet. 582 | return 583 | 584 | statusN := modsJsonReq.status 585 | 586 | if (statusN >= 200 and statusN < 300) { 587 | ; New file version has been downloaded 588 | FileDelete, % A_ScriptDir "\ItemTester\mods.json" 589 | FileAppend, % modsJsonReq.responseText, % A_ScriptDir "\ItemTester\mods.json" 590 | 591 | ; Update version file with ETag 592 | NewEtag := modsJsonReq.getResponseHeader("ETag") 593 | FileDelete, % A_ScriptDir "\ItemTester\mods.json.version" 594 | FileAppend, % NewEtag, % A_ScriptDir "\ItemTester\mods.json.version" 595 | } else if (statusN == 304) { 596 | ; Not modified - do nothing 597 | } else { 598 | ; Failed :( 599 | Display("Failed to fetch latest mod data :(") 600 | } 601 | } 602 | 603 | ExitFunc() { 604 | ; Clean up temporary files, if able to 605 | FileDelete, %A_Temp%\PoBTestItem.txt 606 | FileDelete, %A_Temp%\PoBTestItem.txt.html 607 | } 608 | 609 | RunLua(cmd) { 610 | stdout := StdoutToVar_CreateProcess(cmd, , , exitcode) 611 | 612 | if (exitcode) { 613 | MsgBox, 4, % "Exit Code: " . exitcode, % "Output:`r `r" . stdout . "`rCopy output to clipboard?" 614 | IfMsgBox, Yes 615 | Clipboard := stdout 616 | } 617 | } 618 | 619 | ; ---------------------------------------------------------------------------------------------------------------------- 620 | ; Function .....: StdoutToVar_CreateProcess 621 | ; Description ..: Runs a command line program and returns its output. 622 | ; Parameters ...: sCmd - Commandline to execute. 623 | ; ..............: sEncoding - Encoding used by the target process. Look at StrGet() for possible values. 624 | ; ..............: sDir - Working directory. 625 | ; ..............: nExitCode - Process exit code, receive it as a byref parameter. 626 | ; Return .......: Command output as a string on success, empty string on error. 627 | ; AHK Version ..: AHK_L x32/64 Unicode/ANSI 628 | ; Author .......: Sean (http://goo.gl/o3VCO8), modified by nfl and by Cyruz 629 | ; License ......: WTFPL - http://www.wtfpl.net/txt/copying/ 630 | ; Changelog ....: Feb. 20, 2007 - Sean version. 631 | ; ..............: Sep. 21, 2011 - nfl version. 632 | ; ..............: Nov. 27, 2013 - Cyruz version (code refactored and exit code). 633 | ; ..............: Mar. 09, 2014 - Removed input, doesn't seem reliable. Some code improvements. 634 | ; ..............: Mar. 16, 2014 - Added encoding parameter as pointed out by lexikos. 635 | ; ..............: Jun. 02, 2014 - Corrected exit code error. 636 | ; ..............: Nov. 02, 2016 - Fixed blocking behavior due to ReadFile thanks to PeekNamedPipe. 637 | ; ---------------------------------------------------------------------------------------------------------------------- 638 | StdoutToVar_CreateProcess(sCmd, sEncoding:="CP0", sDir:="", ByRef nExitCode:=0) { 639 | DllCall( "CreatePipe", PtrP,hStdOutRd, PtrP,hStdOutWr, Ptr,0, UInt,0 ) 640 | DllCall( "SetHandleInformation", Ptr,hStdOutWr, UInt,1, UInt,1 ) 641 | 642 | VarSetCapacity( pi, (A_PtrSize == 4) ? 16 : 24, 0 ) 643 | siSz := VarSetCapacity( si, (A_PtrSize == 4) ? 68 : 104, 0 ) 644 | NumPut( siSz, si, 0, "UInt" ) 645 | NumPut( 0x100, si, (A_PtrSize == 4) ? 44 : 60, "UInt" ) 646 | NumPut( hStdOutWr, si, (A_PtrSize == 4) ? 60 : 88, "Ptr" ) 647 | NumPut( hStdOutWr, si, (A_PtrSize == 4) ? 64 : 96, "Ptr" ) 648 | 649 | If ( !DllCall( "CreateProcess", Ptr,0, Ptr,&sCmd, Ptr,0, Ptr,0, Int,True, UInt,0x08000000 650 | , Ptr,0, Ptr,sDir?&sDir:0, Ptr,&si, Ptr,&pi ) ) 651 | Return "" 652 | , DllCall( "CloseHandle", Ptr,hStdOutWr ) 653 | , DllCall( "CloseHandle", Ptr,hStdOutRd ) 654 | 655 | DllCall( "CloseHandle", Ptr,hStdOutWr ) ; The write pipe must be closed before reading the stdout. 656 | While ( 1 ) 657 | { ; Before reading, we check if the pipe has been written to, so we avoid freezings. 658 | If ( !DllCall( "PeekNamedPipe", Ptr,hStdOutRd, Ptr,0, UInt,0, Ptr,0, UIntP,nTot, Ptr,0 ) ) 659 | Break 660 | If ( !nTot ) 661 | { ; If the pipe buffer is empty, sleep and continue checking. 662 | Sleep, 100 663 | Continue 664 | } ; Pipe buffer is not empty, so we can read it. 665 | VarSetCapacity(sTemp, nTot+1) 666 | DllCall( "ReadFile", Ptr,hStdOutRd, Ptr,&sTemp, UInt,nTot, PtrP,nSize, Ptr,0 ) 667 | sOutput .= StrGet(&sTemp, nSize, sEncoding) 668 | } 669 | 670 | ; * SKAN has managed the exit code through SetLastError. 671 | DllCall( "GetExitCodeProcess", Ptr,NumGet(pi,0), UIntP,nExitCode ) 672 | DllCall( "CloseHandle", Ptr,NumGet(pi,0) ) 673 | DllCall( "CloseHandle", Ptr,NumGet(pi,A_PtrSize) ) 674 | DllCall( "CloseHandle", Ptr,hStdOutRd ) 675 | Return sOutput 676 | } 677 | 678 | IsDir(dir) { 679 | return InStr(FileExist(dir), "D") 680 | } 681 | 682 | /* 683 | 684 | PoB file behavior and locations 685 | 686 | OpenArl's `Path of Building.exe` looks for Launch.lua in the following places: 687 | * The first passed in argument 688 | * The current directory 689 | * `%ProgramData%/Path of Building` 690 | 691 | Path of Building Community Fork also currently uses this exe in its original form as it cannot be recompiled. 692 | 693 | The PoB Community installer in the past has opted for installing into 694 | `%Drive%:/ProgramData/Path of Building Community` and requiring the path to be included 695 | as an argument to the exe in the progran's shortcut. Launching without the shortcut will fail. 696 | 697 | As of writing, the current PoB Community installer installs as if it were the portable version (both exe and data) 698 | into `%AppData%/Path of Building Community` and no longer requires admin permissions. 699 | 700 | Between Community versions 1.4.170.4 and 1.4.170.14 (at least) the installer has randomly switched between these 701 | two different approaches, meaning we have to support both. 702 | 703 | */ 704 | -------------------------------------------------------------------------------- /bin/lua51.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VolatilePulse/PoB-Item-Tester/42c3c2c8741f53a93fb56b0b42876ec73018c812/bin/lua51.dll -------------------------------------------------------------------------------- /bin/luajit.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VolatilePulse/PoB-Item-Tester/42c3c2c8741f53a93fb56b0b42876ec73018c812/bin/luajit.exe -------------------------------------------------------------------------------- /imgs/sshot-dps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VolatilePulse/PoB-Item-Tester/42c3c2c8741f53a93fb56b0b42876ec73018c812/imgs/sshot-dps.png -------------------------------------------------------------------------------- /imgs/sshot-tester.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VolatilePulse/PoB-Item-Tester/42c3c2c8741f53a93fb56b0b42876ec73018c812/imgs/sshot-tester.png -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | These files are used in the development and testing of PoB-Item-Tester. 4 | 5 | ## Usage 6 | 7 | The `test` batch files depend on paths within `TestItem.ini` so require that the main AHK script has run and detected PoB correctly. The build selected for testing is also read from the ini. 8 | 9 | Current directory should always be PoB-Item-Tester root. 10 | 11 | ```bat 12 | $ cd PoB-Item-Tester 13 | $ cmd /c test\testitem.bat 14 | ... 15 | Results output to: ...\PoB-Item-Tester\test\testitems\ring.txt.html 16 | ``` 17 | -------------------------------------------------------------------------------- /test/_testsetup.bat: -------------------------------------------------------------------------------- 1 | REM This callable batch file simply reads TestItem.ini and sets up the environment needed to run the Lua files. 2 | 3 | set BASEDIR=%CD% 4 | 5 | REM Ini reading 6 | for /F "tokens=1,2 delims==" %%A IN ('"type %BASEDIR%\TestItem.ini"') do set "%%A=%%B" 7 | if "%CharacterBuildFileName%"==CURRENT ( 8 | set BUILD=CURRENT 9 | ) else ( 10 | set "BUILD=%BuildDirectory%\%CharacterBuildFileName%" 11 | ) 12 | 13 | REM Get Documents folder 14 | for /F "tokens=3" %%G IN ('REG QUERY "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" /v "Personal"') do set "POB_USERPATH=%%G" 15 | 16 | REM Set up the environment 17 | set "PATH=%PathToPoBInstall%;%PATH%" 18 | set "LUA_PATH=%BASEDIR%\ItemTester\?.lua" 19 | set "LUA_PATH=%LUA_PATH%;%PathToPoB%\lua\?.lua" 20 | set "LUA_PATH=%LUA_PATH%;%PathToPoB%\lua\?\init.lua" 21 | set "LUA_PATH=%LUA_PATH%;%PathToPoBInstall%\lua\?.lua" 22 | set "LUA_PATH=%LUA_PATH%;%PathToPoBInstall%\lua\?\init.lua" 23 | set "LUA_CPATH=%PathToPoBInstall%\?.dll" 24 | set "LUAJIT=%BASEDIR%\bin\luajit.exe" 25 | set "POB_SCRIPTPATH=%PathToPoB%" 26 | set "POB_RUNTIMEPATH=%PathToPoBInstall%" 27 | cd /d %PathToPoB% 28 | -------------------------------------------------------------------------------- /test/testdps.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | call test/_testsetup.bat 4 | 5 | "%LUAJIT%" "%BASEDIR%\ItemTester\SearchDPS.lua" "%BUILD%" 6 | 7 | REM Add OPTIONS on the end to get skill damage stat options 8 | -------------------------------------------------------------------------------- /test/testitem.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | call test/_testsetup.bat 4 | 5 | "%LUAJIT%" "%BASEDIR%\ItemTester\TestItem.lua" "%BUILD%" "%BASEDIR%\test\testitems\ring.txt" 6 | -------------------------------------------------------------------------------- /test/testitems/jewel.txt: -------------------------------------------------------------------------------- 1 | Rarity: Rare 2 | Grim Song 3 | Murderous Eye Jewel 4 | -------- 5 | Abyss 6 | -------- 7 | Requirements: 8 | Level: 30 9 | -------- 10 | Item Level: 78 11 | -------- 12 | +31 to maximum Life 13 | Adds 11 to 19 Cold Damage to Staff Attacks 14 | 5% chance to gain Onslaught for 4 seconds on Kill 15 | -------- 16 | Place into an Abyssal Socket on an Item or into an allocated Jewel Socket on the Passive Skill Tree. Right click to remove from the Socket. 17 | -------------------------------------------------------------------------------- /test/testitems/ring.txt: -------------------------------------------------------------------------------- 1 | Rarity: Rare 2 | Woe Coil 3 | Two-Stone Ring 4 | -------- 5 | Requirements: 6 | Level: 57 7 | -------- 8 | Item Level: 86 9 | -------- 10 | +12% to Fire and Cold Resistances 11 | -------- 12 | Adds 3 to 4 Physical Damage to Attacks 13 | 15% increased Fire Damage 14 | 11% increased Cold Damage 15 | +63 to maximum Life 16 | +45% to Cold Resistance 17 | Adds 10 to 20 Fire Damage to Attacks 18 | -------- 19 | Note: ~price 10 chaos 20 | -------------------------------------------------------------------------------- /test/testupdate.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | call test/_testsetup.bat 4 | 5 | "%LUAJIT%" "%BASEDIR%\ItemTester\UpdateBuild.lua" "%BUILD%" 6 | -------------------------------------------------------------------------------- /test/updatemods-curl.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | curl -o ItemTester\mods.json "https://raw.githubusercontent.com/xanthics/PoE_Weighted_Search/master/mods.json" 4 | -------------------------------------------------------------------------------- /test/updatemods-powershell.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | powershell -Command "(New-Object Net.WebClient).DownloadFile('https://raw.githubusercontent.com/xanthics/PoE_Weighted_Search/master/mods.json', 'ItemTester\mods.json')" 4 | --------------------------------------------------------------------------------