@@ -788,24 +788,151 @@ function ItemClass:ParseRaw(raw, rarity, highQuality)
788
788
if self .base .weapon or self .base .armour then
789
789
local shouldFixRunesOnItem = # self .runes == 0
790
790
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
791
816
for i , modLine in ipairs (self .runeModLines ) do
792
817
local value
793
- local strippedModeLine = modLine .line :gsub (" (%d%.?%d*)" , function (val )
818
+ local strippedModLine = modLine .line :gsub (" (%d%.?%d*)" , function (val )
794
819
value = val
795
820
return " #"
796
821
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
+
806
931
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
809
936
end
810
937
end
811
938
end
@@ -1122,8 +1249,8 @@ function ItemClass:UpdateRunes()
1122
1249
if statOrder [order ] then
1123
1250
-- Combine stats
1124
1251
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 )
1127
1254
start = e + 1
1128
1255
return tonumber (num ) + tonumber (other )
1129
1256
end )
0 commit comments