Skip to content

BMAG v2.0.0: Rewrite#8

Open
uptudev wants to merge 95 commits intoKooluve:mainfrom
uptudev:v2
Open

BMAG v2.0.0: Rewrite#8
uptudev wants to merge 95 commits intoKooluve:mainfrom
uptudev:v2

Conversation

@uptudev
Copy link
Copy Markdown
Contributor

@uptudev uptudev commented Jul 31, 2025

Completely rewrote the mod from scratch, adding a binding menu which allows binding any key or button to any function. Default settings are still the same, but you can now rebind any of the mod functions.

All old GPLv3-licensed code was replaced with new MIT/X11-licensed code, as GPLv3 prohibits shipping software with proprietary code, which Balatro might contain. Steamodded metadata has been updated to the new v1.0.0 format, and a CHANGELOG.md, CODE_OF_CONDUCT.md and CONTRIBUTING.md have been added to track changes and outline contribution guidelines.

Files in ./.dev are used to bootstrap the development process, and shouldn't be included in release archives. The only files that should be included in the releases are:

  • ./localization/,
  • ./src/,
  • ./bmag.json,
  • ./LICENSE, and
  • ./README.md.

To preview, I have v2.0.0 as a release on my fork of the repo here. I will be testing it out in the meantime to ensure no bugs slipped through, but all code is documented and straightforward.

uptudev and others added 30 commits July 13, 2025 00:48
Taking a break for a bit. There will be a part 2 for this commit.

Mostly consists of restructuring the program as well as changing
variable names to be more accurate. Might revert some in pt.2
Slowly but surely this codebase will make sense to me
Remove Handy mention
`sed` messed up one of the localization strings
Create issue templates for GitHub tracking
Merge pull request #7 from uptudev/main
Added headers to make reading issues easier
- Notes on v2 development can be found in the GitHub Issues.
- Legacy code removed.
modify:
* `./LICENSE`: replace GPLv3 license with modified MIT/X11 license.

delete:
- `./README_JP.md`: remove old docs not written by me
- `./README_ZH.md`: remove old docs not written by me
- `./config.lua`: remove old GPLv3 licensed code
- `./localization/en-us.lua`: remove old GPLv3 licensed code
- `./localization/ja.lua`: remove old GPLv3 licensed code
- `./localization/zh_CN.lua`: remove old GPLv3 licensed code

Signed-off-by: uptu <uptu@uptu.dev>
create:
+ `./src/main.lua`: add main source code file

modify:
* `./bmag.lua`: change to only contain Steamodded metadata and a
  `load_file` call to `./src/main.lua`

Signed-off-by: uptu <uptu@uptu.dev>
create:
+ `./CHANGELOG.md`: add a CHANGELOG compliant with the standards set out
  at https://keepachangelog.com/

Signed-off-by: uptu <uptu@uptu.dev>
create:
+ `./src/libs/timers.lua`: add an implementation for a table for
  tracking monotonic keypress timers

modify:
* `./queue.lua` -> `./src/libs/queue.lua`: move the queue implementation
  to the `libs` directory

Signed-off-by: uptu <uptu@uptu.dev>
create:
+ `./.dev/hooks/commit-msg`: add hook to sign off on git commit messages
+ `./.dev/hooks/post-checkout`: add hook to run a bootstrapping script
  on checkout
create:
+ `./.dev/commit-template`: add a default git commit template
modify:
* `./.dev/hooks/commit-msg`: sign the commit message in-place
create:
+ `./.dev/bootstrap.sh`: add bootstrapping script

Signed-off-by: uptu <uptu@uptu.dev>
modify:
* `./.dev/bootstrap.sh`: Error catching and pipeline fix

Signed-off-by: uptu <uptu@uptu.dev>
create:
+ `./CONTRIBUTING.md`: add contribution guidelines

Signed-off-by: uptu <uptu@uptu.dev>
modify:
* `./.dev/bootstrap.sh`: should be all fixed now ;-;

Signed-off-by: uptu <uptu@uptu.dev>
create:
+ `./CODE_OF_CONDUCT.md`: add the Code of Conduct to the repo, based off
  of Contributor Covenant v2.1

Signed-off-by: uptu <uptu@uptu.dev>
uptudev and others added 2 commits August 5, 2025 03:06
modify:
* `./CHANGELOG.md`: reformat to include some missing features
* `./README.md`: add a video preview of the mod at work

Signed-off-by: uptu <uptu@uptu.dev>
modify:
* `./README.md`: replace video link with GitHub built-in embedded video
@Kooluve
Copy link
Copy Markdown
Owner

Kooluve commented Aug 6, 2025

Thank you. I’ll consider merging or transferring this at a later time. That said, I still believe some of BMag’s unique features could be integrated into Handy without too much difficulty—this might be simpler than a full rewrite. I’d suggest reevaluating both approaches.
However, given the effort you’ve already put into these improvements, completing this project would still be worthwhile, even if it may not be strictly necessary.

@Kooluve
Copy link
Copy Markdown
Owner

Kooluve commented Sep 14, 2025

Apologies for the long delay in addressing this project.

I tested version 2.0.1 and noticed that a long right-click incorrectly triggers buttons outside the hand zone (e.g., "Sort Hand," "Play Hand," "View Deck," "Options," etc.). My original implementation included area restrictions to prevent this.

Additionally, I see you replaced the default Sort and Play/Discard key bindings. While the current version makes customization convenient, I’d like to note that most users are likely accustomed to the original key layout. Moreover, since Play/Discard is used more frequently than Sort, and many mice lack side buttons, assigning the scroll wheel to Play/Discard by default might be more practical.

modify:
* `./src/libs/feat.lua`: add two missing `if` statements to ensure that
  the multiselect target is a `Card` and not a UI element like the Sort,
  Discard, or Play buttons.

Signed-off-by: uptu <uptu@uptu.dev>
modify:
* `./CHANGELOG.md`: add v2.0.2 changes
* `./bmag.json`: increment mod version to `2.0.2`

Signed-off-by: uptu <uptu@uptu.dev>
@uptudev
Copy link
Copy Markdown
Contributor Author

uptudev commented Sep 15, 2025

Patched that error with an if statement to ensure the target is a Card, as that should be easier computationally than checking the bounding box every frame that multiselect is processing.

Regarding the control switch, it's extremely easy to accidentally play or discard a hand by bumping the scroll wheel. The change was due to the fact that accidentally sorting your hand is much less detrimental to a run than accidentally playing or discarding a hand because you grabbed your mouse too quickly or bumped the wheel with your fingers.

Given the controls can be rebound much easier, I thought that having a default that is safe and less error prone may be best. If you want, I can change it later tonight, as it just involves swapping out the default table entries in BindMap:new().

@Kooluve
Copy link
Copy Markdown
Owner

Kooluve commented Sep 16, 2025

You're absolutely right. I had considered the potential for accidental triggers before but didn't address it thoroughly. Let’s proceed with your suggested default setup.

I’ll continue with some additional testing and add localization support. If everything goes well, I plan to merge this pull request soon.

@Kooluve
Copy link
Copy Markdown
Owner

Kooluve commented Sep 20, 2025

A few additional minor issues:

  1. right-click highlighting is too immediate
    Currently, right-click triggers highlighting instantly, leaving no room for hesitation. Additionally, a simple right-click (without drag) causes the card to highlight and then immediately unhighlight, which feels redundant.
    In my original implementation, highlighting only began after the cursor moved a certain distance after press down (e.g., Vector_Dist(R_cursor_down.T, C.cursor_hover.T) >= 0.1 * G.MIN_CLICK_DIST).

  2. View Deck button is still triggered by right-click
    The "View Deck" button in the bottom-right corner remains activatable via right-click, which should be restricted.

  3. Right-click selects all card types unintentionally
    Cards like Jokers, Tarot cards, and others (both in hand and in the shop) are currently affected by right-click selection, which may not be intended.

  4. Game crashes when holding right-click and then left-clicking on any menu button

modify:
* `./src/libs/feats.lua`: fix a crash when holding the `multiselect`
  bind down and clicking on a menu button

Signed-off-by: uptu <uptu@uptu.dev>
modify:
* `./src/libs/feats.lua`: remove the ability to select non-hand cards
  with multiselect

Signed-off-by: uptu <uptu@uptu.dev>
modify:
* `./src/libs/feats.lua`: refactor multiselect to ensure multiselecting
  only happens after the user has moved the mouse
* `./src/libs/timers.lua`: set/unset multiselect start position on
  keypress
* `./src/main.lua`: add multiselect start position to mod state

Signed-off-by: uptu <uptu@uptu.dev>
@uptudev
Copy link
Copy Markdown
Contributor Author

uptudev commented Sep 20, 2025

1. right-click highlighting is too immediate
   Currently, right-click triggers highlighting instantly, leaving no room for hesitation. Additionally, a simple right-click (without drag) causes the card to highlight and then immediately unhighlight, which feels redundant.
   In my original implementation, highlighting only began after the cursor moved a certain distance after press down (e.g., Vector_Dist(R_cursor_down.T, C.cursor_hover.T) >= 0.1 * G.MIN_CLICK_DIST).

Fixed by f558f39.

2. View Deck button is still triggered by right-click
   The "View Deck" button in the bottom-right corner remains activatable via right-click, which should be restricted.

3. Right-click selects all card types unintentionally
   Cards like Jokers, Tarot cards, and others (both in hand and in the shop) are currently affected by right-click selection, which may not be intended.

Both fixed by 3e85587.

4. Game crashes when holding right-click and then left-clicking on any menu button

Fixed by 0a7434d.


Drafting a new release right now on my repo; let me know if you find any other issues :)

modify:
* `./CHANGELOG.md`: update with `v2.0.3` changes
* `./bmag.json`: increment version number

Signed-off-by: uptu <uptu@uptu.dev>
Comment thread src/libs/feats.lua Outdated
modify:
* `./src/libs/feats.lua`: fix a minor error introduced during
  multiselect refactor where some state variables wouldn't be populated
  correctly (unlikely but a possible bug regardless)
* `./src/main.lua`: add `prev_target` to mod state variables to use
  instead of `G.CONTROLLER.hovering.prev_target`, which the engine
  overwrites during gameplay, causing erroneous behaviour where one
  could spam-click a card by putting the cursor over the blank space a
  card moves into/away from when selected

Signed-off-by: uptu <uptu@uptu.dev>
@uptudev
Copy link
Copy Markdown
Contributor Author

uptudev commented Sep 20, 2025

Fixed a minor error caused by using game state variables, which allowed the user to spam a card if they got their cursor in the right spot. It's unlikely to cause issues in gameplay, but I fixed it anyways.

There was also one more issue that I found where I assign the prev_prev_target in the wrong branch of my if statement. I can't find any more bugs or errors after fixing it and reviewing my code, so let me know if any more come up and I'll patch them out.

(ignore the force-push; I forgot to update the changelog correctly, then forgot to add it to the commit lol)

modify:
* `./CHANGELOG.md`: add `v2.0.4` changes
* `./bmag.json`: increment version number to `v2.0.4`

Signed-off-by: uptu <uptu@uptu.dev>
@Kooluve
Copy link
Copy Markdown
Owner

Kooluve commented Sep 22, 2025

Thank you for your efforts, all the previous issues have been resolved. However, I've noticed a few problems with the keybinds:

  1. The multiselect keybinding cannot be modified.
  2. The "restart run" action cannot be triggered with any key.

Additional suggestions:

  1. Add a "Restore Default Keybinds" button.
    I've noticed that for many mods, even deleting the mods, mod loader, and config folder, mods's settings still aren't reset. A dedicated reset button would be helpful.
  2. Limit bindable keys to common game keys (letters, numbers, spacebar, etc.).
    Modifier keys (like Ctrl, Alt) or system keys (Win, CapsLock) can cause unintended issues such as StickyKeys activation or unexpected behavior.
  3. Rapid mouse movement during multiselect causes intermittent card selection.
    This seems to occur because the game updates input on a per-frame basis, and fast movement may be missed. This issue exists in both my version and Handy.
    Since most users are unlikely to encounter this (or can avoid it by moving more slowly), I've never prioritized fixing it. What are your thoughts? Do you have a better approach in mind?

@uptudev
Copy link
Copy Markdown
Contributor Author

uptudev commented Sep 22, 2025

I don't know if you have another mod interfering with your input on your end, but multiselect can be bound perfectly fine as of v2.0.4 on my end beyond a minor visual glitch when attempting to rebind the first time; subsequent attempts to rebind work fine. I also cannot replicate your issue with being unable to trigger the restart feature, as it can be bound to either clicks or holds (unbound by default though, because it's the most potentially destructive bind), and both of them work on my end as well. I even removed my old configuration and tried from a new install and it worked fine. Please let me know if the v2.0.5 changes fixed the issue, as they solved the visual glitches I found.

Also ensure you are holding the key down as indicated when trying to rebind any 'hold-only' binds; 'click'-style features like hand sorting and play/discard can be bound to either keypresses or keyholds, but multiselect must be bound to some keyhold, not a press.

I'll be releasing another minor patch in a few minutes with fixes for the visual glitches in the config menu, but as mentioned earlier I couldn't replicate your issues with multiselect nor with the restart function, so I can't patch it unless you can provide more info such as a crash dump or recording of the erroneous behaviour.


Regarding the suggestions:

1. Add a "Restore Default Keybinds" button.
   I've noticed that for many mods, even deleting the mods, mod loader, and config folder, mods's settings still aren't reset. A dedicated reset button would be helpful.

I will probably have one in the next minor patch later today, as this is a great QoL addition and it will be trivial to add.
Added in 1aa142e.

2. Limit bindable keys to common game keys (letters, numbers, spacebar, etc.).
   Modifier keys (like Ctrl, Alt) or system keys (Win, CapsLock) can cause unintended issues such as StickyKeys activation or unexpected behavior.

StickyKeys is OS-specific behaviour that can be disabled from within Windows, and should be unless you explicitly require it; there's no need to further limit input options beyond disallowing Mouse1 and ESC to be bound, as putting the line at modifiers seems completely arbitrary compared to just limiting it to only keys that the game can support, as the current implementation does. If someone doesn't want AltGr bound to a feature, the solution is to not bind it in the first place, rebind it with another key, or to use the (soon to be implemented) reset button to reset.

3. Rapid mouse movement during multiselect causes intermittent card selection.
   This seems to occur because the game updates input on a per-frame basis, and fast movement may be missed. This issue exists in both my version and Handy.
   Since most users are unlikely to encounter this (or can avoid it by moving more slowly), I've never prioritized fixing it. What are your thoughts? Do you have a better approach in mind?

That is a byproduct of how the Love2D game engine operates; any part of the input handling of the game works through frame updates, and input hooks like this mod are bottlenecked by it. In theory a line could be inscribed from the beginning of your multiselect to the end, and any cards that contact that line could be highlighted, but this would be neither more responsive nor easier to implement (if even possible with the way the game operates). You cannot process input any faster than the computer is already processing the rest of the game without creating a forked process that hooks into the memory of the game, but that would involve creating a project similar to DFHack for Balatro; as such, there's no feasible way to really fix it.

modify:
* `./src/libs/input.lua`: fix inconsistent coding style

Signed-off-by: uptu <uptu@uptu.dev>
modify:
* `./localization/en-us.lua`: add localization key for reset button
  text; removed brackets from button text
* `./src/libs/config.lua`: add reset button to config menu; fix buggy
  config functions

Signed-off-by: uptu <uptu@uptu.dev>
modify:
* `./CHANGELOG.md`: add `v2.0.5` changes
* `./bmag.json`: increment version number to `v2.0.5`

Signed-off-by: uptu <uptu@uptu.dev>
@uptudev
Copy link
Copy Markdown
Contributor Author

uptudev commented Sep 22, 2025

Thank you for your efforts, all the previous issues have been resolved. However, I've noticed a few problems with the keybinds:

  1. The multiselect keybinding cannot be modified.

  2. The "restart run" action cannot be triggered with any key.

Let me know if these are still issues for you with v2.0.5. As far as I can tell all bugs with the menu on my end are gone, and that may have fixed the errors you noticed.

@Kooluve
Copy link
Copy Markdown
Owner

Kooluve commented Sep 23, 2025

I hadn't realized there was a distinction between clicking and long-pressing in the config interface, as most games allow keybinds to be set with a simple click—regardless of whether the actual in-game action is a click or a hold. Although I noticed the hint text at the top, I didn’t read it carefully and simply followed my intuition when setting the keys. Even though you’ve added a clearer prompt in version 2.0.5, I’m still not entirely sure whether all players will notice this detail.

Because of this, I initially tried to set the multiselect key with a click (rather than a long press), and then attempted to use the reset function (which I’d set with a click) by holding the key down. After understanding the intended behavior, I was able to configure and use the keys correctly. However, I’m still uncertain whether distinguishing between click and long-press during keybinding aligns with most players’ intuition.

Regarding the restriction of bindable keys—aside from the Windows key, which can cause more noticeable issues—other keys don’t really have significant drawbacks. It’s reasonable to let players decide their own key mappings, so the current approach is fine.

As for the rapid movement issue during multiselect, one possible solution is to sample the mouse position at short intervals (e.g., every 10 frames, though this would need testing) and check for collisions with card hitboxes along the path formed by these points. It’s important not to only record the start and end positions, since players sometimes need to select cards in a non-linear pattern (e.g., drawing a wavy line with the mouse). You can explore whether this is feasible to implement, though I agree it may be a relatively low-priority fix.

Other bugs:

  1. Clicking "Back" in the config interface still saves the settings automatically. The expected behavior might be to only save changes when explicitly confirming (e.g., with an "Apply" or "Save" button).
  2. Keybind Conflict/Glitch: If you assign a key to another function while it's already bound to "Multiselect," so the "Multiselect" keybind is cleared. However, the game continues to perform the multiselect action with that key until you either: a) reset all keybinds, or b) reassign a key to "Multiselect" and use it once.

Enhancement:
In the config interface, the "Suit" option is positioned above the "Value" option. It might be more intuitive to swap their positions (Value above Suit).

@uptudev
Copy link
Copy Markdown
Contributor Author

uptudev commented Sep 23, 2025

No.

Quit responding to me using AI prompts. I've been doing software engineering for over a decade and a half, and I don't need someone getting a chatbot to reiterate what I already said, then providing suggestions they cannot even be bothered to verify, especially when they regularly hallucinate bugs that don't exist. It's a massive waste of my time as someone trying to contribute this PR for over 2 months.

Has your LLM considered the computational cost of checking a bounding box every few frames may be massive when we've already determined pegging input processing to a potentially slow framerate is the bottleneck in the hypothetical you're trying to solve? Parsing input a tenth of the time using collision functions 10x as computationally intensive that probably don't even exist in the engine won't fix anything; Love2D almost certainly doesn't have a way to iterate over every entity making contact with a line, as that would require iterating over every single game object every frame input is parsed. As mentioned earlier when I went over the pros and cons, it's neither feasible to implement, nor more responsive, as cards would only be highlighted every 10 frames.

Listen to what I'm saying and stop asking an LLM trained on millions of unoptimized web dev portfolios to give you suggestions. I'm not adding more extraneous shit; there are over 90 commits in this pull request. Accept the PR or don't.

Regarding the "bugs", the 'Save' button saves your configuration to disk for any subsequent game launches. It's not an 'Apply' button; any bindings set in the menu are immediately applied, and that is intended behaviour when changing settings in a menu.

I might fix the second bug if it actually exists this time. I have doubts given lines 125-129 and 151-155 in ./src/libs/bindmap.lua exist explicitly to handle this behaviour, and it's clear you're using AI which clearly hallucinates to review this for you.

as most games allow keybinds to be set with a simple click—regardless of whether the actual in-game action is a click or a hold

Not when you can bind both clicks and holds to all but one feature; it's called consistency. How many games has the AI that wrote this played again? Oh yeah, zero, so maybe stop asking it.

@Kooluve
Copy link
Copy Markdown
Owner

Kooluve commented Sep 24, 2025

I apologize for the various delays, careless issue submissions, and unwise suggestions I've caused since you submitted this PR.

I am an amateur developer with only basic computer and programming knowledge, so I did rely heavily on free AI Q&A tools during my mod development process. However, in this PR, all my questions and suggestions were based on my own testing and ideas—at this point I wasn’t being careless. The only part where I used AI was for English translation, as I’m not a native speaker and my English skills are limited. Of course, given my limited knowledge, some of my replies might not even be as good as those from free AI, so I'm sorry about this.

I didn't respond for over a month during this period. My life circumstances weren't great at the time, so I didn't feel like putting effort into this, even though I actually had quite a bit of free time. I truly neglected the results of your hard work, I did it wrong.

Although I only performed simple testing and provided feedback during this PR process, I still learned a lot about GitHub and PRs. Since you are the first person I’ve submitted a PR to on GitHub, I truly appreciate your generosity and enthusiastic guidance. My mod has many imperfect aspects, the code is poorly written, and it's already considered outdated on Discord. I believe your substantial improvements can breathe new life into it.


Regarding the rapid movement issue, I admit my consideration was superficial, and I didn’t actually attempt to solve it. As you pointed out, it seems like more trouble than it’s worth. Fortunately, this issue has little practical impact and can be ignored.


About the save function: In my experience, most games with an "Apply" button in the keybinding settings require clicking it to save the settings for the current and future sessions. If players don’t click "Apply" and exit directly, the changes aren’t saved. Players might not realize that settings are saved in real time and "Apply" is meant for subsequent game launches. On the other hand, if there’s no "Apply" button, it’s natural to assume settings are saved in real time.

Your current approach combines these two common methods, which feels counterintuitive and requires additional adaptation. If this is a limitation of Love2D or smods, perhaps more eye-catching explanatory text should be added.


Regarding the second bug, I can consistently reproduce it (game version, smods, and lovely are all up-to-date, with only bmag installed):

  1. Enter the mod configuration menu during a run or from the main menu.
  2. Multiselect an existing keybinding (e.g., default settings).
  3. Rebind the above hold key to another function.
  4. Return to the current run or start a new run.
  5. Multiselect will automatically trigger afterward.
  6. Three temporary fixes: a) Bind any key to multiselect, return to the run, and hold press that key. b) Click the reset button. c) Restart the game.

If needed, I can provide a video with keypress visualization.


About binding keys with clicks and holds: In all the games I’ve played, I’ve never encountered a scenario where holding a key was required for binding. This feels very counterintuitive I think. I also checked several games in my Steam library (like Slay the Spire, Celeste, Monster Hunter or Apex, etc.) and found no such cases. Keybinding typically involves either clicking to input, selecting from options, or assigning a function to a key.

Specifically, in games where clicking and holding perform different actions, the conditions and effects are either:

  • Same condition, similar effects (e.g., normal attack vs. charged attack).

  • Same conditions, different effects (e.g., tapping Shift to toggle walk/run vs. holding Shift to run and releasing to walk).

  • Different conditions, different effects (e.g., key E to interact: tapping to talk to NPCs vs. holding to heal teammates with a medkit).

Thus, even if one key corresponds to multiple functions, only a single key needs to be bound. The distinction between click and hold is handled internally by the game, and users only need to click during keybinding.

Our mod may belongs to the second category. Combining this with the ability to bind both clicks and holds (which is counterintuitive) further blurs the line between binding input and actual operation.

I think it would be better to restrict multiselect and restart to hold-only actions, while other functions are click-only. This would limit all keybinding inputs to clicks. Given the mod’s limited functions, there’s no need to conserve key slots by using holds. This approach wouldn’t restrict user freedom too much, would be more intuitive, and would avoid unexpected issues.

I'm not sure if I'm overthinking the issue of distinguishing between clicks and holds during keybinding. Although it's easy to adapt once you understand the logic, I did experience significant confusion during my initial use.


I hope my suggestions provide some useful insights. If you’re busy, there’s no need to reply quickly—feel free to respond and optimize at your own pace.☺️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants