├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── copy-metadata.py ├── cspell.json ├── docs ├── api.md └── quick-start.md ├── main.lua ├── metadata.xml ├── release.py ├── release.sh ├── resources ├── gfx │ └── ui │ │ └── modconfig │ │ ├── arrows.png │ │ ├── backselect.png │ │ ├── black.png │ │ ├── menu.anm2 │ │ ├── menu.png │ │ ├── menu_overlay.png │ │ ├── offset.png │ │ ├── popup_thin_large.png │ │ ├── popup_thin_medium.png │ │ ├── popup_thin_small.png │ │ ├── popup_wide_large.png │ │ ├── popup_wide_medium.png │ │ ├── popup_wide_small.png │ │ ├── slider.png │ │ └── strike.png └── previewfile_1603631350.jpg ├── scripts ├── inputhelper.lua ├── modconfig.lua └── screenhelper.lua └── steam ├── screenshots ├── 1603631350_preview_250900_20210110125025_1.png ├── 1603631350_preview_250900_20210113160746_1.png ├── 1603631350_preview_250900_20210113160751_1.png ├── 1603631350_preview_250900_20210113160753_1.png ├── 1603631350_preview_250900_20210113160757_1.png ├── 1603631350_preview_250900_20210113160811_1.png └── 1603631350_preview_250900_20210113160813_1.png └── thumbnail.jpg /.gitattributes: -------------------------------------------------------------------------------- 1 | # Prevent Windows systems from cloning this repository with "\r\n" line endings 2 | core.autocrlf=false 3 | 4 | # Convert all files to use "\n" line endings 5 | * text=auto eol=lf 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build_and_lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout the repository 10 | uses: actions/checkout@v3 11 | 12 | # https://github.com/IsaacScript/isaac-steam-workshop-upload 13 | - name: Upload the mod to Steam Workshop (if this is a release commit) 14 | uses: IsaacScript/isaac-steam-workshop-upload@v2 15 | if: "contains(github.event.head_commit.message, 'chore: release') && github.event_name != 'pull_request'" 16 | with: 17 | mod_path: . 18 | ignore_files: cspell.json,release.py,release.sh,steam 19 | change_note: "Version: {VERSION}\n\nChanges for this mod are [url=https://github.com/Zamiell/isaac-mod-config-menu]tracked on GitHub[/url]." 20 | env: 21 | CONFIG_VDF_CONTENTS: ${{ secrets.CONFIG_VDF_CONTENTS }} 22 | 23 | discord: 24 | name: Discord Failure Notification 25 | needs: [build_and_lint] 26 | if: failure() 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: sarisia/actions-status-discord@v1 30 | with: 31 | webhook: ${{ secrets.DISCORD_WEBHOOK }} 32 | status: failure 33 | title: "" 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS-specific artifacts 2 | .DS_Store 3 | thumbs.db 4 | 5 | # Isaac-related files 6 | disable.it 7 | *.dat 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | // These are Visual Studio Code extensions that are intended to be used with this particular 2 | // repository 3 | // https://go.microsoft.com/fwlink/?LinkId=827846 4 | { 5 | "recommendations": [ 6 | "sumneko.lua", // Lua language server 7 | "filloax.isaac-lua-api-vscode", // Isaac API definitions 8 | "streetsidesoftware.code-spell-checker", // A spell-checker extension based on CSpell 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // These are Visual Studio Code settings that should apply to this particular repository 2 | { 3 | // ---------------- 4 | // Vanilla settings 5 | // ---------------- 6 | 7 | "editor.rulers": [100], 8 | "editor.tabSize": 2, 9 | 10 | "files.associations": { 11 | "*.anm2": "xml" 12 | }, 13 | 14 | // Linux line endings are used in this project 15 | "files.eol": "\n", 16 | 17 | // Automatically removing all trailing whitespace when saving a file 18 | "files.trimTrailingWhitespace": true, 19 | 20 | // ----------------- 21 | // Language settings 22 | // ----------------- 23 | 24 | // Automatically run the formatter when certain files are saved. 25 | "[lua]": { 26 | "editor.defaultFormatter": "sumneko.lua", 27 | "editor.formatOnSave": true, 28 | "editor.tabSize": 2, 29 | }, 30 | 31 | // Isaac globals, auto-defined from Filloax's extension. 32 | "Lua.diagnostics.globals": [ 33 | "ActionTriggers", 34 | "ActiveSlot", 35 | "BabySubType", 36 | "BackdropType", 37 | "BatterySubType", 38 | "BedSubType", 39 | "BitSet128", 40 | "BombSubType", 41 | "BombVariant", 42 | "ButtonAction", 43 | "CacheFlag", 44 | "Card", 45 | "Challenge", 46 | "ChampionColor", 47 | "ChestSubType", 48 | "CoinSubType", 49 | "CollectibleType", 50 | "Color", 51 | "CppContainer", 52 | "DamageFlag", 53 | "Difficulty", 54 | "Direction", 55 | "DoorSlot", 56 | "DoorState", 57 | "DoorVariant", 58 | "EffectVariant", 59 | "Entity", 60 | "EntityBomb", 61 | "EntityCollisionClass", 62 | "EntityEffect", 63 | "EntityFamiliar", 64 | "EntityFlag", 65 | "EntityGridCollisionClass", 66 | "EntityKnife", 67 | "EntityLaser", 68 | "EntityNPC", 69 | "EntityPartition", 70 | "EntityPickup", 71 | "EntityPlayer", 72 | "EntityProjectile", 73 | "EntityPtr", 74 | "EntityRef", 75 | "EntityTear", 76 | "EntityType", 77 | "FamiliarVariant", 78 | "Font", 79 | "Game", 80 | "GameStateFlag", 81 | "GetPtrHash", 82 | "GridCollisionClass", 83 | "GridEntity", 84 | "GridEntityDesc", 85 | "GridEntityDoor", 86 | "GridEntityPit", 87 | "GridEntityPoop", 88 | "GridEntityPressurePlate", 89 | "GridEntityRock", 90 | "GridEntitySpikes", 91 | "GridEntityTNT", 92 | "GridEntityType", 93 | "GridRooms", 94 | "HUD", 95 | "HeartSubType", 96 | "Input", 97 | "InputHook", 98 | "Isaac", 99 | "ItemConfig", 100 | "ItemPool", 101 | "ItemPoolType", 102 | "ItemType", 103 | "KColor", 104 | "KeySubType", 105 | "Keyboard", 106 | "LaserOffset", 107 | "LaserSubType", 108 | "Level", 109 | "LevelCurse", 110 | "LevelStage", 111 | "LevelStateFlag", 112 | "LocustSubtypes", 113 | "ModCallbacks", 114 | "Mouse", 115 | "Music", 116 | "MusicManager", 117 | "NpcState", 118 | "NullItemID", 119 | "Options", 120 | "PathFinder", 121 | "PickupPrice", 122 | "PickupVariant", 123 | "PillColor", 124 | "PillEffect", 125 | "PlayerForm", 126 | "PlayerSpriteLayer", 127 | "PlayerType", 128 | "PlayerTypes", 129 | "PoopPickupSubType", 130 | "PoopSpellType", 131 | "ProjectileFlags", 132 | "ProjectileParams", 133 | "ProjectileVariant", 134 | "QueueItemData", 135 | "REPENTANCE", 136 | "RNG", 137 | "Random", 138 | "RandomVector", 139 | "RegisterMod", 140 | "RenderMode", 141 | "Room", 142 | "RoomConfig", 143 | "RoomDescriptor", 144 | "RoomShape", 145 | "RoomTransitionAnim", 146 | "RoomType", 147 | "SFXManager", 148 | "SackSubType", 149 | "SeedEffect", 150 | "Seeds", 151 | "ShockwaveParams", 152 | "SkinColor", 153 | "SortingLayer", 154 | "SoundEffect", 155 | "Sprite", 156 | "StageType", 157 | "StartDebug", 158 | "TearFlags", 159 | "TearParams", 160 | "TearVariant", 161 | "TemporaryEffect", 162 | "TemporaryEffects", 163 | "TrinketType", 164 | "UseFlag", 165 | "Vector", 166 | "WeaponType", 167 | "EID", 168 | "CustomGameOver", 169 | "Encyclopedia", 170 | "DetailedRespawn", 171 | "AwaitingTextInput", 172 | "DeadSeaScrollsMenu" 173 | ], 174 | } 175 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 piber 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mod Config Menu Pure 2 | 3 | 4 | 5 | 6 | ## Introduction 7 | 8 | Mod Config Menu Pure is a library for [The Binding of Isaac: Repentance](https://store.steampowered.com/app/1426300/The_Binding_of_Isaac_Repentance/) that allows other mods to have a settings menu. 9 | 10 | You can find this library [on the Steam Workshop](https://steamcommunity.com/sharedfiles/filedetails/?id=2681875787). 11 | 12 | Credit goes to piber20 for originally creating this library and Chifilly for updating it for Repentance. 13 | 14 | This is a forked version of Mod Config Menu by Zamiel that removes all of the hacks that override internal Lua functionality, which causes problems with other things in the Isaac ecosystem. For this reason, it is called the "Pure" version. 15 | 16 | As of [vanilla patch v1.7.9b](https://bindingofisaacrebirth.fandom.com/wiki/V1.7.9b), other version of Mod Config Menu will not work anymore, like [Mod Config Menu Continued](https://steamcommunity.com/workshop/filedetails/?id=2487535818), so it is recommended to use this version instead. 17 | 18 |
19 | 20 | ## Using Mod Config Menu for Players 21 | 22 | If you are a player of Isaac mods, then using Mod Config Menu should be straightforward. The controls are as follows: 23 | 24 | ### Keyboard 25 | 26 | - By default, you can open the menu by pressing L. (This keyboard binding is customizable from the "Mod Config Menu" sub-menu.) F10 will also always open the menu, which cannot be changed. 27 | - Use the arrow keys or WASD keys to move around. 28 | - E, space, or enter can be used to select an item. 29 | - Esc, backspace, or Q can be used to go back. 30 | 31 | ### Controller 32 | 33 | - By default, you can open the menu by pressing down the right control stick (i.e. R3). (This controller binding is customizable from the "Mod Config Menu" sub-menu.) 34 | - Both control sticks can be used to move around. 35 | - The "a" button can be used to select an item. 36 | - The "b" button can be used to go back. 37 | 38 | By default, there will be two sub-menus installed: "General" and "Mod Config Menu". If you have other mods installed, they may add additional menus. 39 | 40 |
41 | 42 | ## Using Mod Config Menu as a Mod Developer 43 | 44 | In order to use Mod Config Menu Pure inside of your mod, do not use the `require` or `dofile` or `pcall` or `loadfile` functions. Rather, simply check to see if the global variable of `ModConfigMenu` exists, with something along the lines of: 45 | 46 | ```lua 47 | local MOD_NAME = "My Mod" 48 | local VERSION = "1.0.0" 49 | 50 | local function setupMyModConfigMenuSettings() 51 | if ModConfigMenu == nil then 52 | return 53 | end 54 | 55 | ModConfigMenu.AddSpace(MOD_NAME, "Info") 56 | ModConfigMenu.AddText(MOD_NAME, "Info", function() return MOD_NAME end) 57 | ModConfigMenu.AddSpace(MOD_NAME, "Info") 58 | ModConfigMenu.AddText(MOD_NAME, "Info", function() return "Version " .. VERSION end) 59 | end 60 | ``` 61 | 62 | For more information: 63 | 64 | - See [the quick start guide](docs/quick-start.md). 65 | - Also see [the API documentation](docs/api.md). 66 | 67 |
68 | 69 | ## Troubleshooting 70 | 71 | Note that the "Pure" version of Mod Config Menu will not work properly if: 72 | 73 | - You have subscribed to the "Pure" version and you subscribed to a different version at the same time, which will cause a conflict. 74 | - You are subscribed to a mod that uses a standalone version of Mod Config Menu, which will cause a conflict. 75 | - You are subscribed to a mod uses the `require` or `dofile` or `pcall` or `loadfile` functions to initialize or invoke Mod Config Menu. 76 | 77 |
78 | 79 | ## FAQ 80 | 81 | ### Does it work with Repentance? 82 | 83 | Yes. 84 | 85 | ### Does it work with the latest version of Repentance? 86 | 87 | Yes. In [version 1.7.9b](https://bindingofisaacrebirth.fandom.com/wiki/V1.7.9b), the `loadfile` function was removed from the game. But unlike other versions of Mod Config Menu, Mod Config Menu Pure does not use `loadfile` (or any other hacks), so this version continues to work as it did before. 88 | 89 | ### Does it work with Afterbirth+? 90 | 91 | No, because it uses the Repentance-only API for getting the HUD offset. 92 | 93 | ### What do I do if Mod Config Menu Pure causes errors or otherwise does not seem to get loaded properly by a particular mod? 94 | 95 | This is probably because the mod is using the `require` or `dofile` or `pcall` or `loadfile` functions to initialize or invoke Mod Config Menu. Contact the individual mod author to fix this. 96 | 97 | ### What do I do if Mod Config Menu Pure works properly to configure settings, but does not save the settings for future runs? 98 | 99 | Mod Config Menu Pure is not responsible for saving the configuration data of other mods. Doing that is up to the other mods. Thus, if another mod's settings are not being properly saved, then you need to take that issue to the specific mod's developer. 100 | 101 | ### What is Mod Config Menu Continued? 102 | 103 | The original version of Mod Config Menu was made by piber20. [Mod Config Menu Continued](https://steamcommunity.com/sharedfiles/filedetails/?id=2487535818) is an updated version made by Chifilly with the goal of making it work with the Repentance DLC and fixing some bugs. Mod Config Menu Pure is an updated version of Mod Config Menu Continued with the goal of fixing yet more bugs. Thus, Mod Config Menu Continued is not the same thing as Mod Config Menu Pure. 104 | 105 | As of December 8, 2022, Mod Config Menu Continued no longer works with the latest version of the game, so you should use Mod Config Menu Pure instead. 106 | 107 | ### Should I subscribe to multiple versions of Mod Config Menu at the same time? 108 | 109 | No. You should only subscribe to one specific version of Mod Config Menu at a time. 110 | 111 | ### How do I open Mod Config Menu? 112 | 113 | See [the "Using Mod Config Menu for Players" section](#using-mod-config-menu-for-players) above. 114 | 115 | If the default keyboard/controller bindings do not work, then it is possible you have previously remapped them to something else. In this case, you can use the F10 button on the keyboard, which will always open the menu. Then, you can configure the keyboard/controller bindings to the exact thing that you want. 116 | 117 | ### What do I do if saving settings for a mod does not work between game launches? 118 | 119 | Mod Config Menu is not in charge of saving any data besides the ones in the "General" and "Mod Config Menu" pages. If an individual mod does not properly save its data, then you should contact the author of that mod. 120 | 121 | ### Does this have the same functionality (i.e. API) as the other versions of Mod Config Menu? 122 | 123 | Yes. However, it might not work as a drop-in replacement for mods that use the `require` or `dofile` or `pcall` or `loadfile` functions to initialize or invoke Mod Config Menu. Another common issue is using deprecated properties like `ModConfigMenuOptionType.BOOLEAN` instead of `ModConfigMenu.OptionType.BOOLEAN`. If you are a mod author and you want to switch to the pure version, you should test everything thoroughly. 124 | 125 | ### What does it mean to "remove API overrides"? 126 | 127 | The original version overwrote some of the Lua and Isaac API functions, such as `pcall` and `RequireMod`. This version does not overwrite any API functions. 128 | 129 | ### How do I tell what version of Mod Config Menu Pure I have? 130 | 131 | There are 3 ways: 132 | 133 | - You can see the version in the console. 134 | - You can see the version in the game's "log.txt" file. (See the next section.) 135 | - You can see the version at the top of the mod's Lua file. (See the next section.) 136 | 137 | ### Where is the game's "log.txt" file located? 138 | 139 | By default, it is located at the following path: 140 | 141 | ```text 142 | C:\Users\[username]\Documents\My Games\Binding of Isaac Repentance\log.txt 143 | ``` 144 | 145 | ### Where is the Lua code for the mod located? 146 | 147 | By default, it is located at the following path: 148 | 149 | ```text 150 | C:\Program Files (x86)\Steam\steamapps\common\The Binding of Isaac Rebirth\mods\!!mod config menu_2681875787\scripts\modconfig.lua 151 | ``` 152 | 153 | ### Where is the save data for the mod located? 154 | 155 | By default, it is located at the following path: 156 | 157 | ```text 158 | C:\Program Files (x86)\Steam\steamapps\common\The Binding of Isaac Rebirth\data\!!mod config menu\save#.dat 159 | ``` 160 | 161 | The `#` corresponds to the number of the save slot that you are playing on. 162 | 163 | ### How do I reset my Mod Config Menu settings? 164 | 165 | You need to delete the save data file that corresponds to the Isaac save slot that you are on. See the previous section. 166 | 167 | ### Why doesn't Mod Config Menu work for me? 168 | 169 | It works for everyone else, so it has to be something wrong with you. Start by uninstalling the game, completely removing all leftover game files, reinstalling the game, and then only subscribing to this mod on the workshop and nothing else. At this point, you will probably have no errors, so you can then start to introduce other things piece by piece until you find the thing that is causing the problem. For more information on where the various game files are located, see [my directories and save files explanation](https://github.com/Zamiell/isaac-faq/blob/main/directories-and-save-files.md). 170 | 171 | ### What was changed in the last update? 172 | 173 | Look at the [commit history](https://github.com/Zamiell/isaac-mod-config-menu/commits/main). 174 | 175 | ### Why doesn't Mod Config Menu pause the gane when it is open? Why does Mod Config Menu not stop the in-game timer? Why can't I open Mod Config Menu in a room with enemies? 176 | 177 | The Isaac modding API does not allow mods to pause the game or control the in-game timer. Thus, for this reason, mod config menu is only allowed to be open when there are no enemies in the room. Subsequently, you should be mindful that the in-game timer will continue to increase when Mod Config Menu is open, so you might e.g. miss the Boss Rush. 178 | -------------------------------------------------------------------------------- /copy-metadata.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | SCRIPT_PATH = os.path.realpath(__file__) 5 | SOURCE_MOD_DIRECTORY = os.path.dirname(SCRIPT_PATH) 6 | TARGET_MOD_DIRECTORY = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\The Binding of Isaac Rebirth\\mods\\!!mod config menu" 7 | 8 | METADATA_XML = "metadata.xml" 9 | SOURCE_METADATA_XML = os.path.join(SOURCE_MOD_DIRECTORY, METADATA_XML) 10 | TARGET_METADATA_XML = os.path.join(TARGET_MOD_DIRECTORY, METADATA_XML) 11 | 12 | # Copy back the "metadata.xml" file, since it will have an incremented version number. 13 | shutil.copyfile(TARGET_METADATA_XML, SOURCE_METADATA_XML) 14 | 15 | # Clean up the target mod directory. 16 | shutil.rmtree(TARGET_MOD_DIRECTORY) 17 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", 3 | "version": "0.2", 4 | "files": [ 5 | "**" 6 | ], 7 | "ignorePaths": [ 8 | "*.anm2", 9 | "*.pyc", 10 | "*.wav", 11 | ".git/**" 12 | ], 13 | "words": [ 14 | "Bigbooks", 15 | "BOMBDROP", 16 | "Chifilly", 17 | "dofile", 18 | "DPAD", 19 | "ERRORBUZZ", 20 | "FULLSCREEN", 21 | "Gamepad", 22 | "inputhelper", 23 | "ipairs", 24 | "keybind", 25 | "loadfile", 26 | "MCM", 27 | "MENUBACK", 28 | "MENUCONFIRM", 29 | "MENUDOWN", 30 | "MENULEFT", 31 | "MENURIGHT", 32 | "MENUUP", 33 | "Metatables", 34 | "modconfig", 35 | "modconfigmenu", 36 | "pcall", 37 | "pftempestasevencondensed", 38 | "piber", 39 | "PILLCARD", 40 | "Popen", 41 | "Rebindable", 42 | "screenhelper", 43 | "SHOOTDOWN", 44 | "SHOOTLEFT", 45 | "SHOOTRIGHT", 46 | "SHOOTUP", 47 | "Spritesheet", 48 | "steamapps", 49 | "teammeatfont", 50 | "Uncategorized", 51 | "userdata", 52 | "WASD", 53 | "Wofsauge", 54 | "Zamiel", 55 | "Zamiell" 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | 4 | 5 | When Mod Config Menu is installed, a global `ModConfigMenu` is available which contains all the following functions. 6 | 7 | The settings added will show up in the order the functions are called. 8 | 9 | ___Disclaimer__: Some of the following information may be incorrect. I quickly threw this together in a couple of hours, and only did some quick parsing of the code to try and figure out how it works and the flow. Anyone is free to create an issue or pull request with corrections._ 10 | 11 | ### Functions 12 | 13 | #### Category Functions 14 | 15 | ##### GetCategoryIDByName([categoryName](#categoryname): string): number 16 | 17 | Returns the category ID based off of the `categoryName` provided. 18 | Returns `nil` if not a valid category. 19 | Returns what was provided if `categoryName` is not a `string`. 20 | 21 | ##### UpdateCategory([categoryName](#categoryname): string, [categoryData](#categorydata): [categoryData](#categorydata)) 22 | 23 | Updates a category with the supplied data. 24 | 25 | ##### SetCategoryInfo([categoryName](#categoryname): string, info: string) 26 | 27 | Changes category info. 28 | 29 | ##### RemoveCategory([categoryName](#categoryname): string) 30 | 31 | Removes a category entirely. 32 | 33 | #### Subcategory Functions 34 | 35 | ##### GetSubcategoryIDByName(category: string|number, [subcategoryName](#subcategoryname): string): number 36 | 37 | Returns the subcategory ID based off of the `category` (which is either the category name or category ID) and `subcategoryName` provided. 38 | Returns `nil` if not a valid category or subcategory. 39 | Returns what was provided if `category` is not a `string` or `number` or `subcategoryName` is not a `string`. 40 | 41 | ##### UpdateSubcategory([categoryName](#categoryname): string, [subcategoryName](#subcategoryname): string, [subcategoryData](#subcategorydata): [subcategoryData](#subcategorydata)) 42 | 43 | Updates a subcategory with the supplied data. 44 | 45 | ##### RemoveSubcategory([categoryName](#categoryname): string, [subcategoryName](#subcategoryname): string) 46 | 47 | Removes a subcategory entirely. 48 | 49 | #### Setting Functions 50 | 51 | ##### AddSetting([categoryName](#categoryname): string, [subcategoryName](#subcategoryname): string, [settingTable](#settingtable): [settingTable](#settingtable)) 52 | 53 | Add a new setting to the supplied category and subcategory with the provided data. 54 | 55 | ##### RemoveSetting([categoryName](#categoryname): string, [subcategoryName](#subcategoryname): string, settingAttribute: string) 56 | 57 | Remove the setting at the provided category, subcategory and attribute 58 | 59 | ##### AddText([categoryName](#categoryname): string, [subcategoryName](#subcategoryname): string, text: string, color: RGBArray) 60 | 61 | Add text into the mod config menu under the provided category and subcategory. 62 | 63 | ##### AddTitle([categoryName](#categoryname): string, [subcategoryName](#subcategoryname): string, text: string, color: RGBArray) 64 | 65 | Add a title to the mod config menu under the provided category and subcategory. 66 | 67 | ##### AddSpace([categoryName](#categoryname): string, [subcategoryName](#subcategoryname): string) 68 | 69 | Add a space to the mod config menu under the provided category and subcategory. 70 | 71 | ##### SimpleAddSetting(settingType: [OptionType](#optiontype), [categoryName](#categoryname): string, [subcategoryName](#subcategoryname): string, configTableAttribute: ?, minValue: number, maxValue: number, modifyBy: number, defaultValue: any, displayText: string, [displayValueProxies](#displayvalueproxies), [displayDevice](#displaydevice): boolean, info: string, color: RGBArray, functionName: string) 72 | 73 | Create a setting without using a table. 74 | 75 | `functionName` = The name of the function it was called from (only used in error messages, and _really_ only used internally). 76 | 77 | Any `Add` functions that take `categoryName` and `configTableAttribute` will store its data in `ModConfigMenu.Config[categoryName][configTableAttribute]`. Other versions of Mod Config Menu may auto-save this data to file. However, this version of Mod Config Menu does not. Make sure you save and load your data as appropriate. 78 | 79 | _All of the individual `Add*` functions below can be achieved with just `AddSetting` and providing the `Type` parameter in [`settingTable`](#settingtable) to be a [`ModConfigMenu.OptionType`](#optiontype). That is also the way I recommend, because I haven't been able to fully understand the code yet, so I don't know what some of the parameters are for, and how the "overrides" are set up._ 80 | _Any help figuring out what all the parameters and "overrides" are so I can make this readme more accurate would be appreciated._ 81 | 82 | ##### AddBooleanSetting([categoryName](#categoryname): string, [subcategoryName](#subcategoryname): string, configTableAttribute: ?, defaultValue: boolean, displayText: string, [displayValueProxies](#displayvalueproxies): table, info: string, color: RGBArray) 83 | 84 | Add a boolean setting under the provided category and subcategory. 85 | 86 | ##### AddNumberSetting([categoryName](#categoryname): string, [subcategoryName](#subcategoryname): string, configTableAttribute: ?, minValue: number, maxValue: number, modifyBy: number, defaultValue: number, displayText: string, [displayValueProxies](#displayvalueproxies), info: string, color: RGBArray) 87 | 88 | Add a number value setting under the provided category and subcategory. 89 | 90 | ##### AddScrollSetting([categoryName](#categoryname): string, [subcategoryName](#subcategoryname): string, configTableAttribute: ?, defaultValue: number, displayText: string, info: string, color: RGBArray) 91 | 92 | Add a slider setting under the provided category and subcategory. 93 | 94 | ##### AddKeyboardSetting([categoryName](#categoryname): string, [subcategoryName](#subcategoryname): string, configTableAttribute: ?, defaultValue: number, displayText: string, [displayDevice](#displaydevice): boolean, info: string, color: RGBArray) 95 | 96 | Add a keyboard keybinding setting. 97 | 98 | ##### AddControllerSetting([categoryName](#categoryname): string, [subcategoryName](#subcategoryname): string, configTableAttribute: ?, defaultValue: number, displayText: string, [displayDevice](#displaydevice): boolean, info: string, color: RGBArray) 99 | 100 | Add a controller keybinding setting. 101 | 102 | ### Variables and Parameters 103 | 104 | #### Common Parameters 105 | 106 | ##### categoryName 107 | 108 | What needs to be chosen on the left to go to your settings menu. 109 | 110 | ##### subcategoryName 111 | 112 | The secondary section within your tab the setting is in. This is a tab list at the top of your menu. 113 | 114 | ##### categoryData 115 | 116 | A table of data for the category. 117 | 118 | ```lua 119 | { 120 | Name = string -- the name of the category 121 | Info = string -- the description of the category 122 | IsOld = boolean -- not sure of the purpose, only seems to turn the text red 123 | } 124 | ``` 125 | 126 | ##### subcategoryData 127 | 128 | A table of data for the subcategory. 129 | 130 | ```lua 131 | Name = string -- the name of the category 132 | Info = string -- the description of the category 133 | ``` 134 | 135 | ##### settingTable 136 | 137 | A table of data for the setting. 138 | 139 | ```lua 140 | { 141 | -- the type of the setting, see OptionType for more information 142 | Type = ModConfigMenu.OptionType, 143 | 144 | -- the identifier for the setting 145 | Attribute = string, 146 | 147 | -- the default value for the setting 148 | Default = any, 149 | 150 | -- a function that returns the current value of the setting 151 | CurrentSetting = function(), 152 | 153 | -- the minimum value of numeric settings 154 | Minimum = number, 155 | 156 | -- the maximum value of numeric settings 157 | Maximum = number, 158 | 159 | -- a function that returns a string of how the setting will display in the settings menu 160 | Display = function(), 161 | 162 | -- a function that is called whenever the setting is changed (can be used to save your settings for example) 163 | OnChange = function(), 164 | 165 | -- a table of strings that's used as the information for the setting 166 | Info = { string }, 167 | 168 | -- the color of the setting (values are floats between 0 and 1) 169 | Color = { r, g, b }, 170 | } 171 | ``` 172 | 173 | ##### displayValueProxies 174 | 175 | A table that denotes what text will be displayed based on the setting value as the index. 176 | 177 | ```lua 178 | -- this will make "true" show as "On" and "false" show as "Off" 179 | { 180 | [true] = "On", 181 | [false] = "Off" 182 | } 183 | 184 | -- or 185 | 186 | -- this will make 0 show "Sometimes", 1 show "Never" and 2 show "Always" 187 | { 188 | [0] = "Sometimes", 189 | [1] = "Never", 190 | [2] = "Always" 191 | }, 192 | ``` 193 | 194 | ##### displayDevice 195 | 196 | Whether the display text should be suffixed with the control device (`(keyboard)` or `(controller)`). 197 | 198 | #### Enums 199 | 200 | ##### OptionType 201 | 202 | All these option types are in the `ModConfigMenu.OptionType` enum. 203 | 204 | `TEXT` = Plain text. 205 | `SPACE` = A paragraph-type gap rendered in the menu. 206 | `SCROLL` = A slider-bar for numeric values. 207 | `BOOLEAN` = A boolean (true or false). 208 | `NUMBER` = A numeric value. 209 | `KEYBIND_KEYBOARD` = A keybind for keyboards. 210 | `KEYBIND_CONTROLLER` = A keybind for controllers. 211 | `TITLE` = Heading-style text. 212 | -------------------------------------------------------------------------------- /docs/quick-start.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Mod Config Menu 2 | 3 | 4 | 5 | If you want to add config options to your Binding of Isaac mod, then instead of programming your own menu system from scratch, you can register your mod's configuration options with [Mod Config Menu](https://github.com/Zamiell/isaac-mod-config-menu), or MCM for short. MCM is a mod that provides an extendable in-game menu. 6 | 7 |
8 | 9 | ## The `ModConfigMenu` Global Variable 10 | 11 | The first thing you want to do is check for the existence of the `ModConfigMenu` global variable. If it exists then you can proceed. 12 | 13 | ```lua 14 | local function modConfigMenuInit() 15 | if ModConfigMenu == nil then 16 | return 17 | end 18 | 19 | -- Insert code here to add the menu options for your mod 20 | end 21 | ``` 22 | 23 |
24 | 25 | ## Common Scenarios 26 | 27 | In most cases, you will be providing the player with multiple choice options. This can take the form of "on/off" or "A/B/C". 28 | 29 | ### On/Off 30 | 31 | On/off looks like this: 32 | 33 | ```lua 34 | -- Specify a table of the MCM settings for this mod, along with their default values 35 | local settings = { 36 | myBoolean = false, 37 | } 38 | 39 | ModConfigMenu.AddSetting( 40 | "My Settings Page", -- This should be unique for your mod 41 | "Tab 1", -- If you don't want multiple tabs, then set this to nil 42 | { 43 | Type = ModConfigMenu.OptionType.BOOLEAN, 44 | CurrentSetting = function() 45 | return settings.myBoolean 46 | end, 47 | Display = function() 48 | return "My Boolean: " .. (settings.myBoolean and "on" or "off") 49 | end, 50 | OnChange = function(b) 51 | settings.myBoolean = b 52 | end, 53 | Info = { -- This can also be a function instead of a table 54 | "Info on 1st line", 55 | "More info on 2nd line", 56 | } 57 | } 58 | ) 59 | ``` 60 | 61 | ### A/B/C (multiple choice) 62 | 63 | If you need 3 or more options then that looks like this: 64 | 65 | ```lua 66 | local choices = { 67 | "A", 68 | "B", 69 | "C", 70 | } 71 | 72 | -- Specify a table of the MCM settings for this mod, along with their default values 73 | local settings = { 74 | myMultipleChoice = choices[1] -- The first choice by default 75 | } 76 | 77 | local function getTableIndex(tbl, val) 78 | for i, v in ipairs(tbl) do 79 | if v == val then 80 | return i 81 | end 82 | end 83 | 84 | return 0 85 | end 86 | 87 | ModConfigMenu.AddSetting( 88 | "My Settings Page", 89 | "Tab 1", 90 | { 91 | Type = ModConfigMenu.OptionType.NUMBER, 92 | CurrentSetting = function() 93 | return getTableIndex(choices, settings.myMultipleChoice) 94 | end, 95 | Minimum = 1, 96 | Maximum = #choices, 97 | Display = function() 98 | return "My Multiple Choice: " .. settings.myMultipleChoice 99 | end, 100 | OnChange = function(n) 101 | settings.myMultipleChoice = choices[n] 102 | end, 103 | -- Text in the "Info" section will automatically word-wrap, unlike in the main section above 104 | Info = { "Info on 1st line" } 105 | } 106 | ) 107 | ``` 108 | 109 | ### Scroll Bar 110 | 111 | A scroll bar is a multiple choice option that renders as a scroll bar with 10 bars. The player can choose between 0 and 10 out of 10 for a total of 11 options. 112 | 113 | This is commonly used to get a decimal number between 0 and 1 (by i.e. dividing by 10). 114 | 115 | ```lua 116 | local settings = {} 117 | settings.myScrollBar = 0 118 | 119 | ModConfigMenu.AddSetting( 120 | "My Settings Page", 121 | "Tab 1", 122 | { 123 | Type = ModConfigMenu.OptionType.SCROLL, 124 | CurrentSetting = function() 125 | return settings.myScrollBar 126 | end, 127 | Display = function() 128 | return "My Scroll Bar: $scroll" .. settings.myScrollBar 129 | end, 130 | OnChange = function(n) 131 | settings.myScrollBar = n 132 | end, 133 | Info = { "Info on 1st line" } 134 | } 135 | ) 136 | ``` 137 | 138 |
139 | 140 | ## Layout 141 | 142 | You can add a title, text, or vertical spacer like this: 143 | 144 | ```lua 145 | ModConfigMenu.AddTitle("My Settings Page", "Tab 1", "My Title") 146 | ModConfigMenu.AddText("My Settings Page", "Tab 1", "My Text") 147 | ModConfigMenu.AddSpace("My Settings Page", "Tab 1") 148 | ``` 149 | 150 |
151 | 152 | ## Saving and Loading 153 | 154 | ### Saving 155 | 156 | You are responsible for saving your settings, which can be as simple as: 157 | 158 | ```lua 159 | local json = require("json") 160 | 161 | local mod = RegisterMod("MyMod", 1) 162 | 163 | -- If your mod does not store any other save data, then you can simply run the "save" function in 164 | -- every MCM "OnChange" function. Otherwise, you need to run it on both the "OnChange" functions and 165 | -- MC_PRE_GAME_EXIT. 166 | local function save() 167 | -- TODO: use pcall to gracefully handle failure 168 | local jsonString = json.encode(settings) 169 | mod:SaveData(jsonString) 170 | end 171 | ``` 172 | 173 | ### Loading 174 | 175 | You are also responsible for loading your saved settings, which can be as simple as: 176 | 177 | ```lua 178 | -- If your mod does not store any other save data, then you can simply run the "load" function 179 | -- during mod initialization. Otherwise, you need to run it on both initialization and at the 180 | -- beginning of a run. 181 | local function load() 182 | if not mod:HasData() then 183 | return 184 | end 185 | 186 | -- TODO: use pcall to gracefully handle failure 187 | -- TODO: add checks for validate the data 188 | local jsonString = mod:LoadData() 189 | settings = json.decode(jsonString) 190 | end 191 | ``` 192 | 193 | ### IsaacScript 194 | 195 | If you are coding your mod using TypeScript, then the [IsaacScript](https://isaacscript.github.io/) standard library has a save data manager that you should use instead of handling saving and loading yourself manually. 196 | 197 | ```ts 198 | // Config.ts 199 | export class Config { 200 | feature1 = true; 201 | feature2 = 1; 202 | // and so on 203 | } 204 | 205 | // modConfigMenu.ts 206 | const v = { 207 | persistent: { 208 | config: new Config(), 209 | } 210 | } 211 | 212 | function modConfigMenuInit() { 213 | saveDataManager("modConfigMenu", v); 214 | } 215 | ``` 216 | 217 |
218 | 219 | ## Further Reading 220 | 221 | - See the [Mod Reference](https://wofsauge.github.io/IsaacDocs/rep/ModReference.html) documentation for related saving and loading information. 222 | - See the [API documentation](api.md). 223 | -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | -- This script handles saving for the standalone version of Mod Config Menu. 2 | 3 | local mod = RegisterMod("Mod Config Menu Standalone", 1) 4 | 5 | ModConfigMenu = {} 6 | ModConfigMenu.StandaloneMod = mod 7 | 8 | require("scripts.modconfig") 9 | -------------------------------------------------------------------------------- /metadata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | !! Mod Config Menu Pure 4 | !!mod config menu 5 | 2681875787 6 | [h1][b]Mod Config Menu Pure[/b][/h1] 7 | 8 | [h2][b]Introduction[/b][/h2] 9 | 10 | Mod Config Menu is a library to that allows other mods to have a settings menu. You should only subscribe to this mod if you have other mods that use it. 11 | 12 | [h2][b]Installing[/b][/h2] 13 | 14 | Click on the green "Subscribe" button above, which will cause Steam to automatically download the mod. Then, the next time you open the game, the mod will appear in the "Mods" submenu. 15 | 16 | [h2][b]More Info[/b][/h2] 17 | 18 | For more information, read [url=https://github.com/Zamiell/isaac-mod-config-menu/blob/main/README.md]the README file for this mod on GitHub[/url]. 19 | 20 | [h1][b]Before posting a comment below, read the entire README on GitHub. Comments asking questions that have already been covered in the README will be deleted.[/b][/h1] 21 | 22 | 112 23 | Public 24 | 25 | -------------------------------------------------------------------------------- /release.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import shutil 4 | import subprocess 5 | import sys 6 | 7 | SCRIPT_PATH = os.path.realpath(__file__) 8 | SOURCE_MOD_DIRECTORY = os.path.dirname(SCRIPT_PATH) 9 | LUA_FILE_PATH = os.path.join(SOURCE_MOD_DIRECTORY, "scripts", "modconfig.lua") 10 | METADATA_XML_PATH = os.path.join(SOURCE_MOD_DIRECTORY, "metadata.xml") 11 | 12 | 13 | def main(): 14 | if not is_git_clean(): 15 | printf( 16 | "Error: The current working directory must be clean before releasing a new version. Please push your changes to Git." 17 | ) 18 | sys.exit(1) 19 | 20 | # Validate that we can push and pull to the repository and that all commits are remotely synced. 21 | subprocess.run(["git", "branch", "--set-upstream-to=origin/main", "main"]) 22 | subprocess.run(["git", "pull", "--rebase"]) 23 | subprocess.run(["git", "push", "--set-upstream", "origin", "main"]) 24 | 25 | new_version = increment_lua_version() 26 | set_metadata_xml_version(new_version) 27 | 28 | subprocess.run(["git", "add", "--all"]) 29 | subprocess.run(["git", "commit", "--message", f"chore: release {new_version}"]) 30 | subprocess.run(["git", "push", "--set-upstream", "origin", "main"]) 31 | 32 | printf(f"Released version: {new_version}") 33 | 34 | 35 | def is_git_clean(): 36 | stdout_bytes = subprocess.check_output(["git", "status", "--short"]) 37 | stdout = stdout_bytes.decode("utf-8") 38 | trimmed_stdout = stdout.strip() 39 | return len(trimmed_stdout) == 0 40 | 41 | 42 | def increment_lua_version(): 43 | if not os.path.exists(LUA_FILE_PATH): 44 | error(f"Failed to find the Lua file at: {LUA_FILE_PATH}") 45 | 46 | with open(LUA_FILE_PATH) as f: 47 | text = f.read() 48 | 49 | match = re.search(r"local VERSION = (\d+)", text) 50 | if not match: 51 | error(f"Failed to find the version in the Lua file at: {LUA_FILE_PATH}") 52 | 53 | version_string = match.group(1) 54 | version_number = int(version_string) 55 | new_version = version_number + 1 56 | 57 | text = re.sub( 58 | f"local VERSION = {version_string}", 59 | f"local VERSION = {new_version}", 60 | text, 61 | ) 62 | text = re.sub("local IS_DEV = true", "local IS_DEV = false", text) 63 | 64 | with open(LUA_FILE_PATH, "w", newline="\n") as f: 65 | f.write(text) 66 | 67 | return new_version 68 | 69 | 70 | def set_metadata_xml_version(new_version: str): 71 | if not os.path.exists(METADATA_XML_PATH): 72 | error(f'Failed to find the "metadata.xml" file at: {METADATA_XML_PATH}') 73 | 74 | with open(METADATA_XML_PATH) as f: 75 | text = f.read() 76 | 77 | text = re.sub( 78 | r"(.+?)", 79 | f"{new_version}", 80 | text, 81 | ) 82 | 83 | with open(METADATA_XML_PATH, "w", newline="\n") as f: 84 | f.write(text) 85 | 86 | 87 | def error(msg: str): 88 | printf(f"Error: {msg}") 89 | sys.exit(1) 90 | 91 | 92 | def printf(*args): 93 | print(*args, flush=True) 94 | 95 | 96 | if __name__ == "__main__": 97 | main() 98 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e # Exit on any errors 4 | 5 | # Get the directory of this script: 6 | # https://stackoverflow.com/questions/59895/getting-the-source-directory-of-a-bash-script-from-within 7 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 8 | 9 | cd "$DIR" 10 | 11 | python "$DIR/release.py" 12 | -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/arrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/7a707788f58fe57eb03da7ffe211e78ba45db6c8/resources/gfx/ui/modconfig/arrows.png -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/backselect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/7a707788f58fe57eb03da7ffe211e78ba45db6c8/resources/gfx/ui/modconfig/backselect.png -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/7a707788f58fe57eb03da7ffe211e78ba45db6c8/resources/gfx/ui/modconfig/black.png -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/menu.anm2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/7a707788f58fe57eb03da7ffe211e78ba45db6c8/resources/gfx/ui/modconfig/menu.png -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/menu_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/7a707788f58fe57eb03da7ffe211e78ba45db6c8/resources/gfx/ui/modconfig/menu_overlay.png -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/offset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/7a707788f58fe57eb03da7ffe211e78ba45db6c8/resources/gfx/ui/modconfig/offset.png -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/popup_thin_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/7a707788f58fe57eb03da7ffe211e78ba45db6c8/resources/gfx/ui/modconfig/popup_thin_large.png -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/popup_thin_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/7a707788f58fe57eb03da7ffe211e78ba45db6c8/resources/gfx/ui/modconfig/popup_thin_medium.png -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/popup_thin_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/7a707788f58fe57eb03da7ffe211e78ba45db6c8/resources/gfx/ui/modconfig/popup_thin_small.png -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/popup_wide_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/7a707788f58fe57eb03da7ffe211e78ba45db6c8/resources/gfx/ui/modconfig/popup_wide_large.png -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/popup_wide_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/7a707788f58fe57eb03da7ffe211e78ba45db6c8/resources/gfx/ui/modconfig/popup_wide_medium.png -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/popup_wide_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/7a707788f58fe57eb03da7ffe211e78ba45db6c8/resources/gfx/ui/modconfig/popup_wide_small.png -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/slider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/7a707788f58fe57eb03da7ffe211e78ba45db6c8/resources/gfx/ui/modconfig/slider.png -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/strike.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/7a707788f58fe57eb03da7ffe211e78ba45db6c8/resources/gfx/ui/modconfig/strike.png -------------------------------------------------------------------------------- /resources/previewfile_1603631350.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/7a707788f58fe57eb03da7ffe211e78ba45db6c8/resources/previewfile_1603631350.jpg -------------------------------------------------------------------------------- /scripts/inputhelper.lua: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | -- IMPORTANT: DO NOT EDIT THIS FILE!!! -- 3 | ------------------------------------------------------------------------------ 4 | -- This file relies on other versions of itself being the same. -- 5 | -- If you need something in this file changed, please let the creator know! -- 6 | ------------------------------------------------------------------------------ 7 | 8 | -- CODE STARTS BELOW -- 9 | 10 | ------------- 11 | -- version -- 12 | ------------- 13 | 14 | local fileVersion = 1 15 | 16 | --prevent older/same version versions of this script from loading 17 | if InputHelper and InputHelper.Version >= fileVersion then 18 | return InputHelper 19 | end 20 | 21 | if not InputHelper then 22 | InputHelper = {} 23 | InputHelper.Version = fileVersion 24 | elseif InputHelper.Version < fileVersion then 25 | -- handle old versions 26 | if InputHelper.HandleForceActionPressed then 27 | InputHelper.Mod:RemoveCallback(ModCallbacks.MC_INPUT_ACTION, InputHelper.HandleForceActionPressed) 28 | end 29 | 30 | InputHelper.Version = fileVersion 31 | end 32 | 33 | ----------- 34 | -- setup -- 35 | ----------- 36 | 37 | InputHelper.Mod = InputHelper.Mod or RegisterMod("Input Helper", 1) 38 | 39 | --------- 40 | --enums-- 41 | --------- 42 | 43 | Controller = Controller or {} 44 | Controller.DPAD_LEFT = 0 45 | Controller.DPAD_RIGHT = 1 46 | Controller.DPAD_UP = 2 47 | Controller.DPAD_DOWN = 3 48 | Controller.BUTTON_A = 4 49 | Controller.BUTTON_B = 5 50 | Controller.BUTTON_X = 6 51 | Controller.BUTTON_Y = 7 52 | Controller.BUMPER_LEFT = 8 53 | Controller.TRIGGER_LEFT = 9 54 | Controller.STICK_LEFT = 10 55 | Controller.BUMPER_RIGHT = 11 56 | Controller.TRIGGER_RIGHT = 12 57 | Controller.STICK_RIGHT = 13 58 | Controller.BUTTON_BACK = 14 59 | Controller.BUTTON_START = 15 60 | 61 | ---------------- 62 | --id to string-- 63 | ---------------- 64 | 65 | InputHelper.KeyboardToString = InputHelper.KeyboardToString or {} 66 | 67 | for key, num in pairs(Keyboard) do 68 | local keyString = key 69 | 70 | local keyStart, keyEnd = string.find(keyString, "KEY_") 71 | keyString = string.sub(keyString, keyEnd + 1, string.len(keyString)) 72 | 73 | keyString = string.gsub(keyString, "_", " ") 74 | 75 | InputHelper.KeyboardToString[num] = keyString 76 | end 77 | 78 | InputHelper.ControllerToString = InputHelper.ControllerToString or {} 79 | 80 | for button, num in pairs(Controller) do 81 | local buttonString = button 82 | 83 | if string.match(buttonString, "BUTTON_") then 84 | local buttonStart, buttonEnd = string.find(buttonString, "BUTTON_") 85 | buttonString = string.sub(buttonString, buttonEnd + 1, string.len(buttonString)) 86 | end 87 | 88 | if string.match(buttonString, "BUMPER_") then 89 | local bumperStart, bumperEnd = string.find(buttonString, "BUMPER_") 90 | buttonString = string.sub(buttonString, bumperEnd + 1, string.len(buttonString)) .. "_BUMPER" 91 | end 92 | 93 | if string.match(buttonString, "TRIGGER_") then 94 | local triggerStart, triggerEnd = string.find(buttonString, "TRIGGER_") 95 | buttonString = string.sub(buttonString, triggerEnd + 1, string.len(buttonString)) .. "_TRIGGER" 96 | end 97 | 98 | if string.match(buttonString, "STICK_") then 99 | local stickStart, stickEnd = string.find(buttonString, "STICK_") 100 | buttonString = string.sub(buttonString, stickEnd + 1, string.len(buttonString)) .. "_STICK" 101 | end 102 | 103 | buttonString = string.gsub(buttonString, "_", " ") 104 | 105 | InputHelper.ControllerToString[num] = buttonString 106 | end 107 | 108 | ------------------------- 109 | --safe keyboard pressed-- 110 | ------------------------- 111 | 112 | --functions to use that work around a bug related to controller inputs 113 | function InputHelper.KeyboardTriggered(key, controllerIndex) 114 | return Input.IsButtonTriggered(key, controllerIndex) and not Input.IsButtonTriggered(key % 32, controllerIndex) 115 | end 116 | 117 | function InputHelper.KeyboardPressed(key, controllerIndex) 118 | return Input.IsButtonPressed(key, controllerIndex) and not Input.IsButtonPressed(key % 32, controllerIndex) 119 | end 120 | 121 | -------------------------- 122 | --multiple button checks-- 123 | -------------------------- 124 | 125 | function InputHelper.MultipleActionTriggered(actions, controllerIndex) 126 | for i, action in pairs(actions) do 127 | for index = 0, 4 do 128 | if controllerIndex ~= nil then 129 | index = controllerIndex 130 | end 131 | 132 | if Input.IsActionTriggered(action, index) then 133 | return action 134 | end 135 | 136 | if controllerIndex ~= nil then 137 | break 138 | end 139 | end 140 | end 141 | 142 | return nil 143 | end 144 | 145 | function InputHelper.MultipleActionPressed(actions, controllerIndex) 146 | for i, action in pairs(actions) do 147 | for index = 0, 4 do 148 | if controllerIndex ~= nil then 149 | index = controllerIndex 150 | end 151 | 152 | if Input.IsActionPressed(action, index) then 153 | return action 154 | end 155 | 156 | if controllerIndex ~= nil then 157 | break 158 | end 159 | end 160 | end 161 | 162 | return nil 163 | end 164 | 165 | function InputHelper.MultipleButtonTriggered(buttons, controllerIndex) 166 | for i, button in pairs(buttons) do 167 | for index = 0, 4 do 168 | if controllerIndex ~= nil then 169 | index = controllerIndex 170 | end 171 | 172 | if Input.IsButtonTriggered(button, index) then 173 | return button 174 | end 175 | 176 | if controllerIndex ~= nil then 177 | break 178 | end 179 | end 180 | end 181 | 182 | return nil 183 | end 184 | 185 | function InputHelper.MultipleButtonPressed(buttons, controllerIndex) 186 | for i, button in pairs(buttons) do 187 | for index = 0, 4 do 188 | if controllerIndex ~= nil then 189 | index = controllerIndex 190 | end 191 | 192 | if Input.IsButtonPressed(button, index) then 193 | return button 194 | end 195 | 196 | if controllerIndex ~= nil then 197 | break 198 | end 199 | end 200 | end 201 | 202 | return nil 203 | end 204 | 205 | function InputHelper.MultipleKeyboardTriggered(keys, controllerIndex) 206 | for i, key in pairs(keys) do 207 | for index = 0, 4 do 208 | if controllerIndex ~= nil then 209 | index = controllerIndex 210 | end 211 | 212 | if InputHelper.KeyboardTriggered(key, index) then 213 | return key 214 | end 215 | 216 | if controllerIndex ~= nil then 217 | break 218 | end 219 | end 220 | end 221 | 222 | return nil 223 | end 224 | 225 | function InputHelper.MultipleKeyboardPressed(keys, controllerIndex) 226 | for i, key in pairs(keys) do 227 | for index = 0, 4 do 228 | if controllerIndex ~= nil then 229 | index = controllerIndex 230 | end 231 | 232 | if InputHelper.KeyboardPressed(key, index) then 233 | return key 234 | end 235 | 236 | if controllerIndex ~= nil then 237 | break 238 | end 239 | end 240 | end 241 | 242 | return nil 243 | end 244 | 245 | --------------- 246 | --force input-- 247 | --------------- 248 | 249 | local forcingActionTriggered = {} 250 | function InputHelper.ForceActionTriggered(controllerIndex, buttonAction, value) 251 | forcingActionTriggered[controllerIndex] = forcingActionTriggered[controllerIndex] or {} 252 | forcingActionTriggered[controllerIndex][buttonAction] = value 253 | end 254 | 255 | local forcingActionPressed = {} 256 | local forcingActionPressedTimer = {} 257 | function InputHelper.ForceActionPressed(controllerIndex, buttonAction, value, timer) 258 | forcingActionPressed[controllerIndex] = forcingActionPressed[controllerIndex] or {} 259 | forcingActionPressed[controllerIndex][buttonAction] = value 260 | 261 | timer = timer or 1 262 | forcingActionPressedTimer[controllerIndex] = forcingActionPressedTimer[controllerIndex] or {} 263 | forcingActionPressedTimer[controllerIndex][buttonAction] = timer 264 | end 265 | 266 | function InputHelper.HandleForceActionPressed(_, entity, inputHook, buttonAction) 267 | if entity and entity:ToPlayer() then 268 | local player = entity:ToPlayer() 269 | local controllerIndex = player.ControllerIndex 270 | 271 | if inputHook == InputHook.IS_ACTION_TRIGGERED then 272 | if forcingActionTriggered[controllerIndex] 273 | and forcingActionTriggered[controllerIndex][buttonAction] ~= nil 274 | then 275 | local toReturn = forcingActionTriggered[controllerIndex][buttonAction] 276 | forcingActionTriggered[controllerIndex][buttonAction] = nil 277 | 278 | return toReturn 279 | end 280 | elseif inputHook == InputHook.IS_ACTION_PRESSED then 281 | if forcingActionPressed[controllerIndex] and forcingActionPressed[controllerIndex][buttonAction] ~= nil then 282 | local toReturn = forcingActionPressed[controllerIndex][buttonAction] 283 | 284 | forcingActionPressedTimer[controllerIndex][buttonAction] = forcingActionPressedTimer[controllerIndex][ 285 | buttonAction] 286 | - 1 287 | if forcingActionPressedTimer[controllerIndex][buttonAction] <= 0 then 288 | forcingActionPressed[controllerIndex][buttonAction] = nil 289 | end 290 | 291 | return toReturn 292 | end 293 | end 294 | end 295 | end 296 | 297 | InputHelper.Mod:AddCallback(ModCallbacks.MC_INPUT_ACTION, InputHelper.HandleForceActionPressed) 298 | 299 | return InputHelper 300 | -------------------------------------------------------------------------------- /scripts/modconfig.lua: -------------------------------------------------------------------------------- 1 | -- Imports 2 | local json = require("json") 3 | 4 | ------------- 5 | -- version -- 6 | ------------- 7 | 8 | -- The final version of Chifilly's Mod Config Menu fork was 33. 9 | -- For the pure version, we arbitrarily selected a starting point of 100 and incremented from there. 10 | local VERSION = 112 11 | local IS_DEV = false 12 | 13 | ModConfigMenu = {} 14 | ModConfigMenu.Version = VERSION 15 | 16 | ------------------------- 17 | --SAVE HELPER FUNCTIONS-- 18 | ------------------------- 19 | 20 | local SaveHelper = {} 21 | 22 | function SaveHelper.CopyTable(tableToCopy) 23 | local table2 = {} 24 | 25 | for i, value in pairs(tableToCopy) do 26 | if type(value) == "table" then 27 | table2[i] = SaveHelper.CopyTable(value) 28 | else 29 | table2[i] = value 30 | end 31 | end 32 | 33 | return table2 34 | end 35 | 36 | function SaveHelper.FillTable(tableToFill, tableToFillFrom) 37 | for i, value in pairs(tableToFillFrom) do 38 | if tableToFill[i] ~= nil then 39 | if type(value) == "table" then 40 | if type(tableToFill[i]) ~= "table" then 41 | tableToFill[i] = {} 42 | end 43 | 44 | tableToFill[i] = SaveHelper.FillTable(tableToFill[i], value) 45 | else 46 | tableToFill[i] = value 47 | end 48 | else 49 | if type(value) == "table" then 50 | if type(tableToFill[i]) ~= "table" then 51 | tableToFill[i] = {} 52 | end 53 | 54 | tableToFill[i] = SaveHelper.FillTable({}, value) 55 | else 56 | tableToFill[i] = value 57 | end 58 | end 59 | end 60 | 61 | return tableToFill 62 | end 63 | 64 | ----------- 65 | -- setup -- 66 | ----------- 67 | 68 | Isaac.DebugString("Mod Config Menu v" .. tostring(ModConfigMenu.Version) .. " - Loading...") 69 | 70 | local vecZero = Vector(0, 0) 71 | local restartWarnMessage = nil 72 | local rerunWarnMessage = nil 73 | 74 | if not InputHelper then 75 | require("scripts.inputhelper") 76 | if not InputHelper then 77 | error("Mod Config Menu requires Input Helper to function", 2) 78 | end 79 | end 80 | 81 | if not ScreenHelper then 82 | require("scripts.screenhelper") 83 | if not ScreenHelper then 84 | error("Mod Config Menu requires Screen Helper to function", 2) 85 | end 86 | end 87 | 88 | ModConfigMenu.Mod = RegisterMod("Mod Config Menu", 1) 89 | 90 | ---------- 91 | --SAVING-- 92 | ---------- 93 | 94 | ModConfigMenu.ConfigDefault = {} 95 | ModConfigMenu.Config = {} 96 | 97 | --------------------------------- 98 | -- Saving and loading MCM data -- 99 | --------------------------------- 100 | 101 | local function save() 102 | local mod = ModConfigMenu.Mod 103 | 104 | if IS_DEV then 105 | Isaac.DebugString("MCM is saving data.") 106 | end 107 | 108 | local jsonString = json.encode(ModConfigMenu.Config) 109 | if jsonString == nil or jsonString == "" then 110 | return 111 | end 112 | 113 | mod:SaveData(jsonString) 114 | end 115 | 116 | local function load() 117 | local mod = ModConfigMenu.Mod 118 | 119 | if IS_DEV then 120 | Isaac.DebugString("MCM is loading data.") 121 | end 122 | 123 | if not mod:HasData() then 124 | save() 125 | return 126 | end 127 | 128 | local jsonString = mod:LoadData() 129 | if jsonString == nil or jsonString == "" or jsonString == "[]" or jsonString == "{}" then 130 | save() 131 | return 132 | end 133 | 134 | local data = nil 135 | local function loadJSON() 136 | data = json.decode(jsonString) 137 | end 138 | 139 | if not pcall(loadJSON) or data == nil then 140 | Isaac.DebugString("Error: Failed to load Mod Config Menu data from the \"save#.dat\" file.") 141 | return 142 | end 143 | 144 | ModConfigMenu.LoadSave(data) 145 | end 146 | 147 | function ModConfigMenu.LoadSave(data) 148 | local saveData = SaveHelper.CopyTable(ModConfigMenu.ConfigDefault) 149 | if type(data) == "table" then 150 | saveData["General"] = SaveHelper.FillTable(saveData["General"], data["General"] or {}) 151 | saveData["Mod Config Menu"] = SaveHelper.FillTable(saveData["Mod Config Menu"], data["Mod Config Menu"] or data or {}) 152 | if IS_DEV then 153 | Isaac.DebugString("Successfully loaded data from the \"save#.dat\" file.") 154 | end 155 | end 156 | 157 | local currentData = SaveHelper.CopyTable(ModConfigMenu.Config) 158 | saveData = SaveHelper.FillTable(currentData, saveData) 159 | 160 | ModConfigMenu.Config = SaveHelper.CopyTable(saveData) 161 | 162 | --make sure ScreenHelper's offset matches MCM's offset 163 | if ScreenHelper then 164 | ScreenHelper.SetOffset(ModConfigMenu.Config["General"].HudOffset) 165 | end 166 | end 167 | 168 | local function vanillaMCMOptionRegisterSave(mcmOption) 169 | local oldOnChange = mcmOption.OnChange 170 | mcmOption.OnChange = function(currentValue) 171 | local value = oldOnChange(currentValue) 172 | save() 173 | return value 174 | end 175 | end 176 | 177 | -------------- 178 | --game start-- 179 | -------------- 180 | local versionPrintFont = Font() 181 | versionPrintFont:Load("font/pftempestasevencondensed.fnt") 182 | 183 | local versionPrintTimer = 0 184 | local isFirstRun = true 185 | 186 | --returns true if the room is clear and there are no active enemies and there are no projectiles 187 | ModConfigMenu.IgnoreActiveEnemies = ModConfigMenu.IgnoreActiveEnemies or {} 188 | function ModConfigMenu.RoomIsSafe() 189 | local roomHasDanger = false 190 | 191 | for _, entity in pairs(Isaac.GetRoomEntities()) do 192 | if ( 193 | entity:IsActiveEnemy() 194 | and not entity:HasEntityFlags(EntityFlag.FLAG_FRIENDLY) 195 | and ( 196 | not ModConfigMenu.IgnoreActiveEnemies[entity.Type] 197 | or ( 198 | ModConfigMenu.IgnoreActiveEnemies[entity.Type] 199 | and not ModConfigMenu.IgnoreActiveEnemies[entity.Type][ -1] 200 | and not ModConfigMenu.IgnoreActiveEnemies[entity.Type][entity.Variant] 201 | ) 202 | ) 203 | ) then 204 | roomHasDanger = true 205 | elseif ( 206 | entity.Type == EntityType.ENTITY_PROJECTILE 207 | and entity:ToProjectile().ProjectileFlags & ProjectileFlags.CANT_HIT_PLAYER ~= 1 208 | ) then 209 | roomHasDanger = true 210 | elseif entity.Type == EntityType.ENTITY_BOMBDROP then 211 | roomHasDanger = true 212 | end 213 | end 214 | 215 | local game = Game() 216 | local room = game:GetRoom() 217 | 218 | if room:IsClear() and not roomHasDanger then 219 | return true 220 | end 221 | 222 | return false 223 | end 224 | 225 | ModConfigMenu.IsVisible = false 226 | function ModConfigMenu.PostGameStarted() 227 | if IS_DEV then 228 | Isaac.DebugString("POST_GAME_STARTED") 229 | end 230 | 231 | rerunWarnMessage = nil 232 | 233 | load() 234 | 235 | if not isFirstRun then 236 | return 237 | end 238 | isFirstRun = false 239 | 240 | if ModConfigMenu.Config["Mod Config Menu"].ShowControls and isFirstRun then 241 | versionPrintTimer = 120 242 | end 243 | 244 | ModConfigMenu.IsVisible = false 245 | 246 | -- Add Potato Dummy to the ignore list. 247 | local potatoType = Isaac.GetEntityTypeByName("Potato Dummy") 248 | local potatoVariant = Isaac.GetEntityVariantByName("Potato Dummy") 249 | 250 | if potatoType and potatoVariant > 0 then 251 | ModConfigMenu.IgnoreActiveEnemies[potatoType] = ModConfigMenu.IgnoreActiveEnemies or {} 252 | ModConfigMenu.IgnoreActiveEnemies[potatoType][potatoVariant] = true 253 | end 254 | end 255 | 256 | ModConfigMenu.Mod:AddCallback(ModCallbacks.MC_POST_GAME_STARTED, ModConfigMenu.PostGameStarted) 257 | 258 | 259 | --------------- 260 | --post update-- 261 | --------------- 262 | function ModConfigMenu.PostUpdate() 263 | if versionPrintTimer > 0 then 264 | versionPrintTimer = versionPrintTimer - 1 265 | end 266 | end 267 | 268 | ModConfigMenu.Mod:AddCallback(ModCallbacks.MC_POST_UPDATE, ModConfigMenu.PostUpdate) 269 | 270 | 271 | ------------------------------------ 272 | --set up the menu sprites and font-- 273 | ------------------------------------ 274 | function ModConfigMenu.GetMenuAnm2Sprite(animation, frame, color) 275 | local sprite = Sprite() 276 | 277 | sprite:Load("gfx/ui/modconfig/menu.anm2", true) 278 | sprite:SetFrame(animation or "Idle", frame or 0) 279 | 280 | if color then 281 | sprite.Color = color 282 | end 283 | 284 | return sprite 285 | end 286 | 287 | --main menu sprites 288 | local MenuSprite = ModConfigMenu.GetMenuAnm2Sprite("Idle", 0) 289 | local MenuOverlaySprite = ModConfigMenu.GetMenuAnm2Sprite("IdleOverlay", 0) 290 | local PopupSprite = ModConfigMenu.GetMenuAnm2Sprite("Popup", 0) 291 | 292 | --main cursors 293 | local CursorSpriteRight = ModConfigMenu.GetMenuAnm2Sprite("Cursor", 0) 294 | local CursorSpriteUp = ModConfigMenu.GetMenuAnm2Sprite("Cursor", 1) 295 | local CursorSpriteDown = ModConfigMenu.GetMenuAnm2Sprite("Cursor", 2) 296 | 297 | --colors 298 | local colorDefault = Color(1, 1, 1, 1, 0, 0, 0) 299 | local colorHalf = Color(1, 1, 1, 0.5, 0, 0, 0) 300 | 301 | --subcategory pane cursors 302 | local SubcategoryCursorSpriteLeft = ModConfigMenu.GetMenuAnm2Sprite("Cursor", 3, colorHalf) 303 | local SubcategoryCursorSpriteRight = ModConfigMenu.GetMenuAnm2Sprite("Cursor", 0, colorHalf) 304 | 305 | --options pane cursors 306 | local OptionsCursorSpriteUp = ModConfigMenu.GetMenuAnm2Sprite("Cursor", 1, colorHalf) 307 | local OptionsCursorSpriteDown = ModConfigMenu.GetMenuAnm2Sprite("Cursor", 2, colorHalf) 308 | 309 | --other options pane objects 310 | local SubcategoryDividerSprite = ModConfigMenu.GetMenuAnm2Sprite("Divider", 0, colorHalf) 311 | local SliderSprite = ModConfigMenu.GetMenuAnm2Sprite("Slider1", 0) 312 | 313 | --strikeout 314 | local StrikeOutSprite = ModConfigMenu.GetMenuAnm2Sprite("Strikeout", 0) 315 | 316 | --back/select corner papers 317 | local CornerSelect = ModConfigMenu.GetMenuAnm2Sprite("BackSelect", 0) 318 | local CornerBack = ModConfigMenu.GetMenuAnm2Sprite("BackSelect", 1) 319 | local CornerOpen = ModConfigMenu.GetMenuAnm2Sprite("BackSelect", 2) 320 | local CornerExit = ModConfigMenu.GetMenuAnm2Sprite("BackSelect", 3) 321 | 322 | --fonts 323 | local Font10 = Font() 324 | Font10:Load("font/teammeatfont10.fnt") 325 | 326 | local Font12 = Font() 327 | Font12:Load("font/teammeatfont12.fnt") 328 | 329 | local Font16Bold = Font() 330 | Font16Bold:Load("font/teammeatfont16bold.fnt") 331 | 332 | --popups 333 | ModConfigMenu.PopupGfx = ModConfigMenu.PopupGfx or {} 334 | ModConfigMenu.PopupGfx.THIN_SMALL = "gfx/ui/modconfig/popup_thin_small.png" 335 | ModConfigMenu.PopupGfx.THIN_MEDIUM = "gfx/ui/modconfig/popup_thin_medium.png" 336 | ModConfigMenu.PopupGfx.THIN_LARGE = "gfx/ui/modconfig/popup_thin_large.png" 337 | ModConfigMenu.PopupGfx.WIDE_SMALL = "gfx/ui/modconfig/popup_wide_small.png" 338 | ModConfigMenu.PopupGfx.WIDE_MEDIUM = "gfx/ui/modconfig/popup_wide_medium.png" 339 | ModConfigMenu.PopupGfx.WIDE_LARGE = "gfx/ui/modconfig/popup_wide_large.png" 340 | 341 | 342 | ------------------------- 343 | --add setting functions-- 344 | ------------------------- 345 | ModConfigMenu.OptionType = ModConfigMenu.OptionType or {} 346 | ModConfigMenu.OptionType.TEXT = 1 347 | ModConfigMenu.OptionType.SPACE = 2 348 | ModConfigMenu.OptionType.SCROLL = 3 349 | ModConfigMenu.OptionType.BOOLEAN = 4 350 | ModConfigMenu.OptionType.NUMBER = 5 351 | ModConfigMenu.OptionType.KEYBIND_KEYBOARD = 6 352 | ModConfigMenu.OptionType.KEYBIND_CONTROLLER = 7 353 | ModConfigMenu.OptionType.TITLE = 8 354 | 355 | ModConfigMenu.MenuData = ModConfigMenu.MenuData or {} 356 | 357 | --CATEGORY FUNCTIONS 358 | function ModConfigMenu.GetCategoryIDByName(categoryName) 359 | if type(categoryName) ~= "string" then 360 | return categoryName 361 | end 362 | 363 | local categoryID = nil 364 | 365 | for i = 1, #ModConfigMenu.MenuData do 366 | if categoryName == ModConfigMenu.MenuData[i].Name then 367 | categoryID = i 368 | break 369 | end 370 | end 371 | 372 | return categoryID 373 | end 374 | 375 | function ModConfigMenu.UpdateCategory(categoryName, dataTable) 376 | if type(categoryName) ~= "string" and type(categoryName) ~= "number" then 377 | error("ModConfigMenu.UpdateCategory - No valid category name provided", 2) 378 | end 379 | 380 | local categoryID = ModConfigMenu.GetCategoryIDByName(categoryName) 381 | if categoryID == nil then 382 | categoryID = #ModConfigMenu.MenuData + 1 383 | ModConfigMenu.MenuData[categoryID] = {} 384 | ModConfigMenu.MenuData[categoryID].Subcategories = {} 385 | end 386 | 387 | if type(categoryName) == "string" or dataTable.Name then 388 | ModConfigMenu.MenuData[categoryID].Name = dataTable.Name or categoryName 389 | end 390 | 391 | if dataTable.Info then 392 | ModConfigMenu.MenuData[categoryID].Info = dataTable.Info 393 | end 394 | 395 | if dataTable.IsOld then 396 | ModConfigMenu.MenuData[categoryID].IsOld = dataTable.IsOld 397 | end 398 | end 399 | 400 | function ModConfigMenu.SetCategoryInfo(categoryName, info) 401 | if type(categoryName) ~= "string" and type(categoryName) ~= "number" then 402 | error("ModConfigMenu.SetCategoryInfo - No valid category name provided", 2) 403 | end 404 | 405 | ModConfigMenu.UpdateCategory(categoryName, { 406 | Info = info 407 | }) 408 | end 409 | 410 | function ModConfigMenu.RemoveCategory(categoryName) 411 | if type(categoryName) ~= "string" and type(categoryName) ~= "number" then 412 | error("ModConfigMenu.RemoveCategory - No valid category name provided", 2) 413 | end 414 | 415 | local categoryID = ModConfigMenu.GetCategoryIDByName(categoryName) 416 | if categoryID then 417 | table.remove(ModConfigMenu.MenuData, categoryID) 418 | return true 419 | end 420 | 421 | return false 422 | end 423 | 424 | --SUBCATEGORY FUNCTIONS 425 | function ModConfigMenu.GetSubcategoryIDByName(categoryName, subcategoryName) 426 | if type(categoryName) ~= "string" and type(categoryName) ~= "number" then 427 | error("ModConfigMenu.GetSubcategoryIDByName - No valid category name provided", 2) 428 | end 429 | 430 | local categoryID = ModConfigMenu.GetCategoryIDByName(categoryName) 431 | 432 | if type(subcategoryName) ~= "string" then 433 | return subcategoryName 434 | end 435 | 436 | local subcategoryID = nil 437 | 438 | for i = 1, #ModConfigMenu.MenuData[categoryID].Subcategories do 439 | if subcategoryName == ModConfigMenu.MenuData[categoryID].Subcategories[i].Name then 440 | subcategoryID = i 441 | break 442 | end 443 | end 444 | 445 | return subcategoryID 446 | end 447 | 448 | function ModConfigMenu.UpdateSubcategory(categoryName, subcategoryName, dataTable) 449 | if type(categoryName) ~= "string" and type(categoryName) ~= "number" then 450 | error("ModConfigMenu.UpdateSubcategory - No valid category name provided", 2) 451 | end 452 | 453 | if type(subcategoryName) ~= "string" and type(subcategoryName) ~= "number" then 454 | error("ModConfigMenu.UpdateSubcategory - No valid subcategory name provided", 2) 455 | end 456 | 457 | local categoryID = ModConfigMenu.GetCategoryIDByName(categoryName) 458 | if categoryID == nil then 459 | categoryID = #ModConfigMenu.MenuData + 1 460 | ModConfigMenu.MenuData[categoryID] = {} 461 | ModConfigMenu.MenuData[categoryID].Name = tostring(categoryName) 462 | ModConfigMenu.MenuData[categoryID].Subcategories = {} 463 | end 464 | 465 | local subcategoryID = ModConfigMenu.GetSubcategoryIDByName(categoryID, subcategoryName) 466 | if subcategoryID == nil then 467 | subcategoryID = #ModConfigMenu.MenuData[categoryID].Subcategories + 1 468 | ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID] = {} 469 | ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options = {} 470 | end 471 | 472 | if type(subcategoryName) == "string" or dataTable.Name then 473 | ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Name = dataTable.Name or subcategoryName 474 | end 475 | 476 | if dataTable.Info then 477 | ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Info = dataTable.Info 478 | end 479 | end 480 | 481 | function ModConfigMenu.RemoveSubcategory(categoryName, subcategoryName) 482 | if type(categoryName) ~= "string" and type(categoryName) ~= "number" then 483 | error("ModConfigMenu.RemoveSubcategory - No valid category name provided", 2) 484 | end 485 | 486 | if type(subcategoryName) ~= "string" and type(subcategoryName) ~= "number" then 487 | error("ModConfigMenu.RemoveSubcategory - No valid subcategory name provided", 2) 488 | end 489 | 490 | local categoryID = ModConfigMenu.GetCategoryIDByName(categoryName) 491 | if categoryID then 492 | local subcategoryID = ModConfigMenu.GetSubcategoryIDByName(categoryID, subcategoryName) 493 | if subcategoryID then 494 | table.remove(ModConfigMenu.MenuData[categoryID].Subcategories, subcategoryID) 495 | return true 496 | end 497 | end 498 | 499 | return false 500 | end 501 | 502 | --SETTING FUNCTIONS 503 | function ModConfigMenu.AddSetting(categoryName, subcategoryName, settingTable) 504 | if settingTable == nil then 505 | settingTable = subcategoryName 506 | subcategoryName = nil 507 | end 508 | 509 | if type(categoryName) ~= "string" and type(categoryName) ~= "number" then 510 | error("ModConfigMenu.AddSetting - No valid category name provided", 2) 511 | end 512 | 513 | subcategoryName = subcategoryName or "Uncategorized" 514 | if type(subcategoryName) ~= "string" and type(subcategoryName) ~= "number" then 515 | error("ModConfigMenu.AddSetting - No valid subcategory name provided", 2) 516 | end 517 | 518 | local categoryID = ModConfigMenu.GetCategoryIDByName(categoryName) 519 | if categoryID == nil then 520 | categoryID = #ModConfigMenu.MenuData + 1 521 | ModConfigMenu.MenuData[categoryID] = {} 522 | ModConfigMenu.MenuData[categoryID].Name = tostring(categoryName) 523 | ModConfigMenu.MenuData[categoryID].Subcategories = {} 524 | end 525 | 526 | local subcategoryID = ModConfigMenu.GetSubcategoryIDByName(categoryID, subcategoryName) 527 | if subcategoryID == nil then 528 | subcategoryID = #ModConfigMenu.MenuData[categoryID].Subcategories + 1 529 | ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID] = {} 530 | ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Name = tostring(subcategoryName) 531 | ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options = {} 532 | end 533 | 534 | ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options[ 535 | #ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options + 1] = settingTable 536 | 537 | return settingTable 538 | end 539 | 540 | function ModConfigMenu.AddText(categoryName, subcategoryName, text, color) 541 | if color == nil and type(text) ~= "string" and type(text) ~= "function" then 542 | color = text 543 | text = subcategoryName 544 | subcategoryName = nil 545 | end 546 | 547 | if type(categoryName) ~= "string" and type(categoryName) ~= "number" then 548 | error("ModConfigMenu.AddText - No valid category name provided", 2) 549 | end 550 | 551 | subcategoryName = subcategoryName or "Uncategorized" 552 | if type(subcategoryName) ~= "string" and type(subcategoryName) ~= "number" then 553 | error("ModConfigMenu.AddText - No valid subcategory name provided", 2) 554 | end 555 | 556 | local settingTable = { 557 | Type = ModConfigMenu.OptionType.TEXT, 558 | Display = text, 559 | Color = color, 560 | NoCursorHere = true 561 | } 562 | 563 | return ModConfigMenu.AddSetting(categoryName, subcategoryName, settingTable) 564 | end 565 | 566 | function ModConfigMenu.AddTitle(categoryName, subcategoryName, text, color) 567 | if color == nil and type(text) ~= "string" and type(text) ~= "function" then 568 | color = text 569 | text = subcategoryName 570 | subcategoryName = nil 571 | end 572 | 573 | if type(categoryName) ~= "string" and type(categoryName) ~= "number" then 574 | error("ModConfigMenu.AddTitle - No valid category name provided", 2) 575 | end 576 | 577 | subcategoryName = subcategoryName or "Uncategorized" 578 | if type(subcategoryName) ~= "string" and type(subcategoryName) ~= "number" then 579 | error("ModConfigMenu.AddTitle - No valid subcategory name provided", 2) 580 | end 581 | 582 | local settingTable = { 583 | Type = ModConfigMenu.OptionType.TITLE, 584 | Display = text, 585 | Color = color, 586 | NoCursorHere = true 587 | } 588 | 589 | return ModConfigMenu.AddSetting(categoryName, subcategoryName, settingTable) 590 | end 591 | 592 | function ModConfigMenu.AddSpace(categoryName, subcategoryName) 593 | if type(categoryName) ~= "string" and type(categoryName) ~= "number" then 594 | error("ModConfigMenu.AddSpace - No valid category name provided", 2) 595 | end 596 | 597 | subcategoryName = subcategoryName or "Uncategorized" 598 | if type(subcategoryName) ~= "string" and type(subcategoryName) ~= "number" then 599 | error("ModConfigMenu.AddSpace - No valid subcategory name provided", 2) 600 | end 601 | 602 | local settingTable = { 603 | Type = ModConfigMenu.OptionType.SPACE 604 | } 605 | 606 | return ModConfigMenu.AddSetting(categoryName, subcategoryName, settingTable) 607 | end 608 | 609 | local altSlider = false 610 | function ModConfigMenu.SimpleAddSetting(settingType, categoryName, subcategoryName, configTableAttribute, minValue, 611 | maxValue, modifyBy, defaultValue, displayText, displayValueProxies, displayDevice 612 | , info, color, functionName) 613 | --set default values 614 | if defaultValue == nil then 615 | if settingType == ModConfigMenu.OptionType.BOOLEAN then 616 | defaultValue = false 617 | else 618 | defaultValue = 0 619 | end 620 | end 621 | 622 | if settingType == ModConfigMenu.OptionType.NUMBER then 623 | minValue = minValue or 0 624 | maxValue = maxValue or 10 625 | modifyBy = modifyBy or 1 626 | else 627 | minValue = nil 628 | maxValue = nil 629 | modifyBy = nil 630 | end 631 | 632 | functionName = functionName or "SimpleAddSetting" 633 | 634 | --erroring 635 | if categoryName == nil then 636 | error("ModConfigMenu." .. tostring(functionName) .. " - No valid category name provided", 2) 637 | end 638 | if configTableAttribute == nil then 639 | error("ModConfigMenu." .. tostring(functionName) .. " - No valid config table attribute provided", 2) 640 | end 641 | 642 | --create config value 643 | ModConfigMenu.Config[categoryName] = ModConfigMenu.Config[categoryName] or {} 644 | if ModConfigMenu.Config[categoryName][configTableAttribute] == nil then 645 | ModConfigMenu.Config[categoryName][configTableAttribute] = defaultValue 646 | end 647 | 648 | ModConfigMenu.ConfigDefault[categoryName] = ModConfigMenu.ConfigDefault[categoryName] or {} 649 | if ModConfigMenu.ConfigDefault[categoryName][configTableAttribute] == nil then 650 | ModConfigMenu.ConfigDefault[categoryName][configTableAttribute] = defaultValue 651 | end 652 | 653 | --setting 654 | local settingTable = { 655 | Type = settingType, 656 | Attribute = configTableAttribute, 657 | CurrentSetting = function() 658 | return ModConfigMenu.Config[categoryName][configTableAttribute] 659 | end, 660 | Default = defaultValue, 661 | Display = function(cursorIsAtThisOption, configMenuInOptions, lastOptionPos) 662 | local currentValue = ModConfigMenu.Config[categoryName][configTableAttribute] 663 | 664 | local displayString = "" 665 | 666 | if displayText then 667 | displayString = displayText .. ": " 668 | end 669 | 670 | if settingType == ModConfigMenu.OptionType.SCROLL then 671 | displayString = displayString .. "$scroll" .. tostring(math.floor(currentValue)) 672 | elseif settingType == ModConfigMenu.OptionType.KEYBIND_KEYBOARD then 673 | local key = "None" 674 | 675 | if currentValue > -1 then 676 | key = "Unknown Key" 677 | 678 | if InputHelper.KeyboardToString[currentValue] then 679 | key = InputHelper.KeyboardToString[currentValue] 680 | end 681 | end 682 | 683 | displayString = displayString .. key 684 | 685 | if displayDevice then 686 | displayString = displayString .. " (keyboard)" 687 | end 688 | elseif settingType == ModConfigMenu.OptionType.KEYBIND_CONTROLLER then 689 | local key = "None" 690 | 691 | if currentValue > -1 then 692 | key = "Unknown Button" 693 | 694 | if InputHelper.ControllerToString[currentValue] then 695 | key = InputHelper.ControllerToString[currentValue] 696 | end 697 | end 698 | 699 | displayString = displayString .. key 700 | 701 | if displayDevice then 702 | displayString = displayString .. " (controller)" 703 | end 704 | elseif displayValueProxies and displayValueProxies[currentValue] then 705 | displayString = displayString .. tostring(displayValueProxies[currentValue]) 706 | else 707 | displayString = displayString .. tostring(currentValue) 708 | end 709 | 710 | return displayString 711 | end, 712 | OnChange = function(currentValue) 713 | if not currentValue then 714 | if settingType == ModConfigMenu.OptionType.KEYBIND_KEYBOARD or 715 | settingType == ModConfigMenu.OptionType.KEYBIND_CONTROLLER then 716 | currentValue = -1 717 | end 718 | end 719 | 720 | ModConfigMenu.Config[categoryName][configTableAttribute] = currentValue 721 | end, 722 | Info = info, 723 | Color = color 724 | } 725 | 726 | if settingType == ModConfigMenu.OptionType.NUMBER then 727 | settingTable.Minimum = minValue 728 | settingTable.Maximum = maxValue 729 | settingTable.ModifyBy = modifyBy 730 | elseif settingType == ModConfigMenu.OptionType.SCROLL then 731 | settingTable.AltSlider = altSlider 732 | altSlider = not altSlider 733 | elseif settingType == ModConfigMenu.OptionType.KEYBIND_KEYBOARD or 734 | settingType == ModConfigMenu.OptionType.KEYBIND_CONTROLLER then 735 | settingTable.PopupGfx = ModConfigMenu.PopupGfx.WIDE_SMALL 736 | settingTable.PopupWidth = 280 737 | settingTable.Popup = function() 738 | local currentValue = ModConfigMenu.Config[categoryName][configTableAttribute] 739 | 740 | local goBackString = "back" 741 | if ModConfigMenu.Config.LastBackPressed then 742 | if InputHelper.KeyboardToString[ModConfigMenu.Config.LastBackPressed] then 743 | goBackString = InputHelper.KeyboardToString[ModConfigMenu.Config.LastBackPressed] 744 | elseif InputHelper.ControllerToString[ModConfigMenu.Config.LastBackPressed] then 745 | goBackString = InputHelper.ControllerToString[ModConfigMenu.Config.LastBackPressed] 746 | end 747 | end 748 | 749 | local keepSettingString = "" 750 | if currentValue > -1 then 751 | local currentSettingString = nil 752 | if (settingType == ModConfigMenu.OptionType.KEYBIND_KEYBOARD and InputHelper.KeyboardToString[currentValue]) then 753 | currentSettingString = InputHelper.KeyboardToString[currentValue] 754 | elseif ( 755 | settingType == ModConfigMenu.OptionType.KEYBIND_CONTROLLER and InputHelper.ControllerToString[currentValue]) then 756 | currentSettingString = InputHelper.ControllerToString[currentValue] 757 | end 758 | 759 | keepSettingString = "This setting is currently set to \"" .. 760 | currentSettingString .. "\".$newlinePress this button to keep it unchanged.$newline$newline" 761 | end 762 | 763 | local deviceString = "" 764 | if settingType == ModConfigMenu.OptionType.KEYBIND_KEYBOARD then 765 | deviceString = "keyboard" 766 | elseif settingType == ModConfigMenu.OptionType.KEYBIND_CONTROLLER then 767 | deviceString = "controller" 768 | end 769 | 770 | return "Press a button on your " .. 771 | deviceString .. 772 | " to change this setting.$newline$newline" .. 773 | keepSettingString .. "Press \"" .. goBackString .. "\" to go back and clear this setting." 774 | end 775 | end 776 | 777 | return ModConfigMenu.AddSetting(categoryName, subcategoryName, settingTable) 778 | end 779 | 780 | function ModConfigMenu.AddBooleanSetting(categoryName, subcategoryName, configTableAttribute, defaultValue, displayText, 781 | displayValueProxies, info, color) 782 | --move args around 783 | if type(configTableAttribute) ~= "string" then 784 | color = info 785 | info = displayValueProxies 786 | displayValueProxies = displayText 787 | displayText = defaultValue 788 | defaultValue = configTableAttribute 789 | configTableAttribute = subcategoryName 790 | subcategoryName = nil 791 | end 792 | 793 | if type(defaultValue) ~= "boolean" then 794 | color = info 795 | info = displayValueProxies 796 | displayValueProxies = displayText 797 | displayText = defaultValue 798 | defaultValue = false 799 | end 800 | 801 | if type(displayValueProxies) ~= "table" or type(info) == "userdata" or type(info) == "nil" then 802 | color = info 803 | info = displayValueProxies 804 | displayValueProxies = nil 805 | end 806 | 807 | return ModConfigMenu.SimpleAddSetting(ModConfigMenu.OptionType.BOOLEAN, categoryName, subcategoryName, 808 | configTableAttribute, nil, nil, nil, defaultValue, displayText, displayValueProxies, nil, info, color, 809 | "AddBooleanSetting") 810 | end 811 | 812 | function ModConfigMenu.AddNumberSetting(categoryName, subcategoryName, configTableAttribute, minValue, maxValue, modifyBy 813 | , defaultValue, displayText, displayValueProxies, info, color) 814 | --move args around 815 | if type(configTableAttribute) ~= "string" then 816 | color = info 817 | info = displayValueProxies 818 | displayValueProxies = displayText 819 | displayText = defaultValue 820 | defaultValue = modifyBy 821 | modifyBy = maxValue 822 | maxValue = minValue 823 | minValue = configTableAttribute 824 | configTableAttribute = subcategoryName 825 | subcategoryName = nil 826 | end 827 | 828 | if type(defaultValue) == "string" then 829 | color = info 830 | info = displayValueProxies 831 | displayValueProxies = displayText 832 | displayText = defaultValue 833 | defaultValue = modifyBy 834 | modifyBy = nil 835 | end 836 | 837 | if type(displayValueProxies) ~= "table" or type(info) == "userdata" or type(info) == "nil" then 838 | color = info 839 | info = displayValueProxies 840 | displayValueProxies = nil 841 | end 842 | 843 | --set default values 844 | defaultValue = defaultValue or 0 845 | minValue = minValue or 0 846 | maxValue = maxValue or 10 847 | modifyBy = modifyBy or 1 848 | 849 | return ModConfigMenu.SimpleAddSetting(ModConfigMenu.OptionType.NUMBER, categoryName, subcategoryName, 850 | configTableAttribute, minValue, maxValue, modifyBy, defaultValue, displayText, displayValueProxies, nil, info, 851 | color 852 | , 853 | "AddNumberSetting") 854 | end 855 | 856 | function ModConfigMenu.AddScrollSetting(categoryName, subcategoryName, configTableAttribute, defaultValue, displayText, 857 | info, color) 858 | --move args around 859 | if type(configTableAttribute) ~= "string" then 860 | color = info 861 | info = displayText 862 | displayText = defaultValue 863 | defaultValue = configTableAttribute 864 | configTableAttribute = subcategoryName 865 | subcategoryName = nil 866 | end 867 | 868 | if type(defaultValue) ~= "number" then 869 | color = info 870 | info = displayText 871 | displayText = defaultValue 872 | defaultValue = nil 873 | end 874 | 875 | --set default values 876 | defaultValue = defaultValue or 0 877 | 878 | return ModConfigMenu.SimpleAddSetting(ModConfigMenu.OptionType.SCROLL, categoryName, subcategoryName, 879 | configTableAttribute, nil, nil, nil, defaultValue, displayText, nil, nil, info, color, "AddScrollSetting") 880 | end 881 | 882 | function ModConfigMenu.AddKeyboardSetting(categoryName, subcategoryName, configTableAttribute, defaultValue, displayText 883 | , displayDevice, info, color) 884 | --move args around 885 | if type(configTableAttribute) ~= "string" then 886 | color = info 887 | info = displayDevice 888 | displayDevice = displayText 889 | displayText = defaultValue 890 | defaultValue = configTableAttribute 891 | configTableAttribute = subcategoryName 892 | subcategoryName = nil 893 | end 894 | 895 | if type(defaultValue) ~= "number" then 896 | color = info 897 | info = displayText 898 | displayText = defaultValue 899 | defaultValue = nil 900 | end 901 | 902 | if type(displayDevice) ~= "boolean" then 903 | color = info 904 | info = displayDevice 905 | displayDevice = false 906 | end 907 | 908 | --set default values 909 | defaultValue = defaultValue or -1 910 | 911 | return ModConfigMenu.SimpleAddSetting(ModConfigMenu.OptionType.KEYBIND_KEYBOARD, categoryName, subcategoryName, 912 | configTableAttribute, nil, nil, nil, defaultValue, displayText, nil, displayDevice, info, color, 913 | "AddKeyboardSetting") 914 | end 915 | 916 | function ModConfigMenu.AddControllerSetting(categoryName, subcategoryName, configTableAttribute, defaultValue, 917 | displayText, displayDevice, info, color) 918 | --move args around 919 | if type(configTableAttribute) ~= "string" then 920 | color = info 921 | info = displayDevice 922 | displayDevice = displayText 923 | displayText = defaultValue 924 | defaultValue = configTableAttribute 925 | configTableAttribute = subcategoryName 926 | subcategoryName = nil 927 | end 928 | 929 | if type(defaultValue) ~= "number" then 930 | color = info 931 | info = displayText 932 | displayText = defaultValue 933 | defaultValue = nil 934 | end 935 | 936 | if type(displayDevice) ~= "boolean" then 937 | color = info 938 | info = displayDevice 939 | displayDevice = false 940 | end 941 | 942 | --set default values 943 | defaultValue = defaultValue or -1 944 | 945 | return ModConfigMenu.SimpleAddSetting(ModConfigMenu.OptionType.KEYBIND_CONTROLLER, categoryName, subcategoryName, 946 | configTableAttribute, nil, nil, nil, defaultValue, displayText, nil, displayDevice, info, color, 947 | "AddControllerSetting") 948 | end 949 | 950 | function ModConfigMenu.RemoveSetting(categoryName, subcategoryName, settingAttribute) 951 | if settingAttribute == nil then 952 | settingAttribute = subcategoryName 953 | subcategoryName = nil 954 | end 955 | 956 | if type(categoryName) ~= "string" and type(categoryName) ~= "number" then 957 | error("ModConfigMenu.RemoveSetting - No valid category name provided", 2) 958 | end 959 | 960 | subcategoryName = subcategoryName or "Uncategorized" 961 | if type(subcategoryName) ~= "string" and type(subcategoryName) ~= "number" then 962 | error("ModConfigMenu.RemoveSetting - No valid subcategory name provided", 2) 963 | end 964 | 965 | local categoryID = ModConfigMenu.GetCategoryIDByName(categoryName) 966 | if categoryID then 967 | local subcategoryID = ModConfigMenu.GetSubcategoryIDByName(categoryID, subcategoryName) 968 | if subcategoryID then 969 | --loop to find matching attribute 970 | for i = #ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options, 1, -1 do 971 | if ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options[i] 972 | and ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options[i].Attribute 973 | and ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options[i].Attribute == settingAttribute then 974 | table.remove(ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options, i) 975 | return true 976 | end 977 | end 978 | 979 | --loop to find matching display 980 | for i = #ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options, 1, -1 do 981 | if ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options[i] 982 | and ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options[i].Display 983 | and ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options[i].Display == settingAttribute then 984 | table.remove(ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options, i) 985 | return true 986 | end 987 | end 988 | end 989 | end 990 | 991 | return false 992 | end 993 | 994 | -------------------------- 995 | --GENERAL SETTINGS SETUP-- 996 | -------------------------- 997 | ModConfigMenu.SetCategoryInfo("General", "Settings that affect the majority of mods") 998 | 999 | ---------------------- 1000 | --HUD OFFSET SETTING-- 1001 | ---------------------- 1002 | local hudOffsetSetting = ModConfigMenu.AddScrollSetting( 1003 | "General", --category 1004 | "HudOffset", --attribute in table 1005 | Options and Options.HUDOffset * 10 or 0, --default value 1006 | "Hud Offset", --display text 1007 | "How far from the corners of the screen custom hud elements will be.$newlineTry to make this match your base-game setting." 1008 | ) 1009 | 1010 | hudOffsetSetting.HideControls = true -- hide controls so the screen corner graphics are easier to see 1011 | hudOffsetSetting.ShowOffset = true -- shows screen offset 1012 | vanillaMCMOptionRegisterSave(hudOffsetSetting) 1013 | 1014 | --set up callback 1015 | local oldHudOffsetOnChange = hudOffsetSetting.OnChange 1016 | hudOffsetSetting.OnChange = function(currentValue) 1017 | --update screenhelper's offset 1018 | if ScreenHelper then 1019 | ScreenHelper.SetOffset(currentValue) 1020 | end 1021 | 1022 | return oldHudOffsetOnChange(currentValue) 1023 | end 1024 | 1025 | -------------------- 1026 | --OVERLAYS SETTING-- 1027 | -------------------- 1028 | local overlays = ModConfigMenu.AddBooleanSetting( 1029 | "General", --category 1030 | "Overlays", --attribute in table 1031 | true, --default value 1032 | "Overlays", --display text 1033 | { 1034 | --value display text 1035 | [true] = "On", 1036 | [false] = "Off" 1037 | }, 1038 | "Enable or disable custom visual overlays, like screen-wide fog." 1039 | ) 1040 | vanillaMCMOptionRegisterSave(overlays) 1041 | 1042 | ----------------------- 1043 | --CHARGE BARS SETTING-- 1044 | ----------------------- 1045 | local chargeBars = ModConfigMenu.AddBooleanSetting( 1046 | "General", --category 1047 | "ChargeBars", --attribute in table 1048 | false, --default value 1049 | "Charge Bars", --display text 1050 | { 1051 | --value display text 1052 | [true] = "On", 1053 | [false] = "Off" 1054 | }, 1055 | "Enable or disable custom charge bar visuals for mod effects, like those from chargeable items." 1056 | ) 1057 | vanillaMCMOptionRegisterSave(chargeBars) 1058 | 1059 | --------------------- 1060 | --BIG BOOKS SETTING-- 1061 | --------------------- 1062 | local bigBooks = ModConfigMenu.AddBooleanSetting( 1063 | "General", --category 1064 | "BigBooks", --attribute in table 1065 | true, --default value 1066 | "Bigbooks", --display text 1067 | { 1068 | --value display text 1069 | [true] = "On", 1070 | [false] = "Off" 1071 | }, 1072 | "Enable or disable custom big-book overlays which can appear when an active item is used." 1073 | ) 1074 | vanillaMCMOptionRegisterSave(bigBooks) 1075 | 1076 | --------------------- 1077 | --ANNOUNCER SETTING-- 1078 | --------------------- 1079 | 1080 | local announcer = ModConfigMenu.AddNumberSetting( 1081 | "General", --category 1082 | "Announcer", --attribute in table 1083 | 0, --minimum value 1084 | 2, --max value 1085 | 0, --default value, 1086 | "Announcer", --display text 1087 | { 1088 | --value display text 1089 | [0] = "Sometimes", 1090 | [1] = "Never", 1091 | [2] = "Always" 1092 | }, 1093 | "Choose how often a voice-over will play when a pocket item (pill or card) is used." 1094 | ) 1095 | vanillaMCMOptionRegisterSave(announcer) 1096 | 1097 | -------------------------- 1098 | --GENERAL SETTINGS CLOSE-- 1099 | -------------------------- 1100 | 1101 | ModConfigMenu.AddSpace("General") --SPACE 1102 | 1103 | ModConfigMenu.AddText("General", "These settings apply to") 1104 | ModConfigMenu.AddText("General", "all mods which support them") 1105 | 1106 | ---------------------------------- 1107 | --MOD CONFIG MENU SETTINGS SETUP-- 1108 | ---------------------------------- 1109 | 1110 | ModConfigMenu.SetCategoryInfo("Mod Config Menu", 1111 | "Settings specific to Mod Config Menu.$newlineChange keybindings for the menu here.") 1112 | 1113 | ModConfigMenu.AddTitle("Mod Config Menu", "Version " .. tostring(ModConfigMenu.Version) .. " !") --VERSION INDICATOR 1114 | 1115 | ModConfigMenu.AddSpace("Mod Config Menu") --SPACE 1116 | 1117 | ---------------------- 1118 | --OPEN MENU KEYBOARD-- 1119 | ---------------------- 1120 | 1121 | local openMenuKeyboardSetting = ModConfigMenu.AddKeyboardSetting( 1122 | "Mod Config Menu", --category 1123 | "OpenMenuKeyboard", --attribute in table 1124 | Keyboard.KEY_L, --default value 1125 | "Open Menu", --display text 1126 | true, --if (keyboard) is displayed after the key text 1127 | "Choose what button on your keyboard will open Mod Config Menu." 1128 | ) 1129 | 1130 | openMenuKeyboardSetting.IsOpenMenuKeybind = true 1131 | vanillaMCMOptionRegisterSave(openMenuKeyboardSetting) 1132 | 1133 | ------------------------ 1134 | --OPEN MENU CONTROLLER-- 1135 | ------------------------ 1136 | 1137 | local openMenuControllerSetting = ModConfigMenu.AddControllerSetting( 1138 | "Mod Config Menu", --category 1139 | "OpenMenuController", --attribute in table 1140 | Controller.STICK_RIGHT, --default value 1141 | "Open Menu", --display text 1142 | true, --if (controller) is displayed after the key text 1143 | "Choose what button on your controller will open Mod Config Menu." 1144 | ) 1145 | openMenuControllerSetting.IsOpenMenuKeybind = true 1146 | vanillaMCMOptionRegisterSave(openMenuControllerSetting) 1147 | 1148 | --f10 note 1149 | ModConfigMenu.AddText("Mod Config Menu", "F10 will always open this menu.") 1150 | 1151 | ModConfigMenu.AddSpace("Mod Config Menu") --SPACE 1152 | 1153 | ------------ 1154 | --HIDE HUD-- 1155 | ------------ 1156 | 1157 | local hideHudSetting = ModConfigMenu.AddBooleanSetting( 1158 | "Mod Config Menu", --category 1159 | "HideHudInMenu", --attribute in table 1160 | true, --default value 1161 | "Hide HUD", --display text 1162 | { 1163 | --value display text 1164 | [true] = "Yes", 1165 | [false] = "No" 1166 | }, 1167 | "Enable or disable the hud when this menu is open." 1168 | ) 1169 | vanillaMCMOptionRegisterSave(hideHudSetting) 1170 | 1171 | --actively modify the hud visibility as this setting changes 1172 | local oldHideHudOnChange = hideHudSetting.OnChange 1173 | hideHudSetting.OnChange = function(currentValue) 1174 | oldHideHudOnChange(currentValue) 1175 | 1176 | local game = Game() 1177 | if REPENTANCE then 1178 | local hud = game:GetHUD() 1179 | 1180 | if currentValue then 1181 | if hud:IsVisible() then 1182 | hud:SetVisible(false) 1183 | end 1184 | else 1185 | if not hud:IsVisible() then 1186 | hud:SetVisible(true) 1187 | end 1188 | end 1189 | else 1190 | local seeds = game:GetSeeds() 1191 | 1192 | if currentValue then 1193 | if not seeds:HasSeedEffect(SeedEffect.SEED_NO_HUD) then 1194 | seeds:AddSeedEffect(SeedEffect.SEED_NO_HUD) 1195 | end 1196 | else 1197 | if seeds:HasSeedEffect(SeedEffect.SEED_NO_HUD) then 1198 | seeds:RemoveSeedEffect(SeedEffect.SEED_NO_HUD) 1199 | end 1200 | end 1201 | end 1202 | end 1203 | 1204 | ---------------------------- 1205 | --RESET TO DEFAULT KEYBIND-- 1206 | ---------------------------- 1207 | 1208 | local resetKeybindSetting = ModConfigMenu.AddKeyboardSetting( 1209 | "Mod Config Menu", --category 1210 | "ResetToDefault", --attribute in table 1211 | Keyboard.KEY_F11, --default value 1212 | "Reset To Default Keybind", --display text 1213 | "Press this button on your keyboard to reset a setting to its default value." 1214 | ) 1215 | resetKeybindSetting.IsResetKeybind = true 1216 | vanillaMCMOptionRegisterSave(resetKeybindSetting) 1217 | 1218 | ----------------- 1219 | --SHOW CONTROLS-- 1220 | ----------------- 1221 | 1222 | local showControls = ModConfigMenu.AddBooleanSetting( 1223 | "Mod Config Menu", --category 1224 | "ShowControls", --attribute in table 1225 | true, --default value 1226 | "Show Controls", --display text 1227 | { 1228 | --value display text 1229 | [true] = "Yes", 1230 | [false] = "No" 1231 | }, 1232 | "Disable this to remove the back and select widgets at the lower corners of the screen and remove the bottom start-up message." 1233 | ) 1234 | vanillaMCMOptionRegisterSave(showControls) 1235 | 1236 | ModConfigMenu.AddSpace("Mod Config Menu") --SPACE 1237 | 1238 | ----------------- 1239 | --COMPATIBILITY-- 1240 | ----------------- 1241 | 1242 | local compatibilitySetting = ModConfigMenu.AddBooleanSetting( 1243 | "Mod Config Menu", --category 1244 | "CompatibilityLayer", --attribute in table 1245 | false, --default value 1246 | "Disable Legacy Warnings", --display text 1247 | { 1248 | --value display text 1249 | [true] = "Yes", 1250 | [false] = "No" 1251 | }, 1252 | "Use this setting to prevent warnings from being printed to the console for mods that use outdated features of Mod Config Menu." 1253 | ) 1254 | vanillaMCMOptionRegisterSave(compatibilitySetting) 1255 | 1256 | local configMenuSubcategoriesCanShow = 3 1257 | 1258 | local configMenuInSubcategory = false 1259 | local configMenuInOptions = false 1260 | local configMenuInPopup = false 1261 | 1262 | local holdingCounterDown = 0 1263 | local holdingCounterUp = 0 1264 | local holdingCounterRight = 0 1265 | local holdingCounterLeft = 0 1266 | 1267 | local configMenuPositionCursorCategory = 1 1268 | local configMenuPositionCursorSubcategory = 1 1269 | local configMenuPositionCursorOption = 1 1270 | 1271 | local configMenuPositionFirstSubcategory = 1 1272 | 1273 | --valid action presses 1274 | local actionsDown = { ButtonAction.ACTION_DOWN, ButtonAction.ACTION_SHOOTDOWN, ButtonAction.ACTION_MENUDOWN } 1275 | local actionsUp = { ButtonAction.ACTION_UP, ButtonAction.ACTION_SHOOTUP, ButtonAction.ACTION_MENUUP } 1276 | local actionsRight = { ButtonAction.ACTION_RIGHT, ButtonAction.ACTION_SHOOTRIGHT, ButtonAction.ACTION_MENURIGHT } 1277 | local actionsLeft = { ButtonAction.ACTION_LEFT, ButtonAction.ACTION_SHOOTLEFT, ButtonAction.ACTION_MENULEFT } 1278 | local actionsBack = { ButtonAction.ACTION_PILLCARD, ButtonAction.ACTION_MAP, ButtonAction.ACTION_MENUBACK } 1279 | local actionsSelect = { ButtonAction.ACTION_ITEM, ButtonAction.ACTION_PAUSE, ButtonAction.ACTION_MENUCONFIRM, 1280 | ButtonAction.ACTION_BOMB } 1281 | 1282 | --ignore these buttons for the above actions 1283 | local ignoreActionButtons = { Controller.BUTTON_A, Controller.BUTTON_B, Controller.BUTTON_X, Controller.BUTTON_Y } 1284 | 1285 | local currentMenuCategory = nil 1286 | local currentMenuSubcategory = nil 1287 | local currentMenuOption = nil 1288 | local function updateCurrentMenuVars() 1289 | if ModConfigMenu.MenuData[configMenuPositionCursorCategory] then 1290 | currentMenuCategory = ModConfigMenu.MenuData[configMenuPositionCursorCategory] 1291 | if currentMenuCategory.Subcategories and currentMenuCategory.Subcategories[configMenuPositionCursorSubcategory] then 1292 | currentMenuSubcategory = currentMenuCategory.Subcategories[configMenuPositionCursorSubcategory] 1293 | if currentMenuSubcategory.Options and currentMenuSubcategory.Options[configMenuPositionCursorOption] then 1294 | currentMenuOption = currentMenuSubcategory.Options[configMenuPositionCursorOption] 1295 | end 1296 | end 1297 | end 1298 | end 1299 | 1300 | --leaving/entering menu sections 1301 | function ModConfigMenu.EnterPopup() 1302 | if configMenuInSubcategory and configMenuInOptions and not configMenuInPopup then 1303 | local foundValidPopup = false 1304 | if currentMenuOption 1305 | and currentMenuOption.Type 1306 | and currentMenuOption.Type ~= ModConfigMenu.OptionType.SPACE 1307 | and (currentMenuOption.Popup or currentMenuOption.Restart or currentMenuOption.Rerun) then 1308 | foundValidPopup = true 1309 | end 1310 | if foundValidPopup then 1311 | local popupSpritesheet = ModConfigMenu.PopupGfx.THIN_SMALL 1312 | if currentMenuOption.PopupGfx and type(currentMenuOption.PopupGfx) == "string" then 1313 | popupSpritesheet = currentMenuOption.PopupGfx 1314 | end 1315 | PopupSprite:ReplaceSpritesheet(5, popupSpritesheet) 1316 | PopupSprite:LoadGraphics() 1317 | configMenuInPopup = true 1318 | end 1319 | end 1320 | end 1321 | 1322 | function ModConfigMenu.EnterOptions() 1323 | if configMenuInSubcategory and not configMenuInOptions then 1324 | if currentMenuSubcategory 1325 | and currentMenuSubcategory.Options 1326 | and #currentMenuSubcategory.Options > 0 then 1327 | for optionIndex = 1, #currentMenuSubcategory.Options do 1328 | local thisOption = currentMenuSubcategory.Options[optionIndex] 1329 | 1330 | if thisOption.Type 1331 | and thisOption.Type ~= ModConfigMenu.OptionType.SPACE 1332 | and 1333 | ( 1334 | not thisOption.NoCursorHere or 1335 | (type(thisOption.NoCursorHere) == "function" and not thisOption.NoCursorHere())) 1336 | and thisOption.Display then 1337 | configMenuPositionCursorOption = optionIndex 1338 | configMenuInOptions = true 1339 | OptionsCursorSpriteUp.Color = colorDefault 1340 | OptionsCursorSpriteDown.Color = colorDefault 1341 | 1342 | break 1343 | end 1344 | end 1345 | end 1346 | end 1347 | end 1348 | 1349 | function ModConfigMenu.EnterSubcategory() 1350 | if not configMenuInSubcategory then 1351 | configMenuInSubcategory = true 1352 | SubcategoryCursorSpriteLeft.Color = colorDefault 1353 | SubcategoryCursorSpriteRight.Color = colorDefault 1354 | SubcategoryDividerSprite.Color = colorDefault 1355 | 1356 | local hasUsableCategories = false 1357 | if currentMenuCategory.Subcategories then 1358 | for j = 1, #currentMenuCategory.Subcategories do 1359 | if currentMenuCategory.Subcategories[j].Name ~= "Uncategorized" then 1360 | hasUsableCategories = true 1361 | end 1362 | end 1363 | end 1364 | 1365 | if not hasUsableCategories then 1366 | ModConfigMenu.EnterOptions() 1367 | end 1368 | end 1369 | end 1370 | 1371 | function ModConfigMenu.LeavePopup() 1372 | if configMenuInSubcategory and configMenuInOptions and configMenuInPopup then 1373 | if currentMenuOption then 1374 | if currentMenuOption.Restart then 1375 | restartWarnMessage = "One or more settings require you to restart the game" 1376 | elseif currentMenuOption.Rerun then 1377 | rerunWarnMessage = "One or more settings require you to start a new run" 1378 | end 1379 | end 1380 | 1381 | configMenuInPopup = false 1382 | end 1383 | end 1384 | 1385 | function ModConfigMenu.LeaveOptions() 1386 | if configMenuInSubcategory and configMenuInOptions then 1387 | configMenuInOptions = false 1388 | OptionsCursorSpriteUp.Color = colorHalf 1389 | OptionsCursorSpriteDown.Color = colorHalf 1390 | 1391 | local hasUsableCategories = false 1392 | if currentMenuCategory.Subcategories then 1393 | for j = 1, #currentMenuCategory.Subcategories do 1394 | if currentMenuCategory.Subcategories[j].Name ~= "Uncategorized" then 1395 | hasUsableCategories = true 1396 | end 1397 | end 1398 | end 1399 | 1400 | if not hasUsableCategories then 1401 | ModConfigMenu.LeaveSubcategory() 1402 | end 1403 | end 1404 | end 1405 | 1406 | function ModConfigMenu.LeaveSubcategory() 1407 | if configMenuInSubcategory then 1408 | configMenuInSubcategory = false 1409 | SubcategoryCursorSpriteLeft.Color = colorHalf 1410 | SubcategoryCursorSpriteRight.Color = colorHalf 1411 | SubcategoryDividerSprite.Color = colorHalf 1412 | end 1413 | end 1414 | 1415 | local mainSpriteColor = colorDefault 1416 | local optionsSpriteColor = colorDefault 1417 | local optionsSpriteColorAlpha = colorHalf 1418 | local mainFontColor = KColor(34 / 255, 32 / 255, 30 / 255, 1) 1419 | local leftFontColor = KColor(35 / 255, 31 / 255, 30 / 255, 1) 1420 | local leftFontColorSelected = KColor(35 / 255, 50 / 255, 70 / 255, 1) 1421 | 1422 | local optionsFontColor = KColor(34 / 255, 32 / 255, 30 / 255, 1) 1423 | local optionsFontColorAlpha = KColor(34 / 255, 32 / 255, 30 / 255, 0.5) 1424 | local optionsFontColorNoCursor = KColor(34 / 255, 32 / 255, 30 / 255, 0.8) 1425 | local optionsFontColorNoCursorAlpha = KColor(34 / 255, 32 / 255, 30 / 255, 0.4) 1426 | local optionsFontColorTitle = KColor(50 / 255, 0, 0, 1) 1427 | local optionsFontColorTitleAlpha = KColor(50 / 255, 0, 0, 0.5) 1428 | 1429 | local subcategoryFontColor = KColor(34 / 255, 32 / 255, 30 / 255, 1) 1430 | local subcategoryFontColorSelected = KColor(34 / 255, 50 / 255, 70 / 255, 1) 1431 | local subcategoryFontColorAlpha = KColor(34 / 255, 32 / 255, 30 / 255, 0.5) 1432 | local subcategoryFontColorSelectedAlpha = KColor(34 / 255, 50 / 255, 70 / 255, 0.5) 1433 | 1434 | function ModConfigMenu.ConvertDisplayToTextTable(displayValue, lineWidth, font) 1435 | lineWidth = lineWidth or 340 1436 | 1437 | local textTableDisplay = {} 1438 | if type(displayValue) == "function" then 1439 | displayValue = displayValue() 1440 | end 1441 | 1442 | if type(displayValue) == "string" then 1443 | textTableDisplay = { displayValue } 1444 | elseif type(displayValue) == "table" then 1445 | textTableDisplay = SaveHelper.CopyTable(displayValue) 1446 | else 1447 | textTableDisplay = { tostring(displayValue) } 1448 | end 1449 | 1450 | if type(textTableDisplay) == "string" then 1451 | textTableDisplay = { textTableDisplay } 1452 | end 1453 | 1454 | --create new lines based on $newline modifier 1455 | local textTableDisplayAfterNewlines = {} 1456 | for lineIndex = 1, #textTableDisplay do 1457 | local line = textTableDisplay[lineIndex] 1458 | local startIdx, endIdx = string.find(line, "$newline") 1459 | while startIdx do 1460 | local newline = string.sub(line, 0, startIdx - 1) 1461 | table.insert(textTableDisplayAfterNewlines, newline) 1462 | 1463 | line = string.sub(line, endIdx + 1) 1464 | 1465 | startIdx, endIdx = string.find(line, "$newline") 1466 | end 1467 | table.insert(textTableDisplayAfterNewlines, line) 1468 | end 1469 | 1470 | --dynamic string new line creation, based on code by Wofsauge 1471 | local textTableDisplayAfterWordLength = {} 1472 | for lineIndex = 1, #textTableDisplayAfterNewlines do 1473 | local line = textTableDisplayAfterNewlines[lineIndex] 1474 | local curLength = 0 1475 | local text = "" 1476 | for word in string.gmatch(tostring(line), "([^%s]+)") do 1477 | local wordLength = font:GetStringWidthUTF8(word) 1478 | 1479 | if curLength + wordLength <= lineWidth or curLength < 12 then 1480 | text = text .. word .. " " 1481 | curLength = curLength + wordLength 1482 | else 1483 | table.insert(textTableDisplayAfterWordLength, text) 1484 | text = word .. " " 1485 | curLength = wordLength 1486 | end 1487 | end 1488 | table.insert(textTableDisplayAfterWordLength, text) 1489 | end 1490 | 1491 | return textTableDisplayAfterWordLength 1492 | end 1493 | 1494 | --set up screen corner display for hud offset 1495 | local HudOffsetVisualTopLeft = ModConfigMenu.GetMenuAnm2Sprite("Offset", 0) 1496 | local HudOffsetVisualTopRight = ModConfigMenu.GetMenuAnm2Sprite("Offset", 1) 1497 | local HudOffsetVisualBottomRight = ModConfigMenu.GetMenuAnm2Sprite("Offset", 2) 1498 | local HudOffsetVisualBottomLeft = ModConfigMenu.GetMenuAnm2Sprite("Offset", 3) 1499 | 1500 | --render the menu 1501 | local leftCurrentOffset = 0 1502 | local optionsCurrentOffset = 0 1503 | ModConfigMenu.ControlsEnabled = true 1504 | function ModConfigMenu.PostRender() 1505 | local game = Game() 1506 | local isPaused = game:IsPaused() or AwaitingTextInput 1507 | 1508 | local sfx = SFXManager() 1509 | 1510 | local pressingButton = "" 1511 | 1512 | local pressingNonRebindableKey = false 1513 | local pressedToggleMenu = false 1514 | 1515 | local openMenuGlobal = Keyboard.KEY_F10 1516 | local openMenuKeyboard = ModConfigMenu.Config["Mod Config Menu"].OpenMenuKeyboard 1517 | local openMenuController = ModConfigMenu.Config["Mod Config Menu"].OpenMenuController 1518 | 1519 | local takeScreenshot = Keyboard.KEY_F12 1520 | 1521 | --handle version display on game start 1522 | if versionPrintTimer > 0 then 1523 | local bottomRight = ScreenHelper.GetScreenBottomRight(0) 1524 | 1525 | local openMenuButton = Keyboard.KEY_F10 1526 | if type(ModConfigMenu.Config["Mod Config Menu"].OpenMenuKeyboard) == "number" and 1527 | ModConfigMenu.Config["Mod Config Menu"].OpenMenuKeyboard > -1 then 1528 | openMenuButton = ModConfigMenu.Config["Mod Config Menu"].OpenMenuKeyboard 1529 | end 1530 | 1531 | local openMenuButtonString = "Unknown Key" 1532 | if InputHelper.KeyboardToString[openMenuButton] then 1533 | openMenuButtonString = InputHelper.KeyboardToString[openMenuButton] 1534 | end 1535 | 1536 | local text = "Press " .. openMenuButtonString .. " to open Mod Config Menu" 1537 | local versionPrintColor = KColor(1, 1, 0, (math.min(versionPrintTimer, 60) / 60) * 0.5) 1538 | versionPrintFont:DrawString(text, 0, bottomRight.Y - 28, versionPrintColor, math.floor(bottomRight.X), true) 1539 | end 1540 | 1541 | --on-screen warnings 1542 | if restartWarnMessage or rerunWarnMessage then 1543 | local bottomRight = ScreenHelper.GetScreenBottomRight(0) 1544 | 1545 | local text = restartWarnMessage or rerunWarnMessage 1546 | local warningPrintColor = KColor(1, 0, 0, 1) 1547 | versionPrintFont:DrawString(text, 0, bottomRight.Y - 28, warningPrintColor, math.floor(bottomRight.X), true) 1548 | end 1549 | 1550 | --handle toggling the menu 1551 | if ModConfigMenu.ControlsEnabled and not isPaused then 1552 | for i = 0, 4 do 1553 | if InputHelper.KeyboardTriggered(openMenuGlobal, i) 1554 | or (openMenuKeyboard > -1 and InputHelper.KeyboardTriggered(openMenuKeyboard, i)) 1555 | or (openMenuController > -1 and Input.IsButtonTriggered(openMenuController, i)) then 1556 | pressingNonRebindableKey = true 1557 | pressedToggleMenu = true 1558 | if not configMenuInPopup then 1559 | ModConfigMenu.ToggleConfigMenu() 1560 | end 1561 | end 1562 | 1563 | if InputHelper.KeyboardTriggered(takeScreenshot, i) then 1564 | pressingNonRebindableKey = true 1565 | end 1566 | end 1567 | end 1568 | 1569 | --force close the menu in some situations 1570 | if ModConfigMenu.IsVisible then 1571 | if isPaused then 1572 | ModConfigMenu.CloseConfigMenu() 1573 | end 1574 | 1575 | if not ModConfigMenu.RoomIsSafe() then 1576 | ModConfigMenu.CloseConfigMenu() 1577 | 1578 | sfx:Play(SoundEffect.SOUND_BOSS2INTRO_ERRORBUZZ, 0.75, 0, false, 1) 1579 | end 1580 | end 1581 | 1582 | --replace dead sea scrolls' controller setting to not conflict with mcm's 1583 | if DeadSeaScrollsMenu and DeadSeaScrollsMenu.GetGamepadToggleSetting then 1584 | local dssControllerToggle = DeadSeaScrollsMenu.GetGamepadToggleSetting() 1585 | 1586 | if DeadSeaScrollsMenu.SaveGamepadToggleSetting then 1587 | if openMenuController == Controller.STICK_RIGHT and 1588 | (dssControllerToggle == 1 or dssControllerToggle == 3 or dssControllerToggle == 4) then 1589 | DeadSeaScrollsMenu.SaveGamepadToggleSetting(2) --force revelations' menu to only use the left stick 1590 | elseif openMenuController == Controller.STICK_LEFT and 1591 | (dssControllerToggle == 1 or dssControllerToggle == 2 or dssControllerToggle == 4) then 1592 | DeadSeaScrollsMenu.SaveGamepadToggleSetting(3) --force revelations' menu to only use the right stick 1593 | end 1594 | end 1595 | end 1596 | 1597 | if ModConfigMenu.IsVisible then 1598 | if ModConfigMenu.ControlsEnabled and not isPaused then 1599 | for i = 0, game:GetNumPlayers() - 1 do 1600 | local player = Isaac.GetPlayer(i) 1601 | local data = player:GetData() 1602 | 1603 | --freeze players and disable their controls 1604 | player.Velocity = vecZero 1605 | 1606 | if not data.ConfigMenuPlayerPosition then 1607 | data.ConfigMenuPlayerPosition = player.Position 1608 | end 1609 | player.Position = data.ConfigMenuPlayerPosition 1610 | if not data.ConfigMenuPlayerControlsDisabled then 1611 | player.ControlsEnabled = false 1612 | data.ConfigMenuPlayerControlsDisabled = true 1613 | end 1614 | 1615 | --disable toggling revelations menu 1616 | if data.input and data.input.menu and data.input.menu.toggle then 1617 | data.input.menu.toggle = false 1618 | end 1619 | end 1620 | 1621 | if not InputHelper.MultipleButtonPressed(ignoreActionButtons) then 1622 | --pressing buttons 1623 | local downButtonPressed = InputHelper.MultipleActionTriggered(actionsDown) 1624 | if downButtonPressed then 1625 | pressingButton = "DOWN" 1626 | end 1627 | local upButtonPressed = InputHelper.MultipleActionTriggered(actionsUp) 1628 | if upButtonPressed then 1629 | pressingButton = "UP" 1630 | end 1631 | local rightButtonPressed = InputHelper.MultipleActionTriggered(actionsRight) 1632 | if rightButtonPressed then 1633 | pressingButton = "RIGHT" 1634 | end 1635 | local leftButtonPressed = InputHelper.MultipleActionTriggered(actionsLeft) 1636 | if leftButtonPressed then 1637 | pressingButton = "LEFT" 1638 | end 1639 | local backButtonPressed = InputHelper.MultipleActionTriggered(actionsBack) or 1640 | InputHelper.MultipleKeyboardTriggered({ Keyboard.KEY_BACKSPACE }) 1641 | if backButtonPressed then 1642 | pressingButton = "BACK" 1643 | local possiblyPressedButton = InputHelper.MultipleKeyboardTriggered(Keyboard) 1644 | if possiblyPressedButton then 1645 | ModConfigMenu.Config.LastBackPressed = possiblyPressedButton 1646 | end 1647 | end 1648 | local selectButtonPressed = InputHelper.MultipleActionTriggered(actionsSelect) 1649 | if selectButtonPressed then 1650 | pressingButton = "SELECT" 1651 | local possiblyPressedButton = InputHelper.MultipleKeyboardTriggered(Keyboard) 1652 | if possiblyPressedButton then 1653 | ModConfigMenu.Config.LastSelectPressed = possiblyPressedButton 1654 | end 1655 | end 1656 | if ModConfigMenu.Config["Mod Config Menu"].ResetToDefault > -1 and 1657 | InputHelper.MultipleKeyboardTriggered({ ModConfigMenu.Config["Mod Config Menu"].ResetToDefault }) then 1658 | pressingButton = "RESET" 1659 | end 1660 | 1661 | --holding buttons 1662 | if InputHelper.MultipleActionPressed(actionsDown) then 1663 | holdingCounterDown = holdingCounterDown + 1 1664 | else 1665 | holdingCounterDown = 0 1666 | end 1667 | if holdingCounterDown > 20 and holdingCounterDown % 5 == 0 then 1668 | pressingButton = "DOWN" 1669 | end 1670 | if InputHelper.MultipleActionPressed(actionsUp) then 1671 | holdingCounterUp = holdingCounterUp + 1 1672 | else 1673 | holdingCounterUp = 0 1674 | end 1675 | if holdingCounterUp > 20 and holdingCounterUp % 5 == 0 then 1676 | pressingButton = "UP" 1677 | end 1678 | if InputHelper.MultipleActionPressed(actionsRight) then 1679 | holdingCounterRight = holdingCounterRight + 1 1680 | else 1681 | holdingCounterRight = 0 1682 | end 1683 | if holdingCounterRight > 20 and holdingCounterRight % 5 == 0 then 1684 | pressingButton = "RIGHT" 1685 | end 1686 | if InputHelper.MultipleActionPressed(actionsLeft) then 1687 | holdingCounterLeft = holdingCounterLeft + 1 1688 | else 1689 | holdingCounterLeft = 0 1690 | end 1691 | if holdingCounterLeft > 20 and holdingCounterLeft % 5 == 0 then 1692 | pressingButton = "LEFT" 1693 | end 1694 | else 1695 | if InputHelper.MultipleButtonTriggered({ Controller.BUTTON_B }) then 1696 | pressingButton = "BACK" 1697 | pressingNonRebindableKey = true 1698 | end 1699 | if InputHelper.MultipleButtonTriggered({ Controller.BUTTON_A }) then 1700 | pressingButton = "SELECT" 1701 | pressingNonRebindableKey = true 1702 | end 1703 | end 1704 | 1705 | if pressingButton ~= "" then 1706 | pressingNonRebindableKey = true 1707 | end 1708 | end 1709 | 1710 | updateCurrentMenuVars() 1711 | 1712 | local lastCursorCategoryPosition = configMenuPositionCursorCategory 1713 | local lastCursorSubcategoryPosition = configMenuPositionCursorSubcategory 1714 | local lastCursorOptionsPosition = configMenuPositionCursorOption 1715 | 1716 | local enterPopup = false 1717 | local leavePopup = false 1718 | 1719 | local optionChanged = false 1720 | 1721 | local enterOptions = false 1722 | local leaveOptions = false 1723 | 1724 | local enterSubcategory = false 1725 | local leaveSubcategory = false 1726 | 1727 | if configMenuInPopup then 1728 | if currentMenuOption then 1729 | local optionType = currentMenuOption.Type 1730 | local optionCurrent = currentMenuOption.CurrentSetting 1731 | local optionOnChange = currentMenuOption.OnChange 1732 | 1733 | if optionType == ModConfigMenu.OptionType.KEYBIND_KEYBOARD 1734 | or optionType == ModConfigMenu.OptionType.KEYBIND_CONTROLLER 1735 | or currentMenuOption.OnSelect then 1736 | if not isPaused then 1737 | if pressingNonRebindableKey 1738 | and not (pressingButton == "BACK" 1739 | or pressingButton == "LEFT" 1740 | or (currentMenuOption.OnSelect and (pressingButton == "SELECT" or pressingButton == "RIGHT")) 1741 | or (currentMenuOption.IsResetKeybind and pressingButton == "RESET") 1742 | or (currentMenuOption.IsOpenMenuKeybind and pressedToggleMenu)) then 1743 | sfx:Play(SoundEffect.SOUND_BOSS2INTRO_ERRORBUZZ, 0.75, 0, false, 1) 1744 | else 1745 | local numberToChange = nil 1746 | local receivedInput = false 1747 | if optionType == ModConfigMenu.OptionType.KEYBIND_KEYBOARD or 1748 | optionType == ModConfigMenu.OptionType.KEYBIND_CONTROLLER then 1749 | numberToChange = optionCurrent 1750 | 1751 | if type(optionCurrent) == "function" then 1752 | numberToChange = optionCurrent() 1753 | end 1754 | 1755 | if pressingButton == "BACK" or pressingButton == "LEFT" then 1756 | numberToChange = nil 1757 | receivedInput = true 1758 | else 1759 | for i = 0, 4 do 1760 | if optionType == ModConfigMenu.OptionType.KEYBIND_KEYBOARD then 1761 | for j = 32, 400 do 1762 | if InputHelper.KeyboardTriggered(j, i) then 1763 | numberToChange = j 1764 | receivedInput = true 1765 | break 1766 | end 1767 | end 1768 | else 1769 | for j = 0, 31 do 1770 | if Input.IsButtonTriggered(j, i) then 1771 | numberToChange = j 1772 | receivedInput = true 1773 | break 1774 | end 1775 | end 1776 | end 1777 | end 1778 | end 1779 | elseif currentMenuOption.OnSelect then 1780 | if pressingButton == "BACK" or pressingButton == "LEFT" then 1781 | receivedInput = true 1782 | end 1783 | if pressingButton == "SELECT" or pressingButton == "RIGHT" then 1784 | numberToChange = true 1785 | receivedInput = true 1786 | end 1787 | end 1788 | 1789 | if receivedInput then 1790 | if optionType == ModConfigMenu.OptionType.KEYBIND_KEYBOARD or 1791 | optionType == ModConfigMenu.OptionType.KEYBIND_CONTROLLER then 1792 | if type(optionCurrent) == "function" then 1793 | if optionOnChange then 1794 | optionOnChange(numberToChange) 1795 | end 1796 | elseif type(optionCurrent) == "number" then 1797 | currentMenuOption.CurrentSetting = numberToChange 1798 | end 1799 | 1800 | --callback 1801 | --[[ 1802 | CustomCallbackHelper.CallCallbacks 1803 | ( 1804 | CustomCallbacks.MCM_POST_MODIFY_SETTING, --callback id 1805 | nil, 1806 | {currentMenuOption.CurrentSetting, numberToChange}, --args to send 1807 | {currentMenuCategory.Name, currentMenuOption.Attribute} --extra variables 1808 | ) 1809 | --]] 1810 | elseif currentMenuOption.OnSelect and numberToChange then 1811 | currentMenuOption.OnSelect() 1812 | end 1813 | 1814 | leavePopup = true 1815 | 1816 | local sound = currentMenuOption.Sound 1817 | if not sound then 1818 | sound = SoundEffect.SOUND_PLOP 1819 | end 1820 | if sound >= 0 then 1821 | sfx:Play(sound, 1, 0, false, 1) 1822 | end 1823 | end 1824 | end 1825 | end 1826 | end 1827 | end 1828 | 1829 | if currentMenuOption.Restart or currentMenuOption.Rerun then 1830 | --confirmed left press 1831 | if pressingButton == "RIGHT" then 1832 | leavePopup = true 1833 | end 1834 | 1835 | --confirmed back press 1836 | if pressingButton == "SELECT" then 1837 | leavePopup = true 1838 | end 1839 | end 1840 | 1841 | --confirmed left press 1842 | if pressingButton == "LEFT" then 1843 | leavePopup = true 1844 | end 1845 | 1846 | --confirmed back press 1847 | if pressingButton == "BACK" then 1848 | leavePopup = true 1849 | end 1850 | elseif configMenuInOptions then 1851 | --confirmed down press 1852 | if pressingButton == "DOWN" then 1853 | configMenuPositionCursorOption = configMenuPositionCursorOption + 1 --move options cursor down 1854 | end 1855 | 1856 | --confirmed up press 1857 | if pressingButton == "UP" then 1858 | configMenuPositionCursorOption = configMenuPositionCursorOption - 1 --move options cursor up 1859 | end 1860 | 1861 | if pressingButton == "SELECT" or pressingButton == "RIGHT" or pressingButton == "LEFT" or 1862 | (pressingButton == "RESET" and currentMenuOption and currentMenuOption.Default ~= nil) then 1863 | if pressingButton == "LEFT" then 1864 | leaveOptions = true 1865 | end 1866 | 1867 | if currentMenuOption then 1868 | local optionType = currentMenuOption.Type 1869 | local optionCurrent = currentMenuOption.CurrentSetting 1870 | local optionOnChange = currentMenuOption.OnChange 1871 | 1872 | if optionType == ModConfigMenu.OptionType.SCROLL or optionType == ModConfigMenu.OptionType.NUMBER then 1873 | leaveOptions = false 1874 | 1875 | local numberToChange = optionCurrent 1876 | 1877 | if type(optionCurrent) == "function" then 1878 | numberToChange = optionCurrent() 1879 | end 1880 | 1881 | local modifyBy = currentMenuOption.ModifyBy or 1 1882 | modifyBy = math.max(modifyBy, 0.001) 1883 | if math.floor(modifyBy) == modifyBy then --force modify by into being an integer instead of a float if it should be 1884 | modifyBy = math.floor(modifyBy) 1885 | end 1886 | 1887 | if pressingButton == "RIGHT" or pressingButton == "SELECT" then 1888 | numberToChange = numberToChange + modifyBy 1889 | elseif pressingButton == "LEFT" then 1890 | numberToChange = numberToChange - modifyBy 1891 | elseif pressingButton == "RESET" and currentMenuOption.Default ~= nil then 1892 | numberToChange = currentMenuOption.Default 1893 | if type(currentMenuOption.Default) == "function" then 1894 | numberToChange = currentMenuOption.Default() 1895 | end 1896 | end 1897 | 1898 | if optionType == ModConfigMenu.OptionType.SCROLL then 1899 | numberToChange = math.max(math.min(math.floor(numberToChange), 10), 0) 1900 | else 1901 | if currentMenuOption.Maximum and numberToChange > currentMenuOption.Maximum then 1902 | if not currentMenuOption.NoLoopFromMaxMin and currentMenuOption.Minimum then 1903 | numberToChange = currentMenuOption.Minimum 1904 | else 1905 | numberToChange = currentMenuOption.Maximum 1906 | end 1907 | end 1908 | if currentMenuOption.Minimum and numberToChange < currentMenuOption.Minimum then 1909 | if not currentMenuOption.NoLoopFromMaxMin and currentMenuOption.Maximum then 1910 | numberToChange = currentMenuOption.Maximum 1911 | else 1912 | numberToChange = currentMenuOption.Minimum 1913 | end 1914 | end 1915 | end 1916 | 1917 | if math.floor(modifyBy) ~= modifyBy then --check if modify by is a float 1918 | numberToChange = math.floor((numberToChange * 1000) + 0.5) * 0.001 1919 | else 1920 | numberToChange = math.floor(numberToChange) 1921 | end 1922 | 1923 | if type(optionCurrent) == "function" then 1924 | if optionOnChange then 1925 | optionOnChange(numberToChange) 1926 | end 1927 | optionChanged = true 1928 | elseif type(optionCurrent) == "number" then 1929 | currentMenuOption.CurrentSetting = numberToChange 1930 | optionChanged = true 1931 | end 1932 | 1933 | --callback 1934 | --[[ 1935 | CustomCallbackHelper.CallCallbacks 1936 | ( 1937 | CustomCallbacks.MCM_POST_MODIFY_SETTING, --callback id 1938 | nil, 1939 | {currentMenuOption.CurrentSetting, numberToChange}, --args to send 1940 | {currentMenuCategory.Name, currentMenuOption.Attribute} --extra variables 1941 | ) 1942 | --]] 1943 | local sound = currentMenuOption.Sound 1944 | if not sound then 1945 | sound = SoundEffect.SOUND_PLOP 1946 | end 1947 | if sound >= 0 then 1948 | sfx:Play(sound, 1, 0, false, 1) 1949 | end 1950 | elseif optionType == ModConfigMenu.OptionType.BOOLEAN then 1951 | leaveOptions = false 1952 | 1953 | local boolToChange = optionCurrent 1954 | 1955 | if type(optionCurrent) == "function" then 1956 | boolToChange = optionCurrent() 1957 | end 1958 | 1959 | if pressingButton == "RESET" and currentMenuOption.Default ~= nil then 1960 | boolToChange = currentMenuOption.Default 1961 | if type(currentMenuOption.Default) == "function" then 1962 | boolToChange = currentMenuOption.Default() 1963 | end 1964 | else 1965 | boolToChange = (not boolToChange) 1966 | end 1967 | 1968 | if type(optionCurrent) == "function" then 1969 | if optionOnChange then 1970 | optionOnChange(boolToChange) 1971 | end 1972 | optionChanged = true 1973 | elseif type(optionCurrent) == "boolean" then 1974 | currentMenuOption.CurrentSetting = boolToChange 1975 | optionChanged = true 1976 | end 1977 | 1978 | --callback 1979 | --[[ 1980 | CustomCallbackHelper.CallCallbacks 1981 | ( 1982 | CustomCallbacks.MCM_POST_MODIFY_SETTING, --callback id 1983 | nil, 1984 | {currentMenuOption.CurrentSetting, boolToChange}, --args to send 1985 | {currentMenuCategory.Name, currentMenuOption.Attribute} --extra variables 1986 | ) 1987 | --]] 1988 | local sound = currentMenuOption.Sound 1989 | if not sound then 1990 | sound = SoundEffect.SOUND_PLOP 1991 | end 1992 | if sound >= 0 then 1993 | sfx:Play(sound, 1, 0, false, 1) 1994 | end 1995 | elseif ( 1996 | optionType == ModConfigMenu.OptionType.KEYBIND_KEYBOARD or 1997 | optionType == ModConfigMenu.OptionType.KEYBIND_CONTROLLER) and pressingButton == "RESET" and 1998 | currentMenuOption.Default ~= nil then 1999 | local numberToChange = optionCurrent 2000 | 2001 | if type(optionCurrent) == "function" then 2002 | numberToChange = optionCurrent() 2003 | end 2004 | 2005 | numberToChange = currentMenuOption.Default 2006 | if type(currentMenuOption.Default) == "function" then 2007 | numberToChange = currentMenuOption.Default() 2008 | end 2009 | 2010 | if type(optionCurrent) == "function" then 2011 | if optionOnChange then 2012 | optionOnChange(numberToChange) 2013 | end 2014 | optionChanged = true 2015 | elseif type(optionCurrent) == "number" then 2016 | currentMenuOption.CurrentSetting = numberToChange 2017 | optionChanged = true 2018 | end 2019 | 2020 | --callback 2021 | --[[ 2022 | CustomCallbackHelper.CallCallbacks 2023 | ( 2024 | CustomCallbacks.MCM_POST_MODIFY_SETTING, --callback id 2025 | nil, 2026 | {currentMenuOption.CurrentSetting, numberToChange}, --args to send 2027 | {currentMenuCategory.Name, currentMenuOption.Attribute} --extra variables 2028 | ) 2029 | --]] 2030 | local sound = currentMenuOption.Sound 2031 | if not sound then 2032 | sound = SoundEffect.SOUND_PLOP 2033 | end 2034 | if sound >= 0 then 2035 | sfx:Play(sound, 1, 0, false, 1) 2036 | end 2037 | elseif optionType ~= ModConfigMenu.OptionType.SPACE and pressingButton == "RIGHT" then 2038 | if currentMenuOption.Popup then 2039 | enterPopup = true 2040 | elseif currentMenuOption.OnSelect then 2041 | currentMenuOption.OnSelect() 2042 | end 2043 | end 2044 | end 2045 | end 2046 | 2047 | --confirmed back press 2048 | if pressingButton == "BACK" then 2049 | leaveOptions = true 2050 | end 2051 | 2052 | --confirmed select press 2053 | if pressingButton == "SELECT" then 2054 | if currentMenuOption then 2055 | if currentMenuOption.Popup then 2056 | enterPopup = true 2057 | elseif currentMenuOption.OnSelect then 2058 | currentMenuOption.OnSelect() 2059 | end 2060 | end 2061 | end 2062 | 2063 | --reset command 2064 | if optionChanged then 2065 | if currentMenuOption.Restart or currentMenuOption.Rerun then 2066 | enterPopup = true 2067 | end 2068 | end 2069 | elseif configMenuInSubcategory then 2070 | local hasUsableCategories = false 2071 | if currentMenuCategory.Subcategories then 2072 | for j = 1, #currentMenuCategory.Subcategories do 2073 | if currentMenuCategory.Subcategories[j].Name ~= "Uncategorized" then 2074 | hasUsableCategories = true 2075 | end 2076 | end 2077 | end 2078 | if hasUsableCategories then 2079 | --confirmed down press 2080 | if pressingButton == "DOWN" then 2081 | enterOptions = true 2082 | end 2083 | 2084 | --confirmed up press 2085 | if pressingButton == "UP" then 2086 | leaveSubcategory = true 2087 | end 2088 | 2089 | --confirmed right press 2090 | if pressingButton == "RIGHT" then 2091 | configMenuPositionCursorSubcategory = configMenuPositionCursorSubcategory + 1 --move right down 2092 | end 2093 | 2094 | --confirmed left press 2095 | if pressingButton == "LEFT" then 2096 | configMenuPositionCursorSubcategory = configMenuPositionCursorSubcategory - 1 --move cursor left 2097 | end 2098 | 2099 | --confirmed back press 2100 | if pressingButton == "BACK" then 2101 | leaveSubcategory = true 2102 | end 2103 | 2104 | --confirmed select press 2105 | if pressingButton == "SELECT" then 2106 | enterOptions = true 2107 | end 2108 | end 2109 | else 2110 | --confirmed down press 2111 | if pressingButton == "DOWN" then 2112 | configMenuPositionCursorCategory = configMenuPositionCursorCategory + 1 --move left cursor down 2113 | end 2114 | 2115 | --confirmed up press 2116 | if pressingButton == "UP" then 2117 | configMenuPositionCursorCategory = configMenuPositionCursorCategory - 1 --move left cursor up 2118 | end 2119 | 2120 | --confirmed right press 2121 | if pressingButton == "RIGHT" then 2122 | enterSubcategory = true 2123 | end 2124 | 2125 | --confirmed back press 2126 | if pressingButton == "BACK" then 2127 | ModConfigMenu.CloseConfigMenu() 2128 | end 2129 | 2130 | --confirmed select press 2131 | if pressingButton == "SELECT" then 2132 | enterSubcategory = true 2133 | end 2134 | end 2135 | 2136 | --entering popup 2137 | if enterPopup then 2138 | ModConfigMenu.EnterPopup() 2139 | end 2140 | 2141 | --leaving popup 2142 | if leavePopup then 2143 | ModConfigMenu.LeavePopup() 2144 | end 2145 | 2146 | --entering subcategory 2147 | if enterSubcategory then 2148 | ModConfigMenu.EnterSubcategory() 2149 | end 2150 | 2151 | --entering options 2152 | if enterOptions then 2153 | ModConfigMenu.EnterOptions() 2154 | end 2155 | 2156 | --leaving options 2157 | if leaveOptions then 2158 | ModConfigMenu.LeaveOptions() 2159 | end 2160 | 2161 | --leaving subcategory 2162 | if leaveSubcategory then 2163 | ModConfigMenu.LeaveSubcategory() 2164 | end 2165 | 2166 | --category cursor position was changed 2167 | if lastCursorCategoryPosition ~= configMenuPositionCursorCategory then 2168 | if not configMenuInSubcategory then 2169 | --cursor position 2170 | if configMenuPositionCursorCategory < 1 then --move from the top of the list to the bottom 2171 | configMenuPositionCursorCategory = #ModConfigMenu.MenuData 2172 | end 2173 | if configMenuPositionCursorCategory > #ModConfigMenu.MenuData then --move from the bottom of the list to the top 2174 | configMenuPositionCursorCategory = 1 2175 | end 2176 | 2177 | --make sure subcategory and option positions are 1 2178 | configMenuPositionCursorSubcategory = 1 2179 | configMenuPositionFirstSubcategory = 1 2180 | configMenuPositionCursorOption = 1 2181 | optionsCurrentOffset = 0 2182 | end 2183 | end 2184 | 2185 | --subcategory cursor position was changed 2186 | if lastCursorSubcategoryPosition ~= configMenuPositionCursorSubcategory then 2187 | if not configMenuInOptions then 2188 | --cursor position 2189 | if configMenuPositionCursorSubcategory < 1 then --move from the top of the list to the bottom 2190 | configMenuPositionCursorSubcategory = #currentMenuCategory.Subcategories 2191 | end 2192 | if configMenuPositionCursorSubcategory > #currentMenuCategory.Subcategories then --move from the bottom of the list to the top 2193 | configMenuPositionCursorSubcategory = 1 2194 | end 2195 | 2196 | --first category selection to render 2197 | if configMenuPositionFirstSubcategory > 1 and 2198 | configMenuPositionCursorSubcategory <= configMenuPositionFirstSubcategory + 1 then 2199 | configMenuPositionFirstSubcategory = configMenuPositionCursorSubcategory - 1 2200 | end 2201 | if configMenuPositionFirstSubcategory + (configMenuSubcategoriesCanShow - 1) < # 2202 | currentMenuCategory.Subcategories and 2203 | configMenuPositionCursorSubcategory >= 1 + (configMenuSubcategoriesCanShow - 2) then 2204 | configMenuPositionFirstSubcategory = configMenuPositionCursorSubcategory - 2205 | (configMenuSubcategoriesCanShow - 2 2206 | ) 2207 | end 2208 | configMenuPositionFirstSubcategory = math.min(math.max(configMenuPositionFirstSubcategory, 1), 2209 | #currentMenuCategory.Subcategories - (configMenuSubcategoriesCanShow - 1)) 2210 | 2211 | --make sure option positions are 1 2212 | configMenuPositionCursorOption = 1 2213 | optionsCurrentOffset = 0 2214 | end 2215 | end 2216 | 2217 | --options cursor position was changed 2218 | if lastCursorOptionsPosition ~= configMenuPositionCursorOption then 2219 | if configMenuInOptions 2220 | and currentMenuSubcategory 2221 | and currentMenuSubcategory.Options 2222 | and #currentMenuSubcategory.Options > 0 then 2223 | --find next valid option that isn't a space 2224 | local nextValidOptionSelection = configMenuPositionCursorOption 2225 | local optionIndex = configMenuPositionCursorOption 2226 | for i = 1, #currentMenuSubcategory.Options * 2 do 2227 | local thisOption = currentMenuSubcategory.Options[optionIndex] 2228 | 2229 | if thisOption 2230 | and thisOption.Type 2231 | and thisOption.Type ~= ModConfigMenu.OptionType.SPACE 2232 | and 2233 | ( 2234 | not thisOption.NoCursorHere or 2235 | (type(thisOption.NoCursorHere) == "function" and not thisOption.NoCursorHere())) 2236 | and thisOption.Display then 2237 | nextValidOptionSelection = optionIndex 2238 | 2239 | break 2240 | end 2241 | 2242 | if configMenuPositionCursorOption > lastCursorOptionsPosition then 2243 | optionIndex = optionIndex + 1 2244 | elseif configMenuPositionCursorOption < lastCursorOptionsPosition then 2245 | optionIndex = optionIndex - 1 2246 | end 2247 | if optionIndex < 1 then 2248 | optionIndex = #currentMenuSubcategory.Options 2249 | end 2250 | if optionIndex > #currentMenuSubcategory.Options then 2251 | optionIndex = 1 2252 | end 2253 | end 2254 | 2255 | configMenuPositionCursorOption = nextValidOptionSelection 2256 | 2257 | updateCurrentMenuVars() 2258 | 2259 | --first options selection to render 2260 | local hasSubcategories = false 2261 | for j = 1, #currentMenuCategory.Subcategories do 2262 | if currentMenuCategory.Subcategories[j].Name ~= "Uncategorized" then 2263 | hasSubcategories = true 2264 | end 2265 | end 2266 | if hasSubcategories then 2267 | --todo 2268 | end 2269 | end 2270 | end 2271 | 2272 | local centerPos = ScreenHelper.GetScreenCenter() 2273 | 2274 | --title pos handling 2275 | local titlePos = centerPos + Vector(68, -118) 2276 | 2277 | --left pos handling 2278 | 2279 | local leftDesiredOffset = 0 2280 | local leftCanScrollUp = false 2281 | local leftCanScrollDown = false 2282 | 2283 | local numLeft = #ModConfigMenu.MenuData 2284 | 2285 | local leftPos = centerPos + Vector( -142, -102) 2286 | local leftPosTopmost = centerPos.Y - 116 2287 | local leftPosBottommost = centerPos.Y + 90 2288 | 2289 | if numLeft > 7 then 2290 | if configMenuPositionCursorCategory > 6 then 2291 | leftCanScrollUp = true 2292 | 2293 | local cursorScroll = configMenuPositionCursorCategory - 6 2294 | local maxLeftScroll = numLeft - 8 2295 | leftDesiredOffset = math.min(cursorScroll, maxLeftScroll) * -14 2296 | 2297 | if cursorScroll < maxLeftScroll then 2298 | leftCanScrollDown = true 2299 | end 2300 | else 2301 | leftCanScrollDown = true 2302 | end 2303 | end 2304 | 2305 | if leftDesiredOffset ~= leftCurrentOffset then 2306 | local modifyOffset = math.floor(leftDesiredOffset - leftCurrentOffset) / 10 2307 | if modifyOffset > -0.1 and modifyOffset < 0 then 2308 | modifyOffset = -0.1 2309 | end 2310 | if modifyOffset < 0.1 and modifyOffset > 0 then 2311 | modifyOffset = 0.1 2312 | end 2313 | 2314 | leftCurrentOffset = leftCurrentOffset + modifyOffset 2315 | if (leftDesiredOffset - leftCurrentOffset) < 0.25 and (leftDesiredOffset - leftCurrentOffset) > -0.25 then 2316 | leftCurrentOffset = leftDesiredOffset 2317 | end 2318 | end 2319 | 2320 | if leftCurrentOffset ~= 0 then 2321 | leftPos = leftPos + Vector(0, leftCurrentOffset) 2322 | end 2323 | 2324 | --options pos handling 2325 | local optionsDesiredOffset = 0 2326 | local optionsCanScrollUp = false 2327 | local optionsCanScrollDown = false 2328 | 2329 | local numOptions = 0 2330 | 2331 | local optionPos = centerPos + Vector(68, -18) 2332 | local optionPosTopmost = centerPos.Y - 108 2333 | local optionPosBottommost = centerPos.Y + 86 2334 | 2335 | if currentMenuSubcategory 2336 | and currentMenuSubcategory.Options 2337 | and #currentMenuSubcategory.Options > 0 then 2338 | numOptions = #currentMenuSubcategory.Options 2339 | 2340 | local hasSubcategories = false 2341 | if currentMenuCategory.Subcategories then 2342 | for j = 1, #currentMenuCategory.Subcategories do 2343 | if currentMenuCategory.Subcategories[j].Name ~= "Uncategorized" then 2344 | numOptions = numOptions + 2 2345 | hasSubcategories = true 2346 | break 2347 | end 2348 | end 2349 | end 2350 | 2351 | if hasSubcategories then 2352 | optionPos = optionPos + Vector(0, -70) 2353 | else 2354 | optionPos = optionPos + Vector(0, math.min(numOptions - 1, 10) * -7) 2355 | end 2356 | 2357 | if numOptions > 12 then 2358 | if configMenuPositionCursorOption > 6 and configMenuInOptions then 2359 | optionsCanScrollUp = true 2360 | 2361 | local cursorScroll = configMenuPositionCursorOption - 6 2362 | local maxOptionsScroll = numOptions - 12 2363 | optionsDesiredOffset = math.min(cursorScroll, maxOptionsScroll) * -14 2364 | 2365 | if cursorScroll < maxOptionsScroll then 2366 | optionsCanScrollDown = true 2367 | end 2368 | else 2369 | optionsCanScrollDown = true 2370 | end 2371 | end 2372 | end 2373 | 2374 | if optionsDesiredOffset ~= optionsCurrentOffset then 2375 | local modifyOffset = math.floor(optionsDesiredOffset - optionsCurrentOffset) / 10 2376 | if modifyOffset > -0.1 and modifyOffset < 0 then 2377 | modifyOffset = -0.1 2378 | end 2379 | if modifyOffset < 0.1 and modifyOffset > 0 then 2380 | modifyOffset = 0.1 2381 | end 2382 | 2383 | optionsCurrentOffset = optionsCurrentOffset + modifyOffset 2384 | if (optionsDesiredOffset - optionsCurrentOffset) < 0.25 and (optionsDesiredOffset - optionsCurrentOffset) > -0.25 then 2385 | optionsCurrentOffset = optionsDesiredOffset 2386 | end 2387 | end 2388 | 2389 | if optionsCurrentOffset ~= 0 then 2390 | optionPos = optionPos + Vector(0, optionsCurrentOffset) 2391 | end 2392 | 2393 | --info pos handling 2394 | local infoPos = centerPos + Vector( -4, 106) 2395 | 2396 | MenuSprite:Render(centerPos, vecZero, vecZero) 2397 | 2398 | --get if controls can be shown 2399 | local shouldShowControls = true 2400 | if configMenuInOptions and currentMenuOption and currentMenuOption.HideControls then 2401 | shouldShowControls = false 2402 | end 2403 | if not ModConfigMenu.Config["Mod Config Menu"].ShowControls then 2404 | shouldShowControls = false 2405 | end 2406 | 2407 | --category 2408 | local lastLeftPos = leftPos 2409 | local renderedLeft = 0 2410 | for categoryIndex = 1, #ModConfigMenu.MenuData do 2411 | --text 2412 | if lastLeftPos.Y > leftPosTopmost and lastLeftPos.Y < leftPosBottommost then 2413 | local textToDraw = tostring(ModConfigMenu.MenuData[categoryIndex].Name) 2414 | 2415 | local color = leftFontColor 2416 | --[[ 2417 | if configMenuPositionCursorCategory == categoryIndex then 2418 | color = leftFontColorSelected 2419 | end 2420 | ]] 2421 | local posOffset = Font12:GetStringWidthUTF8(textToDraw) / 2 2422 | Font12:DrawString(textToDraw, lastLeftPos.X - posOffset, lastLeftPos.Y - 8, color, 0, true) 2423 | 2424 | --cursor 2425 | if configMenuPositionCursorCategory == categoryIndex then 2426 | CursorSpriteRight:Render(lastLeftPos + Vector((posOffset + 10) * -1, 0), vecZero, vecZero) 2427 | end 2428 | end 2429 | 2430 | --increase counter 2431 | renderedLeft = renderedLeft + 1 2432 | 2433 | --pos mod 2434 | lastLeftPos = lastLeftPos + Vector(0, 16) 2435 | end 2436 | 2437 | --render scroll arrows 2438 | if leftCanScrollUp then 2439 | CursorSpriteUp:Render(centerPos + Vector( -78, -104), vecZero, vecZero) --up arrow 2440 | end 2441 | if leftCanScrollDown then 2442 | CursorSpriteDown:Render(centerPos + Vector( -78, 70), vecZero, vecZero) --down arrow 2443 | end 2444 | 2445 | ------------------------ 2446 | --RENDER SUBCATEGORIES-- 2447 | ------------------------ 2448 | 2449 | local lastOptionPos = optionPos 2450 | local renderedOptions = 0 2451 | 2452 | if currentMenuCategory then 2453 | local hasUncategorizedCategory = false 2454 | local hasSubcategories = false 2455 | local numCategories = 0 2456 | for j = 1, #currentMenuCategory.Subcategories do 2457 | if currentMenuCategory.Subcategories[j].Name == "Uncategorized" then 2458 | hasUncategorizedCategory = true 2459 | else 2460 | hasSubcategories = true 2461 | numCategories = numCategories + 1 2462 | end 2463 | end 2464 | 2465 | if hasSubcategories then 2466 | if hasUncategorizedCategory then 2467 | numCategories = numCategories + 1 2468 | end 2469 | 2470 | if lastOptionPos.Y > optionPosTopmost and lastOptionPos.Y < optionPosBottommost then 2471 | local lastSubcategoryPos = optionPos 2472 | if numCategories == 2 then 2473 | lastSubcategoryPos = lastOptionPos + Vector( -38, 0) 2474 | elseif numCategories >= 3 then 2475 | lastSubcategoryPos = lastOptionPos + Vector( -76, 0) 2476 | end 2477 | 2478 | local renderedSubcategories = 0 2479 | 2480 | for subcategoryIndex = 1, #currentMenuCategory.Subcategories do 2481 | if subcategoryIndex >= configMenuPositionFirstSubcategory then 2482 | local thisSubcategory = currentMenuCategory.Subcategories[subcategoryIndex] 2483 | 2484 | local posOffset = 0 2485 | 2486 | if thisSubcategory.Name then 2487 | local textToDraw = thisSubcategory.Name 2488 | 2489 | textToDraw = tostring(textToDraw) 2490 | 2491 | local color = subcategoryFontColor 2492 | if not configMenuInSubcategory then 2493 | color = subcategoryFontColorAlpha 2494 | --[[ 2495 | elseif configMenuPositionCursorSubcategory == subcategoryIndex and configMenuInSubcategory then 2496 | color = subcategoryFontColorSelected 2497 | ]] 2498 | end 2499 | 2500 | posOffset = Font12:GetStringWidthUTF8(textToDraw) / 2 2501 | Font12:DrawString(textToDraw, lastSubcategoryPos.X - posOffset, lastSubcategoryPos.Y - 8, color, 0, true) 2502 | end 2503 | 2504 | --cursor 2505 | if configMenuPositionCursorSubcategory == subcategoryIndex and configMenuInSubcategory then 2506 | CursorSpriteRight:Render(lastSubcategoryPos + Vector((posOffset + 10) * -1, 0), vecZero, vecZero) 2507 | end 2508 | 2509 | --increase counter 2510 | renderedSubcategories = renderedSubcategories + 1 2511 | 2512 | if renderedSubcategories >= configMenuSubcategoriesCanShow then --if this is the last one we should render 2513 | --render scroll arrows 2514 | if configMenuPositionFirstSubcategory > 1 then --if the first one we rendered wasn't the first in the list 2515 | SubcategoryCursorSpriteLeft:Render(lastOptionPos + Vector( -125, 0), vecZero, vecZero) 2516 | end 2517 | 2518 | if subcategoryIndex < #currentMenuCategory.Subcategories then --if this is not the last thing 2519 | SubcategoryCursorSpriteRight:Render(lastOptionPos + Vector(125, 0), vecZero, vecZero) 2520 | end 2521 | 2522 | break 2523 | end 2524 | 2525 | --pos mod 2526 | lastSubcategoryPos = lastSubcategoryPos + Vector(76, 0) 2527 | end 2528 | end 2529 | end 2530 | 2531 | --subcategory selection counts as an option that gets rendered 2532 | renderedOptions = renderedOptions + 1 2533 | lastOptionPos = lastOptionPos + Vector(0, 14) 2534 | 2535 | --subcategory to options divider 2536 | if lastOptionPos.Y > optionPosTopmost and lastOptionPos.Y < optionPosBottommost then 2537 | SubcategoryDividerSprite:Render(lastOptionPos, vecZero, vecZero) 2538 | end 2539 | 2540 | --subcategory to options divider counts as an option that gets rendered 2541 | renderedOptions = renderedOptions + 1 2542 | lastOptionPos = lastOptionPos + Vector(0, 14) 2543 | end 2544 | end 2545 | 2546 | ------------------ 2547 | --RENDER OPTIONS-- 2548 | ------------------ 2549 | 2550 | local firstOptionPos = lastOptionPos 2551 | 2552 | if currentMenuSubcategory 2553 | and currentMenuSubcategory.Options 2554 | and #currentMenuSubcategory.Options > 0 then 2555 | for optionIndex = 1, #currentMenuSubcategory.Options do 2556 | local thisOption = currentMenuSubcategory.Options[optionIndex] 2557 | 2558 | local cursorIsAtThisOption = configMenuPositionCursorOption == optionIndex and configMenuInOptions 2559 | local posOffset = 10 2560 | 2561 | if lastOptionPos.Y > optionPosTopmost and lastOptionPos.Y < optionPosBottommost then 2562 | if thisOption.Type 2563 | and thisOption.Type ~= ModConfigMenu.OptionType.SPACE 2564 | and thisOption.Display then 2565 | local optionType = thisOption.Type 2566 | local optionDisplay = thisOption.Display 2567 | local optionColor = thisOption.Color 2568 | 2569 | local useAltSlider = thisOption.AltSlider 2570 | 2571 | --get what to draw 2572 | if optionType == ModConfigMenu.OptionType.TEXT 2573 | or optionType == ModConfigMenu.OptionType.BOOLEAN 2574 | or optionType == ModConfigMenu.OptionType.NUMBER 2575 | or optionType == ModConfigMenu.OptionType.KEYBIND_KEYBOARD 2576 | or optionType == ModConfigMenu.OptionType.KEYBIND_CONTROLLER 2577 | or optionType == ModConfigMenu.OptionType.TITLE then 2578 | local textToDraw = optionDisplay 2579 | 2580 | if type(optionDisplay) == "function" then 2581 | textToDraw = optionDisplay(cursorIsAtThisOption, configMenuInOptions, lastOptionPos) 2582 | end 2583 | 2584 | textToDraw = tostring(textToDraw) 2585 | 2586 | local heightOffset = 6 2587 | local font = Font10 2588 | local color = optionsFontColor 2589 | if not configMenuInOptions then 2590 | if thisOption.NoCursorHere then 2591 | color = optionsFontColorNoCursorAlpha 2592 | else 2593 | color = optionsFontColorAlpha 2594 | end 2595 | elseif thisOption.NoCursorHere then 2596 | color = optionsFontColorNoCursor 2597 | end 2598 | if optionType == ModConfigMenu.OptionType.TITLE then 2599 | heightOffset = 8 2600 | font = Font12 2601 | color = optionsFontColorTitle 2602 | if not configMenuInOptions then 2603 | color = optionsFontColorTitleAlpha 2604 | end 2605 | end 2606 | 2607 | if optionColor then 2608 | color = KColor(optionColor[1], optionColor[2], optionColor[3], color.Alpha) 2609 | end 2610 | 2611 | posOffset = font:GetStringWidthUTF8(textToDraw) / 2 2612 | font:DrawString(textToDraw, lastOptionPos.X - posOffset, lastOptionPos.Y - heightOffset, color, 0, true) 2613 | elseif optionType == ModConfigMenu.OptionType.SCROLL then 2614 | local numberToShow = optionDisplay 2615 | 2616 | if type(optionDisplay) == "function" then 2617 | numberToShow = optionDisplay(cursorIsAtThisOption, configMenuInOptions, lastOptionPos) 2618 | end 2619 | 2620 | posOffset = 31 2621 | local scrollOffset = 0 2622 | 2623 | if type(numberToShow) == "number" then 2624 | numberToShow = math.max(math.min(math.floor(numberToShow), 10), 0) 2625 | elseif type(numberToShow) == "string" then 2626 | local numberToShowStart, numberToShowEnd = string.find(numberToShow, "$scroll") 2627 | if numberToShowStart and numberToShowEnd then 2628 | local numberStart = numberToShowEnd + 1 2629 | local numberEnd = numberToShowEnd + 3 2630 | local numberString = string.sub(numberToShow, numberStart, numberEnd) 2631 | numberString = tonumber(numberString) 2632 | if not numberString or (numberString and not type(numberString) == "number") or 2633 | (numberString and type(numberString) == "number" and numberString < 10) then 2634 | numberEnd = numberEnd - 1 2635 | numberString = string.sub(numberToShow, numberStart, numberEnd) 2636 | numberString = tonumber(numberString) 2637 | end 2638 | if numberString and type(numberString) == "number" then 2639 | local textToDrawPreScroll = string.sub(numberToShow, 0, numberToShowStart - 1) 2640 | local textToDrawPostScroll = string.sub(numberToShow, numberEnd, string.len(numberToShow)) 2641 | local textToDraw = textToDrawPreScroll .. " " .. textToDrawPostScroll 2642 | 2643 | local color = optionsFontColor 2644 | if not configMenuInOptions then 2645 | color = optionsFontColorAlpha 2646 | end 2647 | if optionColor then 2648 | color = KColor(optionColor[1], optionColor[2], optionColor[3], color.Alpha) 2649 | end 2650 | 2651 | scrollOffset = posOffset 2652 | posOffset = Font10:GetStringWidthUTF8(textToDraw) / 2 2653 | Font10:DrawString(textToDraw, lastOptionPos.X - posOffset, lastOptionPos.Y - 6, color, 0, true) 2654 | 2655 | scrollOffset = posOffset - (Font10:GetStringWidthUTF8(textToDrawPreScroll) + scrollOffset) 2656 | numberToShow = numberString 2657 | end 2658 | end 2659 | end 2660 | 2661 | local scrollColor = optionsSpriteColor 2662 | if not configMenuInOptions then 2663 | scrollColor = optionsSpriteColorAlpha 2664 | end 2665 | if optionColor then 2666 | scrollColor = Color(optionColor[1], optionColor[2], optionColor[3], scrollColor.A, scrollColor.RO, 2667 | scrollColor.GO 2668 | , scrollColor.BO) 2669 | end 2670 | 2671 | local sliderString = "Slider1" 2672 | if useAltSlider then 2673 | sliderString = "Slider2" 2674 | end 2675 | 2676 | SliderSprite.Color = scrollColor 2677 | SliderSprite:SetFrame(sliderString, numberToShow) 2678 | SliderSprite:Render(lastOptionPos - Vector(scrollOffset, -2), vecZero, vecZero) 2679 | end 2680 | 2681 | local showStrikeout = thisOption.ShowStrikeout 2682 | if posOffset > 0 and (type(showStrikeout) == "boolean" and showStrikeout == true) or 2683 | (type(showStrikeout) == "function" and showStrikeout() == true) then 2684 | if configMenuInOptions then 2685 | StrikeOutSprite.Color = colorDefault 2686 | else 2687 | StrikeOutSprite.Color = colorHalf 2688 | end 2689 | StrikeOutSprite:SetFrame("Strikeout", math.floor(posOffset)) 2690 | StrikeOutSprite:Render(lastOptionPos, vecZero, vecZero) 2691 | end 2692 | end 2693 | 2694 | --cursor 2695 | if cursorIsAtThisOption then 2696 | CursorSpriteRight:Render(lastOptionPos + Vector((posOffset + 10) * -1, 0), vecZero, vecZero) 2697 | end 2698 | end 2699 | 2700 | --increase counter 2701 | renderedOptions = renderedOptions + 1 2702 | 2703 | --pos mod 2704 | lastOptionPos = lastOptionPos + Vector(0, 14) 2705 | end 2706 | 2707 | --render scroll arrows 2708 | if optionsCanScrollUp then 2709 | OptionsCursorSpriteUp:Render(centerPos + Vector(193, -86), vecZero, vecZero) --up arrow 2710 | end 2711 | if optionsCanScrollDown then 2712 | local yPos = 66 2713 | if shouldShowControls then 2714 | yPos = 40 2715 | end 2716 | 2717 | OptionsCursorSpriteDown:Render(centerPos + Vector(193, yPos), vecZero, vecZero) --down arrow 2718 | end 2719 | end 2720 | 2721 | MenuOverlaySprite:Render(centerPos, vecZero, vecZero) 2722 | 2723 | --title 2724 | local titleText = "Mod Config Menu" 2725 | if configMenuInSubcategory then 2726 | titleText = tostring(currentMenuCategory.Name) 2727 | end 2728 | local titleTextOffset = Font16Bold:GetStringWidthUTF8(titleText) / 2 2729 | Font16Bold:DrawString(titleText, titlePos.X - titleTextOffset, titlePos.Y - 9, mainFontColor, 0, true) 2730 | 2731 | --info 2732 | local infoTable = nil 2733 | local isOldInfo = false 2734 | 2735 | if configMenuInOptions then 2736 | if currentMenuOption and currentMenuOption.Info then 2737 | infoTable = currentMenuOption.Info 2738 | end 2739 | elseif configMenuInSubcategory then 2740 | if currentMenuSubcategory and currentMenuSubcategory.Info then 2741 | infoTable = currentMenuSubcategory.Info 2742 | end 2743 | elseif currentMenuCategory and currentMenuCategory.Info then 2744 | infoTable = currentMenuCategory.Info 2745 | if currentMenuCategory.IsOld then 2746 | isOldInfo = true 2747 | end 2748 | end 2749 | 2750 | if infoTable then 2751 | local lineWidth = 340 2752 | if shouldShowControls then 2753 | lineWidth = 260 2754 | end 2755 | 2756 | local infoTableDisplay = ModConfigMenu.ConvertDisplayToTextTable(infoTable, lineWidth, Font10) 2757 | 2758 | local lastInfoPos = infoPos - Vector(0, 6 * #infoTableDisplay) 2759 | for line = 1, #infoTableDisplay do 2760 | --text 2761 | local textToDraw = tostring(infoTableDisplay[line]) 2762 | local posOffset = Font10:GetStringWidthUTF8(textToDraw) / 2 2763 | local color = mainFontColor 2764 | if isOldInfo then 2765 | color = optionsFontColorTitle 2766 | end 2767 | Font10:DrawString(textToDraw, lastInfoPos.X - posOffset, lastInfoPos.Y - 6, color, 0, true) 2768 | 2769 | --pos mod 2770 | lastInfoPos = lastInfoPos + Vector(0, 10) 2771 | end 2772 | end 2773 | 2774 | --hud offset 2775 | if configMenuInOptions 2776 | and currentMenuOption 2777 | and currentMenuOption.ShowOffset 2778 | and ScreenHelper then 2779 | --render the visual 2780 | HudOffsetVisualBottomRight:Render(ScreenHelper.GetScreenBottomRight(), vecZero, vecZero) 2781 | HudOffsetVisualBottomLeft:Render(ScreenHelper.GetScreenBottomLeft(), vecZero, vecZero) 2782 | HudOffsetVisualTopRight:Render(ScreenHelper.GetScreenTopRight(), vecZero, vecZero) 2783 | HudOffsetVisualTopLeft:Render(ScreenHelper.GetScreenTopLeft(), vecZero, vecZero) 2784 | end 2785 | 2786 | --popup 2787 | if configMenuInPopup 2788 | and currentMenuOption 2789 | and (currentMenuOption.Popup or currentMenuOption.Restart or currentMenuOption.Rerun) then 2790 | PopupSprite:Render(centerPos, vecZero, vecZero) 2791 | 2792 | local popupTable = currentMenuOption.Popup 2793 | 2794 | if not popupTable then 2795 | if currentMenuOption.Restart then 2796 | popupTable = "Restart the game for this setting to take effect" 2797 | end 2798 | 2799 | if currentMenuOption.Rerun then 2800 | popupTable = "Start a new run for this setting to take effect" 2801 | end 2802 | end 2803 | 2804 | if popupTable then 2805 | local lineWidth = currentMenuOption.PopupWidth or 180 2806 | 2807 | local popupTableDisplay = ModConfigMenu.ConvertDisplayToTextTable(popupTable, lineWidth, Font10) 2808 | 2809 | local lastPopupPos = (centerPos + Vector(0, 2)) - Vector(0, 6 * #popupTableDisplay) 2810 | for line = 1, #popupTableDisplay do 2811 | --text 2812 | local textToDraw = tostring(popupTableDisplay[line]) 2813 | local posOffset = Font10:GetStringWidthUTF8(textToDraw) / 2 2814 | Font10:DrawString(textToDraw, lastPopupPos.X - posOffset, lastPopupPos.Y - 6, mainFontColor, 0, true) 2815 | 2816 | --pos mod 2817 | lastPopupPos = lastPopupPos + Vector(0, 10) 2818 | end 2819 | end 2820 | end 2821 | 2822 | --controls 2823 | if shouldShowControls then 2824 | --back 2825 | local bottomLeft = ScreenHelper.GetScreenBottomLeft(0) 2826 | if not configMenuInSubcategory then 2827 | CornerExit:Render(bottomLeft, vecZero, vecZero) 2828 | else 2829 | CornerBack:Render(bottomLeft, vecZero, vecZero) 2830 | end 2831 | 2832 | local goBackString = "" 2833 | if ModConfigMenu.Config.LastBackPressed then 2834 | if InputHelper.KeyboardToString[ModConfigMenu.Config.LastBackPressed] then 2835 | goBackString = InputHelper.KeyboardToString[ModConfigMenu.Config.LastBackPressed] 2836 | elseif InputHelper.ControllerToString[ModConfigMenu.Config.LastBackPressed] then 2837 | goBackString = InputHelper.ControllerToString[ModConfigMenu.Config.LastBackPressed] 2838 | end 2839 | end 2840 | Font10:DrawString(goBackString, (bottomLeft.X - Font10:GetStringWidthUTF8(goBackString) / 2) + 36, 2841 | bottomLeft.Y - 24, 2842 | mainFontColor, 0, true) 2843 | 2844 | --select 2845 | local bottomRight = ScreenHelper.GetScreenBottomRight(0) 2846 | if not configMenuInPopup then 2847 | local foundValidPopup = false 2848 | --[[ 2849 | if configMenuInSubcategory 2850 | and configMenuInOptions 2851 | and currentMenuOption 2852 | and currentMenuOption.Type 2853 | and currentMenuOption.Type ~= ModConfigMenu.OptionType.SPACE 2854 | and currentMenuOption.Popup then 2855 | foundValidPopup = true 2856 | end 2857 | ]] 2858 | if foundValidPopup then 2859 | CornerOpen:Render(bottomRight, vecZero, vecZero) 2860 | else 2861 | CornerSelect:Render(bottomRight, vecZero, vecZero) 2862 | end 2863 | 2864 | local selectString = "" 2865 | if ModConfigMenu.Config.LastSelectPressed then 2866 | if InputHelper.KeyboardToString[ModConfigMenu.Config.LastSelectPressed] then 2867 | selectString = InputHelper.KeyboardToString[ModConfigMenu.Config.LastSelectPressed] 2868 | elseif InputHelper.ControllerToString[ModConfigMenu.Config.LastSelectPressed] then 2869 | selectString = InputHelper.ControllerToString[ModConfigMenu.Config.LastSelectPressed] 2870 | end 2871 | end 2872 | Font10:DrawString(selectString, (bottomRight.X - Font10:GetStringWidthUTF8(selectString) / 2) - 36, 2873 | bottomRight.Y - 24, mainFontColor, 0, true) 2874 | end 2875 | end 2876 | else 2877 | for i = 0, game:GetNumPlayers() - 1 do 2878 | local player = Isaac.GetPlayer(i) 2879 | local data = player:GetData() 2880 | 2881 | --enable player controls 2882 | if data.ConfigMenuPlayerPosition then 2883 | data.ConfigMenuPlayerPosition = nil 2884 | end 2885 | if data.ConfigMenuPlayerControlsDisabled then 2886 | player.ControlsEnabled = true 2887 | data.ConfigMenuPlayerControlsDisabled = false 2888 | end 2889 | end 2890 | 2891 | configMenuInSubcategory = false 2892 | configMenuInOptions = false 2893 | configMenuInPopup = false 2894 | 2895 | holdingCounterDown = 0 2896 | holdingCounterUp = 0 2897 | holdingCounterLeft = 0 2898 | holdingCounterRight = 0 2899 | 2900 | configMenuPositionCursorCategory = 1 2901 | configMenuPositionCursorSubcategory = 1 2902 | configMenuPositionCursorOption = 1 2903 | 2904 | configMenuPositionFirstSubcategory = 1 2905 | 2906 | leftCurrentOffset = 0 2907 | optionsCurrentOffset = 0 2908 | end 2909 | end 2910 | 2911 | ModConfigMenu.Mod:AddCallback(ModCallbacks.MC_POST_RENDER, ModConfigMenu.PostRender) 2912 | 2913 | function ModConfigMenu.OpenConfigMenu() 2914 | if ModConfigMenu.RoomIsSafe() then 2915 | if ModConfigMenu.Config["Mod Config Menu"].HideHudInMenu then 2916 | local game = Game() 2917 | if REPENTANCE then 2918 | local hud = game:GetHUD() 2919 | hud:SetVisible(false) 2920 | else 2921 | local seeds = game:GetSeeds() 2922 | seeds:AddSeedEffect(SeedEffect.SEED_NO_HUD) 2923 | end 2924 | end 2925 | 2926 | ModConfigMenu.IsVisible = true 2927 | else 2928 | local sfx = SFXManager() 2929 | sfx:Play(SoundEffect.SOUND_BOSS2INTRO_ERRORBUZZ, 0.75, 0, false, 1) 2930 | end 2931 | end 2932 | 2933 | function ModConfigMenu.CloseConfigMenu() 2934 | ModConfigMenu.LeavePopup() 2935 | ModConfigMenu.LeaveOptions() 2936 | ModConfigMenu.LeaveSubcategory() 2937 | 2938 | local game = Game() 2939 | if REPENTANCE then 2940 | local hud = game:GetHUD() 2941 | hud:SetVisible(true) 2942 | else 2943 | local seeds = game:GetSeeds() 2944 | seeds:RemoveSeedEffect(SeedEffect.SEED_NO_HUD) 2945 | end 2946 | 2947 | 2948 | ModConfigMenu.IsVisible = false 2949 | end 2950 | 2951 | function ModConfigMenu.ToggleConfigMenu() 2952 | if ModConfigMenu.IsVisible then 2953 | ModConfigMenu.CloseConfigMenu() 2954 | else 2955 | ModConfigMenu.OpenConfigMenu() 2956 | end 2957 | end 2958 | 2959 | function ModConfigMenu.InputAction(_, entity, inputHook, buttonAction) 2960 | if ModConfigMenu.IsVisible and buttonAction ~= ButtonAction.ACTION_FULLSCREEN and 2961 | buttonAction ~= ButtonAction.ACTION_CONSOLE then 2962 | if inputHook == InputHook.IS_ACTION_PRESSED or inputHook == InputHook.IS_ACTION_TRIGGERED then 2963 | return false 2964 | else 2965 | return 0 2966 | end 2967 | end 2968 | end 2969 | 2970 | ModConfigMenu.Mod:AddCallback(ModCallbacks.MC_INPUT_ACTION, ModConfigMenu.InputAction) 2971 | 2972 | --console commands that toggle the menu 2973 | local toggleCommands = { 2974 | ["modconfigmenu"] = true, 2975 | ["modconfig"] = true, 2976 | ["mcm"] = true, 2977 | ["mc"] = true 2978 | } 2979 | function ModConfigMenu.ExecuteCmd(_, command, args) 2980 | command = command:lower() 2981 | 2982 | if toggleCommands[command] then 2983 | ModConfigMenu.ToggleConfigMenu() 2984 | end 2985 | end 2986 | 2987 | ModConfigMenu.Mod:AddCallback(ModCallbacks.MC_EXECUTE_CMD, ModConfigMenu.ExecuteCmd) 2988 | 2989 | if ModConfigMenu.StandaloneMod then 2990 | if not ModConfigMenu.StandaloneSaveLoaded then 2991 | ModConfigMenu.StandaloneSaveLoaded = true 2992 | end 2993 | end 2994 | 2995 | ------------ 2996 | --FINISHED-- 2997 | ------------ 2998 | Isaac.DebugString("Mod Config Menu v" .. tostring(ModConfigMenu.Version) .. " - Loaded.") 2999 | print("Mod Config Menu v" .. tostring(ModConfigMenu.Version) .. " - Loaded.") 3000 | 3001 | return ModConfigMenu 3002 | -------------------------------------------------------------------------------- /scripts/screenhelper.lua: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | -- IMPORTANT: DO NOT EDIT THIS FILE!!! -- 3 | ------------------------------------------------------------------------------ 4 | -- This file relies on other versions of itself being the same. -- 5 | -- If you need something in this file changed, please let the creator know! -- 6 | ------------------------------------------------------------------------------ 7 | 8 | ------------- 9 | -- version -- 10 | ------------- 11 | 12 | local fileVersion = 1 13 | 14 | --prevent older/same version versions of this script from loading 15 | if ScreenHelper and ScreenHelper.Version >= fileVersion then 16 | return ScreenHelper 17 | end 18 | 19 | if not ScreenHelper then 20 | ScreenHelper = {} 21 | ScreenHelper.Version = fileVersion 22 | elseif ScreenHelper.Version < fileVersion then 23 | local oldVersion = ScreenHelper.Version 24 | 25 | -- handle old versions 26 | 27 | ScreenHelper.Version = fileVersion 28 | end 29 | 30 | --------------------- 31 | --hud offset helper-- 32 | --------------------- 33 | 34 | ScreenHelper.CurrentScreenOffset = ScreenHelper.CurrentScreenOffset or 0 35 | 36 | function ScreenHelper.SetOffset(num) 37 | num = math.min(math.max(math.floor(num), 0), 10) 38 | 39 | ScreenHelper.CurrentScreenOffset = num 40 | 41 | return num 42 | end 43 | 44 | function ScreenHelper.GetOffset() 45 | return ScreenHelper.CurrentScreenOffset 46 | end 47 | 48 | ------------------------------------ 49 | --screen size and corner functions-- 50 | ------------------------------------ 51 | 52 | local vecZero = Vector(0, 0) 53 | function ScreenHelper.GetScreenSize() 54 | if REPENTANCE then 55 | local screenWidth = Isaac.GetScreenWidth() 56 | local screenHeight = Isaac.GetScreenHeight() 57 | 58 | return Vector(screenWidth, screenHeight) 59 | else --based off of code from kilburn 60 | local game = Game() 61 | local room = game:GetRoom() 62 | 63 | local pos = room:WorldToScreenPosition(vecZero) - room:GetRenderScrollOffset() - game.ScreenShakeOffset 64 | 65 | local rx = pos.X + 60 * 26 / 40 66 | local ry = pos.Y + 140 * (26 / 40) 67 | 68 | return Vector(rx * 2 + 13 * 26, ry * 2 + 7 * 26) 69 | end 70 | end 71 | 72 | function ScreenHelper.GetScreenCenter() 73 | return ScreenHelper.GetScreenSize() / 2 74 | end 75 | 76 | function ScreenHelper.GetScreenBottomRight(offset) 77 | offset = offset or ScreenHelper.GetOffset() 78 | 79 | local pos = ScreenHelper.GetScreenSize() 80 | local hudOffset = Vector( -offset * 2.2, -offset * 1.6) 81 | pos = pos + hudOffset 82 | 83 | return pos 84 | end 85 | 86 | function ScreenHelper.GetScreenBottomLeft(offset) 87 | offset = offset or ScreenHelper.GetOffset() 88 | 89 | local pos = Vector(0, ScreenHelper.GetScreenBottomRight(0).Y) 90 | local hudOffset = Vector(offset * 2.2, -offset * 1.6) 91 | pos = pos + hudOffset 92 | 93 | return pos 94 | end 95 | 96 | function ScreenHelper.GetScreenTopRight(offset) 97 | offset = offset or ScreenHelper.GetOffset() 98 | 99 | local pos = Vector(ScreenHelper.GetScreenBottomRight(0).X, 0) 100 | local hudOffset = Vector( -offset * 2.2, offset * 1.2) 101 | pos = pos + hudOffset 102 | 103 | return pos 104 | end 105 | 106 | function ScreenHelper.GetScreenTopLeft(offset) 107 | offset = offset or ScreenHelper.GetOffset() 108 | 109 | local pos = vecZero 110 | local hudOffset = Vector(offset * 2, offset * 1.2) 111 | pos = pos + hudOffset 112 | 113 | return pos 114 | end 115 | 116 | return ScreenHelper 117 | -------------------------------------------------------------------------------- /steam/screenshots/1603631350_preview_250900_20210110125025_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/7a707788f58fe57eb03da7ffe211e78ba45db6c8/steam/screenshots/1603631350_preview_250900_20210110125025_1.png -------------------------------------------------------------------------------- /steam/screenshots/1603631350_preview_250900_20210113160746_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/7a707788f58fe57eb03da7ffe211e78ba45db6c8/steam/screenshots/1603631350_preview_250900_20210113160746_1.png -------------------------------------------------------------------------------- /steam/screenshots/1603631350_preview_250900_20210113160751_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/7a707788f58fe57eb03da7ffe211e78ba45db6c8/steam/screenshots/1603631350_preview_250900_20210113160751_1.png -------------------------------------------------------------------------------- /steam/screenshots/1603631350_preview_250900_20210113160753_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/7a707788f58fe57eb03da7ffe211e78ba45db6c8/steam/screenshots/1603631350_preview_250900_20210113160753_1.png -------------------------------------------------------------------------------- /steam/screenshots/1603631350_preview_250900_20210113160757_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/7a707788f58fe57eb03da7ffe211e78ba45db6c8/steam/screenshots/1603631350_preview_250900_20210113160757_1.png -------------------------------------------------------------------------------- /steam/screenshots/1603631350_preview_250900_20210113160811_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/7a707788f58fe57eb03da7ffe211e78ba45db6c8/steam/screenshots/1603631350_preview_250900_20210113160811_1.png -------------------------------------------------------------------------------- /steam/screenshots/1603631350_preview_250900_20210113160813_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/7a707788f58fe57eb03da7ffe211e78ba45db6c8/steam/screenshots/1603631350_preview_250900_20210113160813_1.png -------------------------------------------------------------------------------- /steam/thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/7a707788f58fe57eb03da7ffe211e78ba45db6c8/steam/thumbnail.jpg --------------------------------------------------------------------------------