From 9bf247c613fbc2362bea1dbf1cc780d69dc37152 Mon Sep 17 00:00:00 2001 From: xoxorwr Date: Sun, 26 Jul 2026 16:21:56 +0200 Subject: [PATCH 1/7] Refactor custom modifiers block into toggleable blocks --- src/Classes/ConfigTab.lua | 241 ++++++++++++++++++++++++++++++++-- src/Modules/ConfigOptions.lua | 42 +----- 2 files changed, 234 insertions(+), 49 deletions(-) diff --git a/src/Classes/ConfigTab.lua b/src/Classes/ConfigTab.lua index 652b76d0e8..af9931af18 100644 --- a/src/Classes/ConfigTab.lua +++ b/src/Classes/ConfigTab.lua @@ -12,6 +12,88 @@ local s_upper = string.upper local varList = LoadModule("Modules/ConfigOptions") local configVisibility = LoadModule("Modules/ConfigVisibility") +local CustomModBlockClass = newClass("CustomModBlockControl", "Control", "ControlHost", function(self, anchor, rect, configTab, blockIndex, blockData) + self.Control(anchor, rect) + self.ControlHost() + + self.configTab = configTab + self.blockIndex = blockIndex + self.blockData = blockData + + self.controls.enableCheck = new("CheckBoxControl", {"TOPLEFT", self, "TOPLEFT"}, {0, 0, 18}, "", function(state) + blockData.enabled = state + configTab:AddUndoState() + configTab:BuildModList() + configTab.build.buildFlag = true + end) + self.controls.enableCheck.state = blockData.enabled ~= false + + self.controls.titleEdit = new("EditControl", {"LEFT", self.controls.enableCheck, "RIGHT"}, {6, 0, 290, 18}, blockData.title or "", nil, nil, nil, function(buf) + blockData.title = buf + configTab:AddUndoState() + end) + + self.controls.deleteBtn = new("ButtonControl", {"TOPRIGHT", self, "TOPRIGHT"}, {0, 0, 24, 18}, "^1X", function() + local customModsList = configTab.configSets[configTab.activeConfigSetId].customModsList + table.remove(customModsList, blockIndex) + if #customModsList == 0 then + table.insert(customModsList, { title = "Default", enabled = true, text = "" }) + end + configTab:UpdateCustomModsControls() + configTab:AddUndoState() + configTab:BuildModList() + configTab.build.buildFlag = true + end) + + self.controls.textEdit = new("ResizableEditControl", {"TOPLEFT", self, "TOPLEFT"}, {0, 22, 344, 80, 344, 40, 344, 600}, blockData.text or "", nil, "^%C\t\n", nil, function(buf) + blockData.text = buf + configTab:AddUndoState() + configTab:BuildModList() + configTab.build.buildFlag = true + end, 16) + + self.controls.textEdit.inactiveText = function(val) + local inactiveText = "" + for line in val:gmatch("([^\n]*)\n?") do + local strippedLine = StripEscapes(line):gsub("^[%s?]+", ""):gsub("[%s?]+$", "") + local mods, extra = modLib.parseMod(strippedLine) + inactiveText = inactiveText .. ((mods and not extra) and colorCodes.MAGIC or colorCodes.UNSUPPORTED) .. (IsKeyDown("ALT") and strippedLine or line) .. "\n" + end + return inactiveText + end +end) + +function CustomModBlockClass:GetSize() + local textHeight = self.controls.textEdit and self.controls.textEdit.height or 80 + self.height = 22 + textHeight + 4 + return 344, self.height +end + +function CustomModBlockClass:IsMouseOver() + if not self:IsShown() then + return + end + return self:IsMouseInBounds() or self:GetMouseOverControl() +end + +function CustomModBlockClass:OnKeyDown(key, doubleClick) + if not self:IsShown() or not self:IsEnabled() then + return + end + local mOverControl = self:GetMouseOverControl() + if mOverControl and mOverControl.OnKeyDown then + return mOverControl:OnKeyDown(key, doubleClick) + end +end + +function CustomModBlockClass:Draw(viewPort) + if not self:IsShown() then + return + end + self:GetSize() + self:DrawControls(viewPort) +end + local ConfigTabClass = newClass("ConfigTab", "UndoHandler", "ControlHost", "Control", function(self, build) self.UndoHandler() self.ControlHost() @@ -139,13 +221,17 @@ local ConfigTabClass = newClass("ConfigTab", "UndoHandler", "ControlHost", "Cont local height = 20 for _, varControl in pairs(self.varControlList) do if varControl:IsShown() then - height = height + m_max(varControl.height, 16) + 4 + local _, ctrlHeight = varControl:GetSize() + height = height + m_max(ctrlHeight or varControl.height, 16) + 4 end end return m_max(height, 32) end t_insert(self.sectionList, lastSection) t_insert(self.controls, lastSection) + if varData.section == "Custom Modifiers" then + self.customSection = lastSection + end else local control if varData.type == "check" then @@ -609,6 +695,18 @@ local ConfigTabClass = newClass("ConfigTab", "UndoHandler", "ControlHost", "Cont end end self.controls.scrollBar = new("ScrollBarControl", {"TOPRIGHT",self,"TOPRIGHT"}, {0, 0, 18, 0}, 50, "VERTICAL", true) + if self.customSection then + self.controls.customModsAddBlock = new("ButtonControl", {"TOPLEFT", self.customSection, "TOPLEFT"}, {8, 0, 100, 20}, "^7+ Add Block", function() + local customModsList = self.configSets[self.activeConfigSetId].customModsList + t_insert(customModsList, { title = "Block " .. (#customModsList + 1), enabled = true, text = "" }) + self:UpdateCustomModsControls() + self:AddUndoState() + self:BuildModList() + self.build.buildFlag = true + end) + self.customModsBlockControls = { } + self:UpdateCustomModsControls() + end end) function ConfigTabClass:Load(xml, fileName) @@ -665,17 +763,45 @@ function ConfigTabClass:Load(xml, fileName) if not self.configSets[1] then self:NewConfigSet(1, "Default") end - setInputAndPlaceholder(node, 1) + if node.elem == "CustomModifierBlock" then + local block = { + title = node.attrib.title or "Default", + enabled = (node.attrib.enabled == "true" or node.attrib.enabled == nil), + text = node[1] or "" + } + t_insert(self.configSets[1].customModsList, block) + else + setInputAndPlaceholder(node, 1) + end else local configSetId = tonumber(node.attrib.id) self:NewConfigSet(configSetId, node.attrib.title or "Default") self.configSetOrderList[index] = configSetId + self.configSets[configSetId].customModsList = { } for _, child in ipairs(node) do - setInputAndPlaceholder(child, configSetId) + if child.elem == "CustomModifierBlock" then + local block = { + title = child.attrib.title or "Default", + enabled = (child.attrib.enabled == "true" or child.attrib.enabled == nil), + text = child[1] or "" + } + t_insert(self.configSets[configSetId].customModsList, block) + else + setInputAndPlaceholder(child, configSetId) + end end end end + -- Migration check for legacy builds + for _, configSetId in ipairs(self.configSetOrderList) do + local configSet = self.configSets[configSetId] + if not configSet.customModsList or #configSet.customModsList == 0 then + local legacyText = configSet.input and configSet.input.customMods or "" + configSet.customModsList = { { title = "Default", enabled = true, text = legacyText } } + end + end + self:SetActiveConfigSet(tonumber(xml.attrib.activeConfigSet) or 1) self:ResetUndo() end @@ -731,6 +857,19 @@ function ConfigTabClass:Save(xml) end t_insert(child, node) end + if configSet.customModsList then + for _, block in ipairs(configSet.customModsList) do + local blockNode = { + elem = "CustomModifierBlock", + attrib = { + title = block.title or "Default", + enabled = tostring(block.enabled ~= false) + }, + [1] = block.text or "" + } + t_insert(child, blockNode) + end + end end end @@ -747,6 +886,7 @@ function ConfigTabClass:UpdateControls() control:SelByValue(self.configSets[self.activeConfigSetId].input[var] or self:GetDefaultState(var), "val") end end + self:UpdateCustomModsControls() end function ConfigTabClass:Draw(viewPort, inputEvents) @@ -881,6 +1021,45 @@ function ConfigTabClass:BuildModList() end end end + + -- Apply Custom Modifier Blocks + local customModsList = self.configSets[self.activeConfigSetId].customModsList + if customModsList and #customModsList > 0 then + for _, block in ipairs(customModsList) do + if block.enabled ~= false and block.text and #block.text > 0 then + for line in block.text:gmatch("([^\n]*)\n?") do + local strippedLine = StripEscapes(line):gsub("^[%s?]+", ""):gsub("[%s?]+$", "") + local mods, extra = modLib.parseMod(strippedLine) + if mods and not extra then + local source = "Custom" + for i = 1, #mods do + local mod = mods[i] + if mod then + mod = modLib.setSource(mod, source) + modList:AddMod(mod) + end + end + end + end + end + end + -- Fallback for tests/headless + elseif input.customMods and #input.customMods > 0 then + for line in input.customMods:gmatch("([^\n]*)\n?") do + local strippedLine = StripEscapes(line):gsub("^[%s?]+", ""):gsub("[%s?]+$", "") + local mods, extra = modLib.parseMod(strippedLine) + if mods and not extra then + local source = "Custom" + for i = 1, #mods do + local mod = mods[i] + if mod then + mod = modLib.setSource(mod, source) + modList:AddMod(mod) + end + end + end + end + end end function ConfigTabClass:ImportCalcSettings() @@ -921,13 +1100,28 @@ function ConfigTabClass:ImportCalcSettings() end function ConfigTabClass:CreateUndoState() - return copyTable(self.configSets[self.activeConfigSetId].input) + local configSet = self.configSets[self.activeConfigSetId] + return { + input = copyTable(configSet.input), + customModsList = copyTable(configSet.customModsList) + } end function ConfigTabClass:RestoreUndoState(state) - wipeTable(self.configSets[self.activeConfigSetId].input) - for k, v in pairs(state) do - self.configSets[self.activeConfigSetId].input[k] = v + local configSet = self.configSets[self.activeConfigSetId] + if type(state) == "table" and state.input then + wipeTable(configSet.input) + for k, v in pairs(state.input) do + configSet.input[k] = v + end + if state.customModsList then + configSet.customModsList = copyTable(state.customModsList) + end + else + wipeTable(configSet.input) + for k, v in pairs(state) do + configSet.input[k] = v + end end self:UpdateControls() self:BuildModList() @@ -944,7 +1138,7 @@ end -- Creates a new config set function ConfigTabClass:NewConfigSet(configSetId, title) - local configSet = { id = configSetId, title = title, input = { }, placeholder = { } } + local configSet = { id = configSetId, title = title, input = { }, placeholder = { }, customModsList = { { title = "Default", enabled = true, text = "" } } } if not configSetId then configSet.id = 1 while self.configSets[configSet.id] do @@ -965,6 +1159,37 @@ function ConfigTabClass:NewConfigSet(configSetId, title) return configSet end +function ConfigTabClass:UpdateCustomModsControls() + if not self.customSection then + return + end + local configSet = self.configSets[self.activeConfigSetId] + if not configSet then + return + end + if not configSet.customModsList then + configSet.customModsList = { } + end + if #configSet.customModsList == 0 then + t_insert(configSet.customModsList, { title = "Default", enabled = true, text = configSet.input and configSet.input.customMods or "" }) + end + + if self.customModsBlockControls then + for _, ctrl in ipairs(self.customModsBlockControls) do + ctrl.shown = false + end + end + self.customModsBlockControls = { } + self.customSection.varControlList = { self.controls.customModsAddBlock } + + for index, block in ipairs(configSet.customModsList) do + local blockControl = new("CustomModBlockControl", {"TOPLEFT", self.customSection, "TOPLEFT"}, {8, 0, 344, 120}, self, index, block) + t_insert(self.customModsBlockControls, blockControl) + t_insert(self.controls, blockControl) + t_insert(self.customSection.varControlList, blockControl) + end +end + -- Changes the active config set function ConfigTabClass:SetActiveConfigSet(configSetId, init) -- Initialize config sets if needed diff --git a/src/Modules/ConfigOptions.lua b/src/Modules/ConfigOptions.lua index 3f99f89b3d..c9525aa08f 100644 --- a/src/Modules/ConfigOptions.lua +++ b/src/Modules/ConfigOptions.lua @@ -2343,45 +2343,5 @@ Huge sets the radius to 11. -- Section: Custom mods { section = "Custom Modifiers", col = 1 }, - { var = "customMods", type = "text", label = "", doNotHighlight = true, resizable = true, - apply = function(val, modList, enemyModList, build) - for line in val:gmatch("([^\n]*)\n?") do - local strippedLine = StripEscapes(line):gsub("^[%s?]+", ""):gsub("[%s?]+$", "") - local mods, extra = modLib.parseMod(strippedLine) - - if mods and not extra then - local source = "Custom" - for i = 1, #mods do - local mod = mods[i] - - if mod then - mod = modLib.setSource(mod, source) - modList:AddMod(mod) - end - end - end - end - end, - inactiveText = function(val) - local inactiveText = "" - for line in val:gmatch("([^\n]*)\n?") do - local strippedLine = StripEscapes(line):gsub("^[%s?]+", ""):gsub("[%s?]+$", "") - local mods, extra = modLib.parseMod(strippedLine) - inactiveText = inactiveText .. ((mods and not extra) and colorCodes.MAGIC or colorCodes.UNSUPPORTED).. (IsKeyDown("ALT") and strippedLine or line) .. "\n" - end - return inactiveText - end, - tooltip = function(modList) - if not launch.devModeAlt then - return - end - - local out - for _, mod in ipairs(modList) do - if mod.source == "Custom" then - out = (out and out.."\n" or "") .. modLib.formatMod(mod) .. "|" .. mod.source - end - end - return out - end}, } + From 680e9e03fc34e2895d8f268d4726f83a7db85a85 Mon Sep 17 00:00:00 2001 From: xoxorwr Date: Sun, 26 Jul 2026 16:35:31 +0200 Subject: [PATCH 2/7] Fix test suite fallback --- src/Classes/ConfigTab.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Classes/ConfigTab.lua b/src/Classes/ConfigTab.lua index af9931af18..cd7af44b91 100644 --- a/src/Classes/ConfigTab.lua +++ b/src/Classes/ConfigTab.lua @@ -1024,9 +1024,11 @@ function ConfigTabClass:BuildModList() -- Apply Custom Modifier Blocks local customModsList = self.configSets[self.activeConfigSetId].customModsList - if customModsList and #customModsList > 0 then + local hasBlockText = false + if customModsList then for _, block in ipairs(customModsList) do if block.enabled ~= false and block.text and #block.text > 0 then + hasBlockText = true for line in block.text:gmatch("([^\n]*)\n?") do local strippedLine = StripEscapes(line):gsub("^[%s?]+", ""):gsub("[%s?]+$", "") local mods, extra = modLib.parseMod(strippedLine) @@ -1043,8 +1045,9 @@ function ConfigTabClass:BuildModList() end end end + end -- Fallback for tests/headless - elseif input.customMods and #input.customMods > 0 then + if not hasBlockText and input.customMods and #input.customMods > 0 then for line in input.customMods:gmatch("([^\n]*)\n?") do local strippedLine = StripEscapes(line):gsub("^[%s?]+", ""):gsub("[%s?]+$", "") local mods, extra = modLib.parseMod(strippedLine) From 127cb44e68e60c081a6dd7c71d44522bfb47d021 Mon Sep 17 00:00:00 2001 From: xoxorwr Date: Sun, 26 Jul 2026 17:06:12 +0200 Subject: [PATCH 3/7] Add mod browser --- src/Classes/ConfigTab.lua | 144 +++++++++++++++++++++++++++++++++++++- 1 file changed, 142 insertions(+), 2 deletions(-) diff --git a/src/Classes/ConfigTab.lua b/src/Classes/ConfigTab.lua index cd7af44b91..1c40dbc868 100644 --- a/src/Classes/ConfigTab.lua +++ b/src/Classes/ConfigTab.lua @@ -28,11 +28,15 @@ local CustomModBlockClass = newClass("CustomModBlockControl", "Control", "Contro end) self.controls.enableCheck.state = blockData.enabled ~= false - self.controls.titleEdit = new("EditControl", {"LEFT", self.controls.enableCheck, "RIGHT"}, {6, 0, 290, 18}, blockData.title or "", nil, nil, nil, function(buf) + self.controls.titleEdit = new("EditControl", {"LEFT", self.controls.enableCheck, "RIGHT"}, {6, 0, 232, 18}, blockData.title or "", nil, nil, nil, function(buf) blockData.title = buf configTab:AddUndoState() end) + self.controls.addModBtn = new("ButtonControl", {"LEFT", self.controls.titleEdit, "RIGHT"}, {6, 0, 48, 18}, "^7+ Mod", function() + configTab:OpenAddModPopup(blockData) + end) + self.controls.deleteBtn = new("ButtonControl", {"TOPRIGHT", self, "TOPRIGHT"}, {0, 0, 24, 18}, "^1X", function() local customModsList = configTab.configSets[configTab.activeConfigSetId].customModsList table.remove(customModsList, blockIndex) @@ -696,7 +700,7 @@ local ConfigTabClass = newClass("ConfigTab", "UndoHandler", "ControlHost", "Cont end self.controls.scrollBar = new("ScrollBarControl", {"TOPRIGHT",self,"TOPRIGHT"}, {0, 0, 18, 0}, 50, "VERTICAL", true) if self.customSection then - self.controls.customModsAddBlock = new("ButtonControl", {"TOPLEFT", self.customSection, "TOPLEFT"}, {8, 0, 100, 20}, "^7+ Add Block", function() + self.controls.customModsAddBlock = new("ButtonControl", {"TOPLEFT", self.customSection, "TOPLEFT"}, {8, 0, 75, 20}, "^7+ Block", function() local customModsList = self.configSets[self.activeConfigSetId].customModsList t_insert(customModsList, { title = "Block " .. (#customModsList + 1), enabled = true, text = "" }) self:UpdateCustomModsControls() @@ -1220,3 +1224,139 @@ function ConfigTabClass:SetActiveConfigSet(configSetId, init) self.build.buildFlag = true self.build:SyncLoadouts() end + +function ConfigTabClass:OpenAddModPopup(blockData) + local bData = (self.build and self.build.data) or data + local allModsList = { } + local seen = { } + + local function addModEntry(mod) + if type(mod) == "string" then + local stripped = StripEscapes(mod):gsub("^[%s?]+", ""):gsub("[%s?]+$", "") + if #stripped > 0 and not seen[stripped] then + seen[stripped] = true + t_insert(allModsList, stripped) + end + elseif type(mod) == "table" then + for i = 1, #mod do + if type(mod[i]) == "string" then + local stripped = StripEscapes(mod[i]):gsub("^[%s?]+", ""):gsub("[%s?]+$", "") + if #stripped > 0 and not seen[stripped] then + seen[stripped] = true + t_insert(allModsList, stripped) + end + end + end + end + end + + if bData then + if bData.masterMods then + for _, mod in pairs(bData.masterMods) do + addModEntry(mod) + end + end + if bData.itemMods then + for catName, catMods in pairs(bData.itemMods) do + if catName ~= "Item" and type(catMods) == "table" then + for _, mod in pairs(catMods) do + addModEntry(mod) + end + end + end + end + if bData.veiledMods then + for _, mod in pairs(bData.veiledMods) do + addModEntry(mod) + end + end + if bData.beastCraft then + for _, mod in pairs(bData.beastCraft) do + addModEntry(mod) + end + end + end + + table.sort(allModsList) + + local displayList = { } + local controls = { } + + local function updateDisplayList() + wipeTable(displayList) + local searchStr = controls.search and controls.search.buf:lower():gsub("[%-%+%.%[%]%$%^%%%?%*]", "%%%0") or "" + for _, modText in ipairs(allModsList) do + if #searchStr == 0 or modText:lower():match(searchStr) then + t_insert(displayList, modText) + end + end + if #displayList == 0 then + t_insert(displayList, "No matching modifiers found") + end + if controls.listControl then + controls.listControl.selIndex = 1 + controls.listControl.selValue = displayList[1] + controls.listControl.controls.scrollBarV.offset = 0 + end + end + + controls.listControl = new("ListControl", {"TOPLEFT", nil, "TOPLEFT"}, {10, 20, 580, 184}, 16, "VERTICAL", false, displayList) + controls.listControl.font = "VAR" + controls.listControl.hasFocus = true + controls.listControl.GetRowValue = function(self, column, index, value) + return value or "" + end + controls.listControl.OnSelClick = function(self, index, value) + if main.SelectControl then + main:SelectControl(self) + end + self:SelectIndex(index) + end + controls.listControl.OnSelDoubleClick = function(index, value) + if value and value ~= "No matching modifiers found" then + if blockData.text and #blockData.text > 0 and not blockData.text:match("\n$") then + blockData.text = blockData.text .. "\n" + end + blockData.text = (blockData.text or "") .. value + self:UpdateCustomModsControls() + self:AddUndoState() + self:BuildModList() + self.build.buildFlag = true + main:ClosePopup() + end + end + + controls.searchLabel = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {65, 212, 0, 16}, "^7Search:") + controls.search = new("EditControl", {"TOPLEFT", nil, "TOPLEFT"}, {70, 212, 520, 18}, "", nil, "%c", 100, function() + updateDisplayList() + end) + + updateDisplayList() + + controls.save = new("ButtonControl", nil, {-45, 242, 80, 20}, "Add", function() + local selIndex = controls.listControl.selIndex or 1 + local selected = displayList[selIndex] + if selected and selected ~= "No matching modifiers found" then + if blockData.text and #blockData.text > 0 and not blockData.text:match("\n$") then + blockData.text = blockData.text .. "\n" + end + blockData.text = (blockData.text or "") .. selected + self:UpdateCustomModsControls() + self:AddUndoState() + self:BuildModList() + self.build.buildFlag = true + end + main:ClosePopup() + end) + controls.save.enabled = function() + local selIndex = controls.listControl and controls.listControl.selIndex or 1 + local selected = displayList[selIndex] + return selected ~= nil and selected ~= "No matching modifiers found" + end + + controls.close = new("ButtonControl", nil, {45, 242, 80, 20}, "Cancel", function() + main:ClosePopup() + end) + + main:OpenPopup(600, 270, "Mod Browser", controls, "save", nil, "close") +end From 8f5c4619e763794da07933b0bd07f831cf8222e1 Mon Sep 17 00:00:00 2001 From: xoxorwr Date: Sun, 26 Jul 2026 21:06:01 +0200 Subject: [PATCH 4/7] Fuzzy search --- src/Classes/ConfigTab.lua | 67 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/src/Classes/ConfigTab.lua b/src/Classes/ConfigTab.lua index 1c40dbc868..5430c63d8d 100644 --- a/src/Classes/ConfigTab.lua +++ b/src/Classes/ConfigTab.lua @@ -1282,14 +1282,75 @@ function ConfigTabClass:OpenAddModPopup(blockData) local displayList = { } local controls = { } + local function fuzzyScore(modText, searchStr, words) + local modLower = modText:lower() + if modLower:find(searchStr, 1, true) then + return 1 + end + if #words > 1 then + local allFound = true + for i = 1, #words do + if not modLower:find(words[i], 1, true) then + allFound = false + break + end + end + if allFound then + return 2 + end + end + if #words == 1 and #searchStr >= 3 then + local textWords = {} + for word in modLower:gmatch("%w+") do + t_insert(textWords, word) + end + for i = 1, #textWords do + for len1 = 2, #searchStr - 1 do + local part1 = searchStr:sub(1, len1) + local part2 = searchStr:sub(len1 + 1) + if textWords[i]:sub(1, #part1) == part1 and textWords[i + 1] and textWords[i + 1]:sub(1, #part2) == part2 then + return 3 + end + end + end + end + return nil + end + local function updateDisplayList() wipeTable(displayList) - local searchStr = controls.search and controls.search.buf:lower():gsub("[%-%+%.%[%]%$%^%%%?%*]", "%%%0") or "" - for _, modText in ipairs(allModsList) do - if #searchStr == 0 or modText:lower():match(searchStr) then + local searchStr = controls.search.buf:lower():gsub("^[%s]+", ""):gsub("[%s]+$", "") + + if #searchStr == 0 then + for _, modText in ipairs(allModsList) do t_insert(displayList, modText) end + else + local words = {} + for word in searchStr:gmatch("%S+") do + t_insert(words, word) + end + + local matches = {} + for _, modText in ipairs(allModsList) do + local rank = fuzzyScore(modText, searchStr, words) + if rank then + t_insert(matches, { text = modText, rank = rank }) + end + end + + table.sort(matches, function(a, b) + if a.rank ~= b.rank then + return a.rank < b.rank + end + return a.text < b.text + end) + + for _, match in ipairs(matches) do + t_insert(displayList, match.text) + end end + if #displayList == 0 then t_insert(displayList, "No matching modifiers found") end From 3216de126652a26a8ae762a0d90ea142956921d6 Mon Sep 17 00:00:00 2001 From: xoxorwr Date: Sun, 26 Jul 2026 21:56:25 +0200 Subject: [PATCH 5/7] Add stats comparison tooltip --- src/Classes/ConfigTab.lua | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Classes/ConfigTab.lua b/src/Classes/ConfigTab.lua index 5430c63d8d..15b2517b1a 100644 --- a/src/Classes/ConfigTab.lua +++ b/src/Classes/ConfigTab.lua @@ -27,6 +27,22 @@ local CustomModBlockClass = newClass("CustomModBlockControl", "Control", "Contro configTab.build.buildFlag = true end) self.controls.enableCheck.state = blockData.enabled ~= false + self.controls.enableCheck.tooltipFunc = function(tooltip) + if tooltip:CheckForUpdate(configTab.build.outputRevision, blockData) then + if configTab.build.calcsTab then + local calcFunc, calcBase = configTab.build.calcsTab:GetMiscCalculator(configTab.build) + if calcFunc then + local curState = blockData.enabled ~= false + blockData.enabled = not curState + configTab:BuildModList() + local output = calcFunc() + blockData.enabled = curState + configTab:BuildModList() + configTab.build:AddStatComparesToTooltip(tooltip, calcBase, output, curState and "^7Disabling this block will give you:" or "^7Enabling this block will give you:") + end + end + end + end self.controls.titleEdit = new("EditControl", {"LEFT", self.controls.enableCheck, "RIGHT"}, {6, 0, 232, 18}, blockData.title or "", nil, nil, nil, function(buf) blockData.title = buf From 3eeab39957162ee041ff61396f759794a7d1945e Mon Sep 17 00:00:00 2001 From: xoxorwr Date: Sun, 26 Jul 2026 22:02:40 +0200 Subject: [PATCH 6/7] Swap enable/delete position to be consistent with other widgets --- src/Classes/ConfigTab.lua | 44 +++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/Classes/ConfigTab.lua b/src/Classes/ConfigTab.lua index 15b2517b1a..51ddc07ac8 100644 --- a/src/Classes/ConfigTab.lua +++ b/src/Classes/ConfigTab.lua @@ -20,7 +20,28 @@ local CustomModBlockClass = newClass("CustomModBlockControl", "Control", "Contro self.blockIndex = blockIndex self.blockData = blockData - self.controls.enableCheck = new("CheckBoxControl", {"TOPLEFT", self, "TOPLEFT"}, {0, 0, 18}, "", function(state) + self.controls.deleteBtn = new("ButtonControl", {"TOPLEFT", self, "TOPLEFT"}, {0, 0, 20, 18}, "^1X", function() + local customModsList = configTab.configSets[configTab.activeConfigSetId].customModsList + table.remove(customModsList, blockIndex) + if #customModsList == 0 then + table.insert(customModsList, { title = "Default", enabled = true, text = "" }) + end + configTab:UpdateCustomModsControls() + configTab:AddUndoState() + configTab:BuildModList() + configTab.build.buildFlag = true + end) + + self.controls.titleEdit = new("EditControl", {"LEFT", self.controls.deleteBtn, "RIGHT"}, {6, 0, 232, 18}, blockData.title or "", nil, nil, nil, function(buf) + blockData.title = buf + configTab:AddUndoState() + end) + + self.controls.addModBtn = new("ButtonControl", {"LEFT", self.controls.titleEdit, "RIGHT"}, {6, 0, 48, 18}, "^7+ Mod", function() + configTab:OpenAddModPopup(blockData) + end) + + self.controls.enableCheck = new("CheckBoxControl", {"TOPRIGHT", self, "TOPRIGHT"}, {0, 0, 18}, "", function(state) blockData.enabled = state configTab:AddUndoState() configTab:BuildModList() @@ -44,27 +65,6 @@ local CustomModBlockClass = newClass("CustomModBlockControl", "Control", "Contro end end - self.controls.titleEdit = new("EditControl", {"LEFT", self.controls.enableCheck, "RIGHT"}, {6, 0, 232, 18}, blockData.title or "", nil, nil, nil, function(buf) - blockData.title = buf - configTab:AddUndoState() - end) - - self.controls.addModBtn = new("ButtonControl", {"LEFT", self.controls.titleEdit, "RIGHT"}, {6, 0, 48, 18}, "^7+ Mod", function() - configTab:OpenAddModPopup(blockData) - end) - - self.controls.deleteBtn = new("ButtonControl", {"TOPRIGHT", self, "TOPRIGHT"}, {0, 0, 24, 18}, "^1X", function() - local customModsList = configTab.configSets[configTab.activeConfigSetId].customModsList - table.remove(customModsList, blockIndex) - if #customModsList == 0 then - table.insert(customModsList, { title = "Default", enabled = true, text = "" }) - end - configTab:UpdateCustomModsControls() - configTab:AddUndoState() - configTab:BuildModList() - configTab.build.buildFlag = true - end) - self.controls.textEdit = new("ResizableEditControl", {"TOPLEFT", self, "TOPLEFT"}, {0, 22, 344, 80, 344, 40, 344, 600}, blockData.text or "", nil, "^%C\t\n", nil, function(buf) blockData.text = buf configTab:AddUndoState() From c1f303e2dd62cabf76096381ce9f0a26a0a71341 Mon Sep 17 00:00:00 2001 From: xoxorwr Date: Mon, 27 Jul 2026 08:14:01 +0200 Subject: [PATCH 7/7] Add a hover tooltip indicating if mod is supported by engine --- src/Classes/ConfigTab.lua | 41 +++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/src/Classes/ConfigTab.lua b/src/Classes/ConfigTab.lua index 51ddc07ac8..96c79cd37e 100644 --- a/src/Classes/ConfigTab.lua +++ b/src/Classes/ConfigTab.lua @@ -75,7 +75,7 @@ local CustomModBlockClass = newClass("CustomModBlockControl", "Control", "Contro self.controls.textEdit.inactiveText = function(val) local inactiveText = "" for line in val:gmatch("([^\n]*)\n?") do - local strippedLine = StripEscapes(line):gsub("^[%s?]+", ""):gsub("[%s?]+$", "") + local strippedLine = StripEscapes(line):match("^%s*(.-)%s*$") local mods, extra = modLib.parseMod(strippedLine) inactiveText = inactiveText .. ((mods and not extra) and colorCodes.MAGIC or colorCodes.UNSUPPORTED) .. (IsKeyDown("ALT") and strippedLine or line) .. "\n" end @@ -1050,7 +1050,7 @@ function ConfigTabClass:BuildModList() if block.enabled ~= false and block.text and #block.text > 0 then hasBlockText = true for line in block.text:gmatch("([^\n]*)\n?") do - local strippedLine = StripEscapes(line):gsub("^[%s?]+", ""):gsub("[%s?]+$", "") + local strippedLine = StripEscapes(line):match("^%s*(.-)%s*$") local mods, extra = modLib.parseMod(strippedLine) if mods and not extra then local source = "Custom" @@ -1069,7 +1069,7 @@ function ConfigTabClass:BuildModList() -- Fallback for tests/headless if not hasBlockText and input.customMods and #input.customMods > 0 then for line in input.customMods:gmatch("([^\n]*)\n?") do - local strippedLine = StripEscapes(line):gsub("^[%s?]+", ""):gsub("[%s?]+$", "") + local strippedLine = StripEscapes(line):match("^%s*(.-)%s*$") local mods, extra = modLib.parseMod(strippedLine) if mods and not extra then local source = "Custom" @@ -1247,20 +1247,20 @@ function ConfigTabClass:OpenAddModPopup(blockData) local seen = { } local function addModEntry(mod) - if type(mod) == "string" then - local stripped = StripEscapes(mod):gsub("^[%s?]+", ""):gsub("[%s?]+$", "") + local function registerMod(str) + local stripped = StripEscapes(str):match("^%s*(.-)%s*$") if #stripped > 0 and not seen[stripped] then seen[stripped] = true t_insert(allModsList, stripped) end + end + + if type(mod) == "string" then + registerMod(mod) elseif type(mod) == "table" then for i = 1, #mod do if type(mod[i]) == "string" then - local stripped = StripEscapes(mod[i]):gsub("^[%s?]+", ""):gsub("[%s?]+$", "") - if #stripped > 0 and not seen[stripped] then - seen[stripped] = true - t_insert(allModsList, stripped) - end + registerMod(mod[i]) end end end @@ -1346,7 +1346,6 @@ function ConfigTabClass:OpenAddModPopup(blockData) for word in searchStr:gmatch("%S+") do t_insert(words, word) end - local matches = {} for _, modText in ipairs(allModsList) do local rank = fuzzyScore(modText, searchStr, words) @@ -1354,14 +1353,12 @@ function ConfigTabClass:OpenAddModPopup(blockData) t_insert(matches, { text = modText, rank = rank }) end end - table.sort(matches, function(a, b) if a.rank ~= b.rank then return a.rank < b.rank end return a.text < b.text end) - for _, match in ipairs(matches) do t_insert(displayList, match.text) end @@ -1383,6 +1380,18 @@ function ConfigTabClass:OpenAddModPopup(blockData) controls.listControl.GetRowValue = function(self, column, index, value) return value or "" end + controls.listControl.AddValueTooltip = function(self, tooltip, index, value) + tooltip:Clear(true) + if value and #value > 0 and value ~= "No matching modifiers found" then + local cleanText = itemLib.applyRange(value, 0.5) + local mods, extra = modLib.parseMod(cleanText) + if mods and not extra then + tooltip:AddLine(14, "^7Supported: ^2Yes") + else + tooltip:AddLine(14, "^7Supported: ^1No") + end + end + end controls.listControl.OnSelClick = function(self, index, value) if main.SelectControl then main:SelectControl(self) @@ -1391,10 +1400,11 @@ function ConfigTabClass:OpenAddModPopup(blockData) end controls.listControl.OnSelDoubleClick = function(index, value) if value and value ~= "No matching modifiers found" then + local textToAdd = itemLib.applyRange(value, 0.5) if blockData.text and #blockData.text > 0 and not blockData.text:match("\n$") then blockData.text = blockData.text .. "\n" end - blockData.text = (blockData.text or "") .. value + blockData.text = (blockData.text or "") .. textToAdd self:UpdateCustomModsControls() self:AddUndoState() self:BuildModList() @@ -1414,10 +1424,11 @@ function ConfigTabClass:OpenAddModPopup(blockData) local selIndex = controls.listControl.selIndex or 1 local selected = displayList[selIndex] if selected and selected ~= "No matching modifiers found" then + local textToAdd = itemLib.applyRange(selected, 0.5) if blockData.text and #blockData.text > 0 and not blockData.text:match("\n$") then blockData.text = blockData.text .. "\n" end - blockData.text = (blockData.text or "") .. selected + blockData.text = (blockData.text or "") .. textToAdd self:UpdateCustomModsControls() self:AddUndoState() self:BuildModList()