From 23adc0432ab1e0a88647566ffb9f8f59ee7ef0ad Mon Sep 17 00:00:00 2001 From: Lancej <1243476+Lancej@users.noreply.github.com> Date: Tue, 29 Aug 2023 21:16:33 -0500 Subject: [PATCH 1/4] Add item parse tests and fix nil BasePercentile --- spec/System/TestItemParse_spec.lua | 388 +++++++++++++++++++++++++++++ src/Classes/Item.lua | 14 ++ 2 files changed, 402 insertions(+) create mode 100644 spec/System/TestItemParse_spec.lua diff --git a/spec/System/TestItemParse_spec.lua b/spec/System/TestItemParse_spec.lua new file mode 100644 index 0000000000..623ac84c29 --- /dev/null +++ b/spec/System/TestItemParse_spec.lua @@ -0,0 +1,388 @@ +describe("TestItemParse", function() + local function raw(s, base) + base = base or "Plate Vest" + return "Rarity: Rare\nName\n"..base.."\n"..s + end + + it("Rarity", function() + local item = new("Item", "Rarity: Normal\nCoral Ring") + assert.are.equals("NORMAL", item.rarity) + item = new("Item", "Rarity: Magic\nCoral Ring") + assert.are.equals("MAGIC", item.rarity) + item = new("Item", "Rarity: Rare\nName\nCoral Ring") + assert.are.equals("RARE", item.rarity) + item = new("Item", "Rarity: Unique\nName\nCoral Ring") + assert.are.equals("UNIQUE", item.rarity) + item = new("Item", "Rarity: Unique\nName\nCoral Ring\nFoil Unique (Verdant)") + assert.are.equals("RELIC", item.rarity) + end) + + it("Superior/Synthesised", function() + local item = new("Item", raw("", "Superior Plate Vest")) + assert.are.equals("Plate Vest", item.baseName) + item = new("Item", raw("", "Synthesised Plate Vest")) + assert.are.equals("Plate Vest", item.baseName) + item = new("Item", raw("", "Superior Synthesised Plate Vest")) + assert.are.equals("Plate Vest", item.baseName) + end) + + it("Two-Toned Boots", function() + local item = new("Item", raw("", "Two-Toned Boots")) + assert.are.equals("Two-Toned Boots (Armour/Energy Shield)", item.baseName) + item = new("Item", raw("Armour: 10\nEnergy Shield: 10", "Two-Toned Boots")) + assert.are.equals("Two-Toned Boots (Armour/Energy Shield)", item.baseName) + item = new("Item", raw("Armour: 10\nEvasion Rating: 10", "Two-Toned Boots")) + assert.are.equals("Two-Toned Boots (Armour/Evasion)", item.baseName) + item = new("Item", raw("Evasion Rating: 10\nEnergy Shield: 10", "Two-Toned Boots")) + assert.are.equals("Two-Toned Boots (Evasion/Energy Shield)", item.baseName) + end) + + it("Unique ID", function() + local item = new("Item", raw("Unique ID: 40f9711d5bd7ad2bcbddaf71c705607aef0eecd3dcadaafec6c0192f79b82863")) + assert.are.equals("40f9711d5bd7ad2bcbddaf71c705607aef0eecd3dcadaafec6c0192f79b82863", item.uniqueID) + end) + + it("Item Level", function() + local item = new("Item", raw("Item Level: 10")) + assert.are.equals(10, item.itemLevel) + end) + + it("Quality", function() + local item = new("Item", raw("Quality: 10")) + assert.are.equals(10, item.quality) + item = new("Item", raw("Quality: +12% (augmented)")) + assert.are.equals(12, item.quality) + end) + + it("Sockets", function() + local item = new("Item", raw("Sockets: R-G R-B-W A")) + assert.are.same({ + { color = "R", group = 0 }, + { color = "G", group = 0 }, + { color = "R", group = 1 }, + { color = "B", group = 1 }, + { color = "W", group = 1 }, + { color = "A", group = 2 }, + }, item.sockets) + end) + + it("Jewel", function() + local item = new("Item", raw("Radius: Large\nLimited to: 2", "Cobalt Jewel")) + assert.are.equals("Large", item.jewelRadiusLabel) + assert.are.equals(2, item.limit) + end) + + it("Variant name", function() + local item = new("Item", raw("Variant: Pre 3.19.0\nVariant: Current")) + assert.are.same({ "Pre 3.19.0", "Current" }, item.variantList) + end) + + it("Talisman Tier", function() + local item = new("Item", raw("Talisman Tier: 3", "Rotfeather Talisman")) + assert.are.equals(3, item.talismanTier) + end) + + it("Defence", function() + local item = new("Item", raw("Armour: 25")) + assert.are.equals(25, item.armourData.Armour) + item = new("Item", raw("Armour: 25 (augmented)")) + assert.are.equals(25, item.armourData.Armour) + item = new("Item", raw("Evasion Rating: 35", "Shabby Jerkin")) + assert.are.equals(35, item.armourData.Evasion) + item = new("Item", raw("Energy Shield: 15", "Simple Robe")) + assert.are.equals(15, item.armourData.EnergyShield) + item = new("Item", raw("Ward: 180", "Runic Crown")) + assert.are.equals(180, item.armourData.Ward) + end) + + it("Defence BasePercentile", function() + local item = new("Item", raw("ArmourBasePercentile: 0.5")) + assert.are.equals(0.5, item.armourData.ArmourBasePercentile) + item = new("Item", raw("EvasionBasePercentile: 0.6", "Shabby Jerkin")) + assert.are.equals(0.6, item.armourData.EvasionBasePercentile) + item = new("Item", raw("EnergyShieldBasePercentile: 0.7", "Simple Robe")) + assert.are.equals(0.7, item.armourData.EnergyShieldBasePercentile) + item = new("Item", raw("WardBasePercentile: 0.8", "Runic Crown")) + assert.are.equals(0.8, item.armourData.WardBasePercentile) + end) + + it("Requires Level", function() + local item = new("Item", raw("Requires Level 10")) + assert.are.equals(10, item.requirements.level) + item = new("Item", raw("Level: 10")) + assert.are.equals(10, item.requirements.level) + item = new("Item", raw("LevelReq: 10")) + assert.are.equals(10, item.requirements.level) + end) + + it("Alt Variant", function() + local item = new("Item", raw([[ + Has Alt Variant: true + Has Alt Variant Two: true + Has Alt Variant Three: true + Has Alt Variant Four: true + Has Alt Variant Five: true + Selected Variant: 10 + Selected Alt Variant: 11 + Selected Alt Variant Two: 12 + Selected Alt Variant Three: 13 + Selected Alt Variant Four: 14 + Selected Alt Variant Five: 15 + ]])) + assert.truthy(item.hasAltVariant) + assert.truthy(item.hasAltVariant2) + assert.truthy(item.hasAltVariant3) + assert.truthy(item.hasAltVariant4) + assert.truthy(item.hasAltVariant5) + assert.are.equals(10, item.variant) + assert.are.equals(11, item.variantAlt) + assert.are.equals(12, item.variantAlt2) + assert.are.equals(13, item.variantAlt3) + assert.are.equals(14, item.variantAlt4) + assert.are.equals(15, item.variantAlt5) + end) + + it("Prefix/Suffix", function() + local item = new("Item", raw([[ + Prefix: {range:0.1}IncreasedLife1 + Suffix: {range:0.2}ColdResist1 + ]])) + assert.are.equals("IncreasedLife1", item.prefixes[1].modId) + assert.are.equals(0.1, item.prefixes[1].range) + assert.are.equals("ColdResist1", item.suffixes[1].modId) + assert.are.equals(0.2, item.suffixes[1].range) + end) + + it("Implicits", function() + local item = new("Item", raw([[ + Implicits: 2 + +8 to Strength + +10 to Intelligence + +12 to Dexterity + ]])) + assert.are.equals(2, #item.implicitModLines) + assert.are.equals("+8 to Strength", item.implicitModLines[1].line) + assert.are.equals("+10 to Intelligence", item.implicitModLines[2].line) + assert.are.equals(1, #item.explicitModLines) + assert.are.equals("+12 to Dexterity", item.explicitModLines[1].line) + end) + + it("League", function() + local item = new("Item", raw("League: Heist")) + assert.are.equals("Heist", item.league) + end) + + it("Source", function() + local item = new("Item", raw("Source: No longer obtainable")) + assert.are.equals("No longer obtainable", item.source) + end) + + it("Note", function() + local item = new("Item", raw("Note: ~price 1 chaos")) + assert.are.equals("~price 1 chaos", item.note) + end) + + it("Attribute Requirements", function() + local item = new("Item", raw("Dex: 100")) + assert.are.equals(100, item.requirements.dex) + item = new("Item", raw("Int: 101")) + assert.are.equals(101, item.requirements.int) + item = new("Item", raw("Str: 102")) + assert.are.equals(102, item.requirements.str) + end) + + it("Requires Class", function() + local item = new("Item", raw("Requires Class Witch")) + assert.are.equals("Witch", item.classRestriction) + item = new("Item", raw("Class:: Witch")) + assert.are.equals("Witch", item.classRestriction) + end) + + it("Requires Class variant", function() + local item = new("Item", raw([[ + Selected Variant: 2 + +8 to Strength + {variant:1}Requires Class Witch + {variant:2}Requires Class Templar + ]])) + assert.are.equals(2, item.variant) + assert.are.equals("Templar", item.classRestriction) + end) + + it("Influence", function() + local item = new("Item", raw("Shaper Item")) + assert.truthy(item.shaper) + item = new("Item", raw("Elder Item")) + assert.truthy(item.elder) + item = new("Item", raw("Warlord Item")) + assert.truthy(item.adjudicator) + item = new("Item", raw("Hunter Item")) + assert.truthy(item.basilisk) + item = new("Item", raw("Crusader Item")) + assert.truthy(item.crusader) + item = new("Item", raw("Redeemer Item")) + assert.truthy(item.eyrie) + item = new("Item", raw("Searing Exarch Item")) + assert.truthy(item.cleansing) + item = new("Item", raw("Eater of Worlds Item")) + assert.truthy(item.tangle) + end) + + it("short flags", function() + local item = new("Item", raw("Split")) + assert.truthy(item.split) + item = new("Item", raw("Mirrored")) + assert.truthy(item.mirrored) + item = new("Item", raw("Corrupted")) + assert.truthy(item.corrupted) + item = new("Item", raw("Fractured Item")) + assert.truthy(item.fractured) + item = new("Item", raw("Synthesised Item")) + assert.truthy(item.synthesised) + item = new("Item", raw("Crafted: true")) + assert.truthy(item.crafted) + item = new("Item", raw("Unreleased: true")) + assert.truthy(item.unreleased) + end) + + it("long flags", function() + local item = new("Item", raw("This item can be anointed by Cassia")) + assert.truthy(item.canBeAnointed) + item = new("Item", raw("Can have a second Enchantment Modifier")) + assert.truthy(item.canHaveTwoEnchants) + item = new("Item", raw("Can have 1 additional Enchantment Modifiers")) + assert.truthy(item.canHaveTwoEnchants) + item = new("Item", raw("Can have 2 additional Enchantment Modifiers")) + assert.truthy(item.canHaveTwoEnchants) + assert.truthy(item.canHaveThreeEnchants) + item = new("Item", raw("Can have 3 additional Enchantment Modifiers")) + assert.truthy(item.canHaveTwoEnchants) + assert.truthy(item.canHaveThreeEnchants) + assert.truthy(item.canHaveFourEnchants) + item = new("Item", raw("Has a Crucible Passive Skill Tree with only Support Passive Skills")) + assert.truthy(item.canHaveOnlySupportSkillsCrucibleTree) + item = new("Item", raw("Has a Crucible Passive Skill Tree")) + assert.truthy(item.canHaveShieldCrucibleTree) + item = new("Item", raw("Has a Two Handed Sword Crucible Passive Skill Tree")) + assert.truthy(item.canHaveTwoHandedSwordCrucibleTree) + end) + + it("tags", function() + local item = new("Item", raw("{tags:life,physical_damage}+8 to Strength")) + assert.are.same({ "life", "physical_damage" }, item.explicitModLines[1].modTags) + end) + + it("variant", function() + local item = new("Item", raw([[ + Selected Variant: 2 + {variant:1}+8 to Strength + {variant:2,3}+10 to Strength + ]])) + assert.are.equals(2, item.variant) + assert.are.same({ [1] = true }, item.explicitModLines[1].variantList) + assert.are.same({ [2] = true, [3] = true }, item.explicitModLines[2].variantList) + assert.are.equals(10, item.baseModList[1].value) -- variant 2 has +10 to Strength + end) + + it("range", function() + local item = new("Item", raw("{range:0.8}+(8-12) to Strength")) + assert.are.equals(0.8, item.explicitModLines[1].range) + assert.are.equals(11, item.baseModList[1].value) -- range 0.8 of (8-12) = 11 + end) + + it("crafted", function() + local item = new("Item", raw("{crafted}+8 to Strength")) + assert.truthy(item.explicitModLines[1].crafted) + item = new("Item", raw("+8 to Strength (crafted)")) + assert.truthy(item.explicitModLines[1].crafted) + end) + + it("crucible", function() + local item = new("Item", raw("{crucible}+8 to Strength")) + assert.truthy(item.crucibleModLines[1].crucible) + item = new("Item", raw("+8 to Strength (crucible)")) + assert.truthy(item.crucibleModLines[1].crucible) + end) + + it("custom", function() + local item = new("Item", raw("{custom}+8 to Strength")) + assert.truthy(item.explicitModLines[1].custom) + end) + + it("eater", function() + local item = new("Item", raw("{eater}+8 to Strength")) + assert.truthy(item.explicitModLines[1].eater) + end) + + it("enchant", function() + local item = new("Item", raw("+8 to Strength (enchant)")) + assert.are.equals(1, #item.enchantModLines) + -- enchant also sets crafted and implicit + assert.truthy(item.enchantModLines[1].crafted) + assert.truthy(item.enchantModLines[1].implicit) + end) + + it("exarch", function() + local item = new("Item", raw("{exarch}+8 to Strength")) + assert.truthy(item.explicitModLines[1].exarch) + end) + + it("fractured", function() + local item = new("Item", raw("{fractured}+8 to Strength")) + assert.truthy(item.explicitModLines[1].fractured) + item = new("Item", raw("+8 to Strength (fractured)")) + assert.truthy(item.explicitModLines[1].fractured) + end) + + it("implicit", function() + local item = new("Item", raw("+8 to Strength (implicit)")) + assert.truthy(item.implicitModLines[1].implicit) + end) + + it("scourge", function() + local item = new("Item", raw("{scourge}+8 to Strength")) + assert.truthy(item.scourgeModLines[1].scourge) + item = new("Item", raw("+8 to Strength (scourge)")) + assert.truthy(item.scourgeModLines[1].scourge) + end) + + it("synthesis", function() + local item = new("Item", raw("{synthesis}+8 to Strength")) + assert.truthy(item.explicitModLines[1].synthesis) + end) + + it("multiple bases", function() + local item = new("Item", [[ + Ashcaller + Selected Variant: 3 + {variant:1,2,3}Quartz Wand + {variant:4}Carved Wand + ]]) + assert.are.same({ + ["Quartz Wand"] = { line = "Quartz Wand", variantList = { [1] = true, [2] = true, [3] = true } }, + ["Carved Wand"] = { line = "Carved Wand", variantList = { [4] = true } } + }, item.baseLines) + assert.are.equals("Quartz Wand", item.baseName) + + item = new("Item", [[ + Ashcaller + Selected Variant: 4 + {variant:1,2,3}Quartz Wand + {variant:4}Carved Wand + ]]) + assert.are.equals("Carved Wand", item.baseName) + end) + + it("parses text without armour value then changes quality and has correct final armour", function() + local item = new("Item", [[ + Armour Gloves + Iron Gauntlets + Quality: 0 + ]]) + + local original = item.armourData.Armour + item.quality = 20 + item:BuildAndParseRaw() + assert.are.equals(round(original * 1.2), item.armourData.Armour) + end) +end) diff --git a/src/Classes/Item.lua b/src/Classes/Item.lua index 0218605a16..509631256c 100644 --- a/src/Classes/Item.lua +++ b/src/Classes/Item.lua @@ -1382,6 +1382,20 @@ function ItemClass:BuildModListForSlotNum(baseList, slotNum) armourData.Evasion = round((evasionBase + armourEvasionBase + evasionEnergyShieldBase + evasionVariance * (armourData.EvasionBasePercentile or 1)) * (1 + (evasionInc + armourEvasionInc + evasionEnergyShieldInc + defencesInc + qualityScalar) / 100)) armourData.EnergyShield = round((energyShieldBase + evasionEnergyShieldBase + armourEnergyShieldBase + energyShieldVariance * (armourData.EnergyShieldBasePercentile or 1)) * (1 + (energyShieldInc + armourEnergyShieldInc + evasionEnergyShieldInc + defencesInc + qualityScalar) / 100)) armourData.Ward = round((wardBase + wardVariance * (armourData.WardBasePercentile or 1)) * (1 + (wardInc + defencesInc + qualityScalar) / 100)) + + if not armourData.ArmourBasePercentile and armourData.Armour > 0 then + armourData.ArmourBasePercentile = 1 + end + if not armourData.EvasionBasePercentile and armourData.Evasion > 0 then + armourData.EvasionBasePercentile = 1 + end + if not armourData.EnergyShieldBasePercentile and armourData.EnergyShield > 0 then + armourData.EnergyShieldBasePercentile = 1 + end + if not armourData.WardBasePercentile and armourData.Ward > 0 then + armourData.WardBasePercentile = 1 + end + if self.base.armour.BlockChance then armourData.BlockChance = self.base.armour.BlockChance + calcLocal(modList, "BlockChance", "BASE", 0) end From 8247fe592f4d338002e052138f84ce3b120e5597 Mon Sep 17 00:00:00 2001 From: Lancej <1243476+Lancej@users.noreply.github.com> Date: Tue, 29 Aug 2023 22:46:56 -0500 Subject: [PATCH 2/4] Optimize loading items, improve startup time --- src/Classes/Item.lua | 319 +++++++++++++++++++------------------- src/Modules/ItemTools.lua | 74 +++------ src/Modules/Main.lua | 7 +- 3 files changed, 187 insertions(+), 213 deletions(-) diff --git a/src/Classes/Item.lua b/src/Classes/Item.lua index 509631256c..b4bc3a76f8 100644 --- a/src/Classes/Item.lua +++ b/src/Classes/Item.lua @@ -50,9 +50,9 @@ end local influenceInfo = itemLib.influenceInfo -local ItemClass = newClass("Item", function(self, raw) +local ItemClass = newClass("Item", function(self, raw, rarity, highQuality) if raw then - self:ParseRaw(itemLib.sanitiseItemText(raw)) + self:ParseRaw(itemLib.sanitiseItemText(raw), rarity, highQuality) end end) @@ -63,6 +63,16 @@ function ItemClass:ResetInfluence() end end +local influenceItemMap = { } +for _, curInfluenceInfo in ipairs(influenceInfo) do + influenceItemMap[curInfluenceInfo.display.." Item"] = curInfluenceInfo.key +end + +local lineFlags = { + ["crafted"] = true, ["crucible"] = true, ["custom"] = true, ["eater"] = true, ["enchant"] = true, + ["exarch"] = true, ["fractured"] = true, ["implicit"] = true, ["scourge"] = true, ["synthesis"] = true, +} + -- Special function to store unique instances of modifier on specific item slots -- that require special handling for ItemConditions. Only called if line #224 is -- uncommented @@ -256,24 +266,23 @@ function ItemClass:FindModifierSubstring(substring, itemSlotName) return false end +local function specToNumber(s) + local n = s:match("^([%+%-]?[%d%.]+)") + return n and tonumber(n) +end + -- Parse raw item data and extract item name, base type, quality, and modifiers -function ItemClass:ParseRaw(raw) +function ItemClass:ParseRaw(raw, rarity, highQuality) self.raw = raw self.name = "?" - self.rarity = "UNIQUE" + self.rarity = rarity or "UNIQUE" self.quality = nil self.rawLines = { } - for line in string.gmatch(self.raw .. "\r\n", "([^\r\n]*)\r?\n") do - line = line:gsub("^%s+",""):gsub("%s+$","") - -- remove "Superior" from items with quality so base-type matches - if line:match("^Superior ") then - line = line:gsub("Superior ", "") - end - if #line > 0 then - t_insert(self.rawLines, line) - end + -- Find non-blank lines and trim whitespace + for line in raw:gmatch("%s*([^\n]*%S)") do + t_insert(self.rawLines, line) end - local mode = "WIKI" + local mode = rarity and "GAME" or "WIKI" local l = 1 local itemClass if self.rawLines[l] then @@ -332,22 +341,13 @@ function ItemClass:ParseRaw(raw) self.requirements.str = 0 self.requirements.dex = 0 self.requirements.int = 0 + self.baseLines = { } local importedLevelReq local flaskBuffLines local deferJewelRadiusIndexAssignment local gameModeStage = "FINDIMPLICIT" local foundExplicit, foundImplicit - local function processInfluenceLine(line) - for i, curInfluenceInfo in ipairs(influenceInfo) do - if line == curInfluenceInfo.display.." Item" then - self[curInfluenceInfo.key] = true - return true - end - end - return false - end - while self.rawLines[l] do local line = self.rawLines[l] if flaskBuffLines and flaskBuffLines[line] then @@ -364,8 +364,8 @@ function ItemClass:ParseRaw(raw) self.fractured = true elseif line == "Synthesised Item" then self.synthesised = true - elseif processInfluenceLine(line) then - -- self already updated within the helper function + elseif influenceItemMap[line] then + self[influenceItemMap[line]] = true elseif line == "Requirements:" then -- nothing to do else @@ -387,38 +387,23 @@ function ItemClass:ParseRaw(raw) end self.checkSection = false end - local specName, specVal = line:match("^([%a ]+): (%x+)$") - if not specName then - specName, specVal = line:match("^([%a ]+): %+?([%d+%-%.,]+)") - if not tonumber(specVal) then - specName = nil - end - end - if not specName then - specName, specVal = line:match("^([%a ]+): (.+)$") - end - if not specName then - specName, specVal = line:match("^(Requires Class) (.+)$") - end - if not specName then - specVal = line:match("^Class:: (.+)$") - if specVal then + local specName, specVal = line:match("^([%a ]+:?): (.+)$") + if specName then + if specName == "Class:" then specName = "Requires Class" - specVal = specVal:match("%w+") end - end - if not specName then - specName, specVal = line:match("^(Requires) (.+)$") + else + specName, specVal = line:match("^(Requires %a+) (.+)$") end if specName then if specName == "Unique ID" then self.uniqueID = specVal elseif specName == "Item Level" then - self.itemLevel = tonumber(specVal) + self.itemLevel = specToNumber(specVal) elseif specName == "Requires Class" then self.classRestriction = specVal elseif specName == "Quality" then - self.quality = tonumber(specVal) + self.quality = specToNumber(specVal) elseif specName == "Sockets" then local group = 0 for c in specVal:gmatch(".") do @@ -442,7 +427,7 @@ function ItemClass:ParseRaw(raw) end end elseif specName == "Limited to" and self.type == "Jewel" then - self.limit = tonumber(specVal) + self.limit = specToNumber(specVal) elseif specName == "Variant" then if not self.variantList then self.variantList = { } @@ -455,15 +440,17 @@ function ItemClass:ParseRaw(raw) t_insert(self.variantList, specVal) end elseif specName == "Talisman Tier" then - self.talismanTier = tonumber(specVal) + self.talismanTier = specToNumber(specVal) elseif specName == "Armour" or specName == "Evasion Rating" or specName == "Evasion" or specName == "Energy Shield" or specName == "Ward" then if specName == "Evasion Rating" then + specName = "Evasion" if self.baseName == "Two-Toned Boots (Armour/Energy Shield)" then -- Another hack for Two-Toned Boots self.baseName = "Two-Toned Boots (Armour/Evasion)" self.base = data.itemBases[self.baseName] end elseif specName == "Energy Shield" then + specName = "EnergyShield" if self.baseName == "Two-Toned Boots (Armour/Evasion)" then -- Yet another hack for Two-Toned Boots self.baseName = "Two-Toned Boots (Evasion/Energy Shield)" @@ -471,20 +458,17 @@ function ItemClass:ParseRaw(raw) end end self.armourData = self.armourData or { } - specName = specName:gsub("Rating", ""):gsub(" ", "") - self.armourData[specName] = tonumber((specVal:gsub(" (augmented)", ""))) + self.armourData[specName] = specToNumber(specVal) elseif specName:match("BasePercentile") then self.armourData = self.armourData or { } - self.armourData[specName] = tonumber(specVal) or 0 + self.armourData[specName] = specToNumber(specVal) or 0 elseif specName == "Requires Level" then - self.requirements.level = tonumber(specVal) - elseif specName == "Requires" then - self.requirements.level = tonumber(specVal:match("Level (%d+)")) + self.requirements.level = specToNumber(specVal) elseif specName == "Level" then -- Requirements from imported items can't always be trusted - importedLevelReq = tonumber(specVal) + importedLevelReq = specToNumber(specVal) elseif specName == "LevelReq" then - self.requirements.level = tonumber(specVal) + self.requirements.level = specToNumber(specVal) elseif specName == "Has Alt Variant" then self.hasAltVariant = true elseif specName == "Has Alt Variant Two" then @@ -496,17 +480,17 @@ function ItemClass:ParseRaw(raw) elseif specName == "Has Alt Variant Five" then self.hasAltVariant5 = true elseif specName == "Selected Variant" then - self.variant = tonumber(specVal) + self.variant = specToNumber(specVal) elseif specName == "Selected Alt Variant" then - self.variantAlt = tonumber(specVal) + self.variantAlt = specToNumber(specVal) elseif specName == "Selected Alt Variant Two" then - self.variantAlt2 = tonumber(specVal) + self.variantAlt2 = specToNumber(specVal) elseif specName == "Selected Alt Variant Three" then - self.variantAlt3 = tonumber(specVal) + self.variantAlt3 = specToNumber(specVal) elseif specName == "Selected Alt Variant Four" then - self.variantAlt4 = tonumber(specVal) + self.variantAlt4 = specToNumber(specVal) elseif specName == "Selected Alt Variant Five" then - self.variantAlt5 = tonumber(specVal) + self.variantAlt5 = specToNumber(specVal) elseif specName == "Has Variants" or specName == "Selected Variants" then -- Need to skip this line for backwards compatibility -- with builds that used an old Watcher's Eye implementation @@ -536,7 +520,7 @@ function ItemClass:ParseRaw(raw) range = tonumber(range), }) elseif specName == "Implicits" then - implicitLines = tonumber(specVal) or 0 + implicitLines = specToNumber(specVal) or 0 gameModeStage = "EXPLICIT" elseif specName == "Unreleased" then self.unreleased = (specVal == "true") @@ -551,7 +535,7 @@ function ItemClass:ParseRaw(raw) end elseif specName == "Cluster Jewel Node Count" then if self.clusterJewel then - local num = tonumber(specVal) or self.clusterJewel.maxNodes + local num = specToNumber(specVal) or self.clusterJewel.maxNodes self.clusterJewelNodeCount = m_min(m_max(num, self.clusterJewel.minNodes), self.clusterJewel.maxNodes) end elseif specName == "Catalyst" then @@ -561,12 +545,12 @@ function ItemClass:ParseRaw(raw) end end elseif specName == "CatalystQuality" then - self.catalystQuality = tonumber(specVal) + self.catalystQuality = specToNumber(specVal) elseif specName == "Note" then self.note = specVal elseif specName == "Str" or specName == "Strength" or specName == "Dex" or specName == "Dexterity" or specName == "Int" or specName == "Intelligence" then - self.requirements[specName:sub(1,3):lower()] = tonumber(specVal) + self.requirements[specName:sub(1,3):lower()] = specToNumber(specVal) elseif specName == "Critical Strike Range" or specName == "Attacks per Second" or specName == "Weapon Range" or specName == "Critical Strike Chance" or specName == "Physical Damage" or specName == "Elemental Damage" or specName == "Chaos Damage" or specName == "Chance to Block" or specName == "Armour" or @@ -583,19 +567,39 @@ function ItemClass:ParseRaw(raw) gameModeStage = "EXPLICIT" end if not specName or foundExplicit or foundImplicit then - local varSpec = line:match("{variant:([%d,]+)}") - local variantList - if varSpec then - variantList = { } - for varId in varSpec:gmatch("%d+") do - variantList[tonumber(varId)] = true + local modLine = { modTags = {} } + + line = line:gsub("{(%a*):?([^}]*)}", function(k,val) + if k == "variant" then + modLine.variantList = { } + for varId in val:gmatch("%d+") do + modLine.variantList[tonumber(varId)] = true + end + elseif k == "tags" then + for tag in val:gmatch("[%a_]+") do + t_insert(modLine.modTags, tag) + end + elseif k == "range" then + modLine.range = tonumber(val) + elseif lineFlags[k] then + modLine[k] = true end + + return "" + end) + + line = line:gsub(" %((%l+)%)", function(k) + if lineFlags[k] then + modLine[k] = true + end + return "" + end) + + if modLine.enchant then + modLine.crafted = true + modLine.implicit = true end - if line:gsub("({variant:[%d,]+})", "") == "Two-Toned Boots" then - line = "Two-Toned Boots (Armour/Energy Shield)" - end - self.namePrefix = self.namePrefix or "" - self.nameSuffix = self.nameSuffix or "" + local baseName if self.rarity == "NORMAL" or self.rarity == "MAGIC" then -- Exact match (affix-less magic and normal items) @@ -633,87 +637,75 @@ function ItemClass:ParseRaw(raw) end self.name = self.name:gsub(" %(.+%)","") end - if self.variant and variantList then - if variantList[self.variant] then - baseName = line:gsub("Synthesised ",""):gsub("{variant:([%d,]+)}", "") - end - elseif not baseName then - baseName = line:gsub("Synthesised ",""):gsub("{variant:([%d,]+)}", "") + if not baseName then + baseName = line:gsub("^Superior ", ""):gsub("^Synthesised ","") end - if baseName and data.itemBases[baseName] then - self.baseName = baseName - if not (self.rarity == "NORMAL" or self.rarity == "MAGIC") then - self.title = self.name - end - self.type = data.itemBases[baseName].type - self.base = data.itemBases[self.baseName] - self.affixes = (self.base.subType and data.itemMods[self.base.type..self.base.subType]) - or data.itemMods[self.base.type] - or data.itemMods.Item - if self.base.weapon then - self.enchantments = data.enchantments["Weapon"] - elseif self.base.flask then - self.enchantments = data.enchantments["Flask"] - if self.base.utility_flask then + if baseName == "Two-Toned Boots" then + baseName = "Two-Toned Boots (Armour/Energy Shield)" + end + local base = data.itemBases[baseName] + if base then + -- Items with variants can have multiple bases + self.baseLines[baseName] = { line = baseName, variantList = modLine.variantList } + -- Set the actual base if variant matches or doesn't have variants + if not self.variant or not modLine.variantList or modLine.variantList[self.variant] then + self.baseName = baseName + if not (self.rarity == "NORMAL" or self.rarity == "MAGIC") then + self.title = self.name + end + self.type = base.type + self.base = base + self.affixes = (self.base.subType and data.itemMods[self.base.type..self.base.subType]) + or data.itemMods[self.base.type] + or data.itemMods.Item + if self.base.weapon then + self.enchantments = data.enchantments["Weapon"] + elseif self.base.flask then self.enchantments = data.enchantments["Flask"] + if self.base.utility_flask then + self.enchantments = data.enchantments["Flask"] + end + else + self.enchantments = data.enchantments[self.base.type] end - else - self.enchantments = data.enchantments[self.base.type] - end - self.corruptible = self.base.type ~= "Flask" - self.canBeInfluenced = self.base.influenceTags ~= nil - self.clusterJewel = data.clusterJewels and data.clusterJewels.jewels[self.baseName] - self.requirements.str = self.base.req.str or 0 - self.requirements.dex = self.base.req.dex or 0 - self.requirements.int = self.base.req.int or 0 - local maxReq = m_max(self.requirements.str, self.requirements.dex, self.requirements.int) - self.defaultSocketColor = (maxReq == self.requirements.dex and "G") or (maxReq == self.requirements.int and "B") or "R" - if self.base.flask and self.base.flask.buff and not flaskBuffLines then - flaskBuffLines = { } - for _, line in ipairs(self.base.flask.buff) do - flaskBuffLines[line] = true - local modList, extra = modLib.parseMod(line) - t_insert(self.buffModLines, { line = line, extra = extra, modList = modList or { } }) + self.corruptible = self.base.type ~= "Flask" + self.canBeInfluenced = self.base.influenceTags ~= nil + self.clusterJewel = data.clusterJewels and data.clusterJewels.jewels[self.baseName] + self.requirements.str = self.base.req.str or 0 + self.requirements.dex = self.base.req.dex or 0 + self.requirements.int = self.base.req.int or 0 + local maxReq = m_max(self.requirements.str, self.requirements.dex, self.requirements.int) + self.defaultSocketColor = (maxReq == self.requirements.dex and "G") or (maxReq == self.requirements.int and "B") or "R" + if self.base.flask and self.base.flask.buff and not flaskBuffLines then + flaskBuffLines = { } + for _, line in ipairs(self.base.flask.buff) do + flaskBuffLines[line] = true + local modList, extra = modLib.parseMod(line) + t_insert(self.buffModLines, { line = line, extra = extra, modList = modList or { } }) + end end end + -- Base lines don't need mod parsing, skip it + goto continue end - local eater = line:match("{eater}") - local exarch = line:match("{exarch}") - local synthesis = line:match("{synthesis}") - local fractured = line:match("{fractured}") or line:match(" %(fractured%)") - local rangeSpec = line:match("{range:([%d.]+)}") - local enchant = line:match(" %(enchant%)") - local classReq = line:find("Requires Class") - local scourge = line:match("{scourge}") or line:match(" %(scourge%)") - local crucible = line:match("{crucible}") or line:match(" %(crucible%)") - local crafted = line:match("{crafted}") or line:match(" %(crafted%)") or enchant - local custom = line:match("{custom}") - local modTagsText = line:match("{tags:([^}]*)}") or '' - local modTags = {} - for curMod in modTagsText:gmatch("[^,]+") do - curMod = curMod:match('^%s*(.*%S)') or '' -- Trim whitespace - table.insert(modTags, curMod) - end - local implicit = line:match(" %(implicit%)") or enchant - if implicit then + if modLine.implicit then foundImplicit = true gameModeStage = "IMPLICIT" end - line = line:gsub("%b{}", ""):gsub(" %(fractured%)",""):gsub(" %(crafted%)",""):gsub(" %(implicit%)",""):gsub(" %(enchant%)",""):gsub(" %(scourge%)",""):gsub(" %(exarch%)",""):gsub(" %(eater%)",""):gsub(" %(synthesis%)",""):gsub(" %(crucible%)", "") - local catalystScalar = getCatalystScalar(self.catalyst, modTags, self.catalystQuality) + local catalystScalar = getCatalystScalar(self.catalyst, modLine.modTags, self.catalystQuality) local rangedLine = itemLib.applyRange(line, 1, catalystScalar) - local modList, extra = modLib.parseMod(rangedLine or line) + local modList, extra = modLib.parseMod(rangedLine) if (not modList or extra) and self.rawLines[l+1] then -- Try to combine it with the next line - local nextLine = self.rawLines[l+1]:gsub("%b{}", ""):gsub(" ?%(fractured%)",""):gsub(" ?%(crafted%)",""):gsub(" ?%(implicit%)",""):gsub(" ?%(enchant%)",""):gsub(" ?%(scourge%)",""):gsub(" %(exarch%)",""):gsub(" %(eater%)",""):gsub(" %(synthesis%)",""):gsub(" %(crucible%)", "") + local nextLine = self.rawLines[l+1]:gsub("%b{}", ""):gsub(" ?%(%l+%)","") local combLine = line.." "..nextLine rangedLine = itemLib.applyRange(combLine, 1, catalystScalar) - modList, extra = modLib.parseMod(rangedLine or combLine, true) + modList, extra = modLib.parseMod(rangedLine, true) if modList and not extra then line = line.."\n"..nextLine l = l + 1 else - modList, extra = modLib.parseMod(rangedLine or line) + modList, extra = modLib.parseMod(rangedLine) end end @@ -739,27 +731,27 @@ function ItemClass:ParseRaw(raw) self.canHaveTwoHandedSwordCrucibleTree = true end - if data.itemBases[line] then - self.baseLines = self.baseLines or { } - self.baseLines[line] = { line = line, variantList = variantList } - end - local modLines - if enchant or (crafted and #self.enchantModLines + #self.implicitModLines < implicitLines) then + if modLine.enchant or (modLine.crafted and #self.enchantModLines + #self.implicitModLines < implicitLines) then modLines = self.enchantModLines - elseif scourge then + elseif modLine.scourge then modLines = self.scourgeModLines - elseif classReq then + elseif line:find("Requires Class") then modLines = self.classRequirementModLines - elseif implicit or (not crafted and #self.enchantModLines + #self.scourgeModLines + #self.implicitModLines < implicitLines) then + elseif modLine.implicit or (not modLine.crafted and #self.enchantModLines + #self.scourgeModLines + #self.implicitModLines < implicitLines) then modLines = self.implicitModLines - elseif crucible then + elseif modLine.crucible then modLines = self.crucibleModLines else modLines = self.explicitModLines end + modLine.line = line if modList then - t_insert(modLines, { line = line, extra = extra, modList = modList, modTags = modTags, variantList = variantList, scourge = scourge, crucible = crucible, crafted = crafted, custom = custom, fractured = fractured, exarch = exarch, eater = eater, synthesis = synthesis, implicit = implicit, range = rangedLine and (tonumber(rangeSpec) or main.defaultItemAffixQuality), valueScalar = catalystScalar }) + modLine.modList = modList + modLine.extra = extra + modLine.valueScalar = catalystScalar + modLine.range = modLine.range or main.defaultItemAffixQuality + t_insert(modLines, modLine) if mode == "GAME" then if gameModeStage == "FINDIMPLICIT" then gameModeStage = "IMPLICIT" @@ -774,15 +766,20 @@ function ItemClass:ParseRaw(raw) end elseif mode == "GAME" then if gameModeStage == "IMPLICIT" or gameModeStage == "EXPLICIT" or (gameModeStage == "FINDIMPLICIT" and (not data.itemBases[line]) and not (self.name == line) and not line:find("Two%-Toned") and not (self.base and (line == self.base.type or self.base.subType and line == self.base.subType .. " " .. self.base.type))) then - t_insert(modLines, { line = line, extra = line, modList = { }, modTags = { }, variantList = variantList, scourge = scourge, crucible = crucible, crafted = crafted, custom = custom, fractured = fractured, exarch = exarch, eater = eater, synthesis = synthesis, implicit = implicit }) + modLine.modList = { } + modLine.extra = line + t_insert(modLines, modLine) elseif gameModeStage == "FINDEXPLICIT" then gameModeStage = "DONE" end elseif foundExplicit then - t_insert(modLines, { line = line, extra = line, modList = { }, modTags = { }, variantList = variantList, scourge = scourge, crucible = crucible, crafted = crafted, custom = custom, fractured = fractured, exarch = exarch, eater = eater, synthesis = synthesis, implicit = implicit }) + modLine.modList = { } + modLine.extra = line + t_insert(modLines, modLine) end end end + ::continue:: l = l + 1 end if self.baseName and self.title then @@ -858,6 +855,11 @@ function ItemClass:ParseRaw(raw) end if not self.quality then self:NormaliseQuality() + if highQuality then + -- Behavior of NormaliseQuality should be looked at because calling it twice has different results. + -- Leaving it alone for now. Just moving it here from Main.lua so BuildAndParseRaw doesn't need to be called. + self:NormaliseQuality() + end end self:BuildModList() if deferJewelRadiusIndexAssignment then @@ -1529,10 +1531,9 @@ function ItemClass:BuildModList() -- handle understood modifier variable properties if not modLine.extra then if modLine.range then - local strippedModeLine = modLine.line:gsub("\n"," ") - -- Look at the min and max of the range to confirm it's *actually* a range - local rangeMin, rangeMax = itemLib.getLineRangeMinMax(strippedModeLine) - if rangeMin ~= rangeMax then + -- Check if line actually has a range + if modLine.line:find("%((%-?%d+%.?%d*)%-(%-?%d+%.?%d*)%)") then + local strippedModeLine = modLine.line:gsub("\n"," ") local catalystScalar = getCatalystScalar(self.catalyst, modLine.modTags, self.catalystQuality) -- Put the modified value into the string local line = itemLib.applyRange(strippedModeLine, modLine.range, catalystScalar) diff --git a/src/Modules/ItemTools.lua b/src/Modules/ItemTools.lua index 93c51f6048..d2ff6e7d46 100644 --- a/src/Modules/ItemTools.lua +++ b/src/Modules/ItemTools.lua @@ -42,23 +42,6 @@ function itemLib.applyValueScalar(line, valueScalar, numbers, precision) return line end --- Get the min and max of a mod line -function itemLib.getLineRangeMinMax(line) - local rangeMin, rangeMax - line:gsub("%((%d+)%-(%d+) to (%d+)%-(%d+)%)", "(%1-%2) to (%3-%4)") - :gsub("(%+?)%((%-?%d+) to (%d+)%)", "%1(%2-%3)") - :gsub("(%+?)%((%-?%d+%.?%d*)%-(%-?%d+%.?%d*)%)", - function(plus, min, max) - rangeMin = min - rangeMax = max - -- Don't need to return anything here - return "" - end) - -- may be returning nil, nil due to not being a range - -- will be strings if successful - return rangeMin, rangeMax -end - local antonyms = { ["increased"] = "reduced", ["reduced"] = "increased", @@ -66,6 +49,11 @@ local antonyms = { ["less"] = "more", } +local function antonymFunc(num, word) + local antonym = antonyms[word] + return antonym and (num.." "..antonym) or ("-"..num.." "..word) +end + -- Apply range value (0 to 1) to a modifier that has a range: "(x-x)" or "(x-x) to (x-x)" function itemLib.applyRange(line, range, valueScalar) local precisionSame = true @@ -81,11 +69,7 @@ function itemLib.applyRange(line, range, valueScalar) end return (minPrecision < 0 and "" or plus) .. tostring(minPrecision) end) - :gsub("%-(%d+%%) (%a+)", - function(num, word) - local antonym = antonyms[word] - return antonym and (num.." "..antonym) or ("-"..num.." "..word) - end) + :gsub("%-(%d+%%) (%a+)", antonymFunc) if precisionSame and (not valueScalar or valueScalar == 1) then return testLine @@ -116,11 +100,7 @@ function itemLib.applyRange(line, range, valueScalar) local numVal = m_floor((tonumber(min) + range * (tonumber(max) - tonumber(min))) * power + 0.5) / power return (numVal < 0 and "" or plus) .. tostring(numVal) end) - :gsub("%-(%d+%%) (%a+)", - function(num, word) - local antonym = antonyms[word] - return antonym and (num.." "..antonym) or ("-"..num.." "..word) - end) + :gsub("%-(%d+%%) (%a+)", antonymFunc) if numbers == 0 and line:match("(%d+%.?%d*)%%? ") then --If a mod contains x or x% and is not already a ranged value, then only the first number will be scalable as any following numbers will always be conditions or unscalable values. numbers = 1 @@ -134,30 +114,26 @@ end ---@return string function itemLib.sanitiseItemText(text) -- Something something unicode support something grumble - local replacements = { - { "^%s+", "" }, { "%s+$", "" }, { "\r\n", "\n" }, { "%b<>", "" }, - -- UTF-8 - { "\226\128\144", "-" }, -- U+2010 HYPHEN - { "\226\128\145", "-" }, -- U+2011 NON-BREAKING HYPHEN - { "\226\128\146", "-" }, -- U+2012 FIGURE DASH - { "\226\128\147", "-" }, -- U+2013 EN DASH - { "\226\128\148", "-" }, -- U+2014 EM DASH - { "\226\128\149", "-" }, -- U+2015 HORIZONTAL BAR - { "\226\136\146", "-" }, -- U+2212 MINUS SIGN - { "\195\164", "a" }, -- U+00E4 LATIN SMALL LETTER A WITH DIAERESIS - { "\195\182", "o" }, -- U+00F6 LATIN SMALL LETTER O WITH DIAERESIS + -- Only do these replacements if a char from 128-255 or '<' is found first + return text:find("[\128-\255<]") and text + :gsub("%b<>", "") + :gsub("\226\128\144", "-") -- U+2010 HYPHEN + :gsub("\226\128\145", "-") -- U+2011 NON-BREAKING HYPHEN + :gsub("\226\128\146", "-") -- U+2012 FIGURE DASH + :gsub("\226\128\147", "-") -- U+2013 EN DASH + :gsub("\226\128\148", "-") -- U+2014 EM DASH + :gsub("\226\128\149", "-") -- U+2015 HORIZONTAL BAR + :gsub("\226\136\146", "-") -- U+2212 MINUS SIGN + :gsub("\195\164", "a") -- U+00E4 LATIN SMALL LETTER A WITH DIAERESIS + :gsub("\195\182", "o") -- U+00F6 LATIN SMALL LETTER O WITH DIAERESIS -- single-byte: Windows-1252 and similar - { "\150", "-" }, -- U+2013 EN DASH - { "\151", "-" }, -- U+2014 EM DASH - { "\228", "a" }, -- U+00E4 LATIN SMALL LETTER A WITH DIAERESIS - { "\246", "o" }, -- U+00F6 LATIN SMALL LETTER O WITH DIAERESIS + :gsub("\150", "-") -- U+2013 EN DASH + :gsub("\151", "-") -- U+2014 EM DASH + :gsub("\228", "a") -- U+00E4 LATIN SMALL LETTER A WITH DIAERESIS + :gsub("\246", "o") -- U+00F6 LATIN SMALL LETTER O WITH DIAERESIS -- unsupported - { "[\128-\255]", "?" }, - } - for _, r in ipairs(replacements) do - text = text:gsub(r[1], r[2]) - end - return text + :gsub("[\128-\255]", "?") + or text end function itemLib.formatModLine(modLine, dbMode) diff --git a/src/Modules/Main.lua b/src/Modules/Main.lua index 026fe6f981..b13f32c9de 100644 --- a/src/Modules/Main.lua +++ b/src/Modules/Main.lua @@ -116,10 +116,8 @@ function main:Init() self.uniqueDB = { list = { } } for type, typeList in pairs(data.uniques) do for _, raw in pairs(typeList) do - local newItem = new("Item", "Rarity: Unique\n"..raw) + newItem = new("Item", raw, "UNIQUE", true) if newItem.base then - newItem:NormaliseQuality() - newItem:BuildAndParseRaw() self.uniqueDB.list[newItem.name] = newItem elseif launch.devMode then ConPrintf("Unique DB unrecognised item of type '%s':\n%s", type, raw) @@ -128,9 +126,8 @@ function main:Init() end self.rareDB = { list = { } } for _, raw in pairs(data.rares) do - local newItem = new("Item", "Rarity: Rare\n"..raw) + newItem = new("Item", raw, "RARE", true) if newItem.base then - newItem:NormaliseQuality() if newItem.crafted then if newItem.base.implicit and #newItem.implicitModLines == 0 then -- Automatically add implicit From a1ed71ac39495b5f3497cb204eb63b354a3bb0b1 Mon Sep 17 00:00:00 2001 From: Lancej <1243476+Lancej@users.noreply.github.com> Date: Thu, 31 Aug 2023 09:08:52 -0500 Subject: [PATCH 3/4] Fix magic item mods and add test case --- spec/System/TestItemParse_spec.lua | 14 ++++++++++++++ src/Classes/Item.lua | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/spec/System/TestItemParse_spec.lua b/spec/System/TestItemParse_spec.lua index 623ac84c29..ec0352a3c9 100644 --- a/spec/System/TestItemParse_spec.lua +++ b/spec/System/TestItemParse_spec.lua @@ -385,4 +385,18 @@ describe("TestItemParse", function() item:BuildAndParseRaw() assert.are.equals(round(original * 1.2), item.armourData.Armour) end) + + it("magic item", function() + local item = new("Item", [[ + Rarity: MAGIC + Name Prefix Iron Gauntlets -> +50 ignite chance + +50% chance to Ignite + ]]) + + assert.are.equals("Name Prefix ", item.namePrefix) + assert.are.equals(" -> +50 ignite chance", item.nameSuffix) + assert.are.equals("Iron Gauntlets", item.baseName) + assert.are.equals(1, #item.explicitModLines) + assert.are.equals("+50% chance to Ignite", item.explicitModLines[1].line) + end) end) diff --git a/src/Classes/Item.lua b/src/Classes/Item.lua index b4bc3a76f8..f42ee0430b 100644 --- a/src/Classes/Item.lua +++ b/src/Classes/Item.lua @@ -601,7 +601,7 @@ function ItemClass:ParseRaw(raw, rarity, highQuality) end local baseName - if self.rarity == "NORMAL" or self.rarity == "MAGIC" then + if not self.base and (self.rarity == "NORMAL" or self.rarity == "MAGIC") then -- Exact match (affix-less magic and normal items) if self.name:match("Energy Blade") and itemClass then -- Special handling for energy blade base. self.name = itemClass:match("One Hand") and "Energy Blade One Handed" or "Energy Blade Two Handed" From 0ad5661b49b5870dbb3d85fd4d68dc1f3068ab87 Mon Sep 17 00:00:00 2001 From: Lancej <1243476+Lancej@users.noreply.github.com> Date: Fri, 1 Sep 2023 16:36:37 -0500 Subject: [PATCH 4/4] Fix nil namePrefix and nameSuffix for tooltips --- src/Classes/Item.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Classes/Item.lua b/src/Classes/Item.lua index f42ee0430b..cafe6ad754 100644 --- a/src/Classes/Item.lua +++ b/src/Classes/Item.lua @@ -275,6 +275,8 @@ end function ItemClass:ParseRaw(raw, rarity, highQuality) self.raw = raw self.name = "?" + self.namePrefix = "" + self.nameSuffix = "" self.rarity = rarity or "UNIQUE" self.quality = nil self.rawLines = { }