Skip to content

Commit 3f6faea

Browse files
committed
Merge branch 'dev'
2 parents 83134ce + a4b5405 commit 3f6faea

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+18623
-18171
lines changed

CONTRIBUTING.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -123,16 +123,17 @@ dbg.tcpListen('localhost', 9966)
123123
5. Start Path of Building Community
124124
6. Attach the debugger
125125

126-
#### Exporting Data from a GGPK file
126+
#### Exporting GGPK Data from Path of Exile
127127

128-
Note: This tutorial assumes that you are already familiar with the GGPK and its structure.
128+
Note: This tutorial assumes that you are already familiar with the GGPK and its structure. [poe-tool-dev/ggpk.discussion](https://github.com/poe-tool-dev/ggpk.discussion/wiki)
129+
is a good starting point.
129130

130-
The repository also contains the system used to export data from the game's Content.ggpk file. This can be found in the Export folder. The data is exported using the scripts in `./Export/Scripts`, which are run from within the `.dat` viewer.
131+
The `./Data` folder contains generated files which are created using the scripts in the `./Export/Scripts` folder based on Path of Exile game data.
132+
If you change any logic/configuration in `./Export`, you will need to regenerate the appropriate `./Data` files. You can do so by running the `./Export` scripts using the `.dat` viewer at `./Export/Launch.lua`:
131133

132-
How to export data from a GGPK file:
133-
134-
1. Create a shortcut to `Path of Building.exe` with the path to `./Export/Launch.lua` as first argument. You should end up with something like: `"C:\%APPDATA%\Path of Building Community\Path of Building.exe" "C:\PathOfBuilding\Export\Launch.lua"`.
135-
2. Run the shortcut, and the GGPK data viewer UI will appear. If you get an error, be sure you're using the latest release of Path of Building Community.
136-
3. Paste the path to `Content.ggpk` into the text box in the top left, and hit `Enter` to read the GGPK. If successful, you will see a list of the data tables in the GGPK file. Note: This will not work on the GGPK from the torrent file released before league launches, as it contains no `./Data` section.
137-
4. Click `Scripts >>` to show the list of available export scripts. Double-clicking a script will run it, and the box to the right will show any output from the script.
138-
5. If you run into any errors, update the code in `./Export` as necessary and try again.
134+
1. Obtain a copy of an OOZ extractor and copy it into `./Export/ggpk/`.
135+
2. Create a shortcut to `Path of Building.exe` with the path to `./Export/Launch.lua` as first argument. You should end up with something like: `"C:\%APPDATA%\Path of Building Community\Path of Building.exe" "C:\PathOfBuilding\Export\Launch.lua"`.
136+
3. Run the shortcut, and the GGPK data viewer UI will appear. If you get an error, be sure you're using the latest release of Path of Building Community.
137+
4. Paste the path to `Content.ggpk` (or, for Steam users, `C:\Program Files (x86)\Steam\steamapps\common\Path of Exile`) into the text box in the top left, and hit `Enter` to read the GGPK. If successful, you will see a list of the data tables in the GGPK file. Note: This will not work on the GGPK from the torrent file released before league launches, as it contains no `./Data` section.
138+
5. Click `Scripts >>` to show the list of available export scripts. Double-clicking a script will run it, and the box to the right will show any output from the script.
139+
6. If you run into any errors, update the code in `./Export` as necessary and try again.

Classes/CalcsTab.lua

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ local CalcsTabClass = newClass("CalcsTab", "UndoHandler", "ControlHost", "Contro
6363
self:AddUndoState()
6464
self.build.buildFlag = true
6565
end)
66+
}, },{ label = "Skill Stages", playerFlag = "multiStage", { controlName = "mainSkillStageCount",
67+
control = new("EditControl", nil, 0, 0, 52, 16, nil, nil, "%D", nil, function(buf)
68+
local mainSocketGroup = self.build.skillsTab.socketGroupList[self.input.skill_number]
69+
local srcInstance = mainSocketGroup.displaySkillListCalcs[mainSocketGroup.mainActiveSkillCalcs].activeEffect.srcInstance
70+
srcInstance.skillStageCountCalcs = tonumber(buf)
71+
self:AddUndoState()
72+
self.build.buildFlag = true
73+
end)
6674
}, },
6775
{ label = "Active Mines", playerFlag = "mine", { controlName = "mainSkillMineCount",
6876
control = new("EditControl", nil, 0, 0, 52, 16, nil, nil, "%D", nil, function(buf)

Classes/GemSelectControl.lua

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,8 @@ function GemSelectClass:Draw(viewPort)
336336
else
337337
gemList[self.index] = nil
338338
end
339+
self:AddGemTooltip(gemInstance)
340+
self.tooltip:AddSeparator(10)
339341
self.skillsTab.build:AddStatComparesToTooltip(self.tooltip, calcBase, output, "^7Selecting this gem will give you:")
340342
self.tooltip:Draw(x, y + height + 2 + (self.hoverSel - 1) * (height - 4) - scrollBar.offset, width, height - 4, viewPort)
341343
end
@@ -461,7 +463,11 @@ function GemSelectClass:AddCommonGemInfo(gemInstance, grantedEffect, addReq, mer
461463
end
462464
self.tooltip:AddSeparator(10)
463465
if addReq then
464-
self.skillsTab.build:AddRequirementsToTooltip(self.tooltip, gemInstance.reqLevel, gemInstance.reqStr, gemInstance.reqDex, gemInstance.reqInt)
466+
local reqLevel = grantedEffect.levels[gemInstance.level].levelRequirement
467+
local reqStr = calcLib.getGemStatRequirement(reqLevel, grantedEffect.support, gemInstance.gemData.reqStr)
468+
local reqDex = calcLib.getGemStatRequirement(reqLevel, grantedEffect.support, gemInstance.gemData.reqDex)
469+
local reqInt = calcLib.getGemStatRequirement(reqLevel, grantedEffect.support, gemInstance.gemData.reqInt)
470+
self.skillsTab.build:AddRequirementsToTooltip(self.tooltip, reqLevel, reqStr, reqDex, reqInt)
465471
end
466472
if grantedEffect.description then
467473
local wrap = main:WrapString(grantedEffect.description, 16, m_max(DrawStringWidth(16, "VAR", gemInstance.gemData.tagString), 400))

Classes/Item.lua

Lines changed: 110 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ end
6363
-- Parse raw item data and extract item name, base type, quality, and modifiers
6464
function ItemClass:ParseRaw(raw)
6565
self.raw = raw
66-
local data = data
6766
self.name = "?"
6867
self.rarity = "UNIQUE"
6968
self.quality = nil
@@ -97,82 +96,23 @@ function ItemClass:ParseRaw(raw)
9796
end
9897
if self.rawLines[l] then
9998
self.name = self.rawLines[l]
100-
l = l + 1
101-
end
102-
self.namePrefix = ""
103-
self.nameSuffix = ""
104-
if self.rarity == "NORMAL" or self.rarity == "MAGIC" then
105-
for baseName, baseData in pairs(data.itemBases) do
106-
local s, e = self.name:find(baseName, 1, true)
107-
if s then
108-
self.baseName = baseName
109-
self.namePrefix = self.name:sub(1, s - 1)
110-
self.nameSuffix = self.name:sub(e + 1)
111-
self.type = baseData.type
112-
break
113-
end
114-
end
115-
if not self.baseName then
116-
local s, e = self.name:find("Two-Toned Boots", 1, true)
117-
if s then
118-
-- Hack for Two-Toned Boots
119-
self.baseName = "Two-Toned Boots (Armour/Energy Shield)"
120-
self.namePrefix = self.name:sub(1, s - 1)
121-
self.nameSuffix = self.name:sub(e + 1)
122-
self.type = "Boots"
123-
end
124-
end
125-
self.name = self.name:gsub(" %(.+%)","")
126-
elseif self.rawLines[l] and not self.rawLines[l]:match("^%-") then
127-
if self.rawLines[l] == "Two-Toned Boots" then
128-
self.rawLines[l] = "Two-Toned Boots (Armour/Energy Shield)"
129-
end
130-
local baseName = self.rawLines[l]:gsub("Synthesised ","")
131-
if data.itemBases[baseName] then
132-
self.baseName = baseName
133-
self.title = self.name
134-
self.name = self.title .. ", " .. baseName:gsub(" %(.+%)","")
135-
self.type = data.itemBases[baseName].type
99+
-- Found the name for a rare or unique, but let's parse it if it's a magic or normal item to get the base
100+
if not (self.rarity == "NORMAL" or self.rarity == "MAGIC") then
136101
l = l + 1
137102
end
138103
end
139-
self.base = data.itemBases[self.baseName]
140104
self.sockets = { }
141105
self.buffModLines = { }
142106
self.enchantModLines = { }
143107
self.implicitModLines = { }
144108
self.explicitModLines = { }
145109
local implicitLines = 0
146-
if self.base then
147-
self.affixes = (self.base.subType and data.itemMods[self.base.type..self.base.subType])
148-
or data.itemMods[self.base.type]
149-
or data.itemMods.Item
150-
self.enchantments = data.enchantments[self.base.type]
151-
self.corruptable = self.base.type ~= "Flask" and self.base.subType ~= "Cluster"
152-
self.influenceTags = data.specialBaseTags[self.type]
153-
self.canBeInfluenced = self.influenceTags
154-
self.clusterJewel = data.clusterJewels and data.clusterJewels.jewels[self.baseName]
155-
end
156110
self.variantList = nil
157111
self.prefixes = { }
158112
self.suffixes = { }
159113
self.requirements = { }
160-
if self.base then
161-
self.requirements.str = self.base.req.str or 0
162-
self.requirements.dex = self.base.req.dex or 0
163-
self.requirements.int = self.base.req.int or 0
164-
local maxReq = m_max(self.requirements.str, self.requirements.dex, self.requirements.int)
165-
self.defaultSocketColor = (maxReq == self.requirements.dex and "G") or (maxReq == self.requirements.int and "B") or "R"
166-
end
167114
local importedLevelReq
168-
local flaskBuffLines = { }
169-
if self.base and self.base.flask and self.base.flask.buff then
170-
for _, line in ipairs(self.base.flask.buff) do
171-
flaskBuffLines[line] = true
172-
local modList, extra = modLib.parseMod(line)
173-
t_insert(self.buffModLines, { line = line, extra = extra, modList = modList or { } })
174-
end
175-
end
115+
local flaskBuffLines
176116
local deferJewelRadiusIndexAssignment
177117
local gameModeStage = "FINDIMPLICIT"
178118
local foundExplicit, foundImplicit
@@ -189,7 +129,7 @@ function ItemClass:ParseRaw(raw)
189129

190130
while self.rawLines[l] do
191131
local line = self.rawLines[l]
192-
if flaskBuffLines[line] then
132+
if flaskBuffLines and flaskBuffLines[line] then
193133
flaskBuffLines[line] = nil
194134
elseif line == "--------" then
195135
if gameModeStage == "IMPLICIT" then
@@ -232,6 +172,9 @@ function ItemClass:ParseRaw(raw)
232172
self.itemLevel = tonumber(specVal)
233173
elseif specName == "Quality" then
234174
self.quality = tonumber(specVal)
175+
if line:match(" %(augmented%)") and self.quality ~= 30 then
176+
self.quality = 20
177+
end
235178
elseif specName == "Sockets" then
236179
local group = 0
237180
for c in specVal:gmatch(".") do
@@ -367,6 +310,81 @@ function ItemClass:ParseRaw(raw)
367310
variantList[tonumber(varId)] = true
368311
end
369312
end
313+
if line:gsub("({variant:[%d,]+})", "") == "Two-Toned Boots" then
314+
line = "Two-Toned Boots (Armour/Energy Shield)"
315+
end
316+
self.namePrefix = self.namePrefix or ""
317+
self.nameSuffix = self.nameSuffix or ""
318+
if self.rarity == "NORMAL" or self.rarity == "MAGIC" then
319+
-- Exact match (affix-less magic and normal items)
320+
if data.itemBases[self.name] then
321+
self.baseName = self.name
322+
self.type = data.itemBases[self.name].type
323+
else
324+
-- Partial match (magic items with affixes)
325+
for baseName, baseData in pairs(data.itemBases) do
326+
local s, e = self.name:find(baseName, 1, true)
327+
if s then
328+
-- Set the base name if it isn't there, or we found a better match, so replace it
329+
if (self.baseName and string.len(self.namePrefix) > string.len(self.name:sub(1, s - 1)))
330+
or self.baseName == nil or self.baseName == "" then
331+
self.namePrefix = self.name:sub(1, s - 1)
332+
self.nameSuffix = self.name:sub(e + 1)
333+
self.baseName = baseName
334+
self.type = baseData.type
335+
end
336+
end
337+
end
338+
end
339+
if not self.baseName then
340+
local s, e = self.name:find("Two-Toned Boots", 1, true)
341+
if s then
342+
-- Hack for Two-Toned Boots
343+
self.baseName = "Two-Toned Boots (Armour/Energy Shield)"
344+
self.namePrefix = self.name:sub(1, s - 1)
345+
self.nameSuffix = self.name:sub(e + 1)
346+
self.type = "Boots"
347+
end
348+
end
349+
self.name = self.name:gsub(" %(.+%)","")
350+
end
351+
local baseName = self.baseName or ""
352+
if self.variant and varSpec then
353+
if tonumber(varSpec) == self.variant then
354+
baseName = line:gsub("Synthesised ",""):gsub("{variant:([%d,]+)}", "")
355+
end
356+
elseif baseName == "" then
357+
baseName = line:gsub("Synthesised ",""):gsub("{variant:([%d,]+)}", "")
358+
end
359+
if baseName and data.itemBases[baseName] then
360+
self.baseName = baseName
361+
if not (self.rarity == "NORMAL" or self.rarity == "MAGIC") then
362+
self.title = self.name
363+
end
364+
self.type = data.itemBases[baseName].type
365+
self.base = data.itemBases[self.baseName]
366+
self.affixes = (self.base.subType and data.itemMods[self.base.type..self.base.subType])
367+
or data.itemMods[self.base.type]
368+
or data.itemMods.Item
369+
self.enchantments = data.enchantments[self.base.type]
370+
self.corruptable = self.base.type ~= "Flask" and self.base.subType ~= "Cluster"
371+
self.influenceTags = data.specialBaseTags[self.type]
372+
self.canBeInfluenced = self.influenceTags
373+
self.clusterJewel = data.clusterJewels and data.clusterJewels.jewels[self.baseName]
374+
self.requirements.str = self.base.req.str or 0
375+
self.requirements.dex = self.base.req.dex or 0
376+
self.requirements.int = self.base.req.int or 0
377+
local maxReq = m_max(self.requirements.str, self.requirements.dex, self.requirements.int)
378+
self.defaultSocketColor = (maxReq == self.requirements.dex and "G") or (maxReq == self.requirements.int and "B") or "R"
379+
if self.base.flask and self.base.flask.buff and not flaskBuffLines then
380+
flaskBuffLines = { }
381+
for _, line in ipairs(self.base.flask.buff) do
382+
flaskBuffLines[line] = true
383+
local modList, extra = modLib.parseMod(line)
384+
t_insert(self.buffModLines, { line = line, extra = extra, modList = modList or { } })
385+
end
386+
end
387+
end
370388
local fractured = line:match("{fractured}") or line:match(" %(fractured%)")
371389
local rangeSpec = line:match("{range:([%d.]+)}")
372390
local enchant = line:match(" %(enchant%)")
@@ -452,6 +470,9 @@ function ItemClass:ParseRaw(raw)
452470
end
453471
l = l + 1
454472
end
473+
if self.baseName and self.title then
474+
self.name = self.title .. ", " .. self.baseName:gsub(" %(.+%)","")
475+
end
455476
if self.base and not self.requirements.level then
456477
if importedLevelReq and #self.sockets == 0 then
457478
-- Requirements on imported items can only be trusted for items with no sockets
@@ -618,6 +639,18 @@ function ItemClass:BuildRaw()
618639
end
619640
t_insert(rawLines, "Selected Variant: "..self.variant)
620641

642+
local hasVariantBases = false
643+
local i = 1
644+
for _, variantName in ipairs(self.variantList) do
645+
if data.itemBases[variantName] then
646+
t_insert(rawLines, "{variant:"..i.."}"..variantName)
647+
hasVariantBases = true
648+
end
649+
i = i + 1
650+
end
651+
if not hasVariantBases then
652+
t_insert(rawLines, self.baseName)
653+
end
621654
if self.hasAltVariant then
622655
t_insert(rawLines, "Has Alt Variant: true")
623656
t_insert(rawLines, "Selected Alt Variant: "..self.variantAlt)
@@ -852,23 +885,25 @@ function ItemClass:BuildModListForSlotNum(baseList, slotNum)
852885
end
853886
end
854887
end
888+
local extraQuality = sumLocal(modList, "Quality", "BASE", 0)
855889
if self.quality then
856-
modList:NewMod("Multiplier:QualityOn"..slotName, "BASE", self.quality, "Quality")
890+
modList:NewMod("Multiplier:QualityOn"..slotName, "BASE", self.quality + extraQuality, "Quality")
857891
end
858892
if self.base.weapon then
859893
local weaponData = { }
860894
self.weaponData[slotNum] = weaponData
861895
weaponData.type = self.base.type
862896
weaponData.name = self.name
863-
weaponData.AttackSpeedInc = sumLocal(modList, "Speed", "INC", ModFlag.Attack) + m_floor(self.quality / 8 * sumLocal(modList, "AlternateQualityLocalAttackSpeedPer8Quality", "INC", 0))
897+
weaponData.quality = extraQuality + self.quality
898+
weaponData.AttackSpeedInc = sumLocal(modList, "Speed", "INC", ModFlag.Attack) + m_floor(weaponData.quality / 8 * sumLocal(modList, "AlternateQualityLocalAttackSpeedPer8Quality", "INC", 0))
864899
weaponData.AttackRate = round(self.base.weapon.AttackRateBase * (1 + weaponData.AttackSpeedInc / 100), 2)
865-
weaponData.range = self.base.weapon.Range + sumLocal(modList, "WeaponRange", "BASE", 0) + m_floor(self.quality / 10 * sumLocal(modList, "AlternateQualityLocalWeaponRangePer10Quality", "BASE", 0))
900+
weaponData.range = self.base.weapon.Range + sumLocal(modList, "WeaponRange", "BASE", 0) + m_floor(weaponData.quality / 10 * sumLocal(modList, "AlternateQualityLocalWeaponRangePer10Quality", "BASE", 0))
866901
for _, dmgType in pairs(dmgTypeList) do
867902
local min = (self.base.weapon[dmgType.."Min"] or 0) + sumLocal(modList, dmgType.."Min", "BASE", 0)
868903
local max = (self.base.weapon[dmgType.."Max"] or 0) + sumLocal(modList, dmgType.."Max", "BASE", 0)
869904
if dmgType == "Physical" then
870905
local physInc = sumLocal(modList, "PhysicalDamage", "INC", 0)
871-
local qualityScalar = self.quality
906+
local qualityScalar = weaponData.quality
872907
if sumLocal(modList, "AlternateQualityWeapon", "BASE", 0) > 0 then
873908
qualityScalar = 0
874909
end
@@ -885,7 +920,7 @@ function ItemClass:BuildModListForSlotNum(baseList, slotNum)
885920
end
886921
end
887922
end
888-
weaponData.CritChance = round(self.base.weapon.CritChanceBase * (1 + (sumLocal(modList, "CritChance", "INC", 0) + m_floor(self.quality / 4 * sumLocal(modList, "AlternateQualityLocalCritChancePer4Quality", "INC", 0))) / 100), 2)
923+
weaponData.CritChance = round(self.base.weapon.CritChanceBase * (1 + (sumLocal(modList, "CritChance", "INC", 0) + m_floor(weaponData.quality / 4 * sumLocal(modList, "AlternateQualityLocalCritChancePer4Quality", "INC", 0))) / 100), 2)
889924
for _, value in ipairs(modList:List(nil, "WeaponData")) do
890925
weaponData[value.key] = value.value
891926
end
@@ -908,6 +943,7 @@ function ItemClass:BuildModListForSlotNum(baseList, slotNum)
908943
end
909944
elseif self.base.armour then
910945
local armourData = self.armourData
946+
armourData.quality = self.quality + extraQuality
911947
local armourBase = sumLocal(modList, "Armour", "BASE", 0) + (self.base.armour.ArmourBase or 0)
912948
local armourEvasionBase = sumLocal(modList, "ArmourAndEvasion", "BASE", 0)
913949
local evasionBase = sumLocal(modList, "Evasion", "BASE", 0) + (self.base.armour.EvasionBase or 0)
@@ -921,7 +957,7 @@ function ItemClass:BuildModListForSlotNum(baseList, slotNum)
921957
local energyShieldInc = sumLocal(modList, "EnergyShield", "INC", 0)
922958
local armourEnergyShieldInc = sumLocal(modList, "ArmourAndEnergyShield", "INC", 0)
923959
local defencesInc = sumLocal(modList, "Defences", "INC", 0)
924-
local qualityScalar = self.quality
960+
local qualityScalar = armourData.quality
925961
if sumLocal(modList, "AlternateQualityArmour", "BASE", 0) > 0 then
926962
qualityScalar = 0
927963
end
@@ -940,27 +976,28 @@ function ItemClass:BuildModListForSlotNum(baseList, slotNum)
940976
elseif self.base.flask then
941977
local flaskData = self.flaskData
942978
local durationInc = sumLocal(modList, "Duration", "INC", 0)
979+
flaskData.quality = self.quality + extraQuality
943980
if self.base.flask.life or self.base.flask.mana then
944981
-- Recovery flask
945982
flaskData.instantPerc = sumLocal(modList, "FlaskInstantRecovery", "BASE", 0)
946983
local recoveryMod = 1 + sumLocal(modList, "FlaskRecovery", "INC", 0) / 100
947984
local rateMod = 1 + sumLocal(modList, "FlaskRecoveryRate", "INC", 0) / 100
948985
flaskData.duration = self.base.flask.duration * (1 + durationInc / 100) / rateMod
949986
if self.base.flask.life then
950-
flaskData.lifeBase = self.base.flask.life * (1 + self.quality / 100) * recoveryMod
987+
flaskData.lifeBase = self.base.flask.life * (1 + flaskData.quality / 100) * recoveryMod
951988
flaskData.lifeInstant = flaskData.lifeBase * flaskData.instantPerc / 100
952989
flaskData.lifeGradual = flaskData.lifeBase * (1 - flaskData.instantPerc / 100) * (1 + durationInc / 100)
953990
flaskData.lifeTotal = flaskData.lifeInstant + flaskData.lifeGradual
954991
end
955992
if self.base.flask.mana then
956-
flaskData.manaBase = self.base.flask.mana * (1 + self.quality / 100) * recoveryMod
993+
flaskData.manaBase = self.base.flask.mana * (1 + flaskData.quality / 100) * recoveryMod
957994
flaskData.manaInstant = flaskData.manaBase * flaskData.instantPerc / 100
958995
flaskData.manaGradual = flaskData.manaBase * (1 - flaskData.instantPerc / 100) * (1 + durationInc / 100)
959996
flaskData.manaTotal = flaskData.manaInstant + flaskData.manaGradual
960997
end
961998
else
962999
-- Utility flask
963-
flaskData.duration = self.base.flask.duration * (1 + (durationInc + self.quality) / 100)
1000+
flaskData.duration = self.base.flask.duration * (1 + (durationInc + flaskData.quality) / 100)
9641001
end
9651002
flaskData.chargesMax = self.base.flask.chargesMax + sumLocal(modList, "FlaskCharges", "BASE", 0)
9661003
flaskData.chargesUsed = m_floor(self.base.flask.chargesUsed * (1 + sumLocal(modList, "FlaskChargesUsed", "INC", 0) / 100))

0 commit comments

Comments
 (0)