Skip to content

Commit 538e2fb

Browse files
Port unique updater scripts from PoB2
Port the unique item updater scripts from PathOfBuilding-PoE2 (PRs #54, #65, #175) and fix multiple bugs for PoE1 compatibility. Scripts added: - uTextToMods.lua: converts unique item text to mod IDs (run when adding new uniques) - uModsToText.lua: converts mod IDs back to text with fresh GGPK data (run when game data updates) Changes to existing files: - mods.lua: add ModItemExclusive.lua and ModTextMap.lua generation - statdesc.lua: handle '!' negation limits and ranges starting at 0 Bug fixes over the original PoE2 scripts: - Fix uModsToText.lua not flushing mods for last item in each file - Fix mod ID regex misidentifying base type names containing hyphens (e.g. "Two-Point Arrow Quiver") as legacy mod ranges - Fix unresolved text lines losing position among ordered mods - Fix nil access on statOrder when processing legacy-only mods - Fix uTextToMods.lua greedy tag stripping pattern and mod selection - Make usedMods local and move modTextMap load outside loop - Enable all 20 PoE1 item types (original only had axe enabled) Bug fix in Item.lua: - Fix excluded/exclude variable name mismatch creating accidental global - Fix wrong Lua pattern ^%[a ]+ to correct ^[%a ]+ in jewel radius - Fix space indentation to tabs in jewel radius block
1 parent a51cdca commit 538e2fb

5 files changed

Lines changed: 331 additions & 21 deletions

File tree

src/Classes/Item.lua

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,12 @@ local function getTagBasedModifiers(tagName, itemSlotName)
104104
if data.itemTagSpecial[tagName] and data.itemTagSpecial[tagName][itemSlotName] then
105105
for _, specialMod in ipairs(data.itemTagSpecial[tagName][itemSlotName]) do
106106
if dv:lower():find(specialMod:lower()) then
107-
exclude = true
107+
excluded = true
108108
break
109109
end
110110
end
111111
end
112-
if exclude then
112+
if excluded then
113113
found = true
114114
break
115115
end
@@ -131,12 +131,12 @@ local function getTagBasedModifiers(tagName, itemSlotName)
131131
if data.itemTagSpecial[tagName] and data.itemTagSpecial[tagName][itemSlotName] then
132132
for _, specialMod in ipairs(data.itemTagSpecial[tagName][itemSlotName]) do
133133
if dv:lower():find(specialMod:lower()) then
134-
exclude = true
134+
excluded = true
135135
break
136136
end
137137
end
138138
end
139-
if exclude then
139+
if excluded then
140140
found = true
141141
break
142142
end
@@ -444,16 +444,16 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
444444
end
445445
end
446446
elseif specName == "Radius" and self.type == "Jewel" then
447-
self.jewelRadiusLabel = specVal:match("^%a+")
448-
if specVal:match("^%a+") == "Variable" then
449-
-- Jewel radius is variable and must be read from it's mods instead after they are parsed
450-
deferJewelRadiusIndexAssignment = true
451-
else
452-
for index, data in pairs(data.jewelRadius) do
453-
if specVal:match("^%a+") == data.label then
454-
self.jewelRadiusIndex = index
455-
break
456-
end
447+
self.jewelRadiusLabel = specVal:match("^[%a ]+")
448+
if specVal:match("^[%a ]+") == "Variable" then
449+
-- Jewel radius is variable and must be read from it's mods instead after they are parsed
450+
deferJewelRadiusIndexAssignment = true
451+
else
452+
for index, data in pairs(data.jewelRadius) do
453+
if specVal:match("^[%a ]+") == data.label then
454+
self.jewelRadiusIndex = index
455+
break
456+
end
457457
end
458458
end
459459
elseif specName == "Limited to" and self.type == "Jewel" then

src/Export/Scripts/mods.lua

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@ local function writeMods(outName, condFunc)
3333
print("[Jewel]: Skipping '" .. mod.Id .. "'")
3434
goto continue
3535
end
36-
elseif mod.Family[1] and mod.Family[1].Id ~= "AuraBonus" and mod.Family[1].Id ~= "ArbalestBonus" and mod.GenerationType == 3 and not (mod.Domain == 16 or (mod.Domain == 1 and mod.Id:match("^Synthesis") or mod.Id:match("^MutatedUnique") or (mod.Family[2] and mod.Family[2].Id:match("MatchedInfluencesTier")))) then
37-
goto continue
3836
end
3937
local stats, orders = describeMod(mod)
4038
if #orders > 0 then
@@ -137,8 +135,10 @@ end
137135

138136
writeMods("../Data/ModItem.lua", function(mod)
139137
return (mod.Domain == 1 or mod.Domain == 16)
140-
and (mod.GenerationType == 1 or mod.GenerationType == 2 or (mod.GenerationType == 3 and (not mod.Id:match("^MutatedUnique")) and (mod.Id:match("^Synthesis") or (mod.Family[1].Id ~= "AuraBonus" and mod.Family[1].Id ~= "ArbalestBonus") and not (mod.Family[2] and mod.Family[2].Id:match("MatchedInfluencesTier")))) or mod.GenerationType == 5
141-
or mod.GenerationType == 25 or mod.GenerationType == 24 or mod.GenerationType == 28 or mod.GenerationType == 29)
138+
and (mod.GenerationType == 1 or mod.GenerationType == 2
139+
or (mod.GenerationType == 3 and mod.Domain == 1 and mod.Id:match("^Synthesis"))
140+
or (mod.GenerationType == 3 and mod.Domain == 16)
141+
or mod.GenerationType == 5 or mod.GenerationType == 25 or mod.GenerationType == 24 or mod.GenerationType == 28 or mod.GenerationType == 29)
142142
and not mod.Id:match("^Hellscape[UpDown]+sideMap") -- Exclude Scourge map mods
143143
and not mod.Id:match("Royale")
144144
and #mod.AuraFlags == 0
@@ -147,7 +147,7 @@ writeMods("../Data/ModFlask.lua", function(mod)
147147
return mod.Domain == 2 and (mod.GenerationType == 1 or mod.GenerationType == 2)
148148
end)
149149
writeMods("../Data/ModTincture.lua", function(mod)
150-
return (mod.Domain == 34) and (mod.GenerationType == 1 or mod.GenerationType == 2 or mod.GenerationType == 3)
150+
return (mod.Domain == 34) and (mod.GenerationType == 1 or mod.GenerationType == 2)
151151
end)
152152
writeMods("../Data/ModJewel.lua", function(mod)
153153
return (mod.Domain == 10 or mod.Domain == 16) and (mod.GenerationType == 1 or mod.GenerationType == 2 or mod.GenerationType == 5)
@@ -173,6 +173,11 @@ end)
173173
writeMods("../Data/ModNecropolis.lua", function(mod)
174174
return mod.Domain == 1 and mod.Id:match("^NecropolisCrafting")
175175
end)
176+
writeMods("../Data/ModItemExclusive.lua", function(mod) -- contains primarily uniques and items implicits but also other mods only available on a single base or unique.
177+
return (mod.Domain == 1 or mod.Domain == 2 or mod.Domain == 10 or mod.Domain == 21 or mod.Domain == 34) and mod.GenerationType == 3
178+
and (mod.Family[1] and mod.Family[1].Id ~= "AuraBonus")
179+
and not mod.Id:match("^Synthesis") and not mod.Id:match("Royale") and not mod.Id:match("Cowards") and not mod.Id:match("Map") and not mod.Id:match("Ultimatum")
180+
end)
176181
writeMods("../Data/ModGraft.lua", function(mod)
177182
return mod.Domain == 38 and (mod.GenerationType == 1 or mod.GenerationType == 2 or mod.GenerationType == 5)
178183
end)
@@ -183,4 +188,26 @@ writeMods("../Data/ModFoulborn.lua", function(mod)
183188
return mod.Domain == 1 and mod.GenerationType == 3 and mod.Id:match("^MutatedUnique")
184189
end)
185190

191+
-- Generate a unique mod mapping from text to mod
192+
local out = io.open("Uniques/ModTextMap.lua", "w")
193+
local modTextMap = {}
194+
out:write('-- This file is automatically generated, do not edit!\n')
195+
out:write('-- Item data (c) Grinding Gear Games\n\nreturn {\n')
196+
for modName, mod in pairs(LoadModule("../Data/ModItemExclusive.lua")) do
197+
if modTextMap[mod[1]] then
198+
table.insert(modTextMap[mod[1]], modName)
199+
else
200+
modTextMap[mod[1]] = { modName }
201+
end
202+
end
203+
for key, value in pairs(modTextMap) do
204+
out:write('\t["' .. key .. '"] = { ')
205+
for _, modName in ipairs(value) do
206+
out:write('"' .. modName .. '", ')
207+
end
208+
out:write('},\n')
209+
end
210+
out:write('\n}')
211+
out:close()
212+
186213
print("Mods exported.")

src/Export/Scripts/uModsToText.lua

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
if not table.containsId then
2+
dofile("Scripts/mods.lua")
3+
end
4+
local catalystTags = {
5+
["attack"] = true,
6+
["speed"] = true,
7+
["life"] = true,
8+
["mana"] = true,
9+
["caster"] = true,
10+
["attribute"] = true,
11+
["physical"] = true,
12+
["fire"] = true,
13+
["cold"] = true,
14+
["lightning"] = true,
15+
["chaos"] = true,
16+
["defences"] = true,
17+
}
18+
local itemTypes = {
19+
"axe",
20+
"bow",
21+
"claw",
22+
"dagger",
23+
"fishing",
24+
"mace",
25+
"staff",
26+
"sword",
27+
"wand",
28+
"helmet",
29+
"body",
30+
"gloves",
31+
"boots",
32+
"shield",
33+
"quiver",
34+
"amulet",
35+
"ring",
36+
"belt",
37+
"jewel",
38+
"flask",
39+
"tincture",
40+
}
41+
local function writeMods(out, statOrder)
42+
local orders = { }
43+
for order, _ in pairs(statOrder) do
44+
table.insert(orders, order)
45+
end
46+
table.sort(orders)
47+
for _, order in pairs(orders) do
48+
for _, line in ipairs(statOrder[order]) do
49+
out:write(line, "\n")
50+
end
51+
end
52+
end
53+
54+
local uniqueMods = LoadModule("../Data/ModItemExclusive.lua")
55+
for _, name in ipairs(itemTypes) do
56+
local out = io.open("../Data/Uniques/"..name..".lua", "w")
57+
local statOrder = {}
58+
local postModLines = {}
59+
local modLines = 0
60+
local implicits
61+
local nextOrder = 100000
62+
for line in io.lines("Uniques/"..name..".lua") do
63+
if implicits then -- remove 1 downs to 0
64+
implicits = implicits - 1
65+
end
66+
local specName, specVal = line:match("^([%a ]+): (.+)$")
67+
if line:match("]],") then -- start new unique
68+
writeMods(out, statOrder)
69+
for _, line in ipairs(postModLines) do
70+
out:write(line, "\n")
71+
end
72+
out:write(line, "\n")
73+
statOrder = { }
74+
postModLines = { }
75+
modLines = 0
76+
nextOrder = 100000
77+
elseif not specName then
78+
local prefix = ""
79+
local variantString = line:match("({variant:[%d,]+})")
80+
local fractured = line:match("({fractured})") or ""
81+
local cleanLine = line:gsub("{.-}", "")
82+
-- Check if this is a mod ID: purely alphanumeric+underscore, optionally followed by [num,num] ranges
83+
local modName = cleanLine:match("^([%a%d_]+)%[") or cleanLine:match("^([%a%d_]+)$")
84+
local legacy = modName and cleanLine:sub(#modName + 1) or ""
85+
-- Legacy ranges must contain actual brackets, not just stray characters
86+
if legacy ~= "" and not legacy:match("%[") then
87+
legacy = ""
88+
modName = nil
89+
end
90+
local mod = modName and uniqueMods[modName]
91+
if mod or (modName and legacy ~= "") then
92+
modLines = modLines + 1
93+
if variantString then
94+
prefix = prefix ..variantString
95+
end
96+
97+
local tags = {}
98+
if mod then
99+
if isValueInArray({"amulet", "ring"}, name) then
100+
for _, tag in ipairs(mod.modTags) do
101+
if catalystTags[tag] then
102+
table.insert(tags, tag)
103+
end
104+
end
105+
end
106+
end
107+
if tags[1] then
108+
prefix = prefix.."{tags:"..table.concat(tags, ",").."}"
109+
end
110+
prefix = prefix..fractured
111+
local legacyMod
112+
if legacy ~= "" then
113+
local values = { }
114+
for range in legacy:gmatch("%b[]") do
115+
local min, max = range:match("%[([%d%-]+),([%d%-]+)%]")
116+
table.insert(values, { min = tonumber(min), max = tonumber(max) })
117+
end
118+
local mod = dat("Mods"):GetRow("Id", modName)
119+
if mod then
120+
local stats = { }
121+
for i = 1, 6 do
122+
if mod["Stat"..i] then
123+
stats[mod["Stat"..i].Id] = values[i]
124+
end
125+
end
126+
if mod.Type then
127+
stats.Type = mod.Type
128+
end
129+
legacyMod = describeStats(stats)
130+
else
131+
ConPrintf("Warning: Could not find mod data for legacy mod '%s' in %s", modName, name)
132+
end
133+
end
134+
local modText = legacyMod or mod
135+
if modText then
136+
for i, line in ipairs(modText) do
137+
local order = mod and mod.statOrder and mod.statOrder[i] or (nextOrder)
138+
nextOrder = nextOrder + 1
139+
if statOrder[order] then
140+
table.insert(statOrder[order], prefix..line)
141+
else
142+
statOrder[order] = { prefix..line }
143+
end
144+
end
145+
end
146+
else
147+
if modLines > 0 then -- treat as post line e.g. mirrored, or unresolved text mod
148+
-- Unresolved text lines get a sequential order to preserve position among mods
149+
if statOrder[nextOrder] then
150+
table.insert(statOrder[nextOrder], line)
151+
else
152+
statOrder[nextOrder] = { line }
153+
end
154+
nextOrder = nextOrder + 1
155+
else
156+
out:write(line, "\n")
157+
end
158+
end
159+
else
160+
if specName == "Implicits" then
161+
implicits = tonumber(specVal)
162+
end
163+
out:write(line, "\n")
164+
end
165+
if implicits and implicits == 0 then
166+
writeMods(out, statOrder)
167+
implicits = nil
168+
statOrder = { }
169+
modLines = 0
170+
end
171+
end
172+
writeMods(out, statOrder)
173+
for _, line in ipairs(postModLines) do
174+
out:write(line, "\n")
175+
end
176+
out:close()
177+
end
178+
179+
print("Unique text updated.")

src/Export/Scripts/uTextToMods.lua

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
if not table.containsId then
2+
dofile("Scripts/mods.lua")
3+
end
4+
5+
local itemTypes = {
6+
"axe",
7+
"bow",
8+
"claw",
9+
"dagger",
10+
"fishing",
11+
"mace",
12+
"staff",
13+
"sword",
14+
"wand",
15+
"helmet",
16+
"body",
17+
"gloves",
18+
"boots",
19+
"shield",
20+
"quiver",
21+
"amulet",
22+
"ring",
23+
"belt",
24+
"jewel",
25+
"flask",
26+
"tincture",
27+
}
28+
29+
local usedMods = {}
30+
local modTextMap = LoadModule("Uniques/ModTextMap.lua")
31+
32+
for _, name in pairs(itemTypes) do
33+
local out = io.open("Uniques/"..name..".lua", "w")
34+
for line in io.lines("../Data/Uniques/"..name..".lua") do
35+
local specName, specVal = line:match("^([%a ]+): (.+)$")
36+
if not specName and line ~= "]],[[" then
37+
local variants = line:match("{[vV]ariant:([%d,.]+)}")
38+
local fractured = line:match("({fractured})") or ""
39+
local modText = line:gsub("{.-}", ""):gsub("\xe2\x80\x93", "-") -- Clean tag prefixes and EM dash
40+
local possibleMods = modTextMap[modText]
41+
local gggMod
42+
if possibleMods then
43+
-- First pass: prefer mods that match the item type
44+
for _, modName in ipairs(possibleMods) do
45+
if modName:lower():match(name) then
46+
gggMod = modName
47+
usedMods[modName] = true
48+
break
49+
end
50+
end
51+
-- Second pass: prefer mods that haven't already been used
52+
if not gggMod then
53+
for _, modName in ipairs(possibleMods) do
54+
if not usedMods[modName] then
55+
gggMod = modName
56+
usedMods[modName] = true
57+
break
58+
end
59+
end
60+
end
61+
if not gggMod then
62+
gggMod = possibleMods[1]
63+
usedMods[gggMod] = true
64+
ConPrintf("Warning: Multiple possible mods for line '%s' in %s, using '%s'", modText, name, gggMod)
65+
end
66+
out:write(fractured)
67+
if variants then
68+
out:write("{variant:" .. variants:gsub("%.", ",") .. "}")
69+
end
70+
out:write(gggMod, "\n")
71+
else
72+
out:write(line, "\n")
73+
end
74+
else
75+
out:write(line, "\n")
76+
end
77+
end
78+
out:close()
79+
end
80+
81+
print("Unique mods exported.")

0 commit comments

Comments
 (0)