diff --git a/eid_bagofcrafting.lua b/eid_bagofcrafting.lua index 7c8739ee..820be1a3 100644 --- a/eid_bagofcrafting.lua +++ b/eid_bagofcrafting.lua @@ -1,5 +1,7 @@ local game = Game() +require("eid_bagofcrafting_search") + --these aren't local so that they can be saved and reloaded, or cleared in the Mod Config Menu EID.BoC = {} EID.BoC.CurrentPickupCount = -1 @@ -235,6 +237,9 @@ local recheckPickups = false local customRNGSeed = 0x77777770 local customRNGShift = {0,0,0} +local lastSearchValue = ""; +local lastSearchInputEnabled = false; + -- Use local RNG functions to possibly reduce processing time a little bit local function RNGNext() local num = customRNGSeed @@ -930,6 +935,15 @@ function EID:handleBagOfCraftingUpdating() end end + local currentSearchValue = EID:BoCSGetSearchValue() + local currentSearchInputEnabled = EID:BoCSGetSearchInputEnabled() + EID:BoCSHandleInput() + if currentSearchValue ~= lastSearchValue or currentSearchInputEnabled ~= lastSearchInputEnabled then + refreshNextTick = true + end + lastSearchValue = currentSearchValue + lastSearchInputEnabled = currentSearchInputEnabled + -- Check for Hold Tab key inputs if displayingRecipeList and Input.IsActionPressed(EID.Config["BagOfCraftingToggleKey"], EID.bagPlayer.ControllerIndex) then EID.TabDescThisFrame = true @@ -1176,6 +1190,11 @@ function EID:handleBagOfCraftingRendering(ignoreRefreshRate) prevDesc = "" EID.RefreshBagTextbox = false + local searchLine = EID:BoCSGetSearchLine() + if searchLine ~= nil then + prevDesc = prevDesc .. searchLine + end + prevDesc = prevDesc .. getHotkeyString() prevDesc = prevDesc .. getFloorItemsString(true, roomItems) if (EID.Config["BagOfCraftingShowControls"]) then @@ -1191,7 +1210,6 @@ function EID:handleBagOfCraftingRendering(ignoreRefreshRate) prefix = "#{{Trinket159}} " end - local filteredRecipesList = {} local filteredNumResults = 0 -- If we aren't Tainted Cain, we should filter out recipes that don't use everything in our bag @@ -1199,15 +1217,28 @@ function EID:handleBagOfCraftingRendering(ignoreRefreshRate) for _,id in ipairs(sortedIDs) do filteredRecipesList[id] = {} for _, v in ipairs(currentRecipesList[id]) do - if (EID:bagContainsCount(v[1]) == #bagItems) then + local itemName = EID:getObjectName(5, 100, v[2]); + local searchValid = not EID:BoCSGetSearchEnabled() or EID:BoCSCheckItemName(itemName) + + if (searchValid and EID:bagContainsCount(v[1]) == #bagItems) then table.insert(filteredRecipesList[id], v) filteredNumResults = filteredNumResults + 1 end end end else - filteredRecipesList = currentRecipesList - filteredNumResults = numResults + for _,id in ipairs(sortedIDs) do + filteredRecipesList[id] = {} + for _, v in ipairs(currentRecipesList[id]) do + local itemName = EID:getObjectName(5, 100, v[2]); + local searchValid = not EID:BoCSGetSearchEnabled() or EID:BoCSCheckItemName(itemName) + + if (searchValid) then + table.insert(filteredRecipesList[id], v) + filteredNumResults = filteredNumResults + 1 + end + end + end end -- Keeping the offset doesn't work at all with non-Tainted-Cain bag-filtered results; @@ -1229,6 +1260,7 @@ function EID:handleBagOfCraftingRendering(ignoreRefreshRate) if (curOffset > bagOfCraftingOffset+EID.Config["BagOfCraftingResults"]) then break end if not v then break end if (curOffset > bagOfCraftingOffset) then + local itemName = EID:getObjectName(5, 100, v[2]); if not EID.Config["BagOfCraftingDisplayNames"] then prevDesc = prevDesc .."#{{Collectible"..v[2].."}} " --color the equals sign with the item quality, so the order of the list can make sense @@ -1238,7 +1270,7 @@ function EID:handleBagOfCraftingRendering(ignoreRefreshRate) if prevItem ~= v[2] then --substring the first 18 characters of the item name so it fits on one line; is there a way to get around desc line length limits? prevDesc = prevDesc .."#{{Collectible"..v[2].."}} ".. qualities[CraftingItemQualities[v[2]]] .. - string.sub(EID:getObjectName(5, 100, v[2]),1,18).."#" + string.sub(itemName,1,18).."#" else prevDesc = prevDesc .."#" end diff --git a/eid_bagofcrafting_search.lua b/eid_bagofcrafting_search.lua new file mode 100644 index 00000000..67ae685b --- /dev/null +++ b/eid_bagofcrafting_search.lua @@ -0,0 +1,268 @@ +local indexCharMapping = " ??????'????,-./0123456789?;?????abcdefghijklmnopqrstuvwxyz" +local searchValue = "" +local searchInputEnabled = false +local lastBackspaceTrigger = 0 +local backspaceStep = 0 + +--- Returns the current search query +-- @return string +function EID:BoCSGetSearchValue() + return searchValue +end + +--- Enables or disables the search input respectively +-- @param newState boolean +function EID:BoCSSetSearchInputEnabled(newState) + searchInputEnabled = newState + + if newState then + EID:AddCallback(ModCallbacks.MC_INPUT_ACTION, EID.BoCSBlockInputAction) + else + EID:RemoveCallback(ModCallbacks.MC_INPUT_ACTION, EID.BoCSBlockInputAction) + end +end + +--- Returns if the search input is enabled +-- @returns boolean +function EID:BoCSGetSearchInputEnabled() + return searchInputEnabled +end + +--- Gets if the filtering is currently enabled +function EID:BoCSGetSearchEnabled() + return true +end + +--- Returns true if the item name is matched +-- @returns boolean +function EID:BoCSCheckItemName(itemName) + if searchValue == "" then + return true + end + + return string.find(string.lower(itemName), string.lower(searchValue)) +end + +--- Handles any input done related to Bag of Crafting search +function EID:BoCSHandleInput() + if Game():IsPaused() then + return + end + + if EID.bagPlayer == nil then + return + end + + if Input.IsButtonTriggered(Keyboard.KEY_ENTER, EID.bagPlayer.ControllerIndex, true) then + EID:BoCSSetSearchInputEnabled(not searchInputEnabled) + return + end + + if searchInputEnabled then + local newValue = searchValue + local index = 0 + local hasLetterInput = false + + -- Keep in mind that this only works because the respective + -- enum values are integers + for i=Keyboard.KEY_SPACE,Keyboard.KEY_Z do + index = index + 1 + if Input.IsButtonTriggered(i, EID.bagPlayer.ControllerIndex, true) then + local toAppend = string.upper(indexCharMapping:sub(index, index)) + + newValue = newValue .. toAppend + hasLetterInput = true + + -- We only handle one input at a time + break + end + + if hasLetterInput then + return + end + end + + if Input.IsButtonTriggered(Keyboard.KEY_BACKSPACE, EID.bagPlayer.ControllerIndex, true) then + lastBackspaceTrigger = 0 + backspaceStep = 0 + return + end + + local currentFrame = Game():GetFrameCount() + -- 30 Frames = 1 second + -- 15 = 500ms + if Input.IsButtonPressed(Keyboard.KEY_BACKSPACE, EID.bagPlayer.ControllerIndex, true) and ( + backspaceStep == 0 + or + (backspaceStep == 1 and currentFrame - lastBackspaceTrigger > 15) -- we delay the second deletion a little bit more + or + (backspaceStep >= 2 and currentFrame - lastBackspaceTrigger > 3) + ) then + local isControlPressed = ( + Input.IsButtonPressed(Keyboard.KEY_LEFT_CONTROL, EID.bagPlayer.ControllerIndex, true) + or + Input.IsButtonPressed(Keyboard.KEY_RIGHT_CONTROL, EID.bagPlayer.ControllerIndex, true) + ) + + if isControlPressed then + newValue = "" + else + newValue = searchValue:sub(1, -2) + end + + lastBackspaceTrigger = currentFrame + backspaceStep = backspaceStep + 1 + end + + searchValue = newValue + end +end + +--- Returns the line that should be displayed inside of the menu +function EID:BoCSGetSearchLine() + if not searchInputEnabled and (searchValue == nil or searchValue == "") then + return nil + end + + local result = "" + + if searchInputEnabled then + result = "{{ColorGreen}}" + end + + result = result .. "Search: " .. searchValue .. "#" + + return result +end + +--- +-- This is a workaround to block every single Input call that could potentially be +-- used from another mod. +-- We override the Input functions with our own stubs to prevent other mods from +-- doing actions while the user is performing a search. +-- This does NOT block the main game from performing actions like toggling fullscreen +-- or toggling the pause!-- +-- Since this could be useful for multiple reasons, this should probably be put +-- inside the main.lua instead of leaving this here +-- Hooking multiple times WILL impact the performance!-- +-- I've also added a third parameter called "force" +-- This way, we are still able to force the real function to be called instead +-- of our modified code-- +-- This code should not impact the performance in a huge way +-- since the first thing we do is check if the searchInput is enabled anyway +-- We can allow losing a bit of performance when searching +function EID:BoCSHookInput() + local oldInputIsButtonTriggered = Input.IsButtonTriggered + local oldInputIsButtonPressed = Input.IsButtonPressed + local oldInputIsActionTriggered = Input.IsActionTriggered + local oldInputIsActionPressed = Input.IsActionPressed + + Input.IsButtonTriggered = function(key, controller, force) + if force or not searchInputEnabled then + return oldInputIsButtonTriggered(key, controller) + end + + local bagPlayer = nil + if EID.isRepentance then + local hasBag, player = EID:PlayersHaveCollectible(710) + if hasBag then + bagPlayer = player + end + end + + if bagPlayer == nil or controller ~= bagPlayer.ControllerIndex then + return oldInputIsButtonTriggered(key, controller) + end + + return false + end + + Input.IsButtonPressed = function(key, controller, force) + if force or not searchInputEnabled then + return oldInputIsButtonPressed(key, controller) + end + + local bagPlayer = nil + if EID.isRepentance then + local hasBag, player = EID:PlayersHaveCollectible(710) + if hasBag then + bagPlayer = player + end + end + + if bagPlayer == nil or controller ~= bagPlayer.ControllerIndex then + return oldInputIsButtonPressed(key, controller) + end + + return false + end + + Input.IsActionTriggered = function(key, controller, force) + if force or not searchInputEnabled then + return oldInputIsActionTriggered(key, controller) + end + + local bagPlayer = nil + if EID.isRepentance then + local hasBag, player = EID:PlayersHaveCollectible(710) + if hasBag then + bagPlayer = player + end + end + + if bagPlayer == nil or controller ~= bagPlayer.ControllerIndex then + return oldInputIsActionTriggered(key, controller) + end + + return false + end + + Input.IsActionPressed = function(key, controller, force) + if force or not searchInputEnabled then + return oldInputIsActionPressed(key, controller) + end + + local bagPlayer = nil + if EID.isRepentance then + local hasBag, player = EID:PlayersHaveCollectible(710) + if hasBag then + bagPlayer = player + end + end + + if bagPlayer == nil or controller ~= bagPlayer.ControllerIndex then + return oldInputIsActionPressed(key, controller) + end + + return false + end + + Isaac.DebugString("Hooked Input") +end + +--- Blocks all input actions triggered by the games bindings itself +function EID:BoCSBlockInputAction(entity, inputHook, buttonAction) + local bagPlayer = nil + if EID.isRepentance then + local hasBag, player = EID:PlayersHaveCollectible(710) + if hasBag then + bagPlayer = player + end + end + + if bagPlayer == nil then + return nil + end + + if inputHook == InputHook.GET_ACTION_VALUE then + return 0 + end + + if inputHook == InputHook.IS_ACTION_PRESSED or inputHook == InputHook.IS_ACTION_TRIGGERED then + return false + end + + return nil +end + +EID:BoCSHookInput()