├── steam ├── thumbnail.jpg └── 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 ├── .gitignore ├── resources ├── gfx │ └── ui │ │ └── modconfig │ │ ├── black.png │ │ ├── menu.png │ │ ├── arrows.png │ │ ├── offset.png │ │ ├── slider.png │ │ ├── strike.png │ │ ├── backselect.png │ │ ├── menu_overlay.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 │ │ └── menu.anm2 └── previewfile_1603631350.jpg ├── .gitattributes ├── main.lua ├── release.sh ├── .vscode ├── extensions.json └── settings.json ├── copy-metadata.py ├── LICENSE ├── metadata.xml ├── .github └── workflows │ └── ci.yml ├── cspell.json ├── release.py ├── scripts ├── screenhelper.lua ├── inputhelper.lua └── modconfig.lua ├── docs ├── quick-start.md └── api.md └── README.md /steam/thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/HEAD/steam/thumbnail.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS-specific artifacts 2 | .DS_Store 3 | thumbs.db 4 | 5 | # Isaac-related files 6 | disable.it 7 | *.dat 8 | -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/HEAD/resources/gfx/ui/modconfig/black.png -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/HEAD/resources/gfx/ui/modconfig/menu.png -------------------------------------------------------------------------------- /resources/previewfile_1603631350.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/HEAD/resources/previewfile_1603631350.jpg -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/arrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/HEAD/resources/gfx/ui/modconfig/arrows.png -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/offset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/HEAD/resources/gfx/ui/modconfig/offset.png -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/slider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/HEAD/resources/gfx/ui/modconfig/slider.png -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/strike.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/HEAD/resources/gfx/ui/modconfig/strike.png -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/backselect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/HEAD/resources/gfx/ui/modconfig/backselect.png -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/menu_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/HEAD/resources/gfx/ui/modconfig/menu_overlay.png -------------------------------------------------------------------------------- /resources/gfx/ui/modconfig/popup_thin_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/HEAD/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/HEAD/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/HEAD/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/HEAD/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/HEAD/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/HEAD/resources/gfx/ui/modconfig/popup_wide_small.png -------------------------------------------------------------------------------- /steam/screenshots/1603631350_preview_250900_20210110125025_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zamiell/isaac-mod-config-menu/HEAD/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/HEAD/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/HEAD/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/HEAD/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/HEAD/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/HEAD/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/HEAD/steam/screenshots/1603631350_preview_250900_20210113160813_1.png -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 113 23 | Public 24 | 25 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/modconfig.lua: -------------------------------------------------------------------------------- 1 | -- Imports 2 | local json = require("json") 3 | 4 | local IS_DEV = false 5 | 6 | ------------- 7 | -- version -- 8 | ------------- 9 | 10 | -- The final version of Chifilly's Mod Config Menu fork was 33. 11 | -- For the pure version, we arbitrarily selected a starting point of 100 and incremented from there. 12 | local VERSION = 113 13 | 14 | ModConfigMenu = {} 15 | ModConfigMenu.Version = VERSION 16 | 17 | --------------------- 18 | -- local functinos -- 19 | --------------------- 20 | 21 | local function isRepentancePlusInstalled() 22 | local game = Game() 23 | local room = game:GetRoom() 24 | local metatable = getmetatable(room) 25 | return type(metatable) == "table" and type(metatable.DamageGridWithSource) == "function" 26 | end 27 | local IS_REPENTANCE_PLUS = isRepentancePlusInstalled() 28 | 29 | ------------------------- 30 | --SAVE HELPER FUNCTIONS-- 31 | ------------------------- 32 | 33 | local SaveHelper = {} 34 | 35 | function SaveHelper.CopyTable(tableToCopy) 36 | local table2 = {} 37 | 38 | for i, value in pairs(tableToCopy) do 39 | if type(value) == "table" then 40 | table2[i] = SaveHelper.CopyTable(value) 41 | else 42 | table2[i] = value 43 | end 44 | end 45 | 46 | return table2 47 | end 48 | 49 | function SaveHelper.FillTable(tableToFill, tableToFillFrom) 50 | for i, value in pairs(tableToFillFrom) do 51 | if tableToFill[i] ~= nil then 52 | if type(value) == "table" then 53 | if type(tableToFill[i]) ~= "table" then 54 | tableToFill[i] = {} 55 | end 56 | 57 | tableToFill[i] = SaveHelper.FillTable(tableToFill[i], value) 58 | else 59 | tableToFill[i] = value 60 | end 61 | else 62 | if type(value) == "table" then 63 | if type(tableToFill[i]) ~= "table" then 64 | tableToFill[i] = {} 65 | end 66 | 67 | tableToFill[i] = SaveHelper.FillTable({}, value) 68 | else 69 | tableToFill[i] = value 70 | end 71 | end 72 | end 73 | 74 | return tableToFill 75 | end 76 | 77 | ----------- 78 | -- setup -- 79 | ----------- 80 | 81 | Isaac.DebugString("Mod Config Menu v" .. tostring(ModConfigMenu.Version) .. " - Loading...") 82 | 83 | local vecZero = Vector(0, 0) 84 | local restartWarnMessage = nil 85 | local rerunWarnMessage = nil 86 | 87 | if not InputHelper then 88 | require("scripts.inputhelper") 89 | if not InputHelper then 90 | error("Mod Config Menu requires Input Helper to function", 2) 91 | end 92 | end 93 | 94 | if not ScreenHelper then 95 | require("scripts.screenhelper") 96 | if not ScreenHelper then 97 | error("Mod Config Menu requires Screen Helper to function", 2) 98 | end 99 | end 100 | 101 | ModConfigMenu.Mod = RegisterMod("Mod Config Menu", 1) 102 | 103 | ---------- 104 | --SAVING-- 105 | ---------- 106 | 107 | ModConfigMenu.ConfigDefault = {} 108 | ModConfigMenu.Config = {} 109 | 110 | --------------------------------- 111 | -- Saving and loading MCM data -- 112 | --------------------------------- 113 | 114 | local function save() 115 | local mod = ModConfigMenu.Mod 116 | 117 | if IS_DEV then 118 | Isaac.DebugString("MCM is saving data.") 119 | end 120 | 121 | local jsonString = json.encode(ModConfigMenu.Config) 122 | if jsonString == nil or jsonString == "" then 123 | return 124 | end 125 | 126 | mod:SaveData(jsonString) 127 | end 128 | 129 | local function load() 130 | local mod = ModConfigMenu.Mod 131 | 132 | if IS_DEV then 133 | Isaac.DebugString("MCM is loading data.") 134 | end 135 | 136 | if not mod:HasData() then 137 | save() 138 | return 139 | end 140 | 141 | local jsonString = mod:LoadData() 142 | if jsonString == nil or jsonString == "" or jsonString == "[]" or jsonString == "{}" then 143 | save() 144 | return 145 | end 146 | 147 | local data = nil 148 | local function loadJSON() 149 | data = json.decode(jsonString) 150 | end 151 | 152 | if not pcall(loadJSON) or data == nil then 153 | Isaac.DebugString("Error: Failed to load Mod Config Menu data from the \"save#.dat\" file.") 154 | return 155 | end 156 | 157 | ModConfigMenu.LoadSave(data) 158 | end 159 | 160 | function ModConfigMenu.LoadSave(data) 161 | local saveData = SaveHelper.CopyTable(ModConfigMenu.ConfigDefault) 162 | if type(data) == "table" then 163 | saveData["General"] = SaveHelper.FillTable(saveData["General"], data["General"] or {}) 164 | saveData["Mod Config Menu"] = SaveHelper.FillTable(saveData["Mod Config Menu"], data["Mod Config Menu"] or data or {}) 165 | if IS_DEV then 166 | Isaac.DebugString("Successfully loaded data from the \"save#.dat\" file.") 167 | end 168 | end 169 | 170 | local currentData = SaveHelper.CopyTable(ModConfigMenu.Config) 171 | saveData = SaveHelper.FillTable(currentData, saveData) 172 | 173 | ModConfigMenu.Config = SaveHelper.CopyTable(saveData) 174 | 175 | --make sure ScreenHelper's offset matches MCM's offset 176 | if ScreenHelper then 177 | ScreenHelper.SetOffset(ModConfigMenu.Config["General"].HudOffset) 178 | end 179 | end 180 | 181 | local function vanillaMCMOptionRegisterSave(mcmOption) 182 | local oldOnChange = mcmOption.OnChange 183 | mcmOption.OnChange = function(currentValue) 184 | local value = oldOnChange(currentValue) 185 | save() 186 | return value 187 | end 188 | end 189 | 190 | -------------- 191 | --game start-- 192 | -------------- 193 | local versionPrintFont = Font() 194 | versionPrintFont:Load("font/pftempestasevencondensed.fnt") 195 | 196 | local versionPrintTimer = 0 197 | local isFirstRun = true 198 | 199 | --returns true if the room is clear and there are no active enemies and there are no projectiles 200 | ModConfigMenu.IgnoreActiveEnemies = ModConfigMenu.IgnoreActiveEnemies or {} 201 | function ModConfigMenu.RoomIsSafe() 202 | local roomHasDanger = false 203 | 204 | for _, entity in pairs(Isaac.GetRoomEntities()) do 205 | if ( 206 | entity:IsActiveEnemy() 207 | and not entity:HasEntityFlags(EntityFlag.FLAG_FRIENDLY) 208 | and ( 209 | not ModConfigMenu.IgnoreActiveEnemies[entity.Type] 210 | or ( 211 | ModConfigMenu.IgnoreActiveEnemies[entity.Type] 212 | and not ModConfigMenu.IgnoreActiveEnemies[entity.Type][-1] 213 | and not ModConfigMenu.IgnoreActiveEnemies[entity.Type][entity.Variant] 214 | ) 215 | ) 216 | ) then 217 | roomHasDanger = true 218 | elseif ( 219 | entity.Type == EntityType.ENTITY_PROJECTILE 220 | and entity:ToProjectile().ProjectileFlags & ProjectileFlags.CANT_HIT_PLAYER ~= 1 221 | ) then 222 | roomHasDanger = true 223 | elseif entity.Type == EntityType.ENTITY_BOMBDROP then 224 | roomHasDanger = true 225 | end 226 | end 227 | 228 | local game = Game() 229 | local room = game:GetRoom() 230 | 231 | if room:IsClear() and not roomHasDanger then 232 | return true 233 | end 234 | 235 | return false 236 | end 237 | 238 | ModConfigMenu.IsVisible = false 239 | function ModConfigMenu.PostGameStarted() 240 | if IS_DEV then 241 | Isaac.DebugString("POST_GAME_STARTED") 242 | end 243 | 244 | rerunWarnMessage = nil 245 | 246 | load() 247 | 248 | if not isFirstRun then 249 | return 250 | end 251 | isFirstRun = false 252 | 253 | if ModConfigMenu.Config["Mod Config Menu"].ShowControls and isFirstRun then 254 | versionPrintTimer = 120 255 | end 256 | 257 | ModConfigMenu.IsVisible = false 258 | 259 | -- Add Potato Dummy to the ignore list. 260 | local potatoType = Isaac.GetEntityTypeByName("Potato Dummy") 261 | local potatoVariant = Isaac.GetEntityVariantByName("Potato Dummy") 262 | 263 | if potatoType and potatoVariant > 0 then 264 | ModConfigMenu.IgnoreActiveEnemies[potatoType] = ModConfigMenu.IgnoreActiveEnemies or {} 265 | ModConfigMenu.IgnoreActiveEnemies[potatoType][potatoVariant] = true 266 | end 267 | end 268 | 269 | ModConfigMenu.Mod:AddCallback(ModCallbacks.MC_POST_GAME_STARTED, ModConfigMenu.PostGameStarted) 270 | 271 | 272 | --------------- 273 | --post update-- 274 | --------------- 275 | function ModConfigMenu.PostUpdate() 276 | if versionPrintTimer > 0 then 277 | versionPrintTimer = versionPrintTimer - 1 278 | end 279 | end 280 | 281 | ModConfigMenu.Mod:AddCallback(ModCallbacks.MC_POST_UPDATE, ModConfigMenu.PostUpdate) 282 | 283 | 284 | ------------------------------------ 285 | --set up the menu sprites and font-- 286 | ------------------------------------ 287 | function ModConfigMenu.GetMenuAnm2Sprite(animation, frame, color) 288 | local sprite = Sprite() 289 | 290 | sprite:Load("gfx/ui/modconfig/menu.anm2", true) 291 | sprite:SetFrame(animation or "Idle", frame or 0) 292 | 293 | if color then 294 | sprite.Color = color 295 | end 296 | 297 | return sprite 298 | end 299 | 300 | --main menu sprites 301 | local MenuSprite = ModConfigMenu.GetMenuAnm2Sprite("Idle", 0) 302 | local MenuOverlaySprite = ModConfigMenu.GetMenuAnm2Sprite("IdleOverlay", 0) 303 | local PopupSprite = ModConfigMenu.GetMenuAnm2Sprite("Popup", 0) 304 | 305 | --main cursors 306 | local CursorSpriteRight = ModConfigMenu.GetMenuAnm2Sprite("Cursor", 0) 307 | local CursorSpriteUp = ModConfigMenu.GetMenuAnm2Sprite("Cursor", 1) 308 | local CursorSpriteDown = ModConfigMenu.GetMenuAnm2Sprite("Cursor", 2) 309 | 310 | --colors 311 | local colorDefault = Color(1, 1, 1, 1, 0, 0, 0) 312 | local colorHalf = Color(1, 1, 1, 0.5, 0, 0, 0) 313 | 314 | --subcategory pane cursors 315 | local SubcategoryCursorSpriteLeft = ModConfigMenu.GetMenuAnm2Sprite("Cursor", 3, colorHalf) 316 | local SubcategoryCursorSpriteRight = ModConfigMenu.GetMenuAnm2Sprite("Cursor", 0, colorHalf) 317 | 318 | --options pane cursors 319 | local OptionsCursorSpriteUp = ModConfigMenu.GetMenuAnm2Sprite("Cursor", 1, colorHalf) 320 | local OptionsCursorSpriteDown = ModConfigMenu.GetMenuAnm2Sprite("Cursor", 2, colorHalf) 321 | 322 | --other options pane objects 323 | local SubcategoryDividerSprite = ModConfigMenu.GetMenuAnm2Sprite("Divider", 0, colorHalf) 324 | local SliderSprite = ModConfigMenu.GetMenuAnm2Sprite("Slider1", 0) 325 | 326 | --strikeout 327 | local StrikeOutSprite = ModConfigMenu.GetMenuAnm2Sprite("Strikeout", 0) 328 | 329 | --back/select corner papers 330 | local CornerSelect = ModConfigMenu.GetMenuAnm2Sprite("BackSelect", 0) 331 | local CornerBack = ModConfigMenu.GetMenuAnm2Sprite("BackSelect", 1) 332 | local CornerOpen = ModConfigMenu.GetMenuAnm2Sprite("BackSelect", 2) 333 | local CornerExit = ModConfigMenu.GetMenuAnm2Sprite("BackSelect", 3) 334 | 335 | --fonts 336 | local Font10 = Font() 337 | if IS_REPENTANCE_PLUS then 338 | Font10:Load("font/teammeatex/teammeatex10.fnt") 339 | else 340 | Font10:Load("font/teammeatfont10.fnt") 341 | end 342 | 343 | local Font12 = Font() 344 | if IS_REPENTANCE_PLUS then 345 | Font12:Load("font/teammeatex/teammeatex12.fnt") 346 | else 347 | Font12:Load("font/teammeatfont12.fnt") 348 | end 349 | 350 | local Font16Bold = Font() 351 | Font16Bold:Load("font/teammeatfont16bold.fnt") 352 | 353 | --popups 354 | ModConfigMenu.PopupGfx = ModConfigMenu.PopupGfx or {} 355 | ModConfigMenu.PopupGfx.THIN_SMALL = "gfx/ui/modconfig/popup_thin_small.png" 356 | ModConfigMenu.PopupGfx.THIN_MEDIUM = "gfx/ui/modconfig/popup_thin_medium.png" 357 | ModConfigMenu.PopupGfx.THIN_LARGE = "gfx/ui/modconfig/popup_thin_large.png" 358 | ModConfigMenu.PopupGfx.WIDE_SMALL = "gfx/ui/modconfig/popup_wide_small.png" 359 | ModConfigMenu.PopupGfx.WIDE_MEDIUM = "gfx/ui/modconfig/popup_wide_medium.png" 360 | ModConfigMenu.PopupGfx.WIDE_LARGE = "gfx/ui/modconfig/popup_wide_large.png" 361 | 362 | 363 | ------------------------- 364 | --add setting functions-- 365 | ------------------------- 366 | ModConfigMenu.OptionType = ModConfigMenu.OptionType or {} 367 | ModConfigMenu.OptionType.TEXT = 1 368 | ModConfigMenu.OptionType.SPACE = 2 369 | ModConfigMenu.OptionType.SCROLL = 3 370 | ModConfigMenu.OptionType.BOOLEAN = 4 371 | ModConfigMenu.OptionType.NUMBER = 5 372 | ModConfigMenu.OptionType.KEYBIND_KEYBOARD = 6 373 | ModConfigMenu.OptionType.KEYBIND_CONTROLLER = 7 374 | ModConfigMenu.OptionType.TITLE = 8 375 | 376 | ModConfigMenu.MenuData = ModConfigMenu.MenuData or {} 377 | 378 | --CATEGORY FUNCTIONS 379 | function ModConfigMenu.GetCategoryIDByName(categoryName) 380 | if type(categoryName) ~= "string" then 381 | return categoryName 382 | end 383 | 384 | local categoryID = nil 385 | 386 | for i = 1, #ModConfigMenu.MenuData do 387 | if categoryName == ModConfigMenu.MenuData[i].Name then 388 | categoryID = i 389 | break 390 | end 391 | end 392 | 393 | return categoryID 394 | end 395 | 396 | function ModConfigMenu.UpdateCategory(categoryName, dataTable) 397 | if type(categoryName) ~= "string" and type(categoryName) ~= "number" then 398 | error("ModConfigMenu.UpdateCategory - No valid category name provided", 2) 399 | end 400 | 401 | local categoryID = ModConfigMenu.GetCategoryIDByName(categoryName) 402 | if categoryID == nil then 403 | categoryID = #ModConfigMenu.MenuData + 1 404 | ModConfigMenu.MenuData[categoryID] = {} 405 | ModConfigMenu.MenuData[categoryID].Subcategories = {} 406 | end 407 | 408 | if type(categoryName) == "string" or dataTable.Name then 409 | ModConfigMenu.MenuData[categoryID].Name = dataTable.Name or categoryName 410 | end 411 | 412 | if dataTable.Info then 413 | ModConfigMenu.MenuData[categoryID].Info = dataTable.Info 414 | end 415 | 416 | if dataTable.IsOld then 417 | ModConfigMenu.MenuData[categoryID].IsOld = dataTable.IsOld 418 | end 419 | end 420 | 421 | function ModConfigMenu.SetCategoryInfo(categoryName, info) 422 | if type(categoryName) ~= "string" and type(categoryName) ~= "number" then 423 | error("ModConfigMenu.SetCategoryInfo - No valid category name provided", 2) 424 | end 425 | 426 | ModConfigMenu.UpdateCategory(categoryName, { 427 | Info = info 428 | }) 429 | end 430 | 431 | function ModConfigMenu.RemoveCategory(categoryName) 432 | if type(categoryName) ~= "string" and type(categoryName) ~= "number" then 433 | error("ModConfigMenu.RemoveCategory - No valid category name provided", 2) 434 | end 435 | 436 | local categoryID = ModConfigMenu.GetCategoryIDByName(categoryName) 437 | if categoryID then 438 | table.remove(ModConfigMenu.MenuData, categoryID) 439 | return true 440 | end 441 | 442 | return false 443 | end 444 | 445 | --SUBCATEGORY FUNCTIONS 446 | function ModConfigMenu.GetSubcategoryIDByName(categoryName, subcategoryName) 447 | if type(categoryName) ~= "string" and type(categoryName) ~= "number" then 448 | error("ModConfigMenu.GetSubcategoryIDByName - No valid category name provided", 2) 449 | end 450 | 451 | local categoryID = ModConfigMenu.GetCategoryIDByName(categoryName) 452 | 453 | if type(subcategoryName) ~= "string" then 454 | return subcategoryName 455 | end 456 | 457 | local subcategoryID = nil 458 | 459 | for i = 1, #ModConfigMenu.MenuData[categoryID].Subcategories do 460 | if subcategoryName == ModConfigMenu.MenuData[categoryID].Subcategories[i].Name then 461 | subcategoryID = i 462 | break 463 | end 464 | end 465 | 466 | return subcategoryID 467 | end 468 | 469 | function ModConfigMenu.UpdateSubcategory(categoryName, subcategoryName, dataTable) 470 | if type(categoryName) ~= "string" and type(categoryName) ~= "number" then 471 | error("ModConfigMenu.UpdateSubcategory - No valid category name provided", 2) 472 | end 473 | 474 | if type(subcategoryName) ~= "string" and type(subcategoryName) ~= "number" then 475 | error("ModConfigMenu.UpdateSubcategory - No valid subcategory name provided", 2) 476 | end 477 | 478 | local categoryID = ModConfigMenu.GetCategoryIDByName(categoryName) 479 | if categoryID == nil then 480 | categoryID = #ModConfigMenu.MenuData + 1 481 | ModConfigMenu.MenuData[categoryID] = {} 482 | ModConfigMenu.MenuData[categoryID].Name = tostring(categoryName) 483 | ModConfigMenu.MenuData[categoryID].Subcategories = {} 484 | end 485 | 486 | local subcategoryID = ModConfigMenu.GetSubcategoryIDByName(categoryID, subcategoryName) 487 | if subcategoryID == nil then 488 | subcategoryID = #ModConfigMenu.MenuData[categoryID].Subcategories + 1 489 | ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID] = {} 490 | ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options = {} 491 | end 492 | 493 | if type(subcategoryName) == "string" or dataTable.Name then 494 | ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Name = dataTable.Name or subcategoryName 495 | end 496 | 497 | if dataTable.Info then 498 | ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Info = dataTable.Info 499 | end 500 | end 501 | 502 | function ModConfigMenu.RemoveSubcategory(categoryName, subcategoryName) 503 | if type(categoryName) ~= "string" and type(categoryName) ~= "number" then 504 | error("ModConfigMenu.RemoveSubcategory - No valid category name provided", 2) 505 | end 506 | 507 | if type(subcategoryName) ~= "string" and type(subcategoryName) ~= "number" then 508 | error("ModConfigMenu.RemoveSubcategory - No valid subcategory name provided", 2) 509 | end 510 | 511 | local categoryID = ModConfigMenu.GetCategoryIDByName(categoryName) 512 | if categoryID then 513 | local subcategoryID = ModConfigMenu.GetSubcategoryIDByName(categoryID, subcategoryName) 514 | if subcategoryID then 515 | table.remove(ModConfigMenu.MenuData[categoryID].Subcategories, subcategoryID) 516 | return true 517 | end 518 | end 519 | 520 | return false 521 | end 522 | 523 | --SETTING FUNCTIONS 524 | function ModConfigMenu.AddSetting(categoryName, subcategoryName, settingTable) 525 | if settingTable == nil then 526 | settingTable = subcategoryName 527 | subcategoryName = nil 528 | end 529 | 530 | if type(categoryName) ~= "string" and type(categoryName) ~= "number" then 531 | error("ModConfigMenu.AddSetting - No valid category name provided", 2) 532 | end 533 | 534 | subcategoryName = subcategoryName or "Uncategorized" 535 | if type(subcategoryName) ~= "string" and type(subcategoryName) ~= "number" then 536 | error("ModConfigMenu.AddSetting - No valid subcategory name provided", 2) 537 | end 538 | 539 | local categoryID = ModConfigMenu.GetCategoryIDByName(categoryName) 540 | if categoryID == nil then 541 | categoryID = #ModConfigMenu.MenuData + 1 542 | ModConfigMenu.MenuData[categoryID] = {} 543 | ModConfigMenu.MenuData[categoryID].Name = tostring(categoryName) 544 | ModConfigMenu.MenuData[categoryID].Subcategories = {} 545 | end 546 | 547 | local subcategoryID = ModConfigMenu.GetSubcategoryIDByName(categoryID, subcategoryName) 548 | if subcategoryID == nil then 549 | subcategoryID = #ModConfigMenu.MenuData[categoryID].Subcategories + 1 550 | ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID] = {} 551 | ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Name = tostring(subcategoryName) 552 | ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options = {} 553 | end 554 | 555 | ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options[ 556 | #ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options + 1] = settingTable 557 | 558 | return settingTable 559 | end 560 | 561 | function ModConfigMenu.AddText(categoryName, subcategoryName, text, color) 562 | if color == nil and type(text) ~= "string" and type(text) ~= "function" then 563 | color = text 564 | text = subcategoryName 565 | subcategoryName = nil 566 | end 567 | 568 | if type(categoryName) ~= "string" and type(categoryName) ~= "number" then 569 | error("ModConfigMenu.AddText - No valid category name provided", 2) 570 | end 571 | 572 | subcategoryName = subcategoryName or "Uncategorized" 573 | if type(subcategoryName) ~= "string" and type(subcategoryName) ~= "number" then 574 | error("ModConfigMenu.AddText - No valid subcategory name provided", 2) 575 | end 576 | 577 | local settingTable = { 578 | Type = ModConfigMenu.OptionType.TEXT, 579 | Display = text, 580 | Color = color, 581 | NoCursorHere = true 582 | } 583 | 584 | return ModConfigMenu.AddSetting(categoryName, subcategoryName, settingTable) 585 | end 586 | 587 | function ModConfigMenu.AddTitle(categoryName, subcategoryName, text, color) 588 | if color == nil and type(text) ~= "string" and type(text) ~= "function" then 589 | color = text 590 | text = subcategoryName 591 | subcategoryName = nil 592 | end 593 | 594 | if type(categoryName) ~= "string" and type(categoryName) ~= "number" then 595 | error("ModConfigMenu.AddTitle - No valid category name provided", 2) 596 | end 597 | 598 | subcategoryName = subcategoryName or "Uncategorized" 599 | if type(subcategoryName) ~= "string" and type(subcategoryName) ~= "number" then 600 | error("ModConfigMenu.AddTitle - No valid subcategory name provided", 2) 601 | end 602 | 603 | local settingTable = { 604 | Type = ModConfigMenu.OptionType.TITLE, 605 | Display = text, 606 | Color = color, 607 | NoCursorHere = true 608 | } 609 | 610 | return ModConfigMenu.AddSetting(categoryName, subcategoryName, settingTable) 611 | end 612 | 613 | function ModConfigMenu.AddSpace(categoryName, subcategoryName) 614 | if type(categoryName) ~= "string" and type(categoryName) ~= "number" then 615 | error("ModConfigMenu.AddSpace - No valid category name provided", 2) 616 | end 617 | 618 | subcategoryName = subcategoryName or "Uncategorized" 619 | if type(subcategoryName) ~= "string" and type(subcategoryName) ~= "number" then 620 | error("ModConfigMenu.AddSpace - No valid subcategory name provided", 2) 621 | end 622 | 623 | local settingTable = { 624 | Type = ModConfigMenu.OptionType.SPACE 625 | } 626 | 627 | return ModConfigMenu.AddSetting(categoryName, subcategoryName, settingTable) 628 | end 629 | 630 | local altSlider = false 631 | function ModConfigMenu.SimpleAddSetting(settingType, categoryName, subcategoryName, configTableAttribute, minValue, 632 | maxValue, modifyBy, defaultValue, displayText, displayValueProxies, displayDevice 633 | , info, color, functionName) 634 | --set default values 635 | if defaultValue == nil then 636 | if settingType == ModConfigMenu.OptionType.BOOLEAN then 637 | defaultValue = false 638 | else 639 | defaultValue = 0 640 | end 641 | end 642 | 643 | if settingType == ModConfigMenu.OptionType.NUMBER then 644 | minValue = minValue or 0 645 | maxValue = maxValue or 10 646 | modifyBy = modifyBy or 1 647 | else 648 | minValue = nil 649 | maxValue = nil 650 | modifyBy = nil 651 | end 652 | 653 | functionName = functionName or "SimpleAddSetting" 654 | 655 | --erroring 656 | if categoryName == nil then 657 | error("ModConfigMenu." .. tostring(functionName) .. " - No valid category name provided", 2) 658 | end 659 | if configTableAttribute == nil then 660 | error("ModConfigMenu." .. tostring(functionName) .. " - No valid config table attribute provided", 2) 661 | end 662 | 663 | --create config value 664 | ModConfigMenu.Config[categoryName] = ModConfigMenu.Config[categoryName] or {} 665 | if ModConfigMenu.Config[categoryName][configTableAttribute] == nil then 666 | ModConfigMenu.Config[categoryName][configTableAttribute] = defaultValue 667 | end 668 | 669 | ModConfigMenu.ConfigDefault[categoryName] = ModConfigMenu.ConfigDefault[categoryName] or {} 670 | if ModConfigMenu.ConfigDefault[categoryName][configTableAttribute] == nil then 671 | ModConfigMenu.ConfigDefault[categoryName][configTableAttribute] = defaultValue 672 | end 673 | 674 | --setting 675 | local settingTable = { 676 | Type = settingType, 677 | Attribute = configTableAttribute, 678 | CurrentSetting = function() 679 | return ModConfigMenu.Config[categoryName][configTableAttribute] 680 | end, 681 | Default = defaultValue, 682 | Display = function(cursorIsAtThisOption, configMenuInOptions, lastOptionPos) 683 | local currentValue = ModConfigMenu.Config[categoryName][configTableAttribute] 684 | 685 | local displayString = "" 686 | 687 | if displayText then 688 | displayString = displayText .. ": " 689 | end 690 | 691 | if settingType == ModConfigMenu.OptionType.SCROLL then 692 | displayString = displayString .. "$scroll" .. tostring(math.floor(currentValue)) 693 | elseif settingType == ModConfigMenu.OptionType.KEYBIND_KEYBOARD then 694 | local key = "None" 695 | 696 | if currentValue > -1 then 697 | key = "Unknown Key" 698 | 699 | if InputHelper.KeyboardToString[currentValue] then 700 | key = InputHelper.KeyboardToString[currentValue] 701 | end 702 | end 703 | 704 | displayString = displayString .. key 705 | 706 | if displayDevice then 707 | displayString = displayString .. " (keyboard)" 708 | end 709 | elseif settingType == ModConfigMenu.OptionType.KEYBIND_CONTROLLER then 710 | local key = "None" 711 | 712 | if currentValue > -1 then 713 | key = "Unknown Button" 714 | 715 | if InputHelper.ControllerToString[currentValue] then 716 | key = InputHelper.ControllerToString[currentValue] 717 | end 718 | end 719 | 720 | displayString = displayString .. key 721 | 722 | if displayDevice then 723 | displayString = displayString .. " (controller)" 724 | end 725 | elseif displayValueProxies and displayValueProxies[currentValue] then 726 | displayString = displayString .. tostring(displayValueProxies[currentValue]) 727 | else 728 | displayString = displayString .. tostring(currentValue) 729 | end 730 | 731 | return displayString 732 | end, 733 | OnChange = function(currentValue) 734 | if not currentValue then 735 | if settingType == ModConfigMenu.OptionType.KEYBIND_KEYBOARD or 736 | settingType == ModConfigMenu.OptionType.KEYBIND_CONTROLLER then 737 | currentValue = -1 738 | end 739 | end 740 | 741 | ModConfigMenu.Config[categoryName][configTableAttribute] = currentValue 742 | end, 743 | Info = info, 744 | Color = color 745 | } 746 | 747 | if settingType == ModConfigMenu.OptionType.NUMBER then 748 | settingTable.Minimum = minValue 749 | settingTable.Maximum = maxValue 750 | settingTable.ModifyBy = modifyBy 751 | elseif settingType == ModConfigMenu.OptionType.SCROLL then 752 | settingTable.AltSlider = altSlider 753 | altSlider = not altSlider 754 | elseif settingType == ModConfigMenu.OptionType.KEYBIND_KEYBOARD or 755 | settingType == ModConfigMenu.OptionType.KEYBIND_CONTROLLER then 756 | settingTable.PopupGfx = ModConfigMenu.PopupGfx.WIDE_SMALL 757 | settingTable.PopupWidth = 280 758 | settingTable.Popup = function() 759 | local currentValue = ModConfigMenu.Config[categoryName][configTableAttribute] 760 | 761 | local goBackString = "back" 762 | if ModConfigMenu.Config.LastBackPressed then 763 | if InputHelper.KeyboardToString[ModConfigMenu.Config.LastBackPressed] then 764 | goBackString = InputHelper.KeyboardToString[ModConfigMenu.Config.LastBackPressed] 765 | elseif InputHelper.ControllerToString[ModConfigMenu.Config.LastBackPressed] then 766 | goBackString = InputHelper.ControllerToString[ModConfigMenu.Config.LastBackPressed] 767 | end 768 | end 769 | 770 | local keepSettingString = "" 771 | if currentValue > -1 then 772 | local currentSettingString = nil 773 | if (settingType == ModConfigMenu.OptionType.KEYBIND_KEYBOARD and InputHelper.KeyboardToString[currentValue]) then 774 | currentSettingString = InputHelper.KeyboardToString[currentValue] 775 | elseif ( 776 | settingType == ModConfigMenu.OptionType.KEYBIND_CONTROLLER and InputHelper.ControllerToString[currentValue]) then 777 | currentSettingString = InputHelper.ControllerToString[currentValue] 778 | end 779 | 780 | keepSettingString = "This setting is currently set to \"" .. 781 | currentSettingString .. "\".$newlinePress this button to keep it unchanged.$newline$newline" 782 | end 783 | 784 | local deviceString = "" 785 | if settingType == ModConfigMenu.OptionType.KEYBIND_KEYBOARD then 786 | deviceString = "keyboard" 787 | elseif settingType == ModConfigMenu.OptionType.KEYBIND_CONTROLLER then 788 | deviceString = "controller" 789 | end 790 | 791 | return "Press a button on your " .. 792 | deviceString .. 793 | " to change this setting.$newline$newline" .. 794 | keepSettingString .. "Press \"" .. goBackString .. "\" to go back and clear this setting." 795 | end 796 | end 797 | 798 | return ModConfigMenu.AddSetting(categoryName, subcategoryName, settingTable) 799 | end 800 | 801 | function ModConfigMenu.AddBooleanSetting(categoryName, subcategoryName, configTableAttribute, defaultValue, displayText, 802 | displayValueProxies, info, color) 803 | --move args around 804 | if type(configTableAttribute) ~= "string" then 805 | color = info 806 | info = displayValueProxies 807 | displayValueProxies = displayText 808 | displayText = defaultValue 809 | defaultValue = configTableAttribute 810 | configTableAttribute = subcategoryName 811 | subcategoryName = nil 812 | end 813 | 814 | if type(defaultValue) ~= "boolean" then 815 | color = info 816 | info = displayValueProxies 817 | displayValueProxies = displayText 818 | displayText = defaultValue 819 | defaultValue = false 820 | end 821 | 822 | if type(displayValueProxies) ~= "table" or type(info) == "userdata" or type(info) == "nil" then 823 | color = info 824 | info = displayValueProxies 825 | displayValueProxies = nil 826 | end 827 | 828 | return ModConfigMenu.SimpleAddSetting(ModConfigMenu.OptionType.BOOLEAN, categoryName, subcategoryName, 829 | configTableAttribute, nil, nil, nil, defaultValue, displayText, displayValueProxies, nil, info, color, 830 | "AddBooleanSetting") 831 | end 832 | 833 | function ModConfigMenu.AddNumberSetting(categoryName, subcategoryName, configTableAttribute, minValue, maxValue, modifyBy 834 | , defaultValue, displayText, displayValueProxies, info, color) 835 | --move args around 836 | if type(configTableAttribute) ~= "string" then 837 | color = info 838 | info = displayValueProxies 839 | displayValueProxies = displayText 840 | displayText = defaultValue 841 | defaultValue = modifyBy 842 | modifyBy = maxValue 843 | maxValue = minValue 844 | minValue = configTableAttribute 845 | configTableAttribute = subcategoryName 846 | subcategoryName = nil 847 | end 848 | 849 | if type(defaultValue) == "string" then 850 | color = info 851 | info = displayValueProxies 852 | displayValueProxies = displayText 853 | displayText = defaultValue 854 | defaultValue = modifyBy 855 | modifyBy = nil 856 | end 857 | 858 | if type(displayValueProxies) ~= "table" or type(info) == "userdata" or type(info) == "nil" then 859 | color = info 860 | info = displayValueProxies 861 | displayValueProxies = nil 862 | end 863 | 864 | --set default values 865 | defaultValue = defaultValue or 0 866 | minValue = minValue or 0 867 | maxValue = maxValue or 10 868 | modifyBy = modifyBy or 1 869 | 870 | return ModConfigMenu.SimpleAddSetting(ModConfigMenu.OptionType.NUMBER, categoryName, subcategoryName, 871 | configTableAttribute, minValue, maxValue, modifyBy, defaultValue, displayText, displayValueProxies, nil, info, 872 | color 873 | , 874 | "AddNumberSetting") 875 | end 876 | 877 | function ModConfigMenu.AddScrollSetting(categoryName, subcategoryName, configTableAttribute, defaultValue, displayText, 878 | info, color) 879 | --move args around 880 | if type(configTableAttribute) ~= "string" then 881 | color = info 882 | info = displayText 883 | displayText = defaultValue 884 | defaultValue = configTableAttribute 885 | configTableAttribute = subcategoryName 886 | subcategoryName = nil 887 | end 888 | 889 | if type(defaultValue) ~= "number" then 890 | color = info 891 | info = displayText 892 | displayText = defaultValue 893 | defaultValue = nil 894 | end 895 | 896 | --set default values 897 | defaultValue = defaultValue or 0 898 | 899 | return ModConfigMenu.SimpleAddSetting(ModConfigMenu.OptionType.SCROLL, categoryName, subcategoryName, 900 | configTableAttribute, nil, nil, nil, defaultValue, displayText, nil, nil, info, color, "AddScrollSetting") 901 | end 902 | 903 | function ModConfigMenu.AddKeyboardSetting(categoryName, subcategoryName, configTableAttribute, defaultValue, displayText 904 | , displayDevice, info, color) 905 | --move args around 906 | if type(configTableAttribute) ~= "string" then 907 | color = info 908 | info = displayDevice 909 | displayDevice = displayText 910 | displayText = defaultValue 911 | defaultValue = configTableAttribute 912 | configTableAttribute = subcategoryName 913 | subcategoryName = nil 914 | end 915 | 916 | if type(defaultValue) ~= "number" then 917 | color = info 918 | info = displayText 919 | displayText = defaultValue 920 | defaultValue = nil 921 | end 922 | 923 | if type(displayDevice) ~= "boolean" then 924 | color = info 925 | info = displayDevice 926 | displayDevice = false 927 | end 928 | 929 | --set default values 930 | defaultValue = defaultValue or -1 931 | 932 | return ModConfigMenu.SimpleAddSetting(ModConfigMenu.OptionType.KEYBIND_KEYBOARD, categoryName, subcategoryName, 933 | configTableAttribute, nil, nil, nil, defaultValue, displayText, nil, displayDevice, info, color, 934 | "AddKeyboardSetting") 935 | end 936 | 937 | function ModConfigMenu.AddControllerSetting(categoryName, subcategoryName, configTableAttribute, defaultValue, 938 | displayText, displayDevice, info, color) 939 | --move args around 940 | if type(configTableAttribute) ~= "string" then 941 | color = info 942 | info = displayDevice 943 | displayDevice = displayText 944 | displayText = defaultValue 945 | defaultValue = configTableAttribute 946 | configTableAttribute = subcategoryName 947 | subcategoryName = nil 948 | end 949 | 950 | if type(defaultValue) ~= "number" then 951 | color = info 952 | info = displayText 953 | displayText = defaultValue 954 | defaultValue = nil 955 | end 956 | 957 | if type(displayDevice) ~= "boolean" then 958 | color = info 959 | info = displayDevice 960 | displayDevice = false 961 | end 962 | 963 | --set default values 964 | defaultValue = defaultValue or -1 965 | 966 | return ModConfigMenu.SimpleAddSetting(ModConfigMenu.OptionType.KEYBIND_CONTROLLER, categoryName, subcategoryName, 967 | configTableAttribute, nil, nil, nil, defaultValue, displayText, nil, displayDevice, info, color, 968 | "AddControllerSetting") 969 | end 970 | 971 | function ModConfigMenu.RemoveSetting(categoryName, subcategoryName, settingAttribute) 972 | if settingAttribute == nil then 973 | settingAttribute = subcategoryName 974 | subcategoryName = nil 975 | end 976 | 977 | if type(categoryName) ~= "string" and type(categoryName) ~= "number" then 978 | error("ModConfigMenu.RemoveSetting - No valid category name provided", 2) 979 | end 980 | 981 | subcategoryName = subcategoryName or "Uncategorized" 982 | if type(subcategoryName) ~= "string" and type(subcategoryName) ~= "number" then 983 | error("ModConfigMenu.RemoveSetting - No valid subcategory name provided", 2) 984 | end 985 | 986 | local categoryID = ModConfigMenu.GetCategoryIDByName(categoryName) 987 | if categoryID then 988 | local subcategoryID = ModConfigMenu.GetSubcategoryIDByName(categoryID, subcategoryName) 989 | if subcategoryID then 990 | --loop to find matching attribute 991 | for i = #ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options, 1, -1 do 992 | if ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options[i] 993 | and ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options[i].Attribute 994 | and ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options[i].Attribute == settingAttribute then 995 | table.remove(ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options, i) 996 | return true 997 | end 998 | end 999 | 1000 | --loop to find matching display 1001 | for i = #ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options, 1, -1 do 1002 | if ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options[i] 1003 | and ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options[i].Display 1004 | and ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options[i].Display == settingAttribute then 1005 | table.remove(ModConfigMenu.MenuData[categoryID].Subcategories[subcategoryID].Options, i) 1006 | return true 1007 | end 1008 | end 1009 | end 1010 | end 1011 | 1012 | return false 1013 | end 1014 | 1015 | -------------------------- 1016 | --GENERAL SETTINGS SETUP-- 1017 | -------------------------- 1018 | ModConfigMenu.SetCategoryInfo("General", "Settings that affect the majority of mods") 1019 | 1020 | ---------------------- 1021 | --HUD OFFSET SETTING-- 1022 | ---------------------- 1023 | local hudOffsetSetting = ModConfigMenu.AddScrollSetting( 1024 | "General", --category 1025 | "HudOffset", --attribute in table 1026 | Options and Options.HUDOffset * 10 or 0, --default value 1027 | "Hud Offset", --display text 1028 | "How far from the corners of the screen custom hud elements will be.$newlineTry to make this match your base-game setting." 1029 | ) 1030 | 1031 | hudOffsetSetting.HideControls = true -- hide controls so the screen corner graphics are easier to see 1032 | hudOffsetSetting.ShowOffset = true -- shows screen offset 1033 | vanillaMCMOptionRegisterSave(hudOffsetSetting) 1034 | 1035 | --set up callback 1036 | local oldHudOffsetOnChange = hudOffsetSetting.OnChange 1037 | hudOffsetSetting.OnChange = function(currentValue) 1038 | --update screenhelper's offset 1039 | if ScreenHelper then 1040 | ScreenHelper.SetOffset(currentValue) 1041 | end 1042 | 1043 | return oldHudOffsetOnChange(currentValue) 1044 | end 1045 | 1046 | -------------------- 1047 | --OVERLAYS SETTING-- 1048 | -------------------- 1049 | local overlays = ModConfigMenu.AddBooleanSetting( 1050 | "General", --category 1051 | "Overlays", --attribute in table 1052 | true, --default value 1053 | "Overlays", --display text 1054 | { 1055 | --value display text 1056 | [true] = "On", 1057 | [false] = "Off" 1058 | }, 1059 | "Enable or disable custom visual overlays, like screen-wide fog." 1060 | ) 1061 | vanillaMCMOptionRegisterSave(overlays) 1062 | 1063 | ----------------------- 1064 | --CHARGE BARS SETTING-- 1065 | ----------------------- 1066 | local chargeBars = ModConfigMenu.AddBooleanSetting( 1067 | "General", --category 1068 | "ChargeBars", --attribute in table 1069 | false, --default value 1070 | "Charge Bars", --display text 1071 | { 1072 | --value display text 1073 | [true] = "On", 1074 | [false] = "Off" 1075 | }, 1076 | "Enable or disable custom charge bar visuals for mod effects, like those from chargeable items." 1077 | ) 1078 | vanillaMCMOptionRegisterSave(chargeBars) 1079 | 1080 | --------------------- 1081 | --BIG BOOKS SETTING-- 1082 | --------------------- 1083 | local bigBooks = ModConfigMenu.AddBooleanSetting( 1084 | "General", --category 1085 | "BigBooks", --attribute in table 1086 | true, --default value 1087 | "Bigbooks", --display text 1088 | { 1089 | --value display text 1090 | [true] = "On", 1091 | [false] = "Off" 1092 | }, 1093 | "Enable or disable custom big-book overlays which can appear when an active item is used." 1094 | ) 1095 | vanillaMCMOptionRegisterSave(bigBooks) 1096 | 1097 | --------------------- 1098 | --ANNOUNCER SETTING-- 1099 | --------------------- 1100 | 1101 | local announcer = ModConfigMenu.AddNumberSetting( 1102 | "General", --category 1103 | "Announcer", --attribute in table 1104 | 0, --minimum value 1105 | 2, --max value 1106 | 0, --default value, 1107 | "Announcer", --display text 1108 | { 1109 | --value display text 1110 | [0] = "Sometimes", 1111 | [1] = "Never", 1112 | [2] = "Always" 1113 | }, 1114 | "Choose how often a voice-over will play when a pocket item (pill or card) is used." 1115 | ) 1116 | vanillaMCMOptionRegisterSave(announcer) 1117 | 1118 | -------------------------- 1119 | --GENERAL SETTINGS CLOSE-- 1120 | -------------------------- 1121 | 1122 | ModConfigMenu.AddSpace("General") --SPACE 1123 | 1124 | ModConfigMenu.AddText("General", "These settings apply to") 1125 | ModConfigMenu.AddText("General", "all mods which support them") 1126 | 1127 | ---------------------------------- 1128 | --MOD CONFIG MENU SETTINGS SETUP-- 1129 | ---------------------------------- 1130 | 1131 | ModConfigMenu.SetCategoryInfo("Mod Config Menu", 1132 | "Settings specific to Mod Config Menu.$newlineChange keybindings for the menu here.") 1133 | 1134 | ModConfigMenu.AddTitle("Mod Config Menu", "Version " .. tostring(ModConfigMenu.Version) .. " !") --VERSION INDICATOR 1135 | 1136 | ModConfigMenu.AddSpace("Mod Config Menu") --SPACE 1137 | 1138 | ---------------------- 1139 | --OPEN MENU KEYBOARD-- 1140 | ---------------------- 1141 | 1142 | local openMenuKeyboardSetting = ModConfigMenu.AddKeyboardSetting( 1143 | "Mod Config Menu", --category 1144 | "OpenMenuKeyboard", --attribute in table 1145 | Keyboard.KEY_L, --default value 1146 | "Open Menu", --display text 1147 | true, --if (keyboard) is displayed after the key text 1148 | "Choose what button on your keyboard will open Mod Config Menu." 1149 | ) 1150 | 1151 | openMenuKeyboardSetting.IsOpenMenuKeybind = true 1152 | vanillaMCMOptionRegisterSave(openMenuKeyboardSetting) 1153 | 1154 | ------------------------ 1155 | --OPEN MENU CONTROLLER-- 1156 | ------------------------ 1157 | 1158 | local openMenuControllerSetting = ModConfigMenu.AddControllerSetting( 1159 | "Mod Config Menu", --category 1160 | "OpenMenuController", --attribute in table 1161 | Controller.STICK_RIGHT, --default value 1162 | "Open Menu", --display text 1163 | true, --if (controller) is displayed after the key text 1164 | "Choose what button on your controller will open Mod Config Menu." 1165 | ) 1166 | openMenuControllerSetting.IsOpenMenuKeybind = true 1167 | vanillaMCMOptionRegisterSave(openMenuControllerSetting) 1168 | 1169 | --f10 note 1170 | ModConfigMenu.AddText("Mod Config Menu", "F10 will always open this menu.") 1171 | 1172 | ModConfigMenu.AddSpace("Mod Config Menu") --SPACE 1173 | 1174 | ------------ 1175 | --HIDE HUD-- 1176 | ------------ 1177 | 1178 | local hideHudSetting = ModConfigMenu.AddBooleanSetting( 1179 | "Mod Config Menu", --category 1180 | "HideHudInMenu", --attribute in table 1181 | true, --default value 1182 | "Hide HUD", --display text 1183 | { 1184 | --value display text 1185 | [true] = "Yes", 1186 | [false] = "No" 1187 | }, 1188 | "Enable or disable the hud when this menu is open." 1189 | ) 1190 | vanillaMCMOptionRegisterSave(hideHudSetting) 1191 | 1192 | --actively modify the hud visibility as this setting changes 1193 | local oldHideHudOnChange = hideHudSetting.OnChange 1194 | hideHudSetting.OnChange = function(currentValue) 1195 | oldHideHudOnChange(currentValue) 1196 | 1197 | local game = Game() 1198 | if REPENTANCE then 1199 | local hud = game:GetHUD() 1200 | 1201 | if currentValue then 1202 | if hud:IsVisible() then 1203 | hud:SetVisible(false) 1204 | end 1205 | else 1206 | if not hud:IsVisible() then 1207 | hud:SetVisible(true) 1208 | end 1209 | end 1210 | else 1211 | local seeds = game:GetSeeds() 1212 | 1213 | if currentValue then 1214 | if not seeds:HasSeedEffect(SeedEffect.SEED_NO_HUD) then 1215 | seeds:AddSeedEffect(SeedEffect.SEED_NO_HUD) 1216 | end 1217 | else 1218 | if seeds:HasSeedEffect(SeedEffect.SEED_NO_HUD) then 1219 | seeds:RemoveSeedEffect(SeedEffect.SEED_NO_HUD) 1220 | end 1221 | end 1222 | end 1223 | end 1224 | 1225 | ---------------------------- 1226 | --RESET TO DEFAULT KEYBIND-- 1227 | ---------------------------- 1228 | 1229 | local resetKeybindSetting = ModConfigMenu.AddKeyboardSetting( 1230 | "Mod Config Menu", --category 1231 | "ResetToDefault", --attribute in table 1232 | Keyboard.KEY_F11, --default value 1233 | "Reset To Default Keybind", --display text 1234 | "Press this button on your keyboard to reset a setting to its default value." 1235 | ) 1236 | resetKeybindSetting.IsResetKeybind = true 1237 | vanillaMCMOptionRegisterSave(resetKeybindSetting) 1238 | 1239 | ----------------- 1240 | --SHOW CONTROLS-- 1241 | ----------------- 1242 | 1243 | local showControls = ModConfigMenu.AddBooleanSetting( 1244 | "Mod Config Menu", --category 1245 | "ShowControls", --attribute in table 1246 | true, --default value 1247 | "Show Controls", --display text 1248 | { 1249 | --value display text 1250 | [true] = "Yes", 1251 | [false] = "No" 1252 | }, 1253 | "Disable this to remove the back and select widgets at the lower corners of the screen and remove the bottom start-up message." 1254 | ) 1255 | vanillaMCMOptionRegisterSave(showControls) 1256 | 1257 | ModConfigMenu.AddSpace("Mod Config Menu") --SPACE 1258 | 1259 | ----------------- 1260 | --COMPATIBILITY-- 1261 | ----------------- 1262 | 1263 | local compatibilitySetting = ModConfigMenu.AddBooleanSetting( 1264 | "Mod Config Menu", --category 1265 | "CompatibilityLayer", --attribute in table 1266 | false, --default value 1267 | "Disable Legacy Warnings", --display text 1268 | { 1269 | --value display text 1270 | [true] = "Yes", 1271 | [false] = "No" 1272 | }, 1273 | "Use this setting to prevent warnings from being printed to the console for mods that use outdated features of Mod Config Menu." 1274 | ) 1275 | vanillaMCMOptionRegisterSave(compatibilitySetting) 1276 | 1277 | local configMenuSubcategoriesCanShow = 3 1278 | 1279 | local configMenuInSubcategory = false 1280 | local configMenuInOptions = false 1281 | local configMenuInPopup = false 1282 | 1283 | local holdingCounterDown = 0 1284 | local holdingCounterUp = 0 1285 | local holdingCounterRight = 0 1286 | local holdingCounterLeft = 0 1287 | 1288 | local configMenuPositionCursorCategory = 1 1289 | local configMenuPositionCursorSubcategory = 1 1290 | local configMenuPositionCursorOption = 1 1291 | 1292 | local configMenuPositionFirstSubcategory = 1 1293 | 1294 | --valid action presses 1295 | local actionsDown = { ButtonAction.ACTION_DOWN, ButtonAction.ACTION_SHOOTDOWN, ButtonAction.ACTION_MENUDOWN } 1296 | local actionsUp = { ButtonAction.ACTION_UP, ButtonAction.ACTION_SHOOTUP, ButtonAction.ACTION_MENUUP } 1297 | local actionsRight = { ButtonAction.ACTION_RIGHT, ButtonAction.ACTION_SHOOTRIGHT, ButtonAction.ACTION_MENURIGHT } 1298 | local actionsLeft = { ButtonAction.ACTION_LEFT, ButtonAction.ACTION_SHOOTLEFT, ButtonAction.ACTION_MENULEFT } 1299 | local actionsBack = { ButtonAction.ACTION_PILLCARD, ButtonAction.ACTION_MAP, ButtonAction.ACTION_MENUBACK } 1300 | local actionsSelect = { ButtonAction.ACTION_ITEM, ButtonAction.ACTION_PAUSE, ButtonAction.ACTION_MENUCONFIRM, 1301 | ButtonAction.ACTION_BOMB } 1302 | 1303 | --ignore these buttons for the above actions 1304 | local ignoreActionButtons = { Controller.BUTTON_A, Controller.BUTTON_B, Controller.BUTTON_X, Controller.BUTTON_Y } 1305 | 1306 | local currentMenuCategory = nil 1307 | local currentMenuSubcategory = nil 1308 | local currentMenuOption = nil 1309 | local function updateCurrentMenuVars() 1310 | if ModConfigMenu.MenuData[configMenuPositionCursorCategory] then 1311 | currentMenuCategory = ModConfigMenu.MenuData[configMenuPositionCursorCategory] 1312 | if currentMenuCategory.Subcategories and currentMenuCategory.Subcategories[configMenuPositionCursorSubcategory] then 1313 | currentMenuSubcategory = currentMenuCategory.Subcategories[configMenuPositionCursorSubcategory] 1314 | if currentMenuSubcategory.Options and currentMenuSubcategory.Options[configMenuPositionCursorOption] then 1315 | currentMenuOption = currentMenuSubcategory.Options[configMenuPositionCursorOption] 1316 | end 1317 | end 1318 | end 1319 | end 1320 | 1321 | --leaving/entering menu sections 1322 | function ModConfigMenu.EnterPopup() 1323 | if configMenuInSubcategory and configMenuInOptions and not configMenuInPopup then 1324 | local foundValidPopup = false 1325 | if currentMenuOption 1326 | and currentMenuOption.Type 1327 | and currentMenuOption.Type ~= ModConfigMenu.OptionType.SPACE 1328 | and (currentMenuOption.Popup or currentMenuOption.Restart or currentMenuOption.Rerun) then 1329 | foundValidPopup = true 1330 | end 1331 | if foundValidPopup then 1332 | local popupSpritesheet = ModConfigMenu.PopupGfx.THIN_SMALL 1333 | if currentMenuOption.PopupGfx and type(currentMenuOption.PopupGfx) == "string" then 1334 | popupSpritesheet = currentMenuOption.PopupGfx 1335 | end 1336 | PopupSprite:ReplaceSpritesheet(5, popupSpritesheet) 1337 | PopupSprite:LoadGraphics() 1338 | configMenuInPopup = true 1339 | end 1340 | end 1341 | end 1342 | 1343 | function ModConfigMenu.EnterOptions() 1344 | if configMenuInSubcategory and not configMenuInOptions then 1345 | if currentMenuSubcategory 1346 | and currentMenuSubcategory.Options 1347 | and #currentMenuSubcategory.Options > 0 then 1348 | for optionIndex = 1, #currentMenuSubcategory.Options do 1349 | local thisOption = currentMenuSubcategory.Options[optionIndex] 1350 | 1351 | if thisOption.Type 1352 | and thisOption.Type ~= ModConfigMenu.OptionType.SPACE 1353 | and 1354 | ( 1355 | not thisOption.NoCursorHere or 1356 | (type(thisOption.NoCursorHere) == "function" and not thisOption.NoCursorHere())) 1357 | and thisOption.Display then 1358 | configMenuPositionCursorOption = optionIndex 1359 | configMenuInOptions = true 1360 | OptionsCursorSpriteUp.Color = colorDefault 1361 | OptionsCursorSpriteDown.Color = colorDefault 1362 | 1363 | break 1364 | end 1365 | end 1366 | end 1367 | end 1368 | end 1369 | 1370 | function ModConfigMenu.EnterSubcategory() 1371 | if not configMenuInSubcategory then 1372 | configMenuInSubcategory = true 1373 | SubcategoryCursorSpriteLeft.Color = colorDefault 1374 | SubcategoryCursorSpriteRight.Color = colorDefault 1375 | SubcategoryDividerSprite.Color = colorDefault 1376 | 1377 | local hasUsableCategories = false 1378 | if currentMenuCategory.Subcategories then 1379 | for j = 1, #currentMenuCategory.Subcategories do 1380 | if currentMenuCategory.Subcategories[j].Name ~= "Uncategorized" then 1381 | hasUsableCategories = true 1382 | end 1383 | end 1384 | end 1385 | 1386 | if not hasUsableCategories then 1387 | ModConfigMenu.EnterOptions() 1388 | end 1389 | end 1390 | end 1391 | 1392 | function ModConfigMenu.LeavePopup() 1393 | if configMenuInSubcategory and configMenuInOptions and configMenuInPopup then 1394 | if currentMenuOption then 1395 | if currentMenuOption.Restart then 1396 | restartWarnMessage = "One or more settings require you to restart the game" 1397 | elseif currentMenuOption.Rerun then 1398 | rerunWarnMessage = "One or more settings require you to start a new run" 1399 | end 1400 | end 1401 | 1402 | configMenuInPopup = false 1403 | end 1404 | end 1405 | 1406 | function ModConfigMenu.LeaveOptions() 1407 | if configMenuInSubcategory and configMenuInOptions then 1408 | configMenuInOptions = false 1409 | OptionsCursorSpriteUp.Color = colorHalf 1410 | OptionsCursorSpriteDown.Color = colorHalf 1411 | 1412 | local hasUsableCategories = false 1413 | if currentMenuCategory.Subcategories then 1414 | for j = 1, #currentMenuCategory.Subcategories do 1415 | if currentMenuCategory.Subcategories[j].Name ~= "Uncategorized" then 1416 | hasUsableCategories = true 1417 | end 1418 | end 1419 | end 1420 | 1421 | if not hasUsableCategories then 1422 | ModConfigMenu.LeaveSubcategory() 1423 | end 1424 | end 1425 | end 1426 | 1427 | function ModConfigMenu.LeaveSubcategory() 1428 | if configMenuInSubcategory then 1429 | configMenuInSubcategory = false 1430 | SubcategoryCursorSpriteLeft.Color = colorHalf 1431 | SubcategoryCursorSpriteRight.Color = colorHalf 1432 | SubcategoryDividerSprite.Color = colorHalf 1433 | end 1434 | end 1435 | 1436 | local mainSpriteColor = colorDefault 1437 | local optionsSpriteColor = colorDefault 1438 | local optionsSpriteColorAlpha = colorHalf 1439 | local mainFontColor = KColor(34 / 255, 32 / 255, 30 / 255, 1) 1440 | local leftFontColor = KColor(35 / 255, 31 / 255, 30 / 255, 1) 1441 | local leftFontColorSelected = KColor(35 / 255, 50 / 255, 70 / 255, 1) 1442 | 1443 | local optionsFontColor = KColor(34 / 255, 32 / 255, 30 / 255, 1) 1444 | local optionsFontColorAlpha = KColor(34 / 255, 32 / 255, 30 / 255, 0.5) 1445 | local optionsFontColorNoCursor = KColor(34 / 255, 32 / 255, 30 / 255, 0.8) 1446 | local optionsFontColorNoCursorAlpha = KColor(34 / 255, 32 / 255, 30 / 255, 0.4) 1447 | local optionsFontColorTitle = KColor(50 / 255, 0, 0, 1) 1448 | local optionsFontColorTitleAlpha = KColor(50 / 255, 0, 0, 0.5) 1449 | 1450 | local subcategoryFontColor = KColor(34 / 255, 32 / 255, 30 / 255, 1) 1451 | local subcategoryFontColorSelected = KColor(34 / 255, 50 / 255, 70 / 255, 1) 1452 | local subcategoryFontColorAlpha = KColor(34 / 255, 32 / 255, 30 / 255, 0.5) 1453 | local subcategoryFontColorSelectedAlpha = KColor(34 / 255, 50 / 255, 70 / 255, 0.5) 1454 | 1455 | function ModConfigMenu.ConvertDisplayToTextTable(displayValue, lineWidth, font) 1456 | lineWidth = lineWidth or 340 1457 | 1458 | local textTableDisplay = {} 1459 | if type(displayValue) == "function" then 1460 | displayValue = displayValue() 1461 | end 1462 | 1463 | if type(displayValue) == "string" then 1464 | textTableDisplay = { displayValue } 1465 | elseif type(displayValue) == "table" then 1466 | textTableDisplay = SaveHelper.CopyTable(displayValue) 1467 | else 1468 | textTableDisplay = { tostring(displayValue) } 1469 | end 1470 | 1471 | if type(textTableDisplay) == "string" then 1472 | textTableDisplay = { textTableDisplay } 1473 | end 1474 | 1475 | --create new lines based on $newline modifier 1476 | local textTableDisplayAfterNewlines = {} 1477 | for lineIndex = 1, #textTableDisplay do 1478 | local line = textTableDisplay[lineIndex] 1479 | local startIdx, endIdx = string.find(line, "$newline") 1480 | while startIdx do 1481 | local newline = string.sub(line, 0, startIdx - 1) 1482 | table.insert(textTableDisplayAfterNewlines, newline) 1483 | 1484 | line = string.sub(line, endIdx + 1) 1485 | 1486 | startIdx, endIdx = string.find(line, "$newline") 1487 | end 1488 | table.insert(textTableDisplayAfterNewlines, line) 1489 | end 1490 | 1491 | --dynamic string new line creation, based on code by Wofsauge 1492 | local textTableDisplayAfterWordLength = {} 1493 | for lineIndex = 1, #textTableDisplayAfterNewlines do 1494 | local line = textTableDisplayAfterNewlines[lineIndex] 1495 | local curLength = 0 1496 | local text = "" 1497 | for word in string.gmatch(tostring(line), "([^%s]+)") do 1498 | local wordLength = font:GetStringWidthUTF8(word) 1499 | 1500 | if curLength + wordLength <= lineWidth or curLength < 12 then 1501 | text = text .. word .. " " 1502 | curLength = curLength + wordLength 1503 | else 1504 | table.insert(textTableDisplayAfterWordLength, text) 1505 | text = word .. " " 1506 | curLength = wordLength 1507 | end 1508 | end 1509 | table.insert(textTableDisplayAfterWordLength, text) 1510 | end 1511 | 1512 | return textTableDisplayAfterWordLength 1513 | end 1514 | 1515 | --set up screen corner display for hud offset 1516 | local HudOffsetVisualTopLeft = ModConfigMenu.GetMenuAnm2Sprite("Offset", 0) 1517 | local HudOffsetVisualTopRight = ModConfigMenu.GetMenuAnm2Sprite("Offset", 1) 1518 | local HudOffsetVisualBottomRight = ModConfigMenu.GetMenuAnm2Sprite("Offset", 2) 1519 | local HudOffsetVisualBottomLeft = ModConfigMenu.GetMenuAnm2Sprite("Offset", 3) 1520 | 1521 | --render the menu 1522 | local leftCurrentOffset = 0 1523 | local optionsCurrentOffset = 0 1524 | ModConfigMenu.ControlsEnabled = true 1525 | function ModConfigMenu.PostRender() 1526 | local game = Game() 1527 | local isPaused = game:IsPaused() or AwaitingTextInput 1528 | 1529 | local sfx = SFXManager() 1530 | 1531 | local pressingButton = "" 1532 | 1533 | local pressingNonRebindableKey = false 1534 | local pressedToggleMenu = false 1535 | 1536 | local openMenuGlobal = Keyboard.KEY_F10 1537 | local openMenuKeyboard = ModConfigMenu.Config["Mod Config Menu"].OpenMenuKeyboard 1538 | local openMenuController = ModConfigMenu.Config["Mod Config Menu"].OpenMenuController 1539 | 1540 | local takeScreenshot = Keyboard.KEY_F12 1541 | 1542 | --handle version display on game start 1543 | if versionPrintTimer > 0 then 1544 | local bottomRight = ScreenHelper.GetScreenBottomRight(0) 1545 | 1546 | local openMenuButton = Keyboard.KEY_F10 1547 | if type(ModConfigMenu.Config["Mod Config Menu"].OpenMenuKeyboard) == "number" and 1548 | ModConfigMenu.Config["Mod Config Menu"].OpenMenuKeyboard > -1 then 1549 | openMenuButton = ModConfigMenu.Config["Mod Config Menu"].OpenMenuKeyboard 1550 | end 1551 | 1552 | local openMenuButtonString = "Unknown Key" 1553 | if InputHelper.KeyboardToString[openMenuButton] then 1554 | openMenuButtonString = InputHelper.KeyboardToString[openMenuButton] 1555 | end 1556 | 1557 | local text = "Press " .. openMenuButtonString .. " to open Mod Config Menu" 1558 | local versionPrintColor = KColor(1, 1, 0, (math.min(versionPrintTimer, 60) / 60) * 0.5) 1559 | versionPrintFont:DrawStringUTF8(text, 0, bottomRight.Y - 28, versionPrintColor, math.floor(bottomRight.X), true) 1560 | end 1561 | 1562 | --on-screen warnings 1563 | if restartWarnMessage or rerunWarnMessage then 1564 | local bottomRight = ScreenHelper.GetScreenBottomRight(0) 1565 | 1566 | local text = restartWarnMessage or rerunWarnMessage 1567 | local warningPrintColor = KColor(1, 0, 0, 1) 1568 | versionPrintFont:DrawStringUTF8(text, 0, bottomRight.Y - 28, warningPrintColor, math.floor(bottomRight.X), true) 1569 | end 1570 | 1571 | --handle toggling the menu 1572 | if ModConfigMenu.ControlsEnabled and not isPaused then 1573 | for i = 0, 4 do 1574 | if InputHelper.KeyboardTriggered(openMenuGlobal, i) 1575 | or (openMenuKeyboard > -1 and InputHelper.KeyboardTriggered(openMenuKeyboard, i)) 1576 | or (openMenuController > -1 and Input.IsButtonTriggered(openMenuController, i)) then 1577 | pressingNonRebindableKey = true 1578 | pressedToggleMenu = true 1579 | if not configMenuInPopup then 1580 | ModConfigMenu.ToggleConfigMenu() 1581 | end 1582 | end 1583 | 1584 | if InputHelper.KeyboardTriggered(takeScreenshot, i) then 1585 | pressingNonRebindableKey = true 1586 | end 1587 | end 1588 | end 1589 | 1590 | --force close the menu in some situations 1591 | if ModConfigMenu.IsVisible then 1592 | if isPaused then 1593 | ModConfigMenu.CloseConfigMenu() 1594 | end 1595 | 1596 | if not ModConfigMenu.RoomIsSafe() then 1597 | ModConfigMenu.CloseConfigMenu() 1598 | 1599 | sfx:Play(SoundEffect.SOUND_BOSS2INTRO_ERRORBUZZ, 0.75, 0, false, 1) 1600 | end 1601 | end 1602 | 1603 | --replace dead sea scrolls' controller setting to not conflict with mcm's 1604 | if DeadSeaScrollsMenu and DeadSeaScrollsMenu.GetGamepadToggleSetting then 1605 | local dssControllerToggle = DeadSeaScrollsMenu.GetGamepadToggleSetting() 1606 | 1607 | if DeadSeaScrollsMenu.SaveGamepadToggleSetting then 1608 | if openMenuController == Controller.STICK_RIGHT and 1609 | (dssControllerToggle == 1 or dssControllerToggle == 3 or dssControllerToggle == 4) then 1610 | DeadSeaScrollsMenu.SaveGamepadToggleSetting(2) --force revelations' menu to only use the left stick 1611 | elseif openMenuController == Controller.STICK_LEFT and 1612 | (dssControllerToggle == 1 or dssControllerToggle == 2 or dssControllerToggle == 4) then 1613 | DeadSeaScrollsMenu.SaveGamepadToggleSetting(3) --force revelations' menu to only use the right stick 1614 | end 1615 | end 1616 | end 1617 | 1618 | if ModConfigMenu.IsVisible then 1619 | if ModConfigMenu.ControlsEnabled and not isPaused then 1620 | for i = 0, game:GetNumPlayers() - 1 do 1621 | local player = Isaac.GetPlayer(i) 1622 | local data = player:GetData() 1623 | 1624 | --freeze players and disable their controls 1625 | player.Velocity = vecZero 1626 | 1627 | if not data.ConfigMenuPlayerPosition then 1628 | data.ConfigMenuPlayerPosition = player.Position 1629 | end 1630 | player.Position = data.ConfigMenuPlayerPosition 1631 | if not data.ConfigMenuPlayerControlsDisabled then 1632 | player.ControlsEnabled = false 1633 | data.ConfigMenuPlayerControlsDisabled = true 1634 | end 1635 | 1636 | --disable toggling revelations menu 1637 | if data.input and data.input.menu and data.input.menu.toggle then 1638 | data.input.menu.toggle = false 1639 | end 1640 | end 1641 | 1642 | if not InputHelper.MultipleButtonPressed(ignoreActionButtons) then 1643 | --pressing buttons 1644 | local downButtonPressed = InputHelper.MultipleActionTriggered(actionsDown) 1645 | if downButtonPressed then 1646 | pressingButton = "DOWN" 1647 | end 1648 | local upButtonPressed = InputHelper.MultipleActionTriggered(actionsUp) 1649 | if upButtonPressed then 1650 | pressingButton = "UP" 1651 | end 1652 | local rightButtonPressed = InputHelper.MultipleActionTriggered(actionsRight) 1653 | if rightButtonPressed then 1654 | pressingButton = "RIGHT" 1655 | end 1656 | local leftButtonPressed = InputHelper.MultipleActionTriggered(actionsLeft) 1657 | if leftButtonPressed then 1658 | pressingButton = "LEFT" 1659 | end 1660 | local backButtonPressed = InputHelper.MultipleActionTriggered(actionsBack) or 1661 | InputHelper.MultipleKeyboardTriggered({ Keyboard.KEY_BACKSPACE }) 1662 | if backButtonPressed then 1663 | pressingButton = "BACK" 1664 | local possiblyPressedButton = InputHelper.MultipleKeyboardTriggered(Keyboard) 1665 | if possiblyPressedButton then 1666 | ModConfigMenu.Config.LastBackPressed = possiblyPressedButton 1667 | end 1668 | end 1669 | local selectButtonPressed = InputHelper.MultipleActionTriggered(actionsSelect) 1670 | if selectButtonPressed then 1671 | pressingButton = "SELECT" 1672 | local possiblyPressedButton = InputHelper.MultipleKeyboardTriggered(Keyboard) 1673 | if possiblyPressedButton then 1674 | ModConfigMenu.Config.LastSelectPressed = possiblyPressedButton 1675 | end 1676 | end 1677 | if ModConfigMenu.Config["Mod Config Menu"].ResetToDefault > -1 and 1678 | InputHelper.MultipleKeyboardTriggered({ ModConfigMenu.Config["Mod Config Menu"].ResetToDefault }) then 1679 | pressingButton = "RESET" 1680 | end 1681 | 1682 | --holding buttons 1683 | if InputHelper.MultipleActionPressed(actionsDown) then 1684 | holdingCounterDown = holdingCounterDown + 1 1685 | else 1686 | holdingCounterDown = 0 1687 | end 1688 | if holdingCounterDown > 20 and holdingCounterDown % 5 == 0 then 1689 | pressingButton = "DOWN" 1690 | end 1691 | if InputHelper.MultipleActionPressed(actionsUp) then 1692 | holdingCounterUp = holdingCounterUp + 1 1693 | else 1694 | holdingCounterUp = 0 1695 | end 1696 | if holdingCounterUp > 20 and holdingCounterUp % 5 == 0 then 1697 | pressingButton = "UP" 1698 | end 1699 | if InputHelper.MultipleActionPressed(actionsRight) then 1700 | holdingCounterRight = holdingCounterRight + 1 1701 | else 1702 | holdingCounterRight = 0 1703 | end 1704 | if holdingCounterRight > 20 and holdingCounterRight % 5 == 0 then 1705 | pressingButton = "RIGHT" 1706 | end 1707 | if InputHelper.MultipleActionPressed(actionsLeft) then 1708 | holdingCounterLeft = holdingCounterLeft + 1 1709 | else 1710 | holdingCounterLeft = 0 1711 | end 1712 | if holdingCounterLeft > 20 and holdingCounterLeft % 5 == 0 then 1713 | pressingButton = "LEFT" 1714 | end 1715 | else 1716 | if InputHelper.MultipleButtonTriggered({ Controller.BUTTON_B }) then 1717 | pressingButton = "BACK" 1718 | pressingNonRebindableKey = true 1719 | end 1720 | if InputHelper.MultipleButtonTriggered({ Controller.BUTTON_A }) then 1721 | pressingButton = "SELECT" 1722 | pressingNonRebindableKey = true 1723 | end 1724 | end 1725 | 1726 | if pressingButton ~= "" then 1727 | pressingNonRebindableKey = true 1728 | end 1729 | end 1730 | 1731 | updateCurrentMenuVars() 1732 | 1733 | local lastCursorCategoryPosition = configMenuPositionCursorCategory 1734 | local lastCursorSubcategoryPosition = configMenuPositionCursorSubcategory 1735 | local lastCursorOptionsPosition = configMenuPositionCursorOption 1736 | 1737 | local enterPopup = false 1738 | local leavePopup = false 1739 | 1740 | local optionChanged = false 1741 | 1742 | local enterOptions = false 1743 | local leaveOptions = false 1744 | 1745 | local enterSubcategory = false 1746 | local leaveSubcategory = false 1747 | 1748 | if configMenuInPopup then 1749 | if currentMenuOption then 1750 | local optionType = currentMenuOption.Type 1751 | local optionCurrent = currentMenuOption.CurrentSetting 1752 | local optionOnChange = currentMenuOption.OnChange 1753 | 1754 | if optionType == ModConfigMenu.OptionType.KEYBIND_KEYBOARD 1755 | or optionType == ModConfigMenu.OptionType.KEYBIND_CONTROLLER 1756 | or currentMenuOption.OnSelect then 1757 | if not isPaused then 1758 | if pressingNonRebindableKey 1759 | and not (pressingButton == "BACK" 1760 | or pressingButton == "LEFT" 1761 | or (currentMenuOption.OnSelect and (pressingButton == "SELECT" or pressingButton == "RIGHT")) 1762 | or (currentMenuOption.IsResetKeybind and pressingButton == "RESET") 1763 | or (currentMenuOption.IsOpenMenuKeybind and pressedToggleMenu)) then 1764 | sfx:Play(SoundEffect.SOUND_BOSS2INTRO_ERRORBUZZ, 0.75, 0, false, 1) 1765 | else 1766 | local numberToChange = nil 1767 | local receivedInput = false 1768 | if optionType == ModConfigMenu.OptionType.KEYBIND_KEYBOARD or 1769 | optionType == ModConfigMenu.OptionType.KEYBIND_CONTROLLER then 1770 | numberToChange = optionCurrent 1771 | 1772 | if type(optionCurrent) == "function" then 1773 | numberToChange = optionCurrent() 1774 | end 1775 | 1776 | if pressingButton == "BACK" or pressingButton == "LEFT" then 1777 | numberToChange = nil 1778 | receivedInput = true 1779 | else 1780 | for i = 0, 4 do 1781 | if optionType == ModConfigMenu.OptionType.KEYBIND_KEYBOARD then 1782 | for j = 32, 400 do 1783 | if InputHelper.KeyboardTriggered(j, i) then 1784 | numberToChange = j 1785 | receivedInput = true 1786 | break 1787 | end 1788 | end 1789 | else 1790 | for j = 0, 31 do 1791 | if Input.IsButtonTriggered(j, i) then 1792 | numberToChange = j 1793 | receivedInput = true 1794 | break 1795 | end 1796 | end 1797 | end 1798 | end 1799 | end 1800 | elseif currentMenuOption.OnSelect then 1801 | if pressingButton == "BACK" or pressingButton == "LEFT" then 1802 | receivedInput = true 1803 | end 1804 | if pressingButton == "SELECT" or pressingButton == "RIGHT" then 1805 | numberToChange = true 1806 | receivedInput = true 1807 | end 1808 | end 1809 | 1810 | if receivedInput then 1811 | if optionType == ModConfigMenu.OptionType.KEYBIND_KEYBOARD or 1812 | optionType == ModConfigMenu.OptionType.KEYBIND_CONTROLLER then 1813 | if type(optionCurrent) == "function" then 1814 | if optionOnChange then 1815 | optionOnChange(numberToChange) 1816 | end 1817 | elseif type(optionCurrent) == "number" then 1818 | currentMenuOption.CurrentSetting = numberToChange 1819 | end 1820 | 1821 | --callback 1822 | --[[ 1823 | CustomCallbackHelper.CallCallbacks 1824 | ( 1825 | CustomCallbacks.MCM_POST_MODIFY_SETTING, --callback id 1826 | nil, 1827 | {currentMenuOption.CurrentSetting, numberToChange}, --args to send 1828 | {currentMenuCategory.Name, currentMenuOption.Attribute} --extra variables 1829 | ) 1830 | --]] 1831 | elseif currentMenuOption.OnSelect and numberToChange then 1832 | currentMenuOption.OnSelect() 1833 | end 1834 | 1835 | leavePopup = true 1836 | 1837 | local sound = currentMenuOption.Sound 1838 | if not sound then 1839 | sound = SoundEffect.SOUND_PLOP 1840 | end 1841 | if sound >= 0 then 1842 | sfx:Play(sound, 1, 0, false, 1) 1843 | end 1844 | end 1845 | end 1846 | end 1847 | end 1848 | end 1849 | 1850 | if currentMenuOption.Restart or currentMenuOption.Rerun then 1851 | --confirmed left press 1852 | if pressingButton == "RIGHT" then 1853 | leavePopup = true 1854 | end 1855 | 1856 | --confirmed back press 1857 | if pressingButton == "SELECT" then 1858 | leavePopup = true 1859 | end 1860 | end 1861 | 1862 | --confirmed left press 1863 | if pressingButton == "LEFT" then 1864 | leavePopup = true 1865 | end 1866 | 1867 | --confirmed back press 1868 | if pressingButton == "BACK" then 1869 | leavePopup = true 1870 | end 1871 | elseif configMenuInOptions then 1872 | --confirmed down press 1873 | if pressingButton == "DOWN" then 1874 | configMenuPositionCursorOption = configMenuPositionCursorOption + 1 --move options cursor down 1875 | end 1876 | 1877 | --confirmed up press 1878 | if pressingButton == "UP" then 1879 | configMenuPositionCursorOption = configMenuPositionCursorOption - 1 --move options cursor up 1880 | end 1881 | 1882 | if pressingButton == "SELECT" or pressingButton == "RIGHT" or pressingButton == "LEFT" or 1883 | (pressingButton == "RESET" and currentMenuOption and currentMenuOption.Default ~= nil) then 1884 | if pressingButton == "LEFT" then 1885 | leaveOptions = true 1886 | end 1887 | 1888 | if currentMenuOption then 1889 | local optionType = currentMenuOption.Type 1890 | local optionCurrent = currentMenuOption.CurrentSetting 1891 | local optionOnChange = currentMenuOption.OnChange 1892 | 1893 | if optionType == ModConfigMenu.OptionType.SCROLL or optionType == ModConfigMenu.OptionType.NUMBER then 1894 | leaveOptions = false 1895 | 1896 | local numberToChange = optionCurrent 1897 | 1898 | if type(optionCurrent) == "function" then 1899 | numberToChange = optionCurrent() 1900 | end 1901 | 1902 | local modifyBy = currentMenuOption.ModifyBy or 1 1903 | modifyBy = math.max(modifyBy, 0.001) 1904 | if math.floor(modifyBy) == modifyBy then --force modify by into being an integer instead of a float if it should be 1905 | modifyBy = math.floor(modifyBy) 1906 | end 1907 | 1908 | if pressingButton == "RIGHT" or pressingButton == "SELECT" then 1909 | numberToChange = numberToChange + modifyBy 1910 | elseif pressingButton == "LEFT" then 1911 | numberToChange = numberToChange - modifyBy 1912 | elseif pressingButton == "RESET" and currentMenuOption.Default ~= nil then 1913 | numberToChange = currentMenuOption.Default 1914 | if type(currentMenuOption.Default) == "function" then 1915 | numberToChange = currentMenuOption.Default() 1916 | end 1917 | end 1918 | 1919 | if optionType == ModConfigMenu.OptionType.SCROLL then 1920 | numberToChange = math.max(math.min(math.floor(numberToChange), 10), 0) 1921 | else 1922 | if currentMenuOption.Maximum and numberToChange > currentMenuOption.Maximum then 1923 | if not currentMenuOption.NoLoopFromMaxMin and currentMenuOption.Minimum then 1924 | numberToChange = currentMenuOption.Minimum 1925 | else 1926 | numberToChange = currentMenuOption.Maximum 1927 | end 1928 | end 1929 | if currentMenuOption.Minimum and numberToChange < currentMenuOption.Minimum then 1930 | if not currentMenuOption.NoLoopFromMaxMin and currentMenuOption.Maximum then 1931 | numberToChange = currentMenuOption.Maximum 1932 | else 1933 | numberToChange = currentMenuOption.Minimum 1934 | end 1935 | end 1936 | end 1937 | 1938 | if math.floor(modifyBy) ~= modifyBy then --check if modify by is a float 1939 | numberToChange = math.floor((numberToChange * 1000) + 0.5) * 0.001 1940 | else 1941 | numberToChange = math.floor(numberToChange) 1942 | end 1943 | 1944 | if type(optionCurrent) == "function" then 1945 | if optionOnChange then 1946 | optionOnChange(numberToChange) 1947 | end 1948 | optionChanged = true 1949 | elseif type(optionCurrent) == "number" then 1950 | currentMenuOption.CurrentSetting = numberToChange 1951 | optionChanged = true 1952 | end 1953 | 1954 | --callback 1955 | --[[ 1956 | CustomCallbackHelper.CallCallbacks 1957 | ( 1958 | CustomCallbacks.MCM_POST_MODIFY_SETTING, --callback id 1959 | nil, 1960 | {currentMenuOption.CurrentSetting, numberToChange}, --args to send 1961 | {currentMenuCategory.Name, currentMenuOption.Attribute} --extra variables 1962 | ) 1963 | --]] 1964 | local sound = currentMenuOption.Sound 1965 | if not sound then 1966 | sound = SoundEffect.SOUND_PLOP 1967 | end 1968 | if sound >= 0 then 1969 | sfx:Play(sound, 1, 0, false, 1) 1970 | end 1971 | elseif optionType == ModConfigMenu.OptionType.BOOLEAN then 1972 | leaveOptions = false 1973 | 1974 | local boolToChange = optionCurrent 1975 | 1976 | if type(optionCurrent) == "function" then 1977 | boolToChange = optionCurrent() 1978 | end 1979 | 1980 | if pressingButton == "RESET" and currentMenuOption.Default ~= nil then 1981 | boolToChange = currentMenuOption.Default 1982 | if type(currentMenuOption.Default) == "function" then 1983 | boolToChange = currentMenuOption.Default() 1984 | end 1985 | else 1986 | boolToChange = (not boolToChange) 1987 | end 1988 | 1989 | if type(optionCurrent) == "function" then 1990 | if optionOnChange then 1991 | optionOnChange(boolToChange) 1992 | end 1993 | optionChanged = true 1994 | elseif type(optionCurrent) == "boolean" then 1995 | currentMenuOption.CurrentSetting = boolToChange 1996 | optionChanged = true 1997 | end 1998 | 1999 | --callback 2000 | --[[ 2001 | CustomCallbackHelper.CallCallbacks 2002 | ( 2003 | CustomCallbacks.MCM_POST_MODIFY_SETTING, --callback id 2004 | nil, 2005 | {currentMenuOption.CurrentSetting, boolToChange}, --args to send 2006 | {currentMenuCategory.Name, currentMenuOption.Attribute} --extra variables 2007 | ) 2008 | --]] 2009 | local sound = currentMenuOption.Sound 2010 | if not sound then 2011 | sound = SoundEffect.SOUND_PLOP 2012 | end 2013 | if sound >= 0 then 2014 | sfx:Play(sound, 1, 0, false, 1) 2015 | end 2016 | elseif ( 2017 | optionType == ModConfigMenu.OptionType.KEYBIND_KEYBOARD or 2018 | optionType == ModConfigMenu.OptionType.KEYBIND_CONTROLLER) and pressingButton == "RESET" and 2019 | currentMenuOption.Default ~= nil then 2020 | local numberToChange = optionCurrent 2021 | 2022 | if type(optionCurrent) == "function" then 2023 | numberToChange = optionCurrent() 2024 | end 2025 | 2026 | numberToChange = currentMenuOption.Default 2027 | if type(currentMenuOption.Default) == "function" then 2028 | numberToChange = currentMenuOption.Default() 2029 | end 2030 | 2031 | if type(optionCurrent) == "function" then 2032 | if optionOnChange then 2033 | optionOnChange(numberToChange) 2034 | end 2035 | optionChanged = true 2036 | elseif type(optionCurrent) == "number" then 2037 | currentMenuOption.CurrentSetting = numberToChange 2038 | optionChanged = true 2039 | end 2040 | 2041 | --callback 2042 | --[[ 2043 | CustomCallbackHelper.CallCallbacks 2044 | ( 2045 | CustomCallbacks.MCM_POST_MODIFY_SETTING, --callback id 2046 | nil, 2047 | {currentMenuOption.CurrentSetting, numberToChange}, --args to send 2048 | {currentMenuCategory.Name, currentMenuOption.Attribute} --extra variables 2049 | ) 2050 | --]] 2051 | local sound = currentMenuOption.Sound 2052 | if not sound then 2053 | sound = SoundEffect.SOUND_PLOP 2054 | end 2055 | if sound >= 0 then 2056 | sfx:Play(sound, 1, 0, false, 1) 2057 | end 2058 | elseif optionType ~= ModConfigMenu.OptionType.SPACE and pressingButton == "RIGHT" then 2059 | if currentMenuOption.Popup then 2060 | enterPopup = true 2061 | elseif currentMenuOption.OnSelect then 2062 | currentMenuOption.OnSelect() 2063 | end 2064 | end 2065 | end 2066 | end 2067 | 2068 | --confirmed back press 2069 | if pressingButton == "BACK" then 2070 | leaveOptions = true 2071 | end 2072 | 2073 | --confirmed select press 2074 | if pressingButton == "SELECT" then 2075 | if currentMenuOption then 2076 | if currentMenuOption.Popup then 2077 | enterPopup = true 2078 | elseif currentMenuOption.OnSelect then 2079 | currentMenuOption.OnSelect() 2080 | end 2081 | end 2082 | end 2083 | 2084 | --reset command 2085 | if optionChanged then 2086 | if currentMenuOption.Restart or currentMenuOption.Rerun then 2087 | enterPopup = true 2088 | end 2089 | end 2090 | elseif configMenuInSubcategory then 2091 | local hasUsableCategories = false 2092 | if currentMenuCategory.Subcategories then 2093 | for j = 1, #currentMenuCategory.Subcategories do 2094 | if currentMenuCategory.Subcategories[j].Name ~= "Uncategorized" then 2095 | hasUsableCategories = true 2096 | end 2097 | end 2098 | end 2099 | if hasUsableCategories then 2100 | --confirmed down press 2101 | if pressingButton == "DOWN" then 2102 | enterOptions = true 2103 | end 2104 | 2105 | --confirmed up press 2106 | if pressingButton == "UP" then 2107 | leaveSubcategory = true 2108 | end 2109 | 2110 | --confirmed right press 2111 | if pressingButton == "RIGHT" then 2112 | configMenuPositionCursorSubcategory = configMenuPositionCursorSubcategory + 1 --move right down 2113 | end 2114 | 2115 | --confirmed left press 2116 | if pressingButton == "LEFT" then 2117 | configMenuPositionCursorSubcategory = configMenuPositionCursorSubcategory - 1 --move cursor left 2118 | end 2119 | 2120 | --confirmed back press 2121 | if pressingButton == "BACK" then 2122 | leaveSubcategory = true 2123 | end 2124 | 2125 | --confirmed select press 2126 | if pressingButton == "SELECT" then 2127 | enterOptions = true 2128 | end 2129 | end 2130 | else 2131 | --confirmed down press 2132 | if pressingButton == "DOWN" then 2133 | configMenuPositionCursorCategory = configMenuPositionCursorCategory + 1 --move left cursor down 2134 | end 2135 | 2136 | --confirmed up press 2137 | if pressingButton == "UP" then 2138 | configMenuPositionCursorCategory = configMenuPositionCursorCategory - 1 --move left cursor up 2139 | end 2140 | 2141 | --confirmed right press 2142 | if pressingButton == "RIGHT" then 2143 | enterSubcategory = true 2144 | end 2145 | 2146 | --confirmed back press 2147 | if pressingButton == "BACK" then 2148 | ModConfigMenu.CloseConfigMenu() 2149 | end 2150 | 2151 | --confirmed select press 2152 | if pressingButton == "SELECT" then 2153 | enterSubcategory = true 2154 | end 2155 | end 2156 | 2157 | --entering popup 2158 | if enterPopup then 2159 | ModConfigMenu.EnterPopup() 2160 | end 2161 | 2162 | --leaving popup 2163 | if leavePopup then 2164 | ModConfigMenu.LeavePopup() 2165 | end 2166 | 2167 | --entering subcategory 2168 | if enterSubcategory then 2169 | ModConfigMenu.EnterSubcategory() 2170 | end 2171 | 2172 | --entering options 2173 | if enterOptions then 2174 | ModConfigMenu.EnterOptions() 2175 | end 2176 | 2177 | --leaving options 2178 | if leaveOptions then 2179 | ModConfigMenu.LeaveOptions() 2180 | end 2181 | 2182 | --leaving subcategory 2183 | if leaveSubcategory then 2184 | ModConfigMenu.LeaveSubcategory() 2185 | end 2186 | 2187 | --category cursor position was changed 2188 | if lastCursorCategoryPosition ~= configMenuPositionCursorCategory then 2189 | if not configMenuInSubcategory then 2190 | --cursor position 2191 | if configMenuPositionCursorCategory < 1 then --move from the top of the list to the bottom 2192 | configMenuPositionCursorCategory = #ModConfigMenu.MenuData 2193 | end 2194 | if configMenuPositionCursorCategory > #ModConfigMenu.MenuData then --move from the bottom of the list to the top 2195 | configMenuPositionCursorCategory = 1 2196 | end 2197 | 2198 | --make sure subcategory and option positions are 1 2199 | configMenuPositionCursorSubcategory = 1 2200 | configMenuPositionFirstSubcategory = 1 2201 | configMenuPositionCursorOption = 1 2202 | optionsCurrentOffset = 0 2203 | end 2204 | end 2205 | 2206 | --subcategory cursor position was changed 2207 | if lastCursorSubcategoryPosition ~= configMenuPositionCursorSubcategory then 2208 | if not configMenuInOptions then 2209 | --cursor position 2210 | if configMenuPositionCursorSubcategory < 1 then --move from the top of the list to the bottom 2211 | configMenuPositionCursorSubcategory = #currentMenuCategory.Subcategories 2212 | end 2213 | if configMenuPositionCursorSubcategory > #currentMenuCategory.Subcategories then --move from the bottom of the list to the top 2214 | configMenuPositionCursorSubcategory = 1 2215 | end 2216 | 2217 | --first category selection to render 2218 | if configMenuPositionFirstSubcategory > 1 and 2219 | configMenuPositionCursorSubcategory <= configMenuPositionFirstSubcategory + 1 then 2220 | configMenuPositionFirstSubcategory = configMenuPositionCursorSubcategory - 1 2221 | end 2222 | if configMenuPositionFirstSubcategory + (configMenuSubcategoriesCanShow - 1) < # 2223 | currentMenuCategory.Subcategories and 2224 | configMenuPositionCursorSubcategory >= 1 + (configMenuSubcategoriesCanShow - 2) then 2225 | configMenuPositionFirstSubcategory = configMenuPositionCursorSubcategory - 2226 | (configMenuSubcategoriesCanShow - 2 2227 | ) 2228 | end 2229 | configMenuPositionFirstSubcategory = math.min(math.max(configMenuPositionFirstSubcategory, 1), 2230 | #currentMenuCategory.Subcategories - (configMenuSubcategoriesCanShow - 1)) 2231 | 2232 | --make sure option positions are 1 2233 | configMenuPositionCursorOption = 1 2234 | optionsCurrentOffset = 0 2235 | end 2236 | end 2237 | 2238 | --options cursor position was changed 2239 | if lastCursorOptionsPosition ~= configMenuPositionCursorOption then 2240 | if configMenuInOptions 2241 | and currentMenuSubcategory 2242 | and currentMenuSubcategory.Options 2243 | and #currentMenuSubcategory.Options > 0 then 2244 | --find next valid option that isn't a space 2245 | local nextValidOptionSelection = configMenuPositionCursorOption 2246 | local optionIndex = configMenuPositionCursorOption 2247 | for i = 1, #currentMenuSubcategory.Options * 2 do 2248 | local thisOption = currentMenuSubcategory.Options[optionIndex] 2249 | 2250 | if thisOption 2251 | and thisOption.Type 2252 | and thisOption.Type ~= ModConfigMenu.OptionType.SPACE 2253 | and 2254 | ( 2255 | not thisOption.NoCursorHere or 2256 | (type(thisOption.NoCursorHere) == "function" and not thisOption.NoCursorHere())) 2257 | and thisOption.Display then 2258 | nextValidOptionSelection = optionIndex 2259 | 2260 | break 2261 | end 2262 | 2263 | if configMenuPositionCursorOption > lastCursorOptionsPosition then 2264 | optionIndex = optionIndex + 1 2265 | elseif configMenuPositionCursorOption < lastCursorOptionsPosition then 2266 | optionIndex = optionIndex - 1 2267 | end 2268 | if optionIndex < 1 then 2269 | optionIndex = #currentMenuSubcategory.Options 2270 | end 2271 | if optionIndex > #currentMenuSubcategory.Options then 2272 | optionIndex = 1 2273 | end 2274 | end 2275 | 2276 | configMenuPositionCursorOption = nextValidOptionSelection 2277 | 2278 | updateCurrentMenuVars() 2279 | 2280 | --first options selection to render 2281 | local hasSubcategories = false 2282 | for j = 1, #currentMenuCategory.Subcategories do 2283 | if currentMenuCategory.Subcategories[j].Name ~= "Uncategorized" then 2284 | hasSubcategories = true 2285 | end 2286 | end 2287 | if hasSubcategories then 2288 | --todo 2289 | end 2290 | end 2291 | end 2292 | 2293 | local centerPos = ScreenHelper.GetScreenCenter() 2294 | 2295 | --title pos handling 2296 | local titlePos = centerPos + Vector(68, -118) 2297 | 2298 | --left pos handling 2299 | 2300 | local leftDesiredOffset = 0 2301 | local leftCanScrollUp = false 2302 | local leftCanScrollDown = false 2303 | 2304 | local numLeft = #ModConfigMenu.MenuData 2305 | 2306 | local leftPos = centerPos + Vector(-142, -102) 2307 | local leftPosTopmost = centerPos.Y - 116 2308 | local leftPosBottommost = centerPos.Y + 90 2309 | 2310 | if numLeft > 7 then 2311 | if configMenuPositionCursorCategory > 6 then 2312 | leftCanScrollUp = true 2313 | 2314 | local cursorScroll = configMenuPositionCursorCategory - 6 2315 | local maxLeftScroll = numLeft - 8 2316 | leftDesiredOffset = math.min(cursorScroll, maxLeftScroll) * -14 2317 | 2318 | if cursorScroll < maxLeftScroll then 2319 | leftCanScrollDown = true 2320 | end 2321 | else 2322 | leftCanScrollDown = true 2323 | end 2324 | end 2325 | 2326 | if leftDesiredOffset ~= leftCurrentOffset then 2327 | local modifyOffset = math.floor(leftDesiredOffset - leftCurrentOffset) / 10 2328 | if modifyOffset > -0.1 and modifyOffset < 0 then 2329 | modifyOffset = -0.1 2330 | end 2331 | if modifyOffset < 0.1 and modifyOffset > 0 then 2332 | modifyOffset = 0.1 2333 | end 2334 | 2335 | leftCurrentOffset = leftCurrentOffset + modifyOffset 2336 | if (leftDesiredOffset - leftCurrentOffset) < 0.25 and (leftDesiredOffset - leftCurrentOffset) > -0.25 then 2337 | leftCurrentOffset = leftDesiredOffset 2338 | end 2339 | end 2340 | 2341 | if leftCurrentOffset ~= 0 then 2342 | leftPos = leftPos + Vector(0, leftCurrentOffset) 2343 | end 2344 | 2345 | --options pos handling 2346 | local optionsDesiredOffset = 0 2347 | local optionsCanScrollUp = false 2348 | local optionsCanScrollDown = false 2349 | 2350 | local numOptions = 0 2351 | 2352 | local optionPos = centerPos + Vector(68, -18) 2353 | local optionPosTopmost = centerPos.Y - 108 2354 | local optionPosBottommost = centerPos.Y + 86 2355 | 2356 | if currentMenuSubcategory 2357 | and currentMenuSubcategory.Options 2358 | and #currentMenuSubcategory.Options > 0 then 2359 | numOptions = #currentMenuSubcategory.Options 2360 | 2361 | local hasSubcategories = false 2362 | if currentMenuCategory.Subcategories then 2363 | for j = 1, #currentMenuCategory.Subcategories do 2364 | if currentMenuCategory.Subcategories[j].Name ~= "Uncategorized" then 2365 | numOptions = numOptions + 2 2366 | hasSubcategories = true 2367 | break 2368 | end 2369 | end 2370 | end 2371 | 2372 | if hasSubcategories then 2373 | optionPos = optionPos + Vector(0, -70) 2374 | else 2375 | optionPos = optionPos + Vector(0, math.min(numOptions - 1, 10) * -7) 2376 | end 2377 | 2378 | if numOptions > 12 then 2379 | if configMenuPositionCursorOption > 6 and configMenuInOptions then 2380 | optionsCanScrollUp = true 2381 | 2382 | local cursorScroll = configMenuPositionCursorOption - 6 2383 | local maxOptionsScroll = numOptions - 12 2384 | optionsDesiredOffset = math.min(cursorScroll, maxOptionsScroll) * -14 2385 | 2386 | if cursorScroll < maxOptionsScroll then 2387 | optionsCanScrollDown = true 2388 | end 2389 | else 2390 | optionsCanScrollDown = true 2391 | end 2392 | end 2393 | end 2394 | 2395 | if optionsDesiredOffset ~= optionsCurrentOffset then 2396 | local modifyOffset = math.floor(optionsDesiredOffset - optionsCurrentOffset) / 10 2397 | if modifyOffset > -0.1 and modifyOffset < 0 then 2398 | modifyOffset = -0.1 2399 | end 2400 | if modifyOffset < 0.1 and modifyOffset > 0 then 2401 | modifyOffset = 0.1 2402 | end 2403 | 2404 | optionsCurrentOffset = optionsCurrentOffset + modifyOffset 2405 | if (optionsDesiredOffset - optionsCurrentOffset) < 0.25 and (optionsDesiredOffset - optionsCurrentOffset) > -0.25 then 2406 | optionsCurrentOffset = optionsDesiredOffset 2407 | end 2408 | end 2409 | 2410 | if optionsCurrentOffset ~= 0 then 2411 | optionPos = optionPos + Vector(0, optionsCurrentOffset) 2412 | end 2413 | 2414 | --info pos handling 2415 | local infoPos = centerPos + Vector(-4, 106) 2416 | 2417 | MenuSprite:Render(centerPos, vecZero, vecZero) 2418 | 2419 | --get if controls can be shown 2420 | local shouldShowControls = true 2421 | if configMenuInOptions and currentMenuOption and currentMenuOption.HideControls then 2422 | shouldShowControls = false 2423 | end 2424 | if not ModConfigMenu.Config["Mod Config Menu"].ShowControls then 2425 | shouldShowControls = false 2426 | end 2427 | 2428 | --category 2429 | local lastLeftPos = leftPos 2430 | local renderedLeft = 0 2431 | for categoryIndex = 1, #ModConfigMenu.MenuData do 2432 | --text 2433 | if lastLeftPos.Y > leftPosTopmost and lastLeftPos.Y < leftPosBottommost then 2434 | local textToDraw = tostring(ModConfigMenu.MenuData[categoryIndex].Name) 2435 | 2436 | local color = leftFontColor 2437 | --[[ 2438 | if configMenuPositionCursorCategory == categoryIndex then 2439 | color = leftFontColorSelected 2440 | end 2441 | ]] 2442 | local posOffset = Font12:GetStringWidthUTF8(textToDraw) / 2 2443 | Font12:DrawStringUTF8(textToDraw, lastLeftPos.X - posOffset, lastLeftPos.Y - 8, color, 0, true) 2444 | 2445 | --cursor 2446 | if configMenuPositionCursorCategory == categoryIndex then 2447 | CursorSpriteRight:Render(lastLeftPos + Vector((posOffset + 10) * -1, 0), vecZero, vecZero) 2448 | end 2449 | end 2450 | 2451 | --increase counter 2452 | renderedLeft = renderedLeft + 1 2453 | 2454 | --pos mod 2455 | lastLeftPos = lastLeftPos + Vector(0, 16) 2456 | end 2457 | 2458 | --render scroll arrows 2459 | if leftCanScrollUp then 2460 | CursorSpriteUp:Render(centerPos + Vector(-78, -104), vecZero, vecZero) --up arrow 2461 | end 2462 | if leftCanScrollDown then 2463 | CursorSpriteDown:Render(centerPos + Vector(-78, 70), vecZero, vecZero) --down arrow 2464 | end 2465 | 2466 | ------------------------ 2467 | --RENDER SUBCATEGORIES-- 2468 | ------------------------ 2469 | 2470 | local lastOptionPos = optionPos 2471 | local renderedOptions = 0 2472 | 2473 | if currentMenuCategory then 2474 | local hasUncategorizedCategory = false 2475 | local hasSubcategories = false 2476 | local numCategories = 0 2477 | for j = 1, #currentMenuCategory.Subcategories do 2478 | if currentMenuCategory.Subcategories[j].Name == "Uncategorized" then 2479 | hasUncategorizedCategory = true 2480 | else 2481 | hasSubcategories = true 2482 | numCategories = numCategories + 1 2483 | end 2484 | end 2485 | 2486 | if hasSubcategories then 2487 | if hasUncategorizedCategory then 2488 | numCategories = numCategories + 1 2489 | end 2490 | 2491 | if lastOptionPos.Y > optionPosTopmost and lastOptionPos.Y < optionPosBottommost then 2492 | local lastSubcategoryPos = optionPos 2493 | if numCategories == 2 then 2494 | lastSubcategoryPos = lastOptionPos + Vector(-38, 0) 2495 | elseif numCategories >= 3 then 2496 | lastSubcategoryPos = lastOptionPos + Vector(-76, 0) 2497 | end 2498 | 2499 | local renderedSubcategories = 0 2500 | 2501 | for subcategoryIndex = 1, #currentMenuCategory.Subcategories do 2502 | if subcategoryIndex >= configMenuPositionFirstSubcategory then 2503 | local thisSubcategory = currentMenuCategory.Subcategories[subcategoryIndex] 2504 | 2505 | local posOffset = 0 2506 | 2507 | if thisSubcategory.Name then 2508 | local textToDraw = thisSubcategory.Name 2509 | 2510 | textToDraw = tostring(textToDraw) 2511 | 2512 | local color = subcategoryFontColor 2513 | if not configMenuInSubcategory then 2514 | color = subcategoryFontColorAlpha 2515 | --[[ 2516 | elseif configMenuPositionCursorSubcategory == subcategoryIndex and configMenuInSubcategory then 2517 | color = subcategoryFontColorSelected 2518 | ]] 2519 | end 2520 | 2521 | posOffset = Font12:GetStringWidthUTF8(textToDraw) / 2 2522 | Font12:DrawStringUTF8(textToDraw, lastSubcategoryPos.X - posOffset, lastSubcategoryPos.Y - 8, color, 0, true) 2523 | end 2524 | 2525 | --cursor 2526 | if configMenuPositionCursorSubcategory == subcategoryIndex and configMenuInSubcategory then 2527 | CursorSpriteRight:Render(lastSubcategoryPos + Vector((posOffset + 10) * -1, 0), vecZero, vecZero) 2528 | end 2529 | 2530 | --increase counter 2531 | renderedSubcategories = renderedSubcategories + 1 2532 | 2533 | if renderedSubcategories >= configMenuSubcategoriesCanShow then --if this is the last one we should render 2534 | --render scroll arrows 2535 | if configMenuPositionFirstSubcategory > 1 then --if the first one we rendered wasn't the first in the list 2536 | SubcategoryCursorSpriteLeft:Render(lastOptionPos + Vector(-125, 0), vecZero, vecZero) 2537 | end 2538 | 2539 | if subcategoryIndex < #currentMenuCategory.Subcategories then --if this is not the last thing 2540 | SubcategoryCursorSpriteRight:Render(lastOptionPos + Vector(125, 0), vecZero, vecZero) 2541 | end 2542 | 2543 | break 2544 | end 2545 | 2546 | --pos mod 2547 | lastSubcategoryPos = lastSubcategoryPos + Vector(76, 0) 2548 | end 2549 | end 2550 | end 2551 | 2552 | --subcategory selection counts as an option that gets rendered 2553 | renderedOptions = renderedOptions + 1 2554 | lastOptionPos = lastOptionPos + Vector(0, 14) 2555 | 2556 | --subcategory to options divider 2557 | if lastOptionPos.Y > optionPosTopmost and lastOptionPos.Y < optionPosBottommost then 2558 | SubcategoryDividerSprite:Render(lastOptionPos, vecZero, vecZero) 2559 | end 2560 | 2561 | --subcategory to options divider counts as an option that gets rendered 2562 | renderedOptions = renderedOptions + 1 2563 | lastOptionPos = lastOptionPos + Vector(0, 14) 2564 | end 2565 | end 2566 | 2567 | ------------------ 2568 | --RENDER OPTIONS-- 2569 | ------------------ 2570 | 2571 | local firstOptionPos = lastOptionPos 2572 | 2573 | if currentMenuSubcategory 2574 | and currentMenuSubcategory.Options 2575 | and #currentMenuSubcategory.Options > 0 then 2576 | for optionIndex = 1, #currentMenuSubcategory.Options do 2577 | local thisOption = currentMenuSubcategory.Options[optionIndex] 2578 | 2579 | local cursorIsAtThisOption = configMenuPositionCursorOption == optionIndex and configMenuInOptions 2580 | local posOffset = 10 2581 | 2582 | if lastOptionPos.Y > optionPosTopmost and lastOptionPos.Y < optionPosBottommost then 2583 | if thisOption.Type 2584 | and thisOption.Type ~= ModConfigMenu.OptionType.SPACE 2585 | and thisOption.Display then 2586 | local optionType = thisOption.Type 2587 | local optionDisplay = thisOption.Display 2588 | local optionColor = thisOption.Color 2589 | 2590 | local useAltSlider = thisOption.AltSlider 2591 | 2592 | --get what to draw 2593 | if optionType == ModConfigMenu.OptionType.TEXT 2594 | or optionType == ModConfigMenu.OptionType.BOOLEAN 2595 | or optionType == ModConfigMenu.OptionType.NUMBER 2596 | or optionType == ModConfigMenu.OptionType.KEYBIND_KEYBOARD 2597 | or optionType == ModConfigMenu.OptionType.KEYBIND_CONTROLLER 2598 | or optionType == ModConfigMenu.OptionType.TITLE then 2599 | local textToDraw = optionDisplay 2600 | 2601 | if type(optionDisplay) == "function" then 2602 | textToDraw = optionDisplay(cursorIsAtThisOption, configMenuInOptions, lastOptionPos) 2603 | end 2604 | 2605 | textToDraw = tostring(textToDraw) 2606 | 2607 | local heightOffset = 6 2608 | local font = Font10 2609 | local color = optionsFontColor 2610 | if not configMenuInOptions then 2611 | if thisOption.NoCursorHere then 2612 | color = optionsFontColorNoCursorAlpha 2613 | else 2614 | color = optionsFontColorAlpha 2615 | end 2616 | elseif thisOption.NoCursorHere then 2617 | color = optionsFontColorNoCursor 2618 | end 2619 | if optionType == ModConfigMenu.OptionType.TITLE then 2620 | heightOffset = 8 2621 | font = Font12 2622 | color = optionsFontColorTitle 2623 | if not configMenuInOptions then 2624 | color = optionsFontColorTitleAlpha 2625 | end 2626 | end 2627 | 2628 | if optionColor then 2629 | color = KColor(optionColor[1], optionColor[2], optionColor[3], color.Alpha) 2630 | end 2631 | 2632 | posOffset = font:GetStringWidthUTF8(textToDraw) / 2 2633 | font:DrawStringUTF8(textToDraw, lastOptionPos.X - posOffset, lastOptionPos.Y - heightOffset, color, 0, true) 2634 | elseif optionType == ModConfigMenu.OptionType.SCROLL then 2635 | local numberToShow = optionDisplay 2636 | 2637 | if type(optionDisplay) == "function" then 2638 | numberToShow = optionDisplay(cursorIsAtThisOption, configMenuInOptions, lastOptionPos) 2639 | end 2640 | 2641 | posOffset = 31 2642 | local scrollOffset = 0 2643 | 2644 | if type(numberToShow) == "number" then 2645 | numberToShow = math.max(math.min(math.floor(numberToShow), 10), 0) 2646 | elseif type(numberToShow) == "string" then 2647 | local numberToShowStart, numberToShowEnd = string.find(numberToShow, "$scroll") 2648 | if numberToShowStart and numberToShowEnd then 2649 | local numberStart = numberToShowEnd + 1 2650 | local numberEnd = numberToShowEnd + 3 2651 | local numberString = string.sub(numberToShow, numberStart, numberEnd) 2652 | numberString = tonumber(numberString) 2653 | if not numberString or (numberString and not type(numberString) == "number") or 2654 | (numberString and type(numberString) == "number" and numberString < 10) then 2655 | numberEnd = numberEnd - 1 2656 | numberString = string.sub(numberToShow, numberStart, numberEnd) 2657 | numberString = tonumber(numberString) 2658 | end 2659 | if numberString and type(numberString) == "number" then 2660 | local textToDrawPreScroll = string.sub(numberToShow, 0, numberToShowStart - 1) 2661 | local textToDrawPostScroll = string.sub(numberToShow, numberEnd, string.len(numberToShow)) 2662 | local textToDraw = textToDrawPreScroll .. " " .. textToDrawPostScroll 2663 | 2664 | local color = optionsFontColor 2665 | if not configMenuInOptions then 2666 | color = optionsFontColorAlpha 2667 | end 2668 | if optionColor then 2669 | color = KColor(optionColor[1], optionColor[2], optionColor[3], color.Alpha) 2670 | end 2671 | 2672 | scrollOffset = posOffset 2673 | posOffset = Font10:GetStringWidthUTF8(textToDraw) / 2 2674 | Font10:DrawStringUTF8(textToDraw, lastOptionPos.X - posOffset, lastOptionPos.Y - 6, color, 0, true) 2675 | 2676 | scrollOffset = posOffset - (Font10:GetStringWidthUTF8(textToDrawPreScroll) + scrollOffset) 2677 | numberToShow = numberString 2678 | end 2679 | end 2680 | end 2681 | 2682 | local scrollColor = optionsSpriteColor 2683 | if not configMenuInOptions then 2684 | scrollColor = optionsSpriteColorAlpha 2685 | end 2686 | if optionColor then 2687 | scrollColor = Color(optionColor[1], optionColor[2], optionColor[3], scrollColor.A, scrollColor.RO, 2688 | scrollColor.GO 2689 | , scrollColor.BO) 2690 | end 2691 | 2692 | local sliderString = "Slider1" 2693 | if useAltSlider then 2694 | sliderString = "Slider2" 2695 | end 2696 | 2697 | SliderSprite.Color = scrollColor 2698 | SliderSprite:SetFrame(sliderString, numberToShow) 2699 | SliderSprite:Render(lastOptionPos - Vector(scrollOffset, -2), vecZero, vecZero) 2700 | end 2701 | 2702 | local showStrikeout = thisOption.ShowStrikeout 2703 | if posOffset > 0 and (type(showStrikeout) == "boolean" and showStrikeout == true) or 2704 | (type(showStrikeout) == "function" and showStrikeout() == true) then 2705 | if configMenuInOptions then 2706 | StrikeOutSprite.Color = colorDefault 2707 | else 2708 | StrikeOutSprite.Color = colorHalf 2709 | end 2710 | StrikeOutSprite:SetFrame("Strikeout", math.floor(posOffset)) 2711 | StrikeOutSprite:Render(lastOptionPos, vecZero, vecZero) 2712 | end 2713 | end 2714 | 2715 | --cursor 2716 | if cursorIsAtThisOption then 2717 | CursorSpriteRight:Render(lastOptionPos + Vector((posOffset + 10) * -1, 0), vecZero, vecZero) 2718 | end 2719 | end 2720 | 2721 | --increase counter 2722 | renderedOptions = renderedOptions + 1 2723 | 2724 | --pos mod 2725 | lastOptionPos = lastOptionPos + Vector(0, 14) 2726 | end 2727 | 2728 | --render scroll arrows 2729 | if optionsCanScrollUp then 2730 | OptionsCursorSpriteUp:Render(centerPos + Vector(193, -86), vecZero, vecZero) --up arrow 2731 | end 2732 | if optionsCanScrollDown then 2733 | local yPos = 66 2734 | if shouldShowControls then 2735 | yPos = 40 2736 | end 2737 | 2738 | OptionsCursorSpriteDown:Render(centerPos + Vector(193, yPos), vecZero, vecZero) --down arrow 2739 | end 2740 | end 2741 | 2742 | MenuOverlaySprite:Render(centerPos, vecZero, vecZero) 2743 | 2744 | --title 2745 | local titleText = "Mod Config Menu" 2746 | if configMenuInSubcategory then 2747 | titleText = tostring(currentMenuCategory.Name) 2748 | end 2749 | local titleTextOffset = Font16Bold:GetStringWidthUTF8(titleText) / 2 2750 | Font16Bold:DrawStringUTF8(titleText, titlePos.X - titleTextOffset, titlePos.Y - 9, mainFontColor, 0, true) 2751 | 2752 | --info 2753 | local infoTable = nil 2754 | local isOldInfo = false 2755 | 2756 | if configMenuInOptions then 2757 | if currentMenuOption and currentMenuOption.Info then 2758 | infoTable = currentMenuOption.Info 2759 | end 2760 | elseif configMenuInSubcategory then 2761 | if currentMenuSubcategory and currentMenuSubcategory.Info then 2762 | infoTable = currentMenuSubcategory.Info 2763 | end 2764 | elseif currentMenuCategory and currentMenuCategory.Info then 2765 | infoTable = currentMenuCategory.Info 2766 | if currentMenuCategory.IsOld then 2767 | isOldInfo = true 2768 | end 2769 | end 2770 | 2771 | if infoTable then 2772 | local lineWidth = 340 2773 | if shouldShowControls then 2774 | lineWidth = 260 2775 | end 2776 | 2777 | local infoTableDisplay = ModConfigMenu.ConvertDisplayToTextTable(infoTable, lineWidth, Font10) 2778 | 2779 | local lastInfoPos = infoPos - Vector(0, 6 * #infoTableDisplay) 2780 | for line = 1, #infoTableDisplay do 2781 | --text 2782 | local textToDraw = tostring(infoTableDisplay[line]) 2783 | local posOffset = Font10:GetStringWidthUTF8(textToDraw) / 2 2784 | local color = mainFontColor 2785 | if isOldInfo then 2786 | color = optionsFontColorTitle 2787 | end 2788 | Font10:DrawStringUTF8(textToDraw, lastInfoPos.X - posOffset, lastInfoPos.Y - 6, color, 0, true) 2789 | 2790 | --pos mod 2791 | lastInfoPos = lastInfoPos + Vector(0, 10) 2792 | end 2793 | end 2794 | 2795 | --hud offset 2796 | if configMenuInOptions 2797 | and currentMenuOption 2798 | and currentMenuOption.ShowOffset 2799 | and ScreenHelper then 2800 | --render the visual 2801 | HudOffsetVisualBottomRight:Render(ScreenHelper.GetScreenBottomRight(), vecZero, vecZero) 2802 | HudOffsetVisualBottomLeft:Render(ScreenHelper.GetScreenBottomLeft(), vecZero, vecZero) 2803 | HudOffsetVisualTopRight:Render(ScreenHelper.GetScreenTopRight(), vecZero, vecZero) 2804 | HudOffsetVisualTopLeft:Render(ScreenHelper.GetScreenTopLeft(), vecZero, vecZero) 2805 | end 2806 | 2807 | --popup 2808 | if configMenuInPopup 2809 | and currentMenuOption 2810 | and (currentMenuOption.Popup or currentMenuOption.Restart or currentMenuOption.Rerun) then 2811 | PopupSprite:Render(centerPos, vecZero, vecZero) 2812 | 2813 | local popupTable = currentMenuOption.Popup 2814 | 2815 | if not popupTable then 2816 | if currentMenuOption.Restart then 2817 | popupTable = "Restart the game for this setting to take effect" 2818 | end 2819 | 2820 | if currentMenuOption.Rerun then 2821 | popupTable = "Start a new run for this setting to take effect" 2822 | end 2823 | end 2824 | 2825 | if popupTable then 2826 | local lineWidth = currentMenuOption.PopupWidth or 180 2827 | 2828 | local popupTableDisplay = ModConfigMenu.ConvertDisplayToTextTable(popupTable, lineWidth, Font10) 2829 | 2830 | local lastPopupPos = (centerPos + Vector(0, 2)) - Vector(0, 6 * #popupTableDisplay) 2831 | for line = 1, #popupTableDisplay do 2832 | --text 2833 | local textToDraw = tostring(popupTableDisplay[line]) 2834 | local posOffset = Font10:GetStringWidthUTF8(textToDraw) / 2 2835 | Font10:DrawStringUTF8(textToDraw, lastPopupPos.X - posOffset, lastPopupPos.Y - 6, mainFontColor, 0, true) 2836 | 2837 | --pos mod 2838 | lastPopupPos = lastPopupPos + Vector(0, 10) 2839 | end 2840 | end 2841 | end 2842 | 2843 | --controls 2844 | if shouldShowControls then 2845 | --back 2846 | local bottomLeft = ScreenHelper.GetScreenBottomLeft(0) 2847 | if not configMenuInSubcategory then 2848 | CornerExit:Render(bottomLeft, vecZero, vecZero) 2849 | else 2850 | CornerBack:Render(bottomLeft, vecZero, vecZero) 2851 | end 2852 | 2853 | local goBackString = "" 2854 | if ModConfigMenu.Config.LastBackPressed then 2855 | if InputHelper.KeyboardToString[ModConfigMenu.Config.LastBackPressed] then 2856 | goBackString = InputHelper.KeyboardToString[ModConfigMenu.Config.LastBackPressed] 2857 | elseif InputHelper.ControllerToString[ModConfigMenu.Config.LastBackPressed] then 2858 | goBackString = InputHelper.ControllerToString[ModConfigMenu.Config.LastBackPressed] 2859 | end 2860 | end 2861 | Font10:DrawStringUTF8(goBackString, (bottomLeft.X - Font10:GetStringWidthUTF8(goBackString) / 2) + 36, 2862 | bottomLeft.Y - 24, 2863 | mainFontColor, 0, true) 2864 | 2865 | --select 2866 | local bottomRight = ScreenHelper.GetScreenBottomRight(0) 2867 | if not configMenuInPopup then 2868 | local foundValidPopup = false 2869 | --[[ 2870 | if configMenuInSubcategory 2871 | and configMenuInOptions 2872 | and currentMenuOption 2873 | and currentMenuOption.Type 2874 | and currentMenuOption.Type ~= ModConfigMenu.OptionType.SPACE 2875 | and currentMenuOption.Popup then 2876 | foundValidPopup = true 2877 | end 2878 | ]] 2879 | if foundValidPopup then 2880 | CornerOpen:Render(bottomRight, vecZero, vecZero) 2881 | else 2882 | CornerSelect:Render(bottomRight, vecZero, vecZero) 2883 | end 2884 | 2885 | local selectString = "" 2886 | if ModConfigMenu.Config.LastSelectPressed then 2887 | if InputHelper.KeyboardToString[ModConfigMenu.Config.LastSelectPressed] then 2888 | selectString = InputHelper.KeyboardToString[ModConfigMenu.Config.LastSelectPressed] 2889 | elseif InputHelper.ControllerToString[ModConfigMenu.Config.LastSelectPressed] then 2890 | selectString = InputHelper.ControllerToString[ModConfigMenu.Config.LastSelectPressed] 2891 | end 2892 | end 2893 | Font10:DrawStringUTF8(selectString, (bottomRight.X - Font10:GetStringWidthUTF8(selectString) / 2) - 36, 2894 | bottomRight.Y - 24, mainFontColor, 0, true) 2895 | end 2896 | end 2897 | else 2898 | for i = 0, game:GetNumPlayers() - 1 do 2899 | local player = Isaac.GetPlayer(i) 2900 | local data = player:GetData() 2901 | 2902 | --enable player controls 2903 | if data.ConfigMenuPlayerPosition then 2904 | data.ConfigMenuPlayerPosition = nil 2905 | end 2906 | if data.ConfigMenuPlayerControlsDisabled then 2907 | player.ControlsEnabled = true 2908 | data.ConfigMenuPlayerControlsDisabled = false 2909 | end 2910 | end 2911 | 2912 | configMenuInSubcategory = false 2913 | configMenuInOptions = false 2914 | configMenuInPopup = false 2915 | 2916 | holdingCounterDown = 0 2917 | holdingCounterUp = 0 2918 | holdingCounterLeft = 0 2919 | holdingCounterRight = 0 2920 | 2921 | configMenuPositionCursorCategory = 1 2922 | configMenuPositionCursorSubcategory = 1 2923 | configMenuPositionCursorOption = 1 2924 | 2925 | configMenuPositionFirstSubcategory = 1 2926 | 2927 | leftCurrentOffset = 0 2928 | optionsCurrentOffset = 0 2929 | end 2930 | end 2931 | 2932 | ModConfigMenu.Mod:AddCallback(ModCallbacks.MC_POST_RENDER, ModConfigMenu.PostRender) 2933 | 2934 | function ModConfigMenu.OpenConfigMenu() 2935 | if ModConfigMenu.RoomIsSafe() then 2936 | if ModConfigMenu.Config["Mod Config Menu"].HideHudInMenu then 2937 | local game = Game() 2938 | if REPENTANCE then 2939 | local hud = game:GetHUD() 2940 | hud:SetVisible(false) 2941 | else 2942 | local seeds = game:GetSeeds() 2943 | seeds:AddSeedEffect(SeedEffect.SEED_NO_HUD) 2944 | end 2945 | end 2946 | 2947 | ModConfigMenu.IsVisible = true 2948 | else 2949 | local sfx = SFXManager() 2950 | sfx:Play(SoundEffect.SOUND_BOSS2INTRO_ERRORBUZZ, 0.75, 0, false, 1) 2951 | end 2952 | end 2953 | 2954 | function ModConfigMenu.CloseConfigMenu() 2955 | ModConfigMenu.LeavePopup() 2956 | ModConfigMenu.LeaveOptions() 2957 | ModConfigMenu.LeaveSubcategory() 2958 | 2959 | local game = Game() 2960 | if REPENTANCE then 2961 | local hud = game:GetHUD() 2962 | hud:SetVisible(true) 2963 | else 2964 | local seeds = game:GetSeeds() 2965 | seeds:RemoveSeedEffect(SeedEffect.SEED_NO_HUD) 2966 | end 2967 | 2968 | 2969 | ModConfigMenu.IsVisible = false 2970 | end 2971 | 2972 | function ModConfigMenu.ToggleConfigMenu() 2973 | if ModConfigMenu.IsVisible then 2974 | ModConfigMenu.CloseConfigMenu() 2975 | else 2976 | ModConfigMenu.OpenConfigMenu() 2977 | end 2978 | end 2979 | 2980 | function ModConfigMenu.InputAction(_, entity, inputHook, buttonAction) 2981 | if ModConfigMenu.IsVisible and buttonAction ~= ButtonAction.ACTION_FULLSCREEN and 2982 | buttonAction ~= ButtonAction.ACTION_CONSOLE then 2983 | if inputHook == InputHook.IS_ACTION_PRESSED or inputHook == InputHook.IS_ACTION_TRIGGERED then 2984 | return false 2985 | else 2986 | return 0 2987 | end 2988 | end 2989 | end 2990 | 2991 | ModConfigMenu.Mod:AddCallback(ModCallbacks.MC_INPUT_ACTION, ModConfigMenu.InputAction) 2992 | 2993 | --console commands that toggle the menu 2994 | local toggleCommands = { 2995 | ["modconfigmenu"] = true, 2996 | ["modconfig"] = true, 2997 | ["mcm"] = true, 2998 | ["mc"] = true 2999 | } 3000 | function ModConfigMenu.ExecuteCmd(_, command, args) 3001 | command = command:lower() 3002 | 3003 | if toggleCommands[command] then 3004 | ModConfigMenu.ToggleConfigMenu() 3005 | end 3006 | end 3007 | 3008 | ModConfigMenu.Mod:AddCallback(ModCallbacks.MC_EXECUTE_CMD, ModConfigMenu.ExecuteCmd) 3009 | 3010 | if ModConfigMenu.StandaloneMod then 3011 | if not ModConfigMenu.StandaloneSaveLoaded then 3012 | ModConfigMenu.StandaloneSaveLoaded = true 3013 | end 3014 | end 3015 | 3016 | function ModConfigMenu.GetCurrentFocus() 3017 | return { 3018 | category = currentMenuCategory, 3019 | subcategory = currentMenuSubcategory, 3020 | option = currentMenuOption 3021 | } 3022 | end 3023 | 3024 | ------------ 3025 | --FINISHED-- 3026 | ------------ 3027 | Isaac.DebugString("Mod Config Menu v" .. tostring(ModConfigMenu.Version) .. " - Loaded.") 3028 | print("Mod Config Menu v" .. tostring(ModConfigMenu.Version) .. " - Loaded.") 3029 | 3030 | return ModConfigMenu 3031 | --------------------------------------------------------------------------------