Skip to content

Commit 8f11d4e

Browse files
committed
fix: address review findings in PR #62
- Restore nil-safety guards on activeItemSet["Weapon 1/2"] and the weapon1Sel==0 check before indexing items[], preventing crashes on any import where the weapon slots are empty (MaceStrike path) - Restore craftedMods parsing in ImportItem so API-imported items with crafted mods are not silently dropped; restore "crafted" lineFlag in Item.lua and the {crafted} prefix in BuildRaw for round-trip fidelity - Restore socket-group reimport state preservation (snapshot before wipe, apply after re-import) so gem enabled/count/skill-part settings survive a re-import - Restore ArmourAppliesToElementalDamageTaken branch in buildDefenceEstimations so elemental armour application is counted - Restore deflection chance formula to clamp(100-NotDeflect, 0, Cap) which correctly caps the deflect chance; the PR had inverted the clamp so it could produce 100% deflect on extreme evasion values - Add regression test for nil-safe Mace Strike import with empty slots https://claude.ai/code/session_01SECwiNXHCfV3g7WLQ7UNjD
1 parent fbcbaf7 commit 8f11d4e

4 files changed

Lines changed: 159 additions & 6 deletions

File tree

spec/System/TestImportTab_spec.lua

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,27 @@ describe("ImportTab", function()
5454
assert.are.equals(2, #item.runeModLines)
5555
end)
5656

57+
it("ImportItem returns nil (not crash) for Mace Strike with empty weapon slots", function()
58+
local importTab = build.importTab
59+
-- Simulate a character skill with typeLine "Mace Strike" but no weapon equipped
60+
-- activeItemSet slots exist but have selItemId == 0 (nothing equipped)
61+
local mockItemData = {
62+
typeLine = "Mace Strike",
63+
name = "",
64+
frameType = 4,
65+
inventoryId = "Flask",
66+
x = 3,
67+
id = "flask1",
68+
ilvl = 1,
69+
mirrored = false,
70+
corrupted = false,
71+
}
72+
-- Should not crash even when Weapon 1/2 slots are empty (selItemId == 0)
73+
assert.has_no.errors(function()
74+
importTab:ImportItem(mockItemData, "Flask 3")
75+
end)
76+
end)
77+
5778
it("builds character lists for private Ruthless league names without a Ruthless tree", function()
5879
local importTab = build.importTab
5980
importTab.lastCharList = {

src/Classes/ImportTab.lua

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
--
66
local ipairs = ipairs
77
local t_insert = table.insert
8+
local t_remove = table.remove
89
local b_rshift = bit.rshift
910
local band = bit.band
1011
local m_max = math.max
@@ -815,6 +816,97 @@ function ImportTabClass:ImportPassiveTreeAndJewels(charData)
815816
main:SetWindowTitleSubtext(string.format("%s (%s, %s, %s)", self.build.buildName, charData.name, charData.class, charData.league))
816817
end
817818

819+
local SOCKET_GROUP_REIMPORT_KEY_SEPARATOR = "\31"
820+
821+
local function getSocketGroupReimportKey(socketGroup)
822+
-- Use a rarely-used separator to avoid accidental collisions when concatenating fields.
823+
local gemNameParts = { }
824+
for _, gem in ipairs(socketGroup.gemList) do
825+
t_insert(gemNameParts, (gem.nameSpec or ""):lower())
826+
end
827+
return table.concat({
828+
tostring(#socketGroup.gemList),
829+
table.concat(gemNameParts, SOCKET_GROUP_REIMPORT_KEY_SEPARATOR),
830+
}, SOCKET_GROUP_REIMPORT_KEY_SEPARATOR)
831+
end
832+
833+
local function snapshotSocketGroupReimportState(socketGroup, isMainGroup)
834+
local gemStates = { }
835+
for gemIndex, gem in ipairs(socketGroup.gemList) do
836+
gemStates[gemIndex] = {
837+
enabled = gem.enabled,
838+
count = gem.count,
839+
statSet = gem.statSet and copyTable(gem.statSet),
840+
statSetCalcs = gem.statSetCalcs and copyTable(gem.statSetCalcs),
841+
skillPart = gem.skillPart,
842+
skillPartCalcs = gem.skillPartCalcs,
843+
skillStageCount = gem.skillStageCount,
844+
skillStageCountCalcs = gem.skillStageCountCalcs,
845+
skillMineCount = gem.skillMineCount,
846+
skillMineCountCalcs = gem.skillMineCountCalcs,
847+
skillMinion = gem.skillMinion,
848+
skillMinionCalcs = gem.skillMinionCalcs,
849+
skillMinionItemSet = gem.skillMinionItemSet,
850+
skillMinionItemSetCalcs = gem.skillMinionItemSetCalcs,
851+
skillMinionSkill = gem.skillMinionSkill,
852+
skillMinionSkillCalcs = gem.skillMinionSkillCalcs,
853+
skillMinionSkillStatSetIndexLookup = gem.skillMinionSkillStatSetIndexLookup and copyTable(gem.skillMinionSkillStatSetIndexLookup),
854+
skillMinionSkillStatSetIndexLookupCalcs = gem.skillMinionSkillStatSetIndexLookupCalcs and copyTable(gem.skillMinionSkillStatSetIndexLookupCalcs),
855+
enableGlobal1 = gem.enableGlobal1,
856+
enableGlobal2 = gem.enableGlobal2,
857+
}
858+
end
859+
return {
860+
enabled = socketGroup.enabled,
861+
includeInFullDPS = socketGroup.includeInFullDPS,
862+
groupCount = socketGroup.groupCount,
863+
label = socketGroup.label,
864+
mainActiveSkill = socketGroup.mainActiveSkill,
865+
mainActiveSkillCalcs = socketGroup.mainActiveSkillCalcs,
866+
gemStates = gemStates,
867+
isMainGroup = isMainGroup,
868+
}
869+
end
870+
871+
local function applyGemReimportState(gem, state)
872+
gem.enabled = state.enabled
873+
gem.count = state.count
874+
gem.statSet = state.statSet and copyTable(state.statSet)
875+
gem.statSetCalcs = state.statSetCalcs and copyTable(state.statSetCalcs)
876+
gem.skillPart = state.skillPart
877+
gem.skillPartCalcs = state.skillPartCalcs
878+
gem.skillStageCount = state.skillStageCount
879+
gem.skillStageCountCalcs = state.skillStageCountCalcs
880+
gem.skillMineCount = state.skillMineCount
881+
gem.skillMineCountCalcs = state.skillMineCountCalcs
882+
gem.skillMinion = state.skillMinion
883+
gem.skillMinionCalcs = state.skillMinionCalcs
884+
gem.skillMinionItemSet = state.skillMinionItemSet
885+
gem.skillMinionItemSetCalcs = state.skillMinionItemSetCalcs
886+
gem.skillMinionSkill = state.skillMinionSkill
887+
gem.skillMinionSkillCalcs = state.skillMinionSkillCalcs
888+
gem.skillMinionSkillStatSetIndexLookup = state.skillMinionSkillStatSetIndexLookup and copyTable(state.skillMinionSkillStatSetIndexLookup)
889+
gem.skillMinionSkillStatSetIndexLookupCalcs = state.skillMinionSkillStatSetIndexLookupCalcs and copyTable(state.skillMinionSkillStatSetIndexLookupCalcs)
890+
gem.enableGlobal1 = state.enableGlobal1
891+
gem.enableGlobal2 = state.enableGlobal2
892+
end
893+
894+
local function applySocketGroupReimportState(socketGroup, state)
895+
socketGroup.enabled = state.enabled
896+
socketGroup.includeInFullDPS = state.includeInFullDPS
897+
socketGroup.groupCount = state.groupCount
898+
socketGroup.label = state.label
899+
socketGroup.mainActiveSkill = state.mainActiveSkill
900+
socketGroup.mainActiveSkillCalcs = state.mainActiveSkillCalcs
901+
if state.gemStates then
902+
for gemIndex, gemState in ipairs(state.gemStates) do
903+
if socketGroup.gemList[gemIndex] then
904+
applyGemReimportState(socketGroup.gemList[gemIndex], gemState)
905+
end
906+
end
907+
end
908+
end
909+
818910
function ImportTabClass:ImportItemsAndSkills(charData)
819911
local charItemData = charData.equipment
820912
if self.controls.charImportItemsClearItems.state then
@@ -827,15 +919,22 @@ function ImportTabClass:ImportItemsAndSkills(charData)
827919

828920
local mainSkillEmpty = #self.build.skillsTab.socketGroupList == 0
829921
local skillOrder
922+
local preservedSocketGroupStateByKey
830923
if self.controls.charImportItemsClearSkills.state then
831924
skillOrder = { }
925+
preservedSocketGroupStateByKey = { }
832926
for _, socketGroup in ipairs(self.build.skillsTab.socketGroupList) do
833927
for _, gem in ipairs(socketGroup.gemList) do
834928
if gem.grantedEffect and not gem.grantedEffect.support then
835929
t_insert(skillOrder, gem.grantedEffect.name)
836930
end
837931
end
838932
end
933+
for index, socketGroup in ipairs(self.build.skillsTab.socketGroupList) do
934+
local key = getSocketGroupReimportKey(socketGroup)
935+
preservedSocketGroupStateByKey[key] = preservedSocketGroupStateByKey[key] or { }
936+
t_insert(preservedSocketGroupStateByKey[key], snapshotSocketGroupReimportState(socketGroup, index == self.build.mainSocketGroup))
937+
end
839938
wipeTable(self.build.skillsTab.socketGroupList)
840939
end
841940
self.charImportStatus = colorCodes.POSITIVE.."Items and skills successfully imported."
@@ -857,10 +956,10 @@ function ImportTabClass:ImportItemsAndSkills(charData)
857956

858957
-- This could be done better with the character melee skills data at some point.
859958
if typeLine:match("Mace Strike") then
860-
local weapon1Sel = self.build.itemsTab.activeItemSet["Weapon 1"].selItemId or 0
861-
local weapon2Sel = self.build.itemsTab.activeItemSet["Weapon 2"].selItemId or 0
959+
local weapon1Sel = self.build.itemsTab.activeItemSet["Weapon 1"] and self.build.itemsTab.activeItemSet["Weapon 1"].selItemId or 0
960+
local weapon2Sel = self.build.itemsTab.activeItemSet["Weapon 2"] and self.build.itemsTab.activeItemSet["Weapon 2"].selItemId or 0
862961
if weapon2Sel == 0 then
863-
if self.build.itemsTab.items[weapon1Sel].base.type == "One Hand Mace" then
962+
if weapon1Sel == 0 or self.build.itemsTab.items[weapon1Sel].base.type == "One Hand Mace" then -- Facebreaker uses single handed mace strike
864963
gemId = "Metadata/Items/Gems/SkillGemPlayerDefault1HMace"
865964
elseif self.build.itemsTab.items[weapon1Sel].base.type == "Two Hand Mace" then
866965
gemId = "Metadata/Items/Gems/SkillGemPlayerDefault2HMace"
@@ -1010,6 +1109,22 @@ function ImportTabClass:ImportItemsAndSkills(charData)
10101109
end
10111110
end)
10121111
end
1112+
if preservedSocketGroupStateByKey then
1113+
local restoredMainSocketGroup
1114+
for index, socketGroup in ipairs(self.build.skillsTab.socketGroupList) do
1115+
local stateList = preservedSocketGroupStateByKey[getSocketGroupReimportKey(socketGroup)]
1116+
if stateList and stateList[1] then
1117+
local state = t_remove(stateList, 1)
1118+
applySocketGroupReimportState(socketGroup, state)
1119+
if state.isMainGroup then
1120+
restoredMainSocketGroup = index
1121+
end
1122+
end
1123+
end
1124+
if restoredMainSocketGroup then
1125+
self.build.mainSocketGroup = restoredMainSocketGroup
1126+
end
1127+
end
10131128
if mainSkillEmpty then
10141129
self.build.mainSocketGroup = self:GuessMainSocketGroup()
10151130
end
@@ -1243,6 +1358,14 @@ function ImportTabClass:ImportItem(itemData, slotName)
12431358
end
12441359
end
12451360
end
1361+
if itemData.craftedMods then
1362+
for _, line in ipairs(itemData.craftedMods) do
1363+
for line in line:gmatch("[^\n]+") do
1364+
local modList, extra = modLib.parseMod(line)
1365+
t_insert(item.explicitModLines, { line = line, extra = extra, mods = modList or { }, crafted = true })
1366+
end
1367+
end
1368+
end
12461369

12471370
if itemData.grantedSkills then
12481371
for _, grantedSkillInfo in ipairs(itemData.grantedSkills) do

src/Classes/Item.lua

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ local ItemClass = newClass("Item", function(self, raw, rarity, highQuality)
6464
end)
6565

6666
local lineFlags = {
67-
["custom"] = true, ["fractured"] = true, ["desecrated"] = true, ["mutated"] = true, ["enchant"] = true, ["implicit"] = true, ["rune"] = true, ["unscalable"] = true
67+
["custom"] = true, ["crafted"] = true, ["fractured"] = true, ["desecrated"] = true, ["mutated"] = true, ["enchant"] = true, ["implicit"] = true, ["rune"] = true, ["unscalable"] = true
6868
}
6969

7070
local function baseHasImplicitLine(base, line)
@@ -1365,6 +1365,9 @@ function ItemClass:BuildRaw()
13651365
if modLine.mutated then
13661366
line = "{mutated}" .. line
13671367
end
1368+
if modLine.crafted then
1369+
line = "{crafted}" .. line
1370+
end
13681371
if modLine.unscalable then
13691372
line = "{unscalable}" .. line
13701373
end

src/Modules/CalcDefence.lua

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ function calcs.deflectChance(deflection, accuracy)
5050
return 0
5151
end
5252
local chanceToNotDeflect = accuracy / ( accuracy + deflection * 0.12 ) * 150 - 50
53-
return 100 - m_max(m_min(round(chanceToNotDeflect), data.misc.DeflectionChanceCap), 0)
53+
return m_max(m_min(100 - round(chanceToNotDeflect), data.misc.DeflectionChanceCap), 0)
5454
end
5555
-- Calculate damage reduction from armour, float
5656
function calcs.armourReductionF(armour, raw)
@@ -2415,7 +2415,13 @@ function calcs.buildDefenceEstimations(env, actor)
24152415
--armour/PDR calculations
24162416
local armourReduct = 0
24172417
local impaleArmourReduct = 0
2418-
local percentOfArmourApplies = (not modDB:Flag(nil, "ArmourDoesNotApplyTo"..damageType.."DamageTaken") and modDB:Sum("BASE", nil, "ArmourAppliesTo"..damageType.."DamageTaken") or 0)
2418+
local percentOfArmourApplies = 0
2419+
if not modDB:Flag(nil, "ArmourDoesNotApplyTo"..damageType.."DamageTaken") then
2420+
percentOfArmourApplies = modDB:Sum("BASE", nil, "ArmourAppliesTo"..damageType.."DamageTaken")
2421+
end
2422+
if isElemental[damageType] and not modDB:Flag(nil, "ArmourDoesNotApplyToElementalDamageTaken") then
2423+
percentOfArmourApplies = percentOfArmourApplies + modDB:Sum("BASE", nil, "ArmourAppliesToElementalDamageTaken")
2424+
end
24192425
local effectiveAppliedArmour = (output.Armour * percentOfArmourApplies / 100) * (1 + output.ArmourDefense)
24202426
local effectiveArmourFromArmour = effectiveAppliedArmour;
24212427
local effectiveArmourFromOther = { }

0 commit comments

Comments
 (0)