Skip to content

Commit ecdafa0

Browse files
Add proper support for pasting items with lesser and greater runes (#1010)
* Add proper support for importing lesser and greater runes. * Fix forgetting to pass the starting count and sum * Optimise pruning on bottom to only decrease if value is still reachable. * Spelling * Fix bug with decimals needs a bit more checking if floating point precision is an issue. * Fix bug with no forming a proper solution for match of all duplicate runes * Fix crashes with decimals * Fix floating point issues and crash if failed to find a match. * Refactor unique check to occur on both increment and decrement and fix missing floating point handling on greedy search solutions * Fix vistor check and result greater than 0 check. * Fix bug regarding incorrectly handling count * Fix the too small value pruning it should not work properly based off breakpoint analysis * Add comment about something to refactor in the future * Spelling * Minor cleanup no need to do FP checks on a count. * Remove count >= 0 check which currently does nothing. * Improve the description a bit a fix a typo in a comment. This also unfortunately removes the 1 rune type check as I am not confident in it always producing a minimal number of runes. Cause if it doesn't then problems could arise of then not having enough rune slots to find the other runes on the item. * Spelling
1 parent c4b0ef0 commit ecdafa0

File tree

1 file changed

+141
-14
lines changed

1 file changed

+141
-14
lines changed

src/Classes/Item.lua

Lines changed: 141 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -788,24 +788,151 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
788788
if self.base.weapon or self.base.armour then
789789
local shouldFixRunesOnItem = #self.runes == 0
790790

791+
-- Form a key value table with the following format
792+
-- { [strippedModLine] = { { runeName1, runeValue1 }, etc, }, etc}
793+
-- This will be used to more easily grab the relevant runes that combinations will need to be of.
794+
-- This could be refactored to only needs to be called once.
795+
local statGroupedRunes = { }
796+
local type = self.base.weapon and "weapon" or "armour" -- minor optimisation
797+
for runeName, runeMods in pairs(data.itemMods.Runes) do
798+
-- gets the first value in the mod and its stripped line.
799+
local runeValue
800+
local runeStrippedModeLine = runeMods[type][1]:gsub("(%d%.?%d*)", function(val)
801+
runeValue = val
802+
return "#"
803+
end)
804+
if statGroupedRunes[runeStrippedModeLine] == nil then
805+
statGroupedRunes[runeStrippedModeLine] = { }
806+
end
807+
t_insert(statGroupedRunes[runeStrippedModeLine], { runeName, runeValue });
808+
end
809+
810+
-- Sort table to ensure first entries are always largest.
811+
for _, runes in pairs(statGroupedRunes) do
812+
table.sort(runes, function(a, b) return a[2] > b[2] end)
813+
end
814+
815+
local remainingRunes = self.itemSocketCount
791816
for i, modLine in ipairs(self.runeModLines) do
792817
local value
793-
local strippedModeLine = modLine.line:gsub("(%d%.?%d*)", function(val)
818+
local strippedModLine = modLine.line:gsub("(%d%.?%d*)", function(val)
794819
value = val
795820
return "#"
796821
end)
797-
for name, runeMods in pairs(data.itemMods.Runes) do
798-
local runeValue
799-
local runeStrippedModeLine = (self.base.weapon and runeMods.weapon or runeMods.armour)[1]:gsub("(%d%.?%d*)", function(val)
800-
runeValue = val
801-
return "#"
802-
end)
803-
if strippedModeLine == runeStrippedModeLine then
804-
modLine.soulcore = name:match("Soul Core") ~= nil
805-
modLine.runeCount = round(value/runeValue)
822+
local groupedRunes = statGroupedRunes[strippedModLine]
823+
if groupedRunes then -- found the rune category with the relevant stat.
824+
-- First a greedy base is found using the runes in the groupedRunes. If this matches the target value then that set of runes is applied.
825+
-- If the greedy base isn't a solution we search all the possible combinations that could lead to a valid combination.
826+
-- This done by recursing through all combinations that could lead to a valid value and pruning values that exceed the number
827+
-- of runes and solutions that it would be impossible to reach the target value from. Visited combinations are recorded and are used such
828+
-- that candidates are only searched once. This makes for a fairly efficient algorithm that doesn't search unneeded values very much.
829+
local function getNumberOfRunesOfEachType(values, target)
830+
local function adjustCombination(values, target, result, best, visited, sum, count)
831+
-- This is used to avoid unnecessary checks on decrement.
832+
local function checkAndAdjustCombination(values, target, result, best, visited, sum, count)
833+
-- If it's a valid solution, update best
834+
if math.abs(sum-target) < 1e-9 then
835+
if not best.count or count < best.count then
836+
best.count = count
837+
-- Copy solution to avoid side effects from continued searching.
838+
local solution = {}
839+
for k, v in pairs(result) do
840+
solution[k] = v
841+
end
842+
best.solution = solution
843+
end
844+
return
845+
end
846+
847+
-- Prune if we already used more runes than the best found
848+
if best.count and count >= best.count then return end
849+
850+
return adjustCombination(values, target, result, best, visited, sum, count)
851+
end
852+
853+
for _, v in ipairs(values) do
854+
local function checkUnique(result)
855+
-- Generate a unique key from the result table this prevents duplicates combinations being searched
856+
local key = ""
857+
for value, count in pairs(result) do
858+
if count > 0 then
859+
key = key .. value .. "x" .. count .. " "
860+
end
861+
end
862+
if visited[key] then
863+
return false
864+
else
865+
visited[key] = true
866+
return true
867+
end
868+
end
869+
870+
-- Incrementing is done first as to reach the target you will need to add a count as such it should be more efficient.
871+
-- Try increasing (if it doesn't overshoot or exceed maximum number of remaining runes)
872+
if sum + tonumber(v) <= target + 1e-9 and count < remainingRunes then
873+
result[v] = (result[v] or 0) + 1
874+
if checkUnique(result) then
875+
checkAndAdjustCombination(values, target, result, best, visited, sum + v, count + 1)
876+
end
877+
result[v] = result[v] - 1
878+
end
879+
880+
-- Try decreasing (if possible and only if target is still reachable).
881+
if (result[v] or 0) > 0 and (not best.count or target - 1e-9 < sum - tonumber(v) + values[1] * (best.count - count + 1)) then
882+
result[v] = result[v] - 1
883+
if checkUnique(result) then
884+
adjustCombination(values, target, result, best, visited, sum - v, count - 1)
885+
end
886+
result[v] = result[v] + 1
887+
end
888+
end
889+
end
890+
891+
-- Step 1: Perform greedy search and tests if a single rune is used as these are the most common use case.
892+
local greedySolution = {}
893+
local leftover = target
894+
895+
for _, v in ipairs(values) do
896+
local count = math.floor(leftover / v)
897+
greedySolution[v] = count
898+
leftover = leftover - count * v
899+
end
900+
901+
local greedyCount = 0
902+
for v, c in pairs(greedySolution) do
903+
greedyCount = greedyCount + c
904+
end
905+
if math.abs(leftover) <= 1e-9 then -- Greedy search found a solution
906+
return greedySolution, greedyCount
907+
end
908+
909+
-- Step 2. Perform search starting from the greedy base
910+
local best = {count = nil, solution = nil}
911+
local visited = {}
912+
913+
adjustCombination(values, target, greedySolution, best, visited, target - leftover, greedyCount)
914+
915+
return best.solution, best.count
916+
end
917+
918+
local values = { }
919+
for i, runes in ipairs(groupedRunes) do
920+
t_insert(values, runes[2])
921+
end
922+
local result, numRunes = getNumberOfRunesOfEachType(values, tonumber(value))
923+
924+
if result then -- we have found a valid combo for that rune category
925+
remainingRunes = remainingRunes - numRunes
926+
-- this code should probably be refactored to based off stored self.runes rather than the recomputed amounts off the runeModLines this
927+
-- is too avoid having to run the relatively expensive recomputation every time the item is parsed even if we know the runes on the item already.
928+
modLine.soulcore = groupedRunes[1][1]:match("Soul Core") ~= nil
929+
modLine.runeCount = numRunes
930+
806931
if shouldFixRunesOnItem then
807-
for i = 1, modLine.runeCount do
808-
t_insert(self.runes, name)
932+
for i, rune in ipairs(groupedRunes) do
933+
for _ = 1, tonumber(result[rune[2]]) do
934+
t_insert(self.runes, groupedRunes[i][1])
935+
end
809936
end
810937
end
811938
end
@@ -1122,8 +1249,8 @@ function ItemClass:UpdateRunes()
11221249
if statOrder[order] then
11231250
-- Combine stats
11241251
local start = 1
1125-
statOrder[order].line = statOrder[order].line:gsub("%d+", function(num)
1126-
local s, e, other = line:find("(%d+)", start)
1252+
statOrder[order].line = statOrder[order].line:gsub("(%d%.?%d*)", function(num)
1253+
local s, e, other = line:find("(%d%.?%d*)", start)
11271254
start = e + 1
11281255
return tonumber(num) + tonumber(other)
11291256
end)

0 commit comments

Comments
 (0)