├── examples ├── fart.wav ├── cursor.png ├── loading.png ├── stupid.mp3 ├── pre_fart.wav ├── floor_puke.png ├── testlib │ └── mod.lua ├── anothermod.lua ├── require.lua ├── imports.lua ├── jetpackfuel.lua ├── exports.lua ├── vlads_cape.lua ├── death_warp.lua ├── rearm_arrowtraps.lua ├── big_explosions.lua ├── pos_type.lua ├── customized_crate_drops.lua ├── waddler_storage.lua ├── metadata.lua ├── co_subtheme.lua ├── talking_pets.lua ├── hemophobia.lua ├── save_load.lua ├── char_info.lua ├── cursor.lua ├── sparktrap_mayhem.lua ├── jelly.lua ├── drunk_dash.lua ├── prng.lua ├── spawn_hooks.lua ├── inventory.lua ├── custom_texture.lua ├── illumination.lua ├── guns.lua ├── options2.lua ├── olmec.lua ├── rando │ └── projectile.lua ├── barrymod.lua ├── checkpoint.lua ├── room_visualizer.lua ├── skipintro.lua ├── duat_from_any_altar.lua └── custom_feats.lua ├── res ├── overlunky.ico └── injector.rc ├── docs ├── fonts │ ├── slate.woff │ ├── slate.woff2 │ ├── slate-7b7da4fe.ttf │ └── slate-cfc9d06b.eot ├── images │ ├── logo-75fa61e7.png │ ├── favicon-184ef374.ico │ └── navbar-cad8cdcb.png ├── examples │ ├── savegame.md │ ├── spawn.lua │ ├── game_manager.lua │ ├── players.lua │ ├── fix_liquid_out_of_bounds.lua │ ├── pick_up.lua │ ├── state.lua │ ├── force_dark_level.lua │ ├── prinspect.lua │ ├── online.lua │ ├── activate_tiamat_position_hack.lua │ ├── Color.lua │ ├── get_bounds.lua │ ├── GuiDrawContext.lua │ ├── get_entities_by_type.lua │ ├── options.lua │ ├── get_entities_by.lua │ ├── register_option_bool.lua │ ├── clear_callback.md │ ├── set_interval.md │ ├── prng.lua │ ├── set_level_string.lua │ ├── spawn_entity.lua │ ├── Door.lua │ ├── draw_text_size.lua │ ├── set_ending_unlock.lua │ ├── io.lua │ ├── set_setting.lua │ ├── activate_sparktraps_hack.lua │ ├── Hud.lua │ ├── add_item_to_shop.lua │ ├── RenderInfo.md │ ├── meta.lua │ ├── set_camera_layer_control_enabled.lua │ ├── ON.RENDER_POST_BLURRED_BACKGROUND.lua │ ├── spawn_shopkeeper.lua │ ├── ScreenConstellation.lua │ ├── spawn_roomowner.lua │ ├── ThemeInfo.lua │ ├── CustomTheme.md │ ├── set_post_floor_update.lua │ ├── set_storage_layer.lua │ └── EntityDB.md ├── script-api.md ├── generate_slate.sh ├── src │ ├── index.html.md │ └── light.html.md ├── generate_all.sh ├── generate_util.py ├── game_data │ ├── game_settings.txt │ ├── vanilla_sound_params.txt │ └── spawn_chances.txt └── parse_cache.py ├── .gitattributes ├── src ├── game_api │ ├── string_aliases.hpp │ ├── constants.hpp │ ├── script │ │ ├── usertypes │ │ │ ├── socket_lua.hpp │ │ │ ├── bucket_lua.hpp │ │ │ ├── color_lua.hpp │ │ │ ├── drops_lua.hpp │ │ │ ├── entity_lua.hpp │ │ │ ├── hitbox_lua.hpp │ │ │ ├── logic_lua.hpp │ │ │ ├── player_lua.hpp │ │ │ ├── prng_lua.hpp │ │ │ ├── screen_lua.hpp │ │ │ ├── spawn_lua.hpp │ │ │ ├── state_lua.hpp │ │ │ ├── steam_lua.hpp │ │ │ ├── behavior_lua.hpp │ │ │ ├── flags_lua.hpp │ │ │ ├── game_manager_lua.hpp │ │ │ ├── options_lua.hpp │ │ │ ├── texture_lua.hpp │ │ │ ├── vtables_lua.hpp │ │ │ ├── deprecated_func.hpp │ │ │ ├── entities_fx_lua.hpp │ │ │ ├── global_players_lua.hpp │ │ │ ├── particles_lua.hpp │ │ │ ├── char_state_lua.hpp │ │ │ ├── entities_chars_lua.hpp │ │ │ ├── entities_items_lua.hpp │ │ │ ├── entity_casting_lua.hpp │ │ │ ├── game_patches_lua.hpp │ │ │ ├── screen_arena_lua.hpp │ │ │ ├── entities_backgrounds_lua.hpp │ │ │ ├── entities_floors_lua.hpp │ │ │ ├── entities_liquids_lua.hpp │ │ │ ├── entities_logical_lua.hpp │ │ │ ├── entities_mounts_lua.hpp │ │ │ ├── entities_monsters_lua.hpp │ │ │ ├── entities_activefloors_lua.hpp │ │ │ ├── entities_decorations_lua.hpp │ │ │ ├── sound_lua.hpp │ │ │ ├── save_context.hpp │ │ │ ├── char_state_lua.cpp │ │ │ ├── steam_lua.cpp │ │ │ ├── entities_liquids_lua.cpp │ │ │ ├── theme_vtable_lua.cpp │ │ │ ├── flags_lua.cpp │ │ │ ├── entities_decorations_lua.cpp │ │ │ ├── color_lua.cpp │ │ │ └── drops_lua.cpp │ │ ├── lua_require.hpp │ │ ├── script_util.hpp │ │ ├── handle_lua_function.hpp │ │ ├── lua_vm.hpp │ │ └── script_impl.hpp │ ├── containers │ │ ├── game_vector.hpp │ │ ├── custom_vector.hpp │ │ ├── game_string.hpp │ │ ├── custom_string.hpp │ │ ├── game_set.hpp │ │ ├── custom_set.hpp │ │ ├── game_map.hpp │ │ ├── custom_map.hpp │ │ ├── game_unordered_set.hpp │ │ ├── custom_unordered_set.hpp │ │ ├── custom_unordered_map.hpp │ │ ├── game_unordered_map.hpp │ │ ├── identity_hasher.hpp │ │ ├── game_allocator.cpp │ │ ├── custom_allocator.cpp │ │ ├── game_allocator.hpp │ │ └── custom_allocator.hpp │ ├── entity_hooks_info.hpp │ ├── overloaded.hpp │ ├── lua_libs │ │ └── lua_libs.hpp │ ├── online.cpp │ ├── entities_mounts.cpp │ ├── game_manager.cpp │ ├── audio_buffer.hpp │ ├── entities_liquids.cpp │ ├── virtual_table.cpp │ ├── entity_structs.hpp │ ├── level_api_types.hpp │ ├── hookable_vtable.hpp │ ├── strings.hpp │ ├── character_def.hpp │ ├── game_api.cpp │ ├── entities_liquids.hpp │ ├── strings_get_hashes.py │ ├── entities_items.cpp │ ├── CMakeLists.txt │ ├── entities_decorations.hpp │ ├── socket.hpp │ ├── window_api.hpp │ ├── search.hpp │ ├── entities_activefloors.cpp │ ├── drops.hpp │ ├── console.hpp │ ├── illumination.cpp │ ├── texture.hpp │ ├── game_patches.hpp │ ├── prng.cpp │ ├── savestate.cpp │ ├── illumination.hpp │ ├── file_api.hpp │ ├── ghidra_byte_string.hpp │ ├── savestate.hpp │ ├── steam_api.hpp │ └── entities_backgrounds.hpp ├── version │ ├── version.hpp │ ├── version.cpp │ └── CMakeLists.txt ├── injected │ ├── decode_audio_file.hpp │ ├── CMakeLists.txt │ ├── decode_audio_file.cpp │ └── ui.hpp ├── spel2_dll │ └── CMakeLists.txt ├── info_dump │ └── CMakeLists.txt ├── injector │ ├── CMakeLists.txt │ ├── injector.h │ ├── cmd_line.cpp │ └── cmd_line.h └── shared │ └── CMakeLists.txt ├── .gitignore ├── .clang-format ├── .github └── workflows │ ├── docs_verify.yml │ ├── build-docs.yml │ ├── clang_format_verify.yml │ ├── continous_integration.yml │ └── whip-build.yml ├── .gitmodules ├── .vscode └── tasks.json.example ├── LICENSE ├── cmake ├── clang-format.cmake └── link_sys_library.cmake └── CMakeLists.txt /examples/fart.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spelunky-fyi/overlunky/HEAD/examples/fart.wav -------------------------------------------------------------------------------- /res/overlunky.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spelunky-fyi/overlunky/HEAD/res/overlunky.ico -------------------------------------------------------------------------------- /examples/cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spelunky-fyi/overlunky/HEAD/examples/cursor.png -------------------------------------------------------------------------------- /examples/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spelunky-fyi/overlunky/HEAD/examples/loading.png -------------------------------------------------------------------------------- /examples/stupid.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spelunky-fyi/overlunky/HEAD/examples/stupid.mp3 -------------------------------------------------------------------------------- /docs/fonts/slate.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spelunky-fyi/overlunky/HEAD/docs/fonts/slate.woff -------------------------------------------------------------------------------- /docs/fonts/slate.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spelunky-fyi/overlunky/HEAD/docs/fonts/slate.woff2 -------------------------------------------------------------------------------- /examples/pre_fart.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spelunky-fyi/overlunky/HEAD/examples/pre_fart.wav -------------------------------------------------------------------------------- /examples/floor_puke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spelunky-fyi/overlunky/HEAD/examples/floor_puke.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | docs/script-api.md diff 2 | docs/game_data/spel2.lua linguist-generated -------------------------------------------------------------------------------- /docs/fonts/slate-7b7da4fe.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spelunky-fyi/overlunky/HEAD/docs/fonts/slate-7b7da4fe.ttf -------------------------------------------------------------------------------- /docs/fonts/slate-cfc9d06b.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spelunky-fyi/overlunky/HEAD/docs/fonts/slate-cfc9d06b.eot -------------------------------------------------------------------------------- /docs/images/logo-75fa61e7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spelunky-fyi/overlunky/HEAD/docs/images/logo-75fa61e7.png -------------------------------------------------------------------------------- /docs/examples/savegame.md: -------------------------------------------------------------------------------- 1 | > Print best time from savegame 2 | 3 | ```lua 4 | prinspect(savegame.time_best) 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/images/favicon-184ef374.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spelunky-fyi/overlunky/HEAD/docs/images/favicon-184ef374.ico -------------------------------------------------------------------------------- /docs/images/navbar-cad8cdcb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spelunky-fyi/overlunky/HEAD/docs/images/navbar-cad8cdcb.png -------------------------------------------------------------------------------- /docs/examples/spawn.lua: -------------------------------------------------------------------------------- 1 | -- spawn a jetpack next to the player 2 | spawn(ENT_TYPE.ITEM_JETPACK, 1, 0, LAYER.PLAYER, 0, 0) 3 | -------------------------------------------------------------------------------- /docs/examples/game_manager.lua: -------------------------------------------------------------------------------- 1 | if game_manager.game_props.game_has_focus == false then 2 | message("Come back soon!") 3 | end 4 | -------------------------------------------------------------------------------- /src/game_api/string_aliases.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | using VANILLA_SOUND = std::string; // NoAlias 6 | -------------------------------------------------------------------------------- /examples/testlib/mod.lua: -------------------------------------------------------------------------------- 1 | local module = {} 2 | 3 | function module.hello() 4 | message("Hello from testlib") 5 | end 6 | 7 | return module 8 | -------------------------------------------------------------------------------- /docs/examples/players.lua: -------------------------------------------------------------------------------- 1 | -- Make the player invisible, use only in single player only mods 2 | 3 | players[1].flags = set_flag(players[1].flags, 1) 4 | -------------------------------------------------------------------------------- /src/version/version.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | std::string_view get_version(); 6 | const char* get_version_cstr(); 7 | -------------------------------------------------------------------------------- /docs/examples/fix_liquid_out_of_bounds.lua: -------------------------------------------------------------------------------- 1 | -- call this in ON.FRAME if needed in your custom level 2 | set_callback(fix_liquid_out_of_bounds, ON.FRAME) 3 | -------------------------------------------------------------------------------- /docs/examples/pick_up.lua: -------------------------------------------------------------------------------- 1 | -- spawn and equip a jetpack on the player 2 | pick_up(players[1].uid, spawn(ENT_TYPE.ITEM_JETPACK, 0, 0, LAYER.PLAYER, 0, 0)) 3 | -------------------------------------------------------------------------------- /src/game_api/constants.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | inline constexpr std::u16string_view no_return_str{u"~[:NO_RETURN:]#"}; 6 | -------------------------------------------------------------------------------- /src/injected/decode_audio_file.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "audio_buffer.hpp" 4 | 5 | DecodedAudioBuffer LoadAudioFile(const char* file_path); 6 | -------------------------------------------------------------------------------- /docs/examples/state.lua: -------------------------------------------------------------------------------- 1 | if state.time_level > 300 and state.theme == THEME.DWELLING then 2 | toast("Congratulations for lasting 5 seconds in Dwelling") 3 | end 4 | -------------------------------------------------------------------------------- /res/injector.rc: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Icon 4 | // 5 | IDI_ICON1 ICON "overlunky.ico" 6 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/socket_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for state, optional 4 | 5 | namespace NSocket 6 | { 7 | void register_usertypes(sol::state& lua); 8 | }; 9 | -------------------------------------------------------------------------------- /docs/examples/force_dark_level.lua: -------------------------------------------------------------------------------- 1 | -- forces any level to be dark, even bosses 2 | set_callback(function() 3 | state.level_flags = set_flag(state.level_flags, 18) 4 | end, ON.POST_ROOM_GENERATION) 5 | -------------------------------------------------------------------------------- /docs/examples/prinspect.lua: -------------------------------------------------------------------------------- 1 | prinspect(state.level, state.level_next) 2 | local some_stuff_in_a_table = { 3 | some = state.time_total, 4 | stuff = state.world 5 | } 6 | prinspect(some_stuff_in_a_table) 7 | -------------------------------------------------------------------------------- /src/game_api/containers/game_vector.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "game_allocator.hpp" 4 | 5 | #include 6 | 7 | template 8 | using game_vector = std::vector>; 9 | -------------------------------------------------------------------------------- /src/game_api/containers/custom_vector.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "custom_allocator.hpp" 4 | 5 | #include 6 | 7 | template 8 | using custom_vector = std::vector>; 9 | -------------------------------------------------------------------------------- /src/game_api/containers/game_string.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "game_allocator.hpp" 4 | 5 | #include 6 | 7 | using game_string = std::basic_string, game_allocator>; 8 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/bucket_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NBucket 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/color_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NColor 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/drops_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NDrops 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/entity_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NEntity 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/hitbox_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NHitbox 9 | { 10 | void register_usertypes(sol::state& lua); 11 | } 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/logic_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NLogic 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/player_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NPlayer 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/prng_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NPRNG 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/screen_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NScreen 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/spawn_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NSpawn 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/state_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NState 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/steam_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NSteam 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/containers/custom_string.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "custom_allocator.hpp" 4 | 5 | #include 6 | 7 | using custom_string = std::basic_string, custom_allocator>; 8 | -------------------------------------------------------------------------------- /src/game_api/containers/game_set.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "game_allocator.hpp" 4 | 5 | #include 6 | 7 | template > 8 | using game_set = std::set>; 9 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/behavior_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NBehavior 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/flags_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NEntityFlags 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/game_manager_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NGM 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/options_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NOptions 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/texture_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NTexture 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/vtables_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NVTables 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea 3 | .vscode 4 | /cmake-build-debug/ 5 | /cmake-build-relwithdebinfo/ 6 | build* 7 | out 8 | .vs 9 | /CMakePresets.json 10 | *.aps 11 | docs/slate 12 | docs/.db 13 | *.pyc 14 | /CMakeSettings.json 15 | -------------------------------------------------------------------------------- /docs/examples/online.lua: -------------------------------------------------------------------------------- 1 | message = "Currently playing: " 2 | for _, p in pairs(online.online_players) do 3 | if p.ready_state ~= 0 then 4 | message = message .. p.player_name .. " " 5 | end 6 | end 7 | print(message) 8 | -------------------------------------------------------------------------------- /src/game_api/containers/custom_set.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "custom_allocator.hpp" 4 | 5 | #include 6 | 7 | template > 8 | using custom_set = std::set>; 9 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/deprecated_func.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NDeprecated 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/entities_fx_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NEntitiesFX 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/global_players_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NGPlayers 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/particles_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NParticles 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/char_state_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NCharacterState 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/entities_chars_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NEntitiesChars 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/entities_items_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NEntitiesItems 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/entity_casting_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NEntityCasting 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/game_patches_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NGamePatches 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/screen_arena_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NScreenArena 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/entities_backgrounds_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NEntitiesBG 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/entities_floors_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NEntitiesFloors 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/entities_liquids_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NEntitiesLiquids 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/entities_logical_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NEntitiesLogical 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/entities_mounts_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NEntitiesMounts 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/entities_monsters_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NEntitiesMonsters 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/entities_activefloors_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NEntitiesActiveFloors 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/entities_decorations_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | namespace NEntitiesDecorations 9 | { 10 | void register_usertypes(sol::state& lua); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/containers/game_map.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "game_allocator.hpp" 4 | 5 | #include 6 | 7 | template > 8 | using game_map = std::map>>; 9 | -------------------------------------------------------------------------------- /src/game_api/containers/custom_map.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "custom_allocator.hpp" 4 | 5 | #include 6 | 7 | template > 8 | using custom_map = std::map>>; 9 | -------------------------------------------------------------------------------- /src/game_api/containers/game_unordered_set.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "game_allocator.hpp" 4 | 5 | #include 6 | 7 | template 8 | using game_unordered_set = std::unordered_set, std::equal_to, game_allocator>; 9 | -------------------------------------------------------------------------------- /src/game_api/entity_hooks_info.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for uint32_t 4 | #include // for function 5 | 6 | template 7 | struct HookWithId 8 | { 9 | uint32_t id; 10 | std::function fun; 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_api/overloaded.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // helper type for visitor pattern 4 | template 5 | struct overloaded : Ts... 6 | { 7 | using Ts::operator()...; 8 | }; 9 | template 10 | overloaded(Ts...) -> overloaded; 11 | -------------------------------------------------------------------------------- /src/game_api/containers/custom_unordered_set.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "custom_allocator.hpp" 4 | 5 | #include 6 | 7 | template 8 | using custom_unordered_set = std::unordered_set, std::equal_to, custom_allocator>; 9 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/sound_lua.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | class SoundManager; 4 | namespace sol 5 | { 6 | class state; 7 | } // namespace sol 8 | 9 | namespace NSound 10 | { 11 | void register_usertypes(sol::state& lua, SoundManager* sound_manager); 12 | }; 13 | -------------------------------------------------------------------------------- /examples/anothermod.lua: -------------------------------------------------------------------------------- 1 | local module = {} 2 | 3 | function module.hello() 4 | if #players > 0 then 5 | message("Hello from another module and here's a jetpack") 6 | spawn(ENT_TYPE.ITEM_JETPACK, 0, 0, LAYER.PLAYER1, 0, 0) 7 | end 8 | end 9 | 10 | return module 11 | -------------------------------------------------------------------------------- /src/game_api/containers/custom_unordered_map.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "custom_allocator.hpp" 4 | 5 | #include 6 | 7 | template 8 | using custom_unordered_map = std::unordered_map, std::equal_to, custom_allocator>>; 9 | -------------------------------------------------------------------------------- /src/version/version.cpp: -------------------------------------------------------------------------------- 1 | #include "version.hpp" 2 | 3 | #define STRINGIFY(x) #x 4 | #define TOSTRING(x) STRINGIFY(x) 5 | 6 | std::string_view get_version() 7 | { 8 | return TOSTRING(GIT_VERSION); 9 | } 10 | 11 | const char* get_version_cstr() 12 | { 13 | return TOSTRING(GIT_VERSION); 14 | } 15 | -------------------------------------------------------------------------------- /docs/examples/activate_tiamat_position_hack.lua: -------------------------------------------------------------------------------- 1 | activate_tiamat_position_hack(true); 2 | 3 | set_post_entity_spawn(function(ent) 4 | 5 | -- make them same as in the game, but relative to the tiamat entity 6 | ent.attack_x = ent.x - 1 7 | ent.attack_y = ent.y + 2 8 | 9 | end, SPAWN_TYPE.ANY, 0, ENT_TYPE.MONS_TIAMAT) -------------------------------------------------------------------------------- /examples/require.lua: -------------------------------------------------------------------------------- 1 | meta.name = "Require test" 2 | meta.version = "WIP" 3 | meta.description = "Test requiring modules." 4 | meta.author = "Dregu" 5 | 6 | foo = require "testlib.mod" 7 | bar = require "anothermod" 8 | foo.hello() 9 | 10 | set_callback(function() 11 | bar.hello() 12 | end, ON.LEVEL) 13 | -------------------------------------------------------------------------------- /src/game_api/lua_libs/lua_libs.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sol 4 | { 5 | class state; 6 | } // namespace sol 7 | 8 | void require_json_lua(sol::state& lua); 9 | void require_inspect_lua(sol::state& lua); 10 | void require_format_lua(sol::state& lua); 11 | void require_serpent_lua(sol::state& lua); 12 | -------------------------------------------------------------------------------- /docs/script-api.md: -------------------------------------------------------------------------------- 1 | # Overlunky/Playlunky Lua API 2 | ## [Moved to Slate](https://spelunky-fyi.github.io/overlunky/) 3 | The source markdown files are still [available here](https://github.com/spelunky-fyi/overlunky/tree/main/docs/src/includes) if you really liked the old version for some reason or can't handle the big document. -------------------------------------------------------------------------------- /docs/generate_slate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm -rf fonts images javascripts stylesheets 3 | cp -r src/* slate/source/ 4 | cd slate 5 | bundle config set deployment 'true' 6 | bundle config path vendor/bundle 7 | bundle install --jobs 4 --retry 3 8 | bundle exec middleman build 9 | cp -r build/* .. 10 | rm -rf build 11 | cd .. 12 | -------------------------------------------------------------------------------- /src/spel2_dll/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(spel2 SHARED 2 | spel2.h 3 | spel2.cpp) 4 | target_include_directories(spel2 PUBLIC .) 5 | target_link_libraries(spel2 PRIVATE 6 | spel2_api 7 | overlunky_warnings) 8 | set_target_properties(spel2 PROPERTIES 9 | WINDOWS_EXPORT_ALL_SYMBOLS ON) 10 | -------------------------------------------------------------------------------- /examples/imports.lua: -------------------------------------------------------------------------------- 1 | meta.name = "Imports" 2 | meta.author = "Dregu" 3 | meta.version = "1.0" 4 | meta.description = "Imports the stuff from exports.lua" 5 | 6 | local stuff = import("dregu/exports", "1.0") -- version optional 7 | set_callback(function() 8 | stuff.foo("hello") 9 | print(stuff.bar()) 10 | end, ON.LEVEL) 11 | -------------------------------------------------------------------------------- /src/game_api/containers/game_unordered_map.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "game_allocator.hpp" 4 | 5 | #include 6 | 7 | template , class Equal = std::equal_to> 8 | using game_unordered_map = std::unordered_map>>; 9 | -------------------------------------------------------------------------------- /docs/examples/Color.lua: -------------------------------------------------------------------------------- 1 | -- make a semi transparent red color and print it in different formats 2 | local color = Color:red() 3 | color.a = 0.5 4 | local r, g, b, a = color:get_rgba() 5 | prinspect(r, g, b, a) -- 255, 0, 0, 128 6 | prinspect(color.r, color.g, color.b, color.a) -- 1.0, 0.0, 0.0, 0.5 7 | prinspect(string.format("%x"), color:get_ucolor()) -- 800000ff 8 | -------------------------------------------------------------------------------- /examples/jetpackfuel.lua: -------------------------------------------------------------------------------- 1 | meta.name = "Jetpack fuel test" 2 | meta.version = "WIP" 3 | meta.description = "Give unlimited jetpack fuel" 4 | meta.author = "Zappatic" 5 | 6 | -- give unlimited fuel to jetpacks 7 | set_callback(function() 8 | for i, player in ipairs(players) do 9 | player:set_jetpack_fuel(255) 10 | end 11 | end, ON.GAMEFRAME) 12 | -------------------------------------------------------------------------------- /docs/examples/get_bounds.lua: -------------------------------------------------------------------------------- 1 | -- Draw the level boundaries 2 | set_callback(function(draw_ctx) 3 | local xmin, ymax, xmax, ymin = get_bounds() 4 | local sx, sy = screen_position(xmin, ymax) -- top left 5 | local sx2, sy2 = screen_position(xmax, ymin) -- bottom right 6 | draw_ctx:draw_rect(sx, sy, sx2, sy2, 4, 0, rgba(255, 255, 255, 255)) 7 | end, ON.GUIFRAME) 8 | -------------------------------------------------------------------------------- /docs/examples/GuiDrawContext.lua: -------------------------------------------------------------------------------- 1 | -- Draw the level boundaries 2 | set_callback(function(draw_ctx) 3 | local xmin, ymax, xmax, ymin = get_bounds() 4 | local sx, sy = screen_position(xmin, ymax) -- top left 5 | local sx2, sy2 = screen_position(xmax, ymin) -- bottom right 6 | draw_ctx:draw_rect(sx, sy, sx2, sy2, 4, 0, rgba(255, 255, 255, 255)) 7 | end, ON.GUIFRAME) 8 | -------------------------------------------------------------------------------- /docs/examples/get_entities_by_type.lua: -------------------------------------------------------------------------------- 1 | local types = {ENT_TYPE.MONS_SNAKE, ENT_TYPE.MONS_BAT} 2 | set_callback(function() 3 | local uids = get_entities_by_type(ENT_TYPE.MONS_SNAKE, ENT_TYPE.MONS_BAT) 4 | -- is not the same thing as this, but also works 5 | local uids2 = get_entities_by_type(types) 6 | print(tostring(#uids).." == "..tostring(#uids2)) 7 | end, ON.LEVEL) 8 | -------------------------------------------------------------------------------- /docs/examples/options.lua: -------------------------------------------------------------------------------- 1 | register_option_bool("bomb_bag", "BombBag", "Spawn bomb bag at the start of every level", false) 2 | 3 | set_callback(function() 4 | if options.bomb_bag then 5 | -- Spawn the bomb bag at player location thanks to the LAYER.PLAYER1 6 | spawn_entity_snapped_to_floor(ENT_TYPE.ITEM_PICKUP_BOMBBAG, 0, 0, LAYER.PLAYER1) 7 | end 8 | end, ON.LEVEL) 9 | -------------------------------------------------------------------------------- /src/info_dump/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(info_dump SHARED 2 | main.cpp) 3 | target_link_libraries(info_dump PRIVATE 4 | shared 5 | spel2_api 6 | overlunky_warnings) 7 | target_link_libraries_system(info_dump PRIVATE 8 | nlohmann_json::nlohmann_json) 9 | 10 | if(MSVC) 11 | target_compile_options(info_dump PRIVATE /bigobj) 12 | endif() 13 | -------------------------------------------------------------------------------- /docs/examples/get_entities_by.lua: -------------------------------------------------------------------------------- 1 | -- find all cavemen and give them bombs 2 | -- using a type and mask in get_entities_by speeds up the search, cause the api knows which bucket to search in 3 | for i,uid in ipairs(get_entities_by(ENT_TYPE.MONS_CAVEMAN, MASK.MONSTER, LAYER.BOTH)) do 4 | local x, y, l = get_position(uid) 5 | spawn_entity_snapped_to_floor(ENT_TYPE.ITEM_BOMB, x, y, l) 6 | end 7 | -------------------------------------------------------------------------------- /docs/examples/register_option_bool.lua: -------------------------------------------------------------------------------- 1 | register_option_bool("bomb_bag", "BombBag", "Spawn bomb bag at the start of every level", false) 2 | 3 | set_callback(function() 4 | if options.bomb_bag then 5 | -- Spawn the bomb bag at player location thanks to the LAYER.PLAYER1 6 | spawn_entity_snapped_to_floor(ENT_TYPE.ITEM_PICKUP_BOMBBAG, 0, 0, LAYER.PLAYER1) 7 | end 8 | end, ON.LEVEL) 9 | -------------------------------------------------------------------------------- /src/game_api/online.cpp: -------------------------------------------------------------------------------- 1 | #include "online.hpp" 2 | 3 | #include // for check_format_string, format, vformat 4 | 5 | #include "search.hpp" // for get_address 6 | 7 | Online* get_online() 8 | { 9 | static Online* o = *(Online**)get_address("online"); 10 | return o; 11 | } 12 | 13 | std::string OnlineLobby::get_code() 14 | { 15 | return fmt::format("{:X}", code); 16 | } 17 | -------------------------------------------------------------------------------- /docs/src/index.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Overlunky/Playlunky Lua API 3 | 4 | includes: 5 | - home 6 | - globals 7 | - types 8 | - events 9 | - enums 10 | - casting 11 | 12 | search: true 13 | 14 | code_clipboard: true 15 | 16 | meta: 17 | - name: description 18 | content: Documentation for the unofficial Spelunky 2 Lua modding API 19 | --- 20 | 21 | [Switch to Light Mode](light.html) 22 | -------------------------------------------------------------------------------- /docs/src/light.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Overlunky/Playlunky Lua API 3 | 4 | includes: 5 | - home 6 | - globals 7 | - types 8 | - events 9 | - enums 10 | - casting 11 | 12 | search: true 13 | 14 | code_clipboard: true 15 | 16 | meta: 17 | - name: description 18 | content: Documentation for the unofficial Spelunky 2 Lua modding API 19 | --- 20 | 21 | [Switch to Dark Mode](index.html) 22 | -------------------------------------------------------------------------------- /src/game_api/entities_mounts.cpp: -------------------------------------------------------------------------------- 1 | #include "entities_mounts.hpp" 2 | 3 | #include "movable.hpp" // for Movable 4 | #include "search.hpp" // for get_address 5 | 6 | class Entity; 7 | 8 | void Mount::carry(Movable* rider) 9 | { 10 | using Carry = void(Entity*, Entity*); 11 | static Carry* carry = (Carry*)get_address("mount_carry"); 12 | rider->move_state = 0x11; 13 | return carry(this, rider); 14 | } 15 | -------------------------------------------------------------------------------- /docs/examples/clear_callback.md: -------------------------------------------------------------------------------- 1 | > Create three explosions and then clear the interval 2 | 3 | ```lua 4 | local count = 0 -- this upvalues to the interval 5 | set_interval(function() 6 | count = count + 1 7 | spawn(ENT_TYPE.FX_EXPLOSION, 0, 0, LAYER.FRONT, 0, 0) 8 | if count >= 3 then 9 | -- calling this without parameters clears the callback that's calling it 10 | clear_callback() 11 | end 12 | end, 60) 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/examples/set_interval.md: -------------------------------------------------------------------------------- 1 | > Create three explosions and then clear the interval 2 | 3 | ```lua 4 | local count = 0 -- this upvalues to the interval 5 | set_interval(function() 6 | count = count + 1 7 | spawn(ENT_TYPE.FX_EXPLOSION, 0, 0, LAYER.FRONT, 0, 0) 8 | if count >= 3 then 9 | -- calling this without parameters clears the fallback that's calling it 10 | clear_callback() 11 | end 12 | end, 60) 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/exports.lua: -------------------------------------------------------------------------------- 1 | meta.name = "Exports" 2 | meta.author = "Dregu" 3 | meta.version = "1.0" 4 | meta.descrtiption = "Exports functions for imports.lua and other scripts to use" 5 | 6 | local function some_local(str) 7 | print(str) 8 | end 9 | 10 | exports = { 11 | foo = function(test) 12 | some_local(test) 13 | end, 14 | bar = function() 15 | return F"{state.world}-{state.level}" 16 | end 17 | } 18 | -------------------------------------------------------------------------------- /src/game_api/game_manager.cpp: -------------------------------------------------------------------------------- 1 | #include "game_manager.hpp" 2 | 3 | #include "search.hpp" // for get_address 4 | 5 | GameManager* get_game_manager() 6 | { 7 | static GameManager** gm = (GameManager**)get_address("game_manager"sv); 8 | return *gm; 9 | } 10 | 11 | RawInput* get_raw_input() 12 | { 13 | static auto offset = get_address("input_table"); 14 | return reinterpret_cast(offset); 15 | } 16 | -------------------------------------------------------------------------------- /docs/examples/prng.lua: -------------------------------------------------------------------------------- 1 | --Make it so there is 50% chance that the Ankh will be destroyed 2 | 3 | set_callback(function () 4 | -- more or less 50% chance 5 | if prng:random(2) == 1 then 6 | -- get all Ankh's in a level 7 | ankhs = get_entities_by(ENT_TYPE.ITEM_PICKUP_ANKH, MASK.ITEM, LAYER.BOTH) 8 | for _, uid in pairs(ankhs) do 9 | get_entity(uid):destroy() 10 | end 11 | end 12 | end, ON.LEVEL) 13 | -------------------------------------------------------------------------------- /docs/generate_all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This is not for humans to run, only the workflow, as you can see it edits your git config to be a robot etc 3 | python generate_emmylua.py 4 | python generate.py 5 | ./generate_slate.sh 6 | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" 7 | git config --local user.name "github-actions[bot]" 8 | git add fonts images javascripts stylesheets 9 | git commit -am "update slate[no ci]" 10 | -------------------------------------------------------------------------------- /docs/examples/set_level_string.lua: -------------------------------------------------------------------------------- 1 | -- set the level string shown in hud, journal and game over 2 | -- also change the one used in transitions for consistency 3 | set_callback(function() 4 | if state.screen_next == SCREEN.LEVEL then 5 | local level_str = "test" .. tostring(state.level_count) 6 | set_level_string(level_str) 7 | change_string(hash_to_stringid(0xda7c0c5b), F"{level_str} COMPLETED!") 8 | end 9 | end, ON.PRE_LOAD_SCREEN) 10 | -------------------------------------------------------------------------------- /docs/examples/spawn_entity.lua: -------------------------------------------------------------------------------- 1 | -- spawn megajelly on top of player using absolute coordinates on level start 2 | set_callback(function() 3 | local x, y, layer = get_position(players[1].uid) 4 | spawn_entity(ENT_TYPE.MONS_MEGAJELLYFISH, x, y+3, layer, 0, 0) 5 | end, ON.LEVEL) 6 | 7 | -- spawn clover next to player using player-relative coordinates 8 | set_callback(function() 9 | spawn(ENT_TYPE.ITEM_PICKUP_CLOVER, 1, 0, LAYER.PLAYER1, 0, 0) 10 | end, ON.LEVEL) 11 | -------------------------------------------------------------------------------- /docs/examples/Door.lua: -------------------------------------------------------------------------------- 1 | --If you want the locked door to look like the closed exit door at Hundun 2 | 3 | function close_hundun_door(door) 4 | door:unlock(false) 5 | for _, uid in pairs(get_entities_overlapping_grid(door.x, door.y, door.layer)) do 6 | ent = get_entity(uid) 7 | if ent.type.id == ENT_TYPE.BG_DOOR then 8 | ent:set_texture(TEXTURE.DATA_TEXTURES_DECO_EGGPLANT_0) 9 | return 10 | end 11 | end 12 | end 13 | 14 | -------------------------------------------------------------------------------- /docs/examples/draw_text_size.lua: -------------------------------------------------------------------------------- 1 | -- draw text 2 | set_callback(function(draw_ctx) 3 | -- get a random color 4 | local color = math.random(0, 0xffffffff) 5 | -- zoom the font size based on frame 6 | local size = (get_frame() % 199)+1 7 | local text = 'Awesome!' 8 | -- calculate size of text 9 | local w, h = draw_text_size(size, text) 10 | -- draw to the center of screen 11 | draw_ctx:draw_text(0-w/2, 0-h/2, size, text, color) 12 | end, ON.GUIFRAME) 13 | -------------------------------------------------------------------------------- /docs/examples/set_ending_unlock.lua: -------------------------------------------------------------------------------- 1 | -- change character unlocked by endings to pilot 2 | set_ending_unlock(ENT_TYPE.CHAR_PILOT) 3 | 4 | -- change texture of the actual savior in endings to pilot 5 | set_callback(function() 6 | set_post_entity_spawn(function(ent) 7 | if state.screen == SCREEN.WIN then 8 | ent:set_texture(TEXTURE.DATA_TEXTURES_CHAR_PINK_0) 9 | end 10 | clear_callback() 11 | end, SPAWN_TYPE.SYSTEMIC, MASK.PLAYER) 12 | end, ON.WIN) 13 | -------------------------------------------------------------------------------- /docs/examples/io.lua: -------------------------------------------------------------------------------- 1 | -- Write a data file 2 | -- Data will be written to Mods/Data/[scriptname.lua or Mod Name]/timestamp.txt 3 | local f = io.open_data(tostring(os.time()) .. ".txt", "w") 4 | if f then 5 | f:write("hello world at " .. os.date()) 6 | f:close() 7 | end 8 | 9 | -- List all files in data dir and read them out 10 | for _, v in pairs(list_data_dir()) do 11 | local f = io.open_data(v) 12 | if f then 13 | print(v .. ": " .. f:read("a")) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /docs/examples/set_setting.lua: -------------------------------------------------------------------------------- 1 | -- set some visual settings needed by your mod 2 | -- doing this here will reapply these after visiting the options, which would reset them to real values 3 | 4 | set_callback(function() 5 | if state.screen_next == SCREEN.LEVEL then 6 | -- use the secret tiny hud size 7 | set_setting(GAME_SETTING.HUD_SIZE, 3) 8 | -- force opaque textboxes 9 | set_setting(GAME_SETTING.TEXTBOX_OPACITY, 0) 10 | end 11 | end, ON.PRE_LOAD_SCREEN) 12 | -------------------------------------------------------------------------------- /examples/vlads_cape.lua: -------------------------------------------------------------------------------- 1 | meta.name = "Vlad's cape multi-jump" 2 | meta.version = "WIP" 3 | meta.description = "Unlimited jumping with Vlad's cape" 4 | meta.author = "Zappatic" 5 | 6 | -- give unlimited jumps to all vlad's capes 7 | -- note that this disables floating! 8 | set_callback(function() 9 | vladscapes = get_entities_by_type(ENT_TYPE.ITEM_VLADS_CAPE) 10 | for i, cape_uid in ipairs(vladscapes) do 11 | get_entity(cape_uid).can_double_jump = true 12 | end 13 | end, ON.GAMEFRAME) 14 | -------------------------------------------------------------------------------- /docs/examples/activate_sparktraps_hack.lua: -------------------------------------------------------------------------------- 1 | activate_sparktraps_hack(true); 2 | 3 | -- set random speed, direction and distance for the spark 4 | set_post_entity_spawn(function(ent) 5 | 6 | direction = 1 7 | if prng:random_chance(2, PRNG_CLASS.ENTITY_VARIATION) then 8 | direction = -1 9 | end 10 | 11 | ent.speed = prng:random_float(PRNG_CLASS.ENTITY_VARIATION) * 0.1 * direction 12 | ent.distance = prng:random_float(PRNG_CLASS.ENTITY_VARIATION) * 10 13 | 14 | end, SPAWN_TYPE.ANY, 0, ENT_TYPE.ITEM_SPARK) -------------------------------------------------------------------------------- /src/version/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(overlunky_version STATIC 2 | version.hpp version.cpp) 3 | target_include_directories(overlunky_version PUBLIC .) 4 | 5 | execute_process( 6 | COMMAND git describe --always --dirty=-modified 7 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} 8 | OUTPUT_VARIABLE OVERLUNKY_GIT_VERSION 9 | OUTPUT_STRIP_TRAILING_WHITESPACE 10 | ) 11 | target_compile_definitions(overlunky_version PRIVATE 12 | GIT_VERSION=${OVERLUNKY_GIT_VERSION} 13 | ) 14 | -------------------------------------------------------------------------------- /src/game_api/script/lua_require.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for object 4 | #include // for string 5 | 6 | namespace sol 7 | { 8 | class state; 9 | } // namespace sol 10 | 11 | // This implementation makes the loaded chunk inherit the env from the loading chunk 12 | void register_custom_require(sol::state& lua); 13 | sol::object custom_require(std::string path); 14 | sol::object custom_loadlib(std::string path, std::string func); 15 | int custom_loader(struct lua_State* L); 16 | -------------------------------------------------------------------------------- /src/game_api/containers/identity_hasher.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | template 7 | struct identity_hasher 8 | { 9 | [[nodiscard]] size_t operator()(const T& val) const noexcept 10 | { 11 | return static_cast(val); 12 | } 13 | }; 14 | 15 | template <> 16 | struct identity_hasher 17 | { 18 | template 19 | [[nodiscard]] size_t operator()(const T& val) const noexcept 20 | { 21 | return static_cast(val); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /docs/examples/Hud.lua: -------------------------------------------------------------------------------- 1 | set_callback(function(ctx, hud) 2 | -- draw on screen bottom but keep neat animations 3 | if hud.y > 0 then hud.y = -hud.y end 4 | -- spoof some values 5 | hud.data.inventory[1].health = prng:random_int(1, 99, 0) 6 | -- hide generic pickup items 7 | hud.data.inventory[1].item_count = 0 8 | -- hide money element 9 | hud.data.money.opacity = 0 10 | -- get real current opacity of p1 inventory element 11 | prinspect(hud.data.players[1].opacity * hud.data.opacity * hud.opacity) 12 | end, ON.RENDER_PRE_HUD) 13 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | Language: Cpp 2 | Standard: c++20 3 | Cpp11BracedListStyle: true 4 | IndentWidth: 4 5 | BreakBeforeBraces: Allman 6 | AlignAfterOpenBracket: AlwaysBreak 7 | ColumnLimit: 0 8 | BinPackArguments: false 9 | BinPackParameters: false 10 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 11 | AllowAllArgumentsOnNextLine: true 12 | AllowShortBlocksOnASingleLine: Always 13 | AllowShortFunctionsOnASingleLine: None 14 | PointerAlignment: Left 15 | RequiresClausePosition: OwnLine 16 | IndentRequiresClause: false 17 | InsertNewlineAtEOF: true 18 | -------------------------------------------------------------------------------- /src/game_api/audio_buffer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | enum class SoundFormat 7 | { 8 | PCM_8, 9 | PCM_16, 10 | PCM_24, 11 | PCM_32, 12 | PCM_64, 13 | PCM_FLOAT, 14 | PCM_DOUBLE 15 | }; 16 | 17 | struct DecodedAudioBuffer 18 | { 19 | std::int32_t num_channels; 20 | std::int32_t frequency; 21 | SoundFormat format; 22 | std::unique_ptr data; 23 | std::size_t data_size; 24 | }; 25 | 26 | using DecodeAudioFile = DecodedAudioBuffer(const char* file_path); 27 | -------------------------------------------------------------------------------- /.github/workflows/docs_verify.yml: -------------------------------------------------------------------------------- 1 | name: Docs validator 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | check: 12 | runs-on: windows-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | submodules: true 17 | - uses: actions/setup-python@v4 18 | with: 19 | python-version: 3.11 20 | - name: Checking if all the types are documented in the API doc 21 | run: | 22 | cd docs 23 | python validator.py 24 | -------------------------------------------------------------------------------- /docs/examples/add_item_to_shop.lua: -------------------------------------------------------------------------------- 1 | function remove_from_shop(ent_uid) 2 | local ent = get_entity(ent_uid) 3 | if ent then 4 | -- technically this function is all you need 5 | ent:liberate_from_shop(true) 6 | end 7 | 8 | -- Be aware that removing all the items with the method below will grant you the "Big Spender" quest 9 | -- removing items from the owned_items is not required 10 | -- game only does it when you buy from the store and not when items are destroyed or shop disabled by aggro 11 | state.room_owners.owned_items:erase(ent_uid) 12 | end 13 | -------------------------------------------------------------------------------- /src/game_api/containers/game_allocator.cpp: -------------------------------------------------------------------------------- 1 | #include "game_allocator.hpp" 2 | 3 | #include // for operator""sv 4 | 5 | #include "search.hpp" // for get_address 6 | 7 | using GameMallocFun = decltype(game_malloc); 8 | using GameFreeFun = decltype(game_free); 9 | 10 | void* game_malloc(std::size_t size) 11 | { 12 | static GameMallocFun* _malloc = *(GameMallocFun**)get_address("game_malloc"sv); 13 | return _malloc(size); 14 | } 15 | void game_free(void* mem) 16 | { 17 | static GameFreeFun* _free = *(GameFreeFun**)get_address("game_free"sv); 18 | _free(mem); 19 | } 20 | -------------------------------------------------------------------------------- /examples/death_warp.lua: -------------------------------------------------------------------------------- 1 | meta.name = 'Death warp' 2 | meta.version = 'WIP' 3 | meta.description = 'Automatic instant restart on death.' 4 | meta.author = 'Dregu' 5 | 6 | -- actually instant restart on death 7 | set_callback(function() 8 | if state.screen ~= 12 then 9 | return 10 | end 11 | local hp = 0 12 | for i, player in ipairs(players) do 13 | hp = hp + player.health 14 | end 15 | if hp == 0 then 16 | state.quest_flags = set_flag(state.quest_flags, 1) 17 | warp(state.world_start, state.level_start, state.theme_start) 18 | end 19 | end, ON.FRAME) 20 | -------------------------------------------------------------------------------- /src/game_api/entities_liquids.cpp: -------------------------------------------------------------------------------- 1 | #include "entities_liquids.hpp" 2 | 3 | #include "heap_base.hpp" // for HeapBase 4 | #include "liquid_engine.hpp" // for LiquidPhysicsEngine 5 | 6 | uint32_t Liquid::get_liquid_flags() 7 | { 8 | auto liquid_engine = HeapBase::get().liquid_physics()->get_correct_liquid_engine(type->id); 9 | return liquid_engine->liquid_flags[*liquid_id]; 10 | } 11 | 12 | void Liquid::set_liquid_flags(uint32_t liquid_flags) 13 | { 14 | auto liquid_engine = HeapBase::get().liquid_physics()->get_correct_liquid_engine(type->id); 15 | liquid_engine->liquid_flags[*liquid_id] = liquid_flags; 16 | } 17 | -------------------------------------------------------------------------------- /docs/examples/RenderInfo.md: -------------------------------------------------------------------------------- 1 | > For using a custom normal map: 2 | 3 | ```lua 4 | set_post_entity_spawn(function(ent) 5 | -- Doesn't really make sense with this texture, you can use your custom normal texture id here 6 | ent.rendering_info:set_normal_map_texture(TEXTURE.DATA_TEXTURES_FLOORSTYLED_GOLD_NORMAL_0) 7 | ent.rendering_info.shader = 30 -- Make sure to set the shader to one that uses normal map 8 | end, SPAWN_TYPE.LEVEL_GEN, MASK.FLOOR, ENT_TYPE.FLOORSTYLED_MINEWOOD) 9 | ``` 10 | 11 | > Note: if using set_texture_num, make sure to have used set_second_texture/set_third_texture before, since not doing so can lead to crashes 12 | -------------------------------------------------------------------------------- /src/injector/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(injector 2 | main.cpp 3 | cmd_line.cpp 4 | cmd_line.h 5 | injector.cpp 6 | injector.h 7 | ${CMAKE_SOURCE_DIR}/res/injector.rc) 8 | target_link_libraries(injector PRIVATE 9 | shared 10 | overlunky_warnings 11 | lib_detours_overlunky) 12 | add_dependencies(injector injected) 13 | 14 | if(MSVC AND EXISTS ${SPELUNKY_INSTALL_DIR}) 15 | set_target_properties(injector PROPERTIES 16 | VS_DEBUGGER_COMMAND_ARGUMENTS "--launch_game \"${SPELUNKY_INSTALL_DIR}\"") 17 | endif() 18 | 19 | set_target_properties(injector PROPERTIES OUTPUT_NAME Overlunky) 20 | -------------------------------------------------------------------------------- /src/game_api/virtual_table.cpp: -------------------------------------------------------------------------------- 1 | #include "virtual_table.hpp" 2 | 3 | #include "memory.hpp" // for Memory 4 | #include "search.hpp" // for get_address 5 | 6 | size_t get_virtual_function_address(VTABLE_OFFSET table_entry, uint32_t function_index) 7 | { 8 | static auto first_table_entry = get_address("virtual_functions_table"); 9 | 10 | auto& mem = Memory::get(); 11 | if (first_table_entry == 0) 12 | { 13 | return 0; 14 | } 15 | size_t* func_address = reinterpret_cast(first_table_entry + ((static_cast(table_entry) + function_index) * sizeof(size_t))); 16 | return *func_address - mem.exe_address(); 17 | } 18 | -------------------------------------------------------------------------------- /docs/examples/meta.lua: -------------------------------------------------------------------------------- 1 | meta.name = "Awesome Mod" 2 | meta.version = "1.0" 3 | meta.author = "You" 4 | meta.description = [[ 5 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut at nulla porttitor, lobortis magna at, tempus dolor. Cras non ligula tellus. Duis tincidunt sodales velit et ornare. Mauris eu sapien finibus dolor dictum malesuada in non elit. 6 | 7 | Aenean luctus leo ut diam ornare viverra. Nunc condimentum interdum elit, quis porttitor quam finibus ac. Nam mattis, lectus commodo placerat tristique, justo justo congue dui, sed sodales nunc sem ut neque. 8 | ]] 9 | -- set this to enable unsafe mode 10 | --meta.unsafe = true 11 | 12 | -- rest of your mod goes here 13 | -------------------------------------------------------------------------------- /examples/rearm_arrowtraps.lua: -------------------------------------------------------------------------------- 1 | meta.name = "Rearm arrowtraps" 2 | meta.version = "WIP" 3 | meta.description = "Every ten seconds, all arrow traps (plain and poison) get rearmed" 4 | meta.author = "Zappatic" 5 | 6 | set_callback(function() 7 | set_arrowtrap_projectile(ENT_TYPE.ITEM_LIGHT_ARROW, ENT_TYPE.ITEM_METAL_ARROW) 8 | set_interval(function() 9 | arrowtraps = get_entities_by_type(ENT_TYPE.FLOOR_ARROW_TRAP, ENT_TYPE.FLOOR_POISONED_ARROW_TRAP) 10 | for i, trap_id in ipairs(arrowtraps) do 11 | trap = get_entity(trap_id) 12 | trap:rearm() 13 | end 14 | message("Rearmed "..tostring(#arrowtraps).." arrow traps") 15 | end, 600) 16 | end, ON.LEVEL) 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/generate_util.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | def setup_stdout(filename): 5 | sys.stdout = sys.__stdout__ 6 | print(f"Generating file '{filename}'...") 7 | sys.stdout = open(f"{filename}", "w") 8 | 9 | 10 | def cleanup_stdout(): 11 | sys.stdout.close() 12 | sys.stdout = sys.__stdout__ 13 | 14 | 15 | def print_collecting_info(data): 16 | print(f"Collecting {data} data...") 17 | 18 | 19 | def print_console(*args, **kwargs): 20 | sys.stdout, stdout = sys.__stdout__, sys.stdout 21 | print(*args, **kwargs) 22 | sys.stdout = stdout 23 | 24 | 25 | def breakpoint(): 26 | sys.stdout, stdout = sys.__stdout__, sys.stdout 27 | import pdb 28 | 29 | pdb.set_trace() 30 | sys.stdout = stdout 31 | -------------------------------------------------------------------------------- /docs/examples/set_camera_layer_control_enabled.lua: -------------------------------------------------------------------------------- 1 | set_camera_layer_control_enabled(false) 2 | 3 | g_current_timer = nil 4 | -- default load_time 36 5 | function change_layer(layer_to, load_time) 6 | 7 | if state.camera_layer == layer_to then 8 | return 9 | end 10 | if g_current_timer ~= nil then 11 | clear_callback(g_current_timer) 12 | g_current_timer = nil 13 | end 14 | -- if we don't want the load time, we can just change the actual layer 15 | if load_time == nil or load_time == 0 then 16 | state.camera_layer = layer_to 17 | return 18 | end 19 | 20 | state.layer_transition_timer = load_time 21 | state.transition_to_layer = layer_to 22 | state.camera_layer = layer_to 23 | end 24 | -------------------------------------------------------------------------------- /src/game_api/script/script_util.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for size_t 4 | #include // for ImVec2, ImGuiInputTextCallback, ImDrawList (ptr ... 5 | #include // for string 6 | 7 | float screenify(float dis); 8 | 9 | ImVec2 screenify(ImVec2 pos); 10 | ImVec2 screenify_fix(ImVec2 pos); 11 | 12 | ImVec2 normalize(ImVec2 pos); 13 | 14 | void AddImageRotated(ImDrawList* draw_list, ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col, float angle, const ImVec2& rel_pivot); 15 | 16 | std::string sanitize(std::string data); 17 | 18 | bool InputString(const char* label, std::string* str, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data); 19 | -------------------------------------------------------------------------------- /src/game_api/entity_structs.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for uint8_t, int32_t 4 | 5 | enum class REPEAT_TYPE : uint8_t 6 | { 7 | NoRepeat, 8 | Linear, 9 | BackAndForth, 10 | }; 11 | 12 | enum class SHAPE : uint8_t 13 | { 14 | RECTANGLE = 1, 15 | CIRCLE = 2, 16 | }; 17 | 18 | struct Animation 19 | { 20 | int32_t first_tile; 21 | // num_tiles 22 | int32_t count; 23 | int32_t interval; 24 | uint8_t id; 25 | REPEAT_TYPE repeat; 26 | }; 27 | 28 | struct Rect 29 | { 30 | float offsetx; 31 | float offsety; 32 | float hitboxx; 33 | float hitboxy; 34 | }; 35 | 36 | struct CollisionInfo 37 | { 38 | Rect rect; 39 | SHAPE shape; 40 | bool hitbox_enabled; 41 | uint8_t field_3A; 42 | uint8_t field_3B; 43 | }; 44 | -------------------------------------------------------------------------------- /src/game_api/level_api_types.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "aliases.hpp" 7 | 8 | struct ShortTileCodeDef 9 | { 10 | /// Tile code that is used by default when this short tile code is encountered. Defaults to 0. 11 | TILE_CODE tile_code{0}; 12 | /// Chance in percent to pick `tile_code` over `alt_tile_code`, ignored if `chance == 0`. Defaults to 100. 13 | uint8_t chance{100}; 14 | /// Alternative tile code, ignored if `chance == 100`. Defaults to 0. 15 | TILE_CODE alt_tile_code{0}; 16 | 17 | bool operator==(const ShortTileCodeDef&) const = default; 18 | }; 19 | 20 | using SingleRoomData = std::array, 8>; 21 | struct LevelGenRoomData 22 | { 23 | SingleRoomData front_layer; 24 | std::optional back_layer; 25 | }; 26 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/save_context.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for string 4 | #include // for string_view 5 | 6 | namespace sol 7 | { 8 | class state; 9 | } // namespace sol 10 | 11 | class SaveContext 12 | { 13 | public: 14 | SaveContext(std::string_view script_path, std::string_view script_name); 15 | 16 | bool Save(std::string data) const; 17 | 18 | private: 19 | std::string_view script_path; 20 | std::string_view script_name; 21 | }; 22 | class LoadContext 23 | { 24 | public: 25 | LoadContext(std::string_view script_path, std::string_view script_name); 26 | 27 | std::string Load() const; 28 | 29 | private: 30 | std::string_view script_path; 31 | std::string_view script_name; 32 | }; 33 | 34 | namespace NSaveContext 35 | { 36 | void register_usertypes(sol::state& lua); 37 | } 38 | -------------------------------------------------------------------------------- /docs/examples/ON.RENDER_POST_BLURRED_BACKGROUND.lua: -------------------------------------------------------------------------------- 1 | -- replace journal book background with a piece of paper, drawn on top of the blurred bg 2 | set_callback(function(ctx, blur) 3 | local src = Quad:new(AABB:new(0.535, 0.21, 0.85, 0.93)) 4 | local dest = Quad:new(AABB:new(-1, 1, 1, -1)) 5 | local col = Color:white() 6 | col.a = game_manager.journal_ui.opacity --or simply 'blur' to draw behind pause menu too 7 | ctx:draw_screen_texture(TEXTURE.DATA_TEXTURES_JOURNAL_TOP_MAIN_0, src, dest, col) 8 | -- hide real book offscreen, only drawing pages 9 | game_manager.journal_ui.book_background.y = 2 10 | end, ON.RENDER_POST_BLURRED_BACKGROUND) 11 | 12 | -- the previous cb is not called for death screen, default to normal book bg there 13 | set_pre_render_screen(SCREEN.DEATH, function(ctx) 14 | game_manager.journal_ui.book_background.y = 0 15 | end) 16 | -------------------------------------------------------------------------------- /src/game_api/hookable_vtable.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for copy 4 | #include // for size_t 5 | #include // for uint32_t 6 | 7 | #include "hook_handler.hpp" // for CallbackType 8 | #include "script/safe_cb.hpp" // for FrontBinder, BackBinder 9 | #include "util.hpp" // for function_signature, LiteralString 10 | 11 | // For usage examples see vtables_lua.cpp 12 | // Note: Can bind types that are default constructible 13 | 14 | template < 15 | LiteralString Name, 16 | std::uint32_t Index, 17 | function_signature Signature, 18 | instance_of BindBack = BackBinder<>, 19 | bool DoHooks = true> 20 | struct VTableEntry; 21 | 22 | template < 23 | class SelfT, 24 | CallbackType CbType, 25 | class... VTableEntries> 26 | struct HookableVTable; 27 | 28 | #include "hookable_vtable.inl" 29 | -------------------------------------------------------------------------------- /examples/big_explosions.lua: -------------------------------------------------------------------------------- 1 | meta.name = "Big explosions" 2 | meta.version = "WIP" 3 | meta.description = "Makes huge explosions" 4 | meta.author = "Zappatic" 5 | 6 | -- make it so players aren't affected by explosions 7 | set_explosion_mask(MASK.MOUNT | MASK.MONSTER | MASK.ITEM | MASK.ACTIVEFLOOR | MASK.FLOOR) 8 | 9 | -- enlarge the hitboxes of the explosion entities, which increases the destruction radius 10 | set_post_entity_spawn(function(ent) 11 | ent.hitboxx = 10 12 | ent.hitboxy = 10 13 | end, SPAWN_TYPE.ANY, 0, ENT_TYPE.FX_POWEREDEXPLOSION) 14 | 15 | set_post_entity_spawn(function(ent) 16 | ent.hitboxx = 10 17 | ent.hitboxy = 10 18 | end, SPAWN_TYPE.ANY, 0, ENT_TYPE.FX_EXPLOSION) 19 | 20 | -- make the bombs look bigger 21 | set_post_entity_spawn(function(ent) 22 | ent.scale_hor = 5 23 | ent.scale_ver = 5 24 | end, SPAWN_TYPE.ANY, 0, ENT_TYPE.ITEM_BOMB) 25 | -------------------------------------------------------------------------------- /src/game_api/strings.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for uint32_t 4 | #include // for u16string, allocator 5 | #include // for u16string_view 6 | #include // for vector 7 | 8 | #include "aliases.hpp" // for STRINGID 9 | 10 | const std::vector& get_string_hashes(); 11 | 12 | void strings_init(); 13 | const char16_t** get_strings_table(); 14 | STRINGID hash_to_stringid(uint32_t hash); 15 | const char16_t* get_string(STRINGID string_id); 16 | void change_string(STRINGID string_id, std::u16string_view str); 17 | STRINGID add_string(std::u16string str); 18 | void clear_custom_shopitem_names(); 19 | void add_custom_name(uint32_t uid, std::u16string name); 20 | void clear_custom_name(uint32_t uid); 21 | STRINGID pointer_to_stringid(size_t ptr); 22 | std::u16string get_entity_name(ENT_TYPE id, bool fallback_strategy); 23 | -------------------------------------------------------------------------------- /src/shared/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(shared INTERFACE) 2 | target_include_directories(shared INTERFACE . ../neo/src) 3 | target_link_libraries(shared INTERFACE 4 | overlunky_version) 5 | target_link_libraries_system(shared INTERFACE 6 | fmt) 7 | target_precompile_headers(shared INTERFACE 8 | "logger.h" 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ) 30 | 31 | target_sources(shared INTERFACE logger.h olfont.h tokenize.h) 32 | -------------------------------------------------------------------------------- /src/game_api/script/handle_lua_function.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for optional 4 | 5 | #include // for function 6 | 7 | #include "script/lua_backend.hpp" // for LuaBackend 8 | 9 | // Helper to cast an entity to its real type as a Lua userdata 10 | inline auto cast_entity(class Entity* ent); 11 | 12 | template 13 | using optional_function_result = std::conditional_t< 14 | std::is_void_v, 15 | bool, 16 | std::optional>; 17 | 18 | // Calls a Lua function and tries to cast the return value to Ret. Will return an empty optional if 19 | // the Lua function returns nothing, nil or caused an error. Errors are logged in the backend. 20 | template 21 | optional_function_result handle_function(LuaBackend* calling_backend, sol::function func, Args&&... args); 22 | 23 | #include "handle_lua_function.inl" 24 | -------------------------------------------------------------------------------- /examples/pos_type.lua: -------------------------------------------------------------------------------- 1 | meta.name = "Position Type Test" 2 | meta.author = "Dregu" 3 | meta.version = "WIP" 4 | meta.description = "Test all position_is_valid flags and draw some circles." 5 | 6 | for k,v in pairs(POS_TYPE) do 7 | register_option_bool(k, k, false) 8 | end 9 | 10 | set_callback(function(ctx) 11 | flags = 0 12 | for k,v in pairs(POS_TYPE) do 13 | if options[k] then 14 | flags = flags | v 15 | end 16 | end 17 | local ax, ay, bx, by = get_bounds() 18 | for y = ay-0.5, by+0.5, -1 do 19 | for x = ax+0.5, bx-0.5 do 20 | if position_is_valid(x, y, state.camera_layer, flags) then 21 | local sx, sy = screen_position(x, y) 22 | local d = screen_distance(0.3) 23 | ctx:draw_circle(sx, sy, d, 2, 0x9900ff00) 24 | end 25 | end 26 | end 27 | end, ON.GUIFRAME) 28 | -------------------------------------------------------------------------------- /src/injector/injector.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for LPTHREAD_START_ROUTINE, DWORD, HANDLE, LPVOID 4 | #include // for optional 5 | #include // for size_t 6 | #include // for string 7 | 8 | struct MemoryMap 9 | { 10 | size_t addr; 11 | std::string name; 12 | }; 13 | 14 | struct ProcessInfo 15 | { 16 | std::string name; 17 | DWORD pid; 18 | }; 19 | 20 | struct Process 21 | { 22 | HANDLE handle; 23 | ProcessInfo info; 24 | }; 25 | 26 | void inject_dll(const Process& proc, const std::string& name); 27 | LPTHREAD_START_ROUTINE find_function(const Process& proc, const std::string& library, const std::string& function); 28 | void call(const Process& proc, LPTHREAD_START_ROUTINE addr, LPVOID args); 29 | std::optional find_process(std::string name); 30 | bool find_dll_in_process(DWORD pid, const std::string& name); 31 | -------------------------------------------------------------------------------- /src/game_api/script/lua_vm.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for shared_ptr 4 | #include // for mutex 5 | #include // for environment, protected_function_result 6 | #include // for string_view 7 | 8 | extern std::recursive_mutex global_lua_lock; 9 | 10 | std::shared_ptr acquire_lua_vm(class SoundManager* sound_manager = nullptr); 11 | sol::state& get_lua_vm(class SoundManager* sound_manager = nullptr); 12 | 13 | sol::protected_function_result execute_lua(sol::environment& env, std::string_view code, bool pass = false); 14 | 15 | void populate_lua_env(sol::environment& env); 16 | void hide_unsafe_libraries(sol::environment& env); 17 | void expose_unsafe_libraries(sol::environment& env); 18 | void add_partial_safe_libraries(sol::environment& env); 19 | bool check_safe_io_path(const std::string& filepath, const std::string& basepath); 20 | -------------------------------------------------------------------------------- /src/injected/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(injected SHARED 2 | ui.cpp ui.hpp 3 | ui_util.cpp ui_util.hpp 4 | decode_audio_file.cpp decode_audio_file.hpp 5 | main.cpp) 6 | target_link_libraries(injected PRIVATE 7 | shared 8 | spel2_api) 9 | target_link_libraries_system(injected PUBLIC 10 | imgui 11 | toml11::toml11 12 | libnyquist 13 | Shlwapi) 14 | target_include_directories(injected PRIVATE 15 | ${LUA_INCLUDE_DIRS}) 16 | target_link_libraries(injected PRIVATE overlunky_warnings) 17 | 18 | if(MSVC) 19 | target_compile_options(injected PRIVATE /Zc:__cplusplus) 20 | target_compile_definitions(injected PRIVATE _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING) 21 | endif() 22 | 23 | target_compile_definitions(imgui PUBLIC IMGUI_USE_WCHAR32) 24 | 25 | set_target_properties(injected PROPERTIES OUTPUT_NAME Overlunky) 26 | -------------------------------------------------------------------------------- /docs/examples/spawn_shopkeeper.lua: -------------------------------------------------------------------------------- 1 | -- spawns a shopkeeper selling a shotgun next to you 2 | -- converts the current room to a ROOM_TEMPLATE.SHOP with shop music and zoom effect 3 | local x, y, l = get_position(get_player(1).uid) 4 | local owner = spawn_shopkeeper(x+1, y, l) 5 | add_item_to_shop(spawn_on_floor(ENT_TYPE.ITEM_SHOTGUN, x-1, y, l), owner) 6 | 7 | -- spawns a shopkeeper selling a puppy next to you 8 | -- also converts the room to a shop, but after the shopkeeper is spawned 9 | -- this enables the safe zone for moving items, but disables shop music and zoom for whatever reason 10 | local x, y, l = get_position(get_player(1).uid) 11 | local owner = spawn_shopkeeper(x+1, y, l, ROOM_TEMPLATE.SIDE) 12 | add_item_to_shop(spawn_on_floor(ENT_TYPE.MONS_PET_DOG, x-1, y, l), owner) 13 | local ctx = PostRoomGenerationContext:new() 14 | local rx, ry = get_room_index(x, y) 15 | ctx:set_room_template(rx, ry, l, ROOM_TEMPLATE.SHOP) 16 | -------------------------------------------------------------------------------- /examples/customized_crate_drops.lua: -------------------------------------------------------------------------------- 1 | meta.name = "Customized Crate Drops" 2 | meta.version = "WIP" 3 | meta.description = "Makes all crates drop items in the color of the opening player" 4 | meta.author = "Malacath" 5 | 6 | set_post_entity_spawn(function(crate) 7 | local function customize_drop(crate, killer_or_opener) 8 | if killer_or_opener.get_heart_color then 9 | -- declaration is seperate from definition because otherwise it can not become an upvalue of the function 10 | local cb 11 | 12 | cb = set_post_entity_spawn(function(ent) 13 | ent.color = killer_or_opener:get_heart_color() 14 | clear_callback(cb) 15 | end, SPAWN_TYPE.ANY, MASK.ANY, crate.inside) 16 | end 17 | end 18 | set_on_kill(crate.uid, customize_drop) 19 | set_on_open(crate.uid, customize_drop) 20 | end, SPAWN_TYPE.ANY, MASK.ANY, ENT_TYPE.ITEM_CRATE) 21 | -------------------------------------------------------------------------------- /src/game_api/character_def.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for uint32_t 4 | #include // for u16string_view 5 | 6 | #include "color.hpp" // for Color 7 | 8 | namespace NCharacterDB 9 | { 10 | std::uint32_t get_character_index(std::uint32_t entity_type); 11 | 12 | const char16_t* get_character_full_name(std::uint32_t character_index); 13 | const char16_t* get_character_short_name(std::uint32_t character_index); 14 | Color get_character_heart_color(std::uint32_t character_index); 15 | bool get_character_gender(std::uint32_t character_index); 16 | 17 | void set_character_full_name(std::uint32_t character_index, std::u16string_view name); 18 | void set_character_short_name(std::uint32_t character_index, std::u16string_view name); 19 | void set_character_heart_color(std::uint32_t character_index, Color color); 20 | void set_character_gender(std::uint32_t character_index, bool female); 21 | } // namespace NCharacterDB 22 | -------------------------------------------------------------------------------- /docs/examples/ScreenConstellation.lua: -------------------------------------------------------------------------------- 1 | -- forces any level transition to immediately go to constellation ending with custom text 2 | set_callback(function() 3 | if state.screen_next == SCREEN.TRANSITION then 4 | if state.level_count == 0 then state.level_count = 1 end -- no /0 5 | state.win_state = WIN_STATE.COSMIC_OCEAN_WIN 6 | state.screen_next = SCREEN.CONSTELLATION 7 | state.world_next = 8 8 | state.level_next = 99 9 | state.theme_next = THEME.COSMIC_OCEAN 10 | state.level_gen.themes[THEME.COSMIC_OCEAN].sub_theme = state.level_gen.themes[state.theme] 11 | state:force_current_theme(THEME.COSMIC_OCEAN) 12 | set_global_interval(function() 13 | if state.screen_constellation.sequence_state == 2 then 14 | state.screen_constellation.constellation_text = "Lol u stars now" 15 | clear_callback() 16 | end 17 | end, 1) 18 | end 19 | end, ON.PRE_LOAD_SCREEN) 20 | -------------------------------------------------------------------------------- /src/game_api/game_api.cpp: -------------------------------------------------------------------------------- 1 | #include "game_api.hpp" 2 | 3 | #include "search.hpp" // for get_address 4 | #include "state.hpp" // for StateMemory, get_state_ptr 5 | 6 | GameAPI* GameAPI::get() 7 | { 8 | static_assert(sizeof(GameAPI) == 0x60); 9 | using GetGameAPI = GameAPI*(); 10 | static auto addr = (GetGameAPI*)get_address("get_game_api"); 11 | return addr(); 12 | } 13 | 14 | float GameAPI::get_current_zoom() const 15 | { 16 | auto state = get_state_ptr(); 17 | return renderer->current_zoom + get_layer_transition_zoom_offset(state->camera_layer); 18 | } 19 | 20 | void GameAPI::set_zoom(std::optional current, std::optional target) 21 | { 22 | if (current.has_value()) 23 | { 24 | renderer->current_zoom = current.value(); // - renderer->current_zoom_offset; 25 | } 26 | if (target.has_value()) 27 | { 28 | renderer->target_zoom = target.value(); // - renderer->target_zoom_offset; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/game_api/entities_liquids.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for uint8_t, int32_t, uint16_t, uint32_t 4 | #include // for list, list<>::const_iterator 5 | #include // for allocator 6 | 7 | #include "entity.hpp" // for Entity 8 | 9 | struct Illumination; 10 | 11 | class Liquid : public Entity 12 | { 13 | public: 14 | Entity* fx_surface; 15 | float x_pos; 16 | float y_pos; 17 | std::list::const_iterator liquid_id; // the id's change all the time, but the iterator is static 18 | uint16_t unknown_readonly1; 19 | uint16_t unknown_readonly2; 20 | uint8_t unknown_timer1; 21 | uint8_t pos_update_timer; // when 0, updates x_pos and y_pos 22 | uint8_t unknown_timer3; 23 | uint8_t unk21; // probably padding 24 | 25 | uint32_t get_liquid_flags(); 26 | void set_liquid_flags(uint32_t flags); 27 | }; 28 | 29 | class Lava : public Liquid 30 | { 31 | public: 32 | Illumination* emitted_light; 33 | }; 34 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/imgui"] 2 | path = src/imgui 3 | url = https://github.com/ocornut/imgui 4 | [submodule "src/toml11"] 5 | path = src/toml11 6 | url = https://github.com/ToruNiina/toml11 7 | [submodule "src/sol2"] 8 | path = src/sol2 9 | url = https://github.com/ThePhD/sol2 10 | [submodule "src/fmt"] 11 | path = src/fmt 12 | url = https://github.com/fmtlib/fmt.git 13 | [submodule "src/libnyquist"] 14 | path = src/libnyquist 15 | url = https://github.com/ddiakopoulos/libnyquist.git 16 | [submodule "src/json"] 17 | path = src/json 18 | url = https://github.com/nlohmann/json.git 19 | [submodule "src/Detours"] 20 | path = src/Detours 21 | url = https://github.com/microsoft/Detours.git 22 | [submodule "src/sockpp"] 23 | path = src/sockpp 24 | url = https://github.com/fpagliughi/sockpp 25 | [submodule "docs/slate"] 26 | path = docs/slate 27 | url = https://github.com/spelunky-fyi/slate 28 | [submodule "src/neo"] 29 | path = src/neo 30 | url = https://github.com/vector-of-bool/neo-fun.git 31 | -------------------------------------------------------------------------------- /examples/waddler_storage.lua: -------------------------------------------------------------------------------- 1 | meta.name = 'Waddler Storage' 2 | meta.description = 'Save waddler items on death and load in the next run.' 3 | meta.author = 'Dregu' 4 | meta.version = '1.0' 5 | 6 | function table.clone(org) 7 | return {table.unpack(org)} 8 | end 9 | 10 | -- save in the death screen 11 | set_callback(function() 12 | storage = table.clone(state.waddler_storage) 13 | storage_meta = table.clone(state.waddler_metadata) 14 | prinspect("saved", storage) 15 | end, ON.DEATH) -- or RESET if you want to save on quick restart too 16 | 17 | -- load when loading the first level 18 | set_callback(function() 19 | if test_flag(state.quest_flags, 1) and storage and state.screen == SCREEN.LEVEL then 20 | state.waddler_storage = table.clone(storage) 21 | state.waddler_metadata = table.clone(storage_meta) 22 | storage = nil 23 | storage_meta = nil 24 | prinspect("loaded", table.clone(state.waddler_storage)) 25 | end 26 | end, ON.PRE_LEVEL_GENERATION) 27 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/char_state_lua.cpp: -------------------------------------------------------------------------------- 1 | #include "char_state_lua.hpp" 2 | 3 | #include // for state 4 | #include // for get 5 | 6 | namespace NCharacterState 7 | { 8 | void register_usertypes(sol::state& lua) 9 | { 10 | lua.create_named_table( 11 | "CHAR_STATE", 12 | "FLAILING", 13 | 0, 14 | "STANDING", 15 | 1, 16 | "SITTING", 17 | 2, 18 | "HANGING", 19 | 4, 20 | "DUCKING", 21 | 5, 22 | "CLIMBING", 23 | 6, 24 | "PUSHING", 25 | 7, 26 | "JUMPING", 27 | 8, 28 | "FALLING", 29 | 9, 30 | "DROPPING", 31 | 10, 32 | "ATTACKING", 33 | 12, 34 | "THROWING", 35 | 17, 36 | "STUNNED", 37 | 18, 38 | "ENTERING", 39 | 19, 40 | "LOADING", 41 | 20, 42 | "EXITING", 43 | 21, 44 | "DYING", 45 | 22); 46 | } 47 | } // namespace NCharacterState 48 | -------------------------------------------------------------------------------- /src/game_api/strings_get_hashes.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import sys 4 | 5 | sys.stdout = open("string_hashes.cpp", "w") 6 | 7 | strings_path = "../../docs/game_data/strings00_hashed.str" 8 | 9 | print( 10 | """ 11 | // THIS FILE IS AUTO-GENERATED 12 | // If you need to make changes to it, please change "strings_get_hashes.py" 13 | 14 | #include // for uint32_t 15 | #include // for less 16 | #include // for operator new 17 | #include // for min 18 | #include // for allocator, vector 19 | 20 | #include "aliases.hpp" // for STRINGID 21 | #include "strings.hpp" 22 | 23 | const std::vector string_hashes = {""" 24 | ) 25 | 26 | data = open(strings_path, "r").read().split("\n") 27 | for line in data: 28 | if line == "": 29 | continue 30 | if line[0] == "#": 31 | continue 32 | print(" " + line[0:10] + ",") 33 | 34 | print( 35 | """ 36 | }; 37 | 38 | const std::vector& get_string_hashes() 39 | { 40 | return string_hashes; 41 | }""" 42 | ) 43 | -------------------------------------------------------------------------------- /docs/examples/spawn_roomowner.lua: -------------------------------------------------------------------------------- 1 | -- spawns waddler selling pets 2 | -- all the aggro etc mechanics from a normal shop still apply 3 | local x, y, l = get_position(get_player(1).uid) 4 | local owner = spawn_roomowner(ENT_TYPE.MONS_STORAGEGUY, x+1, y, l, ROOM_TEMPLATE.SHOP) 5 | add_item_to_shop(spawn_on_floor(ENT_TYPE.MONS_PET_DOG, x-1, y, l), owner) 6 | add_item_to_shop(spawn_on_floor(ENT_TYPE.MONS_PET_CAT, x-2, y, l), owner) 7 | add_item_to_shop(spawn_on_floor(ENT_TYPE.MONS_PET_HAMSTER, x-3, y, l), owner) 8 | 9 | -- use in a tile code to add shops to custom levels 10 | -- this may spawn some shop related decorations too 11 | define_tile_code("pet_shop_boys") 12 | set_pre_tile_code_callback(function(x, y, layer) 13 | local owner = spawn_roomowner(ENT_TYPE.MONS_YANG, x, y, layer, ROOM_TEMPLATE.SHOP) 14 | -- another dude for the meme, this has nothing to do with the shop 15 | spawn_on_floor(ENT_TYPE.MONS_BODYGUARD, x+1, y, layer) 16 | add_item_to_shop(spawn_on_floor(ENT_TYPE.MONS_PET_HAMSTER, x+2, y, layer), owner) 17 | return true 18 | end, "pet_shop_boys") 19 | -------------------------------------------------------------------------------- /docs/game_data/game_settings.txt: -------------------------------------------------------------------------------- 1 | WINDOW_SCALE: 0 2 | RESOLUTION_SCALE: 1 3 | RESOLUTIONX: 2 4 | RESOLUTIONY: 3 5 | FREQUENCY_NUMERATOR: 4 6 | FREQUENCY_DENOMINATOR: 5 7 | WINDOW_MODE: 6 8 | VSYNC: 7 9 | MONITOR: 8 10 | VFX: 9 11 | BRIGHTNESS: 10 12 | SOUND_ENABLED: 11 13 | SOUND_VOLUME: 12 14 | MUSIC_ENABLED: 13 15 | MUSIC_VOLUME: 14 16 | MASTER_ENABLED: 15 17 | MASTER_VOLUME: 16 18 | OVERSCAN: 17 19 | CURRENT_PROFILE: 18 20 | PREV_LANGUAGE: 19 21 | PET_STYLE: 20 22 | SCREEN_SHAKE: 21 23 | INSTANT_RESTART: 22 24 | HUD_STYLE: 23 25 | HUD_SIZE: 24 26 | LEVEL_TIMER: 25 27 | TIMER_DETAIL: 26 28 | LEVEL_NUMBER: 27 29 | ANGRY_SHOPKEEPER: 28 30 | CLASSIC_AGGRO_MUSIC: 29 31 | BUTTON_PROMPTS: 30 32 | BUTTON_TEXTURE: 31 33 | FEAT_POPUPS: 32 34 | TEXTBOX_SIZE: 33 35 | TEXTBOX_DURATION: 34 36 | TEXTBOX_OPACITY: 35 37 | LEVEL_FEELINGS: 36 38 | DIALOG_TEXT: 37 39 | KALI_TEXT: 38 40 | GHOST_TEXT: 39 41 | LANGUAGE: 40 42 | BRIGHT_FLASHES: 41 43 | CROSSPLAY: 42 44 | INPUT_DELAY: 43 45 | OUTPUT_DELAY: 44 46 | PSEUDONYMIZATION: 45 47 | CROSSPROGRESS_ENABLED: 46 48 | CROSSPROGRESS_AUTOSYNC: 47 49 | -------------------------------------------------------------------------------- /examples/metadata.lua: -------------------------------------------------------------------------------- 1 | -- This script tests the metadata parser. 2 | -- When a script is first loaded in disabled state, only the lines containing metadata should be executed. 3 | -- It is not a good example what to do! 4 | 5 | -- Seriously, don't do this 6 | meta = { 7 | name = "Metadata test", 8 | version = "1.0" } 9 | 10 | meta.author= 'Dregu' 11 | 12 | -- If you came here looking for examples, this is what I think you should actually do, because it's concise and easy to read for people: 13 | --meta.name = "Metadata test" 14 | --meta.version = "0.2" 15 | --meta.author = "Dregu" 16 | --meta.description = "This script tests the metadata parser." 17 | 18 | message("Don't print this before I hit Enable") 19 | 20 | -- super dumb multiline 21 | meta.description = [[You can put these after your real code too, but don't be an idiot. 22 | This is a multiline string. 23 | Third line is the charm!]].. 24 | " Really pushing the limits here by combining these..." -- lol 25 | 26 | message("Please don't print this either") 27 | -------------------------------------------------------------------------------- /src/game_api/containers/custom_allocator.cpp: -------------------------------------------------------------------------------- 1 | #include "custom_allocator.hpp" 2 | 3 | #include // for operator""sv 4 | 5 | #include "heap_base.hpp" // for OnHeapPointer 6 | #include "memory.hpp" // for memory_read, Memory 7 | #include "search.hpp" // for get_address 8 | 9 | using CustomMallocFun = void*(void*, std::size_t); 10 | using CustomFreeFun = void*(void*, void*); 11 | 12 | void* custom_malloc(std::size_t size) 13 | { 14 | static CustomMallocFun* _malloc = (CustomMallocFun*)get_address("custom_malloc"sv); 15 | static size_t _heap_ptr_malloc_base = *reinterpret_cast(get_address("malloc_base"sv)); 16 | void* _alloc_base = OnHeapPointer(_heap_ptr_malloc_base).decode(); 17 | return _malloc(_alloc_base, size); 18 | } 19 | void custom_free(void* mem) 20 | { 21 | static CustomFreeFun* _free = (CustomFreeFun*)get_address("custom_free"sv); 22 | static size_t _heap_ptr_malloc_base = *reinterpret_cast(get_address("malloc_base"sv)); 23 | void* _alloc_base = OnHeapPointer(_heap_ptr_malloc_base).decode(); 24 | _free(_alloc_base, mem); 25 | } 26 | -------------------------------------------------------------------------------- /.vscode/tasks.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Build Release", 6 | "type": "shell", 7 | "options": { 8 | "cwd": "${workspaceRoot}" 9 | }, 10 | "command": "mkdir -Force build; cd build; cmake ..; cmake --build . --config Release --target ALL_BUILD -- /maxcpucount:14", 11 | }, 12 | { 13 | "label": "Build Debug", 14 | "type": "shell", 15 | "options": { 16 | "cwd": "${workspaceRoot}" 17 | }, 18 | "command": "mkdir -Force build; cd build; cmake ..; cmake --build . --config Debug --target ALL_BUILD -- /maxcpucount:14", 19 | }, 20 | { 21 | "label": "Build RelWithDebInfo", 22 | "type": "shell", 23 | "options": { 24 | "cwd": "${workspaceRoot}" 25 | }, 26 | "command": "mkdir -Force build; cd build; cmake ..; cmake --build . --config RelWithDebInfo --target ALL_BUILD -- /maxcpucount:14", 27 | }, 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /examples/co_subtheme.lua: -------------------------------------------------------------------------------- 1 | meta.name = 'Choose Cosmic Ocean subtheme' 2 | meta.version = '1' 3 | meta.description = 'Change the subtheme of the next Cosmic Ocean levels' 4 | meta.author = 'Zappatic' 5 | 6 | previous_choice = 1 7 | subtheme_names = {'Reset default behaviour', 'Dwelling', 'Jungle', 'Volcana', 'Tidepool', 'Temple', 'Icecaves', 'Neo Babylon', 'Sunken City'} 8 | subtheme_themeids = {COSUBTHEME.RESET, COSUBTHEME.DWELLING, COSUBTHEME.JUNGLE, COSUBTHEME.VOLCANA, COSUBTHEME.TIDE_POOL, COSUBTHEME.TEMPLE, COSUBTHEME.ICE_CAVES, COSUBTHEME.NEO_BABYLON, COSUBTHEME.SUNKEN_CITY} 9 | 10 | register_option_combo('subtheme', 'Theme', table.concat(subtheme_names, '\0')..'\0\0') 11 | 12 | set_callback(function() 13 | choice = options.subtheme 14 | if previous_choice ~= choice then 15 | force_co_subtheme(subtheme_themeids[choice]) 16 | if choice == 1 then 17 | message("Change to default behaviour") 18 | else 19 | message("Change cosmic ocean subtheme to "..subtheme_names[choice]) 20 | end 21 | previous_choice = choice 22 | end 23 | end, ON.GUIFRAME) 24 | -------------------------------------------------------------------------------- /src/game_api/entities_items.cpp: -------------------------------------------------------------------------------- 1 | #include "entities_items.hpp" 2 | 3 | #include // for operator new 4 | #include // for move 5 | #include // for vector, _Vector_iterator, _Vector_cons... 6 | 7 | #include "entities_chars.hpp" // for PowerupCapable 8 | #include "entity.hpp" // SHAPE 9 | #include "entity_db.hpp" // to_id 10 | 11 | void ParachutePowerup::deploy() 12 | { 13 | auto parachute_powerup = to_id("ENT_TYPE_ITEM_POWERUP_PARACHUTE"); 14 | if (((PowerupCapable*)overlay)->has_powerup(parachute_powerup)) 15 | { 16 | falltime_deploy = 0; 17 | } 18 | else 19 | { 20 | deployed = true; 21 | deployed2 = true; 22 | if ((flags & 0b1001) == 0b1001) 23 | flags = flags ^ 0b1001; // unset 1 & 4 24 | if ((more_flags & 0b1000) != 0b1000) 25 | more_flags = more_flags | 0b1000; // set 4 26 | if (y == 0.0f) 27 | y = 0.5f; 28 | offsety = 0.115f; 29 | hitboxy = 0.285f; 30 | shape = SHAPE::RECTANGLE; 31 | hitbox_enabled = true; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/game_api/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | file(GLOB_RECURSE spel2_sources CONFIGURE_DEPENDS *.cpp) 2 | file(GLOB_RECURSE spel2_headers CONFIGURE_DEPENDS *.h *.hpp *.inl) 3 | 4 | add_library(spel2_api STATIC 5 | ${spel2_sources} 6 | ${spel2_headers}) 7 | target_include_directories(spel2_api PUBLIC .) 8 | target_link_libraries_system(spel2_api PUBLIC 9 | imgui) 10 | target_link_libraries(spel2_api PRIVATE 11 | shared) 12 | target_link_libraries_system(spel2_api PRIVATE 13 | sol2::sol2 14 | ${LUA_LIBRARIES} 15 | lib_detours_overlunky) 16 | target_compile_definitions(spel2_api PRIVATE 17 | SOL_ALL_SAFETIES_ON=1 18 | SOL_PRINT_ERRORS=0) 19 | target_precompile_headers(spel2_api PRIVATE 20 | 21 | ) 22 | target_link_libraries(spel2_api PRIVATE overlunky_warnings) 23 | 24 | if(MSVC) 25 | target_compile_options(spel2_api PRIVATE 26 | /Zm80 27 | /bigobj) 28 | target_compile_definitions(spel2_api PRIVATE _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING) 29 | endif() 30 | -------------------------------------------------------------------------------- /examples/talking_pets.lua: -------------------------------------------------------------------------------- 1 | meta.name = "Talking pets example" 2 | meta.version = "WIP" 3 | meta.description = "Gives pets some oneliners with say()" 4 | meta.author = "Dregu" 5 | 6 | set_callback(function() 7 | pet_interval = set_interval(function() 8 | if #players == 0 then return end 9 | x, y, l = get_position(players[1].uid) 10 | cats = get_entities_at(ENT_TYPE.MONS_PET_CAT, 0, x, y, l, 2) 11 | if #cats > 0 then 12 | say(cats[1], "I hate Mondays.", 0, true) 13 | clear_callback(pet_interval) 14 | end 15 | dogs = get_entities_at(ENT_TYPE.MONS_PET_DOG, 0, x, y, l, 2) 16 | if #dogs > 0 then 17 | say(dogs[1], "Living in the caves is ruff.", 0, true) 18 | clear_callback(pet_interval) 19 | end 20 | hamsters = get_entities_at(ENT_TYPE.MONS_PET_HAMSTER, 0, x, y, l, 2) 21 | if #hamsters > 0 then 22 | say(hamsters[1], "Do you ever feel like you're just living the same day over and over again?", 0, true) 23 | clear_callback(pet_interval) 24 | end 25 | end, 60) 26 | end, ON.LEVEL) 27 | -------------------------------------------------------------------------------- /examples/hemophobia.lua: -------------------------------------------------------------------------------- 1 | meta.name = "Hemophobia" 2 | meta.version = "WIP" 3 | meta.description = "One could call this a true pacifist mode. No, it doesn't help with your hemophobia, it gives you hemophobia." 4 | meta.author = "Dregu" 5 | 6 | health = {4, 4, 4, 4} 7 | for i,player in ipairs(players) do 8 | health[i] = player.health 9 | end 10 | 11 | set_callback(function() 12 | for i,player in ipairs(players) do 13 | health[i] = player.health 14 | end 15 | set_interval(function() 16 | for i,player in ipairs(players) do 17 | if player.inventory.kills_total > 0 or player.health < health[i] then 18 | kill_entity(player.uid) 19 | end 20 | x, y, l = get_position(player.uid) 21 | blood = get_entities_at(ENT_TYPE.ITEM_BLOOD, 0, x, y, l, 1.0) 22 | if #blood > 0 then 23 | kill_entity(player.uid) 24 | end 25 | health[i] = player.health 26 | end 27 | end, 1) 28 | end, ON.LEVEL) 29 | 30 | set_callback(function() 31 | spawn(ENT_TYPE.ITEM_PICKUP_KAPALA, 0, -0.4, -1, 0, 0) 32 | end, ON.START) 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 iojon 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 | -------------------------------------------------------------------------------- /src/game_api/entities_decorations.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "entity.hpp" 4 | #include "sound_manager.hpp" 5 | 6 | struct Illumination; 7 | 8 | class CrossBeam : public Entity 9 | { 10 | public: 11 | int32_t attached_to_side_uid; 12 | int32_t attached_to_top_uid; 13 | }; 14 | 15 | class PalaceSign : public Entity 16 | { 17 | public: 18 | /// The neon buzz sound 19 | SoundMeta* sound; 20 | Illumination* illumination; // illumination1/2 used only in dark level, no pointer in normal level 21 | Illumination* arrow_illumination; 22 | uint8_t arrow_change_timer; 23 | }; 24 | 25 | class DecoRegeneratingBlock : public Entity 26 | { 27 | public: 28 | int8_t unknown1; 29 | uint8_t unknown2; // timer after self_destruct is triggered, modifies the size depending of the unknown1 30 | bool self_destruct; // UNSURE have reverse effect of spawning in after the block is broken, unused in the game? 31 | }; 32 | 33 | class DestructibleBG : public Entity 34 | { 35 | public: 36 | int32_t attached_to_1; // weird, unsure 37 | int32_t attached_to_2; // weird, unsure 38 | bool unknown1; 39 | bool unknown2; 40 | }; 41 | -------------------------------------------------------------------------------- /examples/save_load.lua: -------------------------------------------------------------------------------- 1 | meta.name = "Save/Load" 2 | meta.version = "WIP" 3 | meta.description = "Saves and loads a number." 4 | meta.author = "Malacath" 5 | 6 | local num_saved = 0 7 | local num_levels = 0 8 | 9 | set_callback(function() 10 | message("Num Levels "..tostring(num_levels)) 11 | num_levels = num_levels + 1 12 | end, ON.LEVEL) 13 | 14 | set_callback(function(save_ctx) 15 | local save_data = { 16 | num_saved = num_saved, 17 | num_levels = num_levels, 18 | } 19 | local save_data_str = json.encode(save_data) 20 | save_ctx:save(save_data_str) 21 | message("Saved "..inspect(save_data)) 22 | num_saved = num_saved + 1 23 | end, ON.SAVE) 24 | 25 | set_callback(function(load_ctx) 26 | local load_data_str = load_ctx:load() 27 | if load_data_str ~= "" then 28 | local load_data_str = json.decode(load_data_str) 29 | num_saved = load_data_str.num_saved 30 | num_levels = load_data_str.num_levels 31 | message("Loaded "..inspect(load_data_str)) 32 | end 33 | end, ON.LOAD) 34 | 35 | set_callback(function() 36 | message("Could've saved but didn't, because I didn't take an arg...") 37 | end, ON.SAVE) 38 | -------------------------------------------------------------------------------- /src/game_api/socket.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for atomic_flag 4 | #include // for function 5 | #include // for in_port_t 6 | #include // for string 7 | #include // for thread 8 | 9 | #include "sockpp/udp_socket.h" // for udp_socket 10 | 11 | class UdpServer 12 | { 13 | public: 14 | using SocketCb = std::optional(std::string); 15 | 16 | UdpServer(std::string host, in_port_t port, std::function cb); 17 | ~UdpServer(); 18 | void clear(); 19 | 20 | std::string host; 21 | in_port_t port; 22 | std::function cb; 23 | std::thread thr; 24 | std::atomic_flag kill_thr; 25 | sockpp::udp_socket sock; 26 | }; 27 | 28 | class HttpRequest 29 | { 30 | public: 31 | using HttpCb = void(std::optional, std::optional); 32 | 33 | HttpRequest(std::string url, std::function cb); 34 | std::string url; 35 | std::function cb; 36 | std::string response; 37 | std::string error; 38 | }; 39 | 40 | void dump_network(); 41 | bool http_get(const char* sURL, std::string& out, std::string& err); 42 | -------------------------------------------------------------------------------- /examples/char_info.lua: -------------------------------------------------------------------------------- 1 | meta.name = "Character Info" 2 | meta.version = "WIP" 3 | meta.description = "Draws character names and royal title in their respective color" 4 | meta.author = "Malacath" 5 | 6 | set_callback(function(draw_ctx) 7 | local chars = get_entities_by(0, MASK.PLAYER, LAYER.BOTH) 8 | for _, char_uid in pairs(chars) do 9 | local char = get_entity(char_uid) 10 | local royal_title = nil 11 | if char:is_female() then 12 | royal_title = 'Queen' 13 | else 14 | royal_title = 'King' 15 | end 16 | local name = F'{char:get_name()} aka {royal_title} {char:get_short_name()}' 17 | local color = char:get_heart_color() 18 | local u_color = rgba( 19 | math.floor(color.r * 255), 20 | math.floor(color.g * 255), 21 | math.floor(color.b * 255), 22 | math.floor(color.a * 255) 23 | ) 24 | 25 | local x, y, l = get_render_position(char_uid) 26 | local sx, sy = screen_position(x, y + char.hitboxy + char.offsety) 27 | local tx, ty = draw_text_size(35, name) 28 | draw_ctx:draw_text(sx - tx / 2, sy - ty * 2, 35, name, u_color) 29 | end 30 | end, ON.GUIFRAME) 31 | -------------------------------------------------------------------------------- /.github/workflows/build-docs.yml: -------------------------------------------------------------------------------- 1 | name: Build Slate Docs 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | workflow_dispatch: 7 | inputs: 8 | 9 | jobs: 10 | build: 11 | name: Build Docs 12 | runs-on: ubuntu-latest 13 | env: 14 | ruby-version: '3.0' 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 1 20 | submodules: true 21 | 22 | - name: Set up Ruby 23 | uses: ruby/setup-ruby@v1 24 | with: 25 | ruby-version: ${{ env.ruby-version }} 26 | 27 | - uses: actions/cache@v4 28 | with: 29 | path: docs/slate/vendor/bundle 30 | key: gems-${{ runner.os }}-${{ env.ruby-version }}-${{ hashFiles('**/Gemfile.lock') }} 31 | restore-keys: | 32 | gems-${{ runner.os }}-${{ env.ruby-version }}- 33 | gems-${{ runner.os }}- 34 | 35 | - name: Build 36 | run: | 37 | cd docs 38 | chmod +x generate_all.sh 39 | chmod +x generate_slate.sh 40 | ./generate_all.sh || true 41 | 42 | - name: Push changes 43 | uses: ad-m/github-push-action@master 44 | with: 45 | github_token: ${{ secrets.GITHUB_TOKEN }} 46 | branch: ${{ github.ref }} 47 | -------------------------------------------------------------------------------- /examples/cursor.lua: -------------------------------------------------------------------------------- 1 | meta.name = "HD Cursor" 2 | meta.description = [[Replace the cursor with the HD cursor! 3 | 4 | Note: The feature is enabled by default natively in Overlunky 5 | and doesn't need these files, this is just an example how to do it with lua 6 | ]] 7 | meta.version = "1.0" 8 | meta.author = "Dregu" 9 | 10 | iio = get_io() 11 | 12 | -- load the image 13 | cursor, width, height = create_image("cursor.png") 14 | 15 | last_ms, last_mx, last_my = 0, 0, 0 16 | 17 | if cursor >= 0 then 18 | set_callback(function(ctx) 19 | local mx, my = mouse_position() 20 | 21 | -- update cursor hiding timer 22 | if mx ~= last_mx or my ~= last_my then last_ms = get_ms() end 23 | 24 | -- draw on top of UI windows, including all platform windows outside the game 25 | ctx:draw_layer(DRAW_LAYER.FOREGROUND) 26 | 27 | -- check for recent movement 28 | if get_ms() - last_ms < 2000 then 29 | -- draw the cursor 30 | ctx:draw_image(cursor, mx, my, mx + 0.05, my - 0.05 / 9 * 16, 0, 0, 1, 1, 0xffffffff) 31 | end 32 | 33 | -- hide normal cursor 34 | iio.showcursor = false 35 | 36 | last_mx, last_my = mx, my 37 | end, ON.GUIFRAME) 38 | end 39 | -------------------------------------------------------------------------------- /src/game_api/window_api.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include // for UINT, WPARAM, LPARAM 5 | 6 | bool detect_wine(); 7 | 8 | bool init_hooks(void* swap_chain_ptr); 9 | 10 | using OnInputCallback = bool (*)(UINT, WPARAM, LPARAM); 11 | using ImguiInitCallback = void (*)(struct ImGuiContext*); 12 | using ImguiDrawCallback = void (*)(); 13 | using PreDrawCallback = void (*)(); 14 | using PostDrawCallback = void (*)(); 15 | using OnQuitCallback = void (*)(); 16 | 17 | enum WndProcResult : LRESULT 18 | { 19 | RunImguiWindowProc = 1 << 0, 20 | RunGameWindowProc = 1 << 1, 21 | RunAllWindowProc = RunImguiWindowProc | RunGameWindowProc, 22 | }; 23 | void register_on_input(OnInputCallback on_input); 24 | void register_imgui_pre_init(ImguiInitCallback imgui_init); 25 | void register_imgui_init(ImguiInitCallback imgui_init); 26 | void register_imgui_draw(ImguiDrawCallback imgui_draw); 27 | void register_pre_draw(PreDrawCallback pre_draw); 28 | void register_post_draw(PostDrawCallback post_draw); 29 | void register_on_quit(OnQuitCallback on_quit); 30 | 31 | HWND get_window(); 32 | 33 | void show_cursor(); 34 | void hide_cursor(); 35 | void imgui_vsync(bool enable); 36 | 37 | struct ID3D11Device* get_device(); 38 | -------------------------------------------------------------------------------- /docs/examples/ThemeInfo.lua: -------------------------------------------------------------------------------- 1 | -- When hell freezes over: Examples for hooking ThemeInfo virtuals 2 | 3 | state.level_gen.themes[THEME.VOLCANA]:set_pre_texture_dynamic(function(theme, id) 4 | -- change volcana floor to ice floor 5 | if id == DYNAMIC_TEXTURE.FLOOR then 6 | return TEXTURE.DATA_TEXTURES_FLOOR_ICE_0 7 | end 8 | end) 9 | 10 | state.level_gen.themes[THEME.VOLCANA]:set_pre_spawn_effects(function(theme) 11 | -- run the effects function from another theme to get cool ice particle effects 12 | for i=1,50 do 13 | state.level_gen.themes[THEME.ICE_CAVES]:spawn_effects() 14 | end 15 | -- then we run this to fix the weird camera bounds set by ice caves 16 | state.level_gen.themes[THEME.DWELLING]:spawn_effects() 17 | -- don't spawn volcanic effects 18 | return true 19 | end) 20 | 21 | -- make players cold 22 | state.level_gen.themes[THEME.VOLCANA]:set_post_spawn_players(function(theme) 23 | for i,p in pairs(get_local_players()) do 24 | spawn_over(ENT_TYPE.LOGICAL_FROST_BREATH, p.uid, 0, 0) 25 | end 26 | end) 27 | 28 | -- make vlads bluish 29 | state.level_gen.themes[THEME.VOLCANA]:set_pre_texture_backlayer_lut(function(theme) 30 | return TEXTURE.DATA_TEXTURES_LUT_ICECAVES_0 31 | end) 32 | -------------------------------------------------------------------------------- /examples/sparktrap_mayhem.lua: -------------------------------------------------------------------------------- 1 | meta.name = "Sparktrap mayhem" 2 | meta.version = "WIP" 3 | meta.description = "Every 4 seconds, spark traps change their behaviour" 4 | meta.author = "Zappatic" 5 | 6 | phase = 0 7 | counter = 0 8 | 9 | speed = 0.015 10 | rotation_direction = 1 11 | distance = 3.0 12 | 13 | speeds = { 0.005, 0.015, 0.03, 0.06, 0.12, 0.24 } 14 | 15 | set_callback(function() 16 | if phase == 0 then -- distance sinewave 17 | if counter > 360 then counter = 0 end 18 | distance = 2 + math.sin((math.rad(counter*5))) 19 | elseif phase == 1 then -- flip rotation 20 | rotation_direction = rotation_direction * -1 21 | phase = 3 22 | elseif phase == 2 then -- random speed 23 | speed = speeds[math.random(6)] * rotation_direction 24 | phase = 0 25 | elseif phase == 3 then -- random fixed distance 26 | if counter % 15 == 0 then 27 | distance = math.random(3) 28 | speed = 0.015 * rotation_direction 29 | end 30 | end 31 | modify_sparktraps(speed, distance) 32 | counter = counter + 1 33 | end, ON.GUIFRAME) 34 | 35 | 36 | set_callback(function() 37 | set_interval(function() 38 | phase = math.random(4) - 1 39 | end, 240) 40 | end, ON.LEVEL) 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/game_api/search.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for size_t 4 | #include // for uint8_t 5 | #include // for optional, nullopt 6 | #include // for string 7 | #include // for operator""sv, string_view, string_view_literals 8 | 9 | using namespace std::string_view_literals; 10 | 11 | size_t decode_pc(const char* exe, size_t offset, uint8_t opcode_offset = 3, uint8_t opcode_suffix_offset = 0, uint8_t opcode_addr_size = 4); 12 | size_t decode_imm(const char* exe, size_t offset, uint8_t opcode_offset = 3, uint8_t value_size = 4); 13 | 14 | // Find the location of the instruction (needle) with wildcard (* or \x2a) support 15 | // Optional pattern_name for better error messages 16 | // If is_required is true the function will call std::terminate when the needle can't be found 17 | // Else it will throw std::logic_error 18 | size_t find_inst(const char* exe, std::string_view needle, size_t start, std::optional end = std::nullopt, std::string_view pattern_name = ""sv, bool is_required = true); 19 | 20 | size_t find_after_bundle(size_t exe); 21 | 22 | void preload_addresses(); 23 | size_t get_address(std::string_view address_name); 24 | 25 | void register_application_version(std::string s); 26 | -------------------------------------------------------------------------------- /src/game_api/entities_activefloors.cpp: -------------------------------------------------------------------------------- 1 | #include "entities_activefloors.hpp" 2 | 3 | #include "entity.hpp" // for Entity, to_id, EntityDB 4 | #include "items.hpp" // IWYU pragma: keep 5 | #include "layer.hpp" // for EntityList::Range, EntityList, EntityList::Ent... 6 | #include "sound_manager.hpp" // for construct_soundmeta 7 | 8 | uint8_t Olmec::broken_floaters() 9 | { 10 | static auto olmec_floater_id = to_id("ENT_TYPE_FX_OLMECPART_FLOATER"); 11 | uint8_t broken = 0; 12 | for (auto item : items.entities()) 13 | { 14 | if (item->type->id == olmec_floater_id) 15 | { 16 | if (item->animation_frame == 0x27) 17 | { 18 | broken++; 19 | } 20 | } 21 | } 22 | return broken; 23 | } 24 | 25 | void Drill::trigger(std::optional play_sound_effect) 26 | { 27 | if (move_state != 0 || standing_on_uid != -1) 28 | { 29 | return; 30 | } 31 | detach(true); 32 | move_state = 6; 33 | flags = flags & ~(1U << (10 - 1)); 34 | 35 | sound1 = construct_soundmeta(0x159, false); 36 | sound1->start(); 37 | sound2 = construct_soundmeta(0x153, false); 38 | sound2->start(); 39 | if (play_sound_effect.value_or(false)) 40 | play_sound(0xA4, uid); 41 | } 42 | -------------------------------------------------------------------------------- /examples/jelly.lua: -------------------------------------------------------------------------------- 1 | meta.name = "Jelly mode" 2 | meta.version = "1.0" 3 | meta.description = "Spawns a megajellyfish on every entrance, and a clover for good luck! You can hide in the backlayer, but it will be waiting!" 4 | meta.author = "Dregu" 5 | 6 | current_layer = 0 7 | 8 | -- spawn megajelly using absolute coordinates 9 | set_callback(function() 10 | x, y, layer = get_position(players[1].uid) 11 | spawn_entity(ENT_TYPE.MONS_MEGAJELLYFISH, x, y+3, layer, 0, 0) 12 | -- jellies go dumb when you go to the backlayer, so we'll just spawn a new one when you come back 13 | set_interval(function() 14 | if #players < 1 then return end 15 | x, y, layer = get_position(players[1].uid) 16 | if layer ~= current_layer then 17 | if layer == 0 then 18 | spawn_entity(ENT_TYPE.MONS_MEGAJELLYFISH, x, y+3, layer, 0, 0) 19 | else 20 | jellys = get_entities_by_type(ENT_TYPE.MONS_MEGAJELLYFISH) 21 | for i,v in ipairs(jellys) do 22 | move_entity(v, 0, 0, 0, -1) -- apparently you can't kill these 23 | end 24 | end 25 | current_layer = layer 26 | end 27 | end, 10) 28 | end, ON.LEVEL) 29 | 30 | -- spawn clover using player-relative coordinates 31 | set_callback(function() 32 | spawn(ENT_TYPE.ITEM_PICKUP_CLOVER, 0, 1, LAYER.PLAYER1, 0, 0) 33 | end, ON.LEVEL) 34 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/steam_lua.cpp: -------------------------------------------------------------------------------- 1 | #include "steam_lua.hpp" 2 | 3 | #include // for DetourAttach, DetourTransactionBegin 4 | #include 5 | 6 | #include "memory.hpp" 7 | #include "steam_api.hpp" 8 | #include "vtable_hook.hpp" // for get_hook_function, register_hook_function 9 | 10 | namespace NSteam 11 | { 12 | void register_usertypes(sol::state& lua) 13 | { 14 | /// Check if the user has performed a feat (Real Steam achievement or a hooked one). Returns: `bool unlocked, bool hidden, string name, string description` 15 | lua["get_feat"] = get_feat; 16 | 17 | /// Get the visibility of a feat 18 | lua["get_feat_hidden"] = get_feat_hidden; 19 | 20 | /// Set the visibility of a feat 21 | lua["set_feat_hidden"] = set_feat_hidden; 22 | 23 | /// Helper function to set the title and description strings for a FEAT with change_string, as well as the hidden state. 24 | lua["change_feat"] = [](FEAT feat, bool hidden, std::u16string name, std::u16string description) 25 | { 26 | return change_feat(feat, hidden, name, description); 27 | }; 28 | 29 | lua.create_named_table("FEAT"); 30 | for (unsigned int i = 0; i < g_AllAchievements.size(); ++i) 31 | { 32 | lua["FEAT"][g_AchievementNames[i]] = i + 1; 33 | } 34 | } 35 | } // namespace NSteam 36 | -------------------------------------------------------------------------------- /examples/drunk_dash.lua: -------------------------------------------------------------------------------- 1 | meta.name = "Drunk dash" 2 | meta.version = "WIP" 3 | meta.description = "Press door button to do an awesome dash." 4 | meta.author = "Dregu" 5 | 6 | -- check if player can use something with the door button, so we don't dash when buying something or entering a door 7 | function can_use(uid) 8 | local x, y, l = get_position(uid) 9 | for i,v in ipairs(get_entities_at(ENT_TYPE.FX_BUTTON, 0, x, y, l, 5)) do 10 | local e = get_entity(v) 11 | if e.color.a > 0 then 12 | return true 13 | end 14 | end 15 | return false 16 | end 17 | 18 | -- check every frame 19 | set_callback(function() 20 | -- all players can drunk dash 21 | for i,player in ipairs(players) do 22 | -- check if door button is pressed, we're not already stunned and there's nothing else we can do with the door button 23 | -- we don't want to dash in any of these states either: 24 | -- stunned (18), entering (19), loading (20), exiting (21) or dead(22) 25 | if player:is_button_pressed(BUTTON.DOOR) and player.stun_timer == 0 and not can_use(player.uid) and player.state < 18 then 26 | -- stun player and give them a little push in the right direction 27 | player.stun_timer = 30 28 | player.velocityx = player.movex/5 29 | player.velocityy = player.movey/10+0.1 30 | end 31 | end 32 | end, ON.FRAME) 33 | -------------------------------------------------------------------------------- /docs/parse_cache.py: -------------------------------------------------------------------------------- 1 | import os 2 | import hashlib 3 | import inspect 4 | import pickle 5 | 6 | 7 | def needs_update(files, pickle_file): 8 | 9 | BLOCK_SIZE = 65536 10 | 11 | def hash_files(files): 12 | hash = hashlib.sha256() 13 | for file in files: 14 | with open(file, "rb") as f: 15 | fb = f.read(BLOCK_SIZE) 16 | while len(fb) > 0: 17 | hash.update(fb) 18 | fb = f.read(BLOCK_SIZE) 19 | return hash.hexdigest() 20 | 21 | file_hash = hash_files(files) 22 | db_hash = "" 23 | 24 | if os.path.exists(f"{pickle_file}.db"): 25 | with open(f"{pickle_file}.db", "r") as hash_file: 26 | db_hash = hash_file.read() 27 | 28 | with open(f"{pickle_file}.db", "w") as hash_file: 29 | hash_file.write(file_hash) 30 | 31 | return db_hash != file_hash 32 | 33 | 34 | def do_unpickle(file_path): 35 | if not os.path.exists(file_path): 36 | return None 37 | 38 | with open(file_path, "rb") as file: 39 | return pickle.Unpickler(file).load() 40 | 41 | 42 | def do_pickle(file_path, *args): 43 | combined_object = {} 44 | for name, obj in args: 45 | combined_object[name] = obj 46 | 47 | with open(file_path, "wb") as file: 48 | pickle.Pickler(file).dump(combined_object) 49 | -------------------------------------------------------------------------------- /docs/examples/CustomTheme.md: -------------------------------------------------------------------------------- 1 | > Very basic example how to use a CustomTheme in the procedural levels. Search the examples or check [Eternal Flame of Gehenna](https://spelunky.fyi/mods/m/gehenna/) for custom levels. 2 | 3 | ```lua 4 | -- create a new theme based on dwelling 5 | local my_theme = CustomTheme:new() 6 | -- get some ember effects from volcana 7 | my_theme:override(THEME_OVERRIDE.SPAWN_EFFECTS, THEME.VOLCANA) 8 | -- change level size 9 | my_theme:post(THEME_OVERRIDE.INIT_LEVEL, function() 10 | state.width = 5 11 | state.height = 15 12 | end) 13 | -- set floor textures to eggy 14 | my_theme.textures[DYNAMIC_TEXTURE.FLOOR] = TEXTURE.DATA_TEXTURES_FLOOR_EGGPLANT_0 15 | set_callback(function(ctx) 16 | -- use the level gen from ice caves 17 | ctx:override_level_files({"generic.lvl", "icecavesarea.lvl"}) 18 | -- force our theme instead of the default 19 | force_custom_theme(my_theme) 20 | end, ON.PRE_LOAD_LEVEL_FILES) 21 | ``` 22 | 23 | > You can call theme functions from other themes, for example to make all growable tile codes work in your theme: 24 | 25 | ```lua 26 | local custom = CustomTheme:new() 27 | custom:post(THEME_OVERRIDE.SPAWN_LEVEL, function() 28 | state.level_gen.themes[THEME.VOLCANA]:spawn_traps() 29 | state.level_gen.themes[THEME.TIDE_POOL]:spawn_traps() 30 | state.level_gen.themes[THEME.JUNGLE]:spawn_traps() 31 | end) 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/examples/set_post_floor_update.lua: -------------------------------------------------------------------------------- 1 | -- Use FLOOR_GENERIC with textures from different themes that update correctly when destroyed. 2 | -- This lets you use the custom tile code 'floor_generic_tidepool' 3 | -- in the level editor to spawn tidepool floor in dwelling for example... 4 | define_tile_code("floor_generic_tidepool") 5 | set_pre_tile_code_callback(function(x, y, layer) 6 | local uid = spawn_grid_entity(ENT_TYPE.FLOOR_GENERIC, x, y, layer) 7 | set_post_floor_update(uid, function(me) 8 | me:set_texture(TEXTURE.DATA_TEXTURES_FLOOR_TIDEPOOL_0) 9 | for i,v in ipairs(entity_get_items_by(me.uid, ENT_TYPE.DECORATION_GENERIC, MASK.DECORATION)) do 10 | local deco = get_entity(v) 11 | deco:set_texture(TEXTURE.DATA_TEXTURES_FLOOR_TIDEPOOL_0) 12 | end 13 | end) 14 | return true 15 | end, "floor_generic_tidepool") 16 | 17 | 18 | -- Fix quicksand decorations when not in temple 19 | set_post_entity_spawn(function(ent) 20 | ent:set_post_floor_update(function(me) 21 | me:set_texture(TEXTURE.DATA_TEXTURES_FLOOR_TEMPLE_0) 22 | for i,v in ipairs(entity_get_items_by(me.uid, ENT_TYPE.DECORATION_GENERIC, MASK.DECORATION)) do 23 | local deco = get_entity(v) 24 | deco:set_texture(TEXTURE.DATA_TEXTURES_FLOOR_TEMPLE_0) 25 | end 26 | end) 27 | end, SPAWN_TYPE.ANY, MASK.FLOOR, ENT_TYPE.FLOOR_QUICKSAND) 28 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/entities_liquids_lua.cpp: -------------------------------------------------------------------------------- 1 | #include "entities_liquids_lua.hpp" 2 | 3 | #include // for max 4 | #include // for operator new 5 | #include // for proxy_key_t, state, basic_table_core... 6 | #include // for operator==, allocator 7 | #include // for get 8 | #include // for move, declval 9 | #include // for min, max 10 | 11 | #include "entities_liquids.hpp" // for Liquid, Lava, Lava::emitted_light 12 | #include "entity.hpp" // for Entity 13 | #include "illumination.hpp" // IWYU pragma: keep 14 | 15 | namespace NEntitiesLiquids 16 | { 17 | void register_usertypes(sol::state& lua) 18 | { 19 | lua["Entity"]["as_liquid"] = &Entity::as; 20 | lua["Entity"]["as_lava"] = &Entity::as; 21 | 22 | lua.new_usertype( 23 | "Liquid", 24 | "fx_surface", 25 | &Liquid::fx_surface, 26 | "get_liquid_flags", 27 | &Liquid::get_liquid_flags, 28 | "set_liquid_flags", 29 | &Liquid::set_liquid_flags, 30 | sol::base_classes, 31 | sol::bases()); 32 | 33 | lua.new_usertype( 34 | "Lava", 35 | "emitted_light", 36 | &Lava::emitted_light, 37 | sol::base_classes, 38 | sol::bases()); 39 | } 40 | } // namespace NEntitiesLiquids 41 | -------------------------------------------------------------------------------- /examples/prng.lua: -------------------------------------------------------------------------------- 1 | meta.name = "PRNG viewer" 2 | meta.version = "WIP" 3 | meta.description = "" 4 | meta.author = "Dregu" 5 | 6 | local t = {} 7 | for i=0,10 do 8 | t[i] = { a="", b="" } 9 | end 10 | 11 | set_callback(function(ctx) 12 | ctx:window('PRNG viewer', 0, 0, 0, 0, true, function(ctx, pos, size) 13 | do 14 | local a, b = get_adventure_seed() 15 | local name = "Adventure Seed" 16 | t[10].a = ctx:win_input_text(F"{name}##SA", string.format("%016X", a)) 17 | t[10].b = ctx:win_input_text(F"##SB", string.format("%016X", b)) 18 | local na = tonumber(t[10].a, 16) 19 | local nb = tonumber(t[10].b, 16) 20 | if na ~= a or nb ~= b then 21 | set_adventure_seed(na, nb) 22 | end 23 | end 24 | for i=0,9 do 25 | ctx:win_separator() 26 | local a, b = prng:get_pair(i) 27 | local name = table.concat(enum_get_names(PRNG_CLASS, i), ", ") or "???" 28 | t[i].a = ctx:win_input_text(F"{i}: {name}##A{i}", string.format("%016X", a)) 29 | t[i].b = ctx:win_input_text(F"##B{i}", string.format("%016X", b)) 30 | local na = tonumber(t[i].a, 16) 31 | local nb = tonumber(t[i].b, 16) 32 | if na ~= a or nb ~= b then 33 | prng:set_pair(i, na, nb) 34 | end 35 | end 36 | end) 37 | end, ON.GUIFRAME) 38 | -------------------------------------------------------------------------------- /cmake/clang-format.cmake: -------------------------------------------------------------------------------- 1 | function(setup_format_target FORMAT_TARGET_NAME) 2 | set(CLANG_FORMAT_UTIL_FILE "${CMAKE_BINARY_DIR}/git-clang-format.py") 3 | 4 | if(NOT EXISTS ${CLANG_FORMAT_UTIL_FILE}) 5 | message(STATUS "Downloading formatting utility from github.com/TheLartians/Format.cmake") 6 | 7 | # Using this one instead of the one in the llvm repo because it allows specifying trees in order to format all files 8 | set(FORMAT_SOURCE_URL "https://raw.githubusercontent.com/TheLartians/Format.cmake/v1.7.1/git-clang-format.py") 9 | 10 | file(DOWNLOAD "${FORMAT_SOURCE_URL}" ${CLANG_FORMAT_UTIL_FILE}) 11 | endif() 12 | 13 | find_program(CLANG_FORMAT_PROGRAM clang-format) 14 | find_program(GIT_PROGRAM git) 15 | find_package(Python) 16 | 17 | set(CLANG_FORMAT_COMMAND ${Python_EXECUTABLE} ${CLANG_FORMAT_UTIL_FILE} 18 | --binary=${CLANG_FORMAT_PROGRAM} 19 | --extensions=c,h,cc,cp,cpp,c++,cxx,hh,hpp,hxx,inl 20 | ) 21 | set(GIT_EMPTY_TREE_HASH 4b825dc642cb6eb9a060e54bf8d69288fbee4904) 22 | 23 | add_custom_target( 24 | ${FORMAT_TARGET_NAME} 25 | COMMAND ${CLANG_FORMAT_COMMAND} ${GIT_EMPTY_TREE_HASH} -f 26 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} 27 | ) 28 | add_custom_target( 29 | ${FORMAT_TARGET_NAME}_changes 30 | COMMAND ${CLANG_FORMAT_COMMAND} -f 31 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} 32 | ) 33 | endfunction() -------------------------------------------------------------------------------- /examples/spawn_hooks.lua: -------------------------------------------------------------------------------- 1 | meta= { 2 | name = "Spawn Hooks", 3 | description = "Examples for how to hook entity spawns.", 4 | author = "Malacath" 5 | } 6 | 7 | -- Glitch player and monster animations 8 | set_post_entity_spawn(function(entity) 9 | set_post_statemachine(entity.uid, function(movable) 10 | if math.random(0, 10) == 0 then 11 | movable.animation_frame = movable.animation_frame + math.random(-3, 3) 12 | end 13 | end) 14 | end, SPAWN_TYPE.ANY, MASK.MONSTER | MASK.PLAYER) 15 | 16 | -- Replace some monsters with mounts 17 | set_pre_entity_spawn(function(ent_type, x, y, l, overlay) 18 | return spawn(ENT_TYPE.MOUNT_ROCKDOG, x, y, l, 0, 0) 19 | end, SPAWN_TYPE.ANY, 0, ENT_TYPE.MONS_SNAKE) 20 | set_pre_entity_spawn(function(ent_type, x, y, l, overlay) 21 | return spawn(ENT_TYPE.MOUNT_TURKEY, x, y, l, 0, 0) 22 | end, SPAWN_TYPE.ANY, 0, ENT_TYPE.MONS_SKELETON) 23 | set_pre_entity_spawn(function(ent_type, x, y, l, overlay) 24 | return spawn(ENT_TYPE.MOUNT_AXOLOTL, x, y, l, 0, 0) 25 | end, SPAWN_TYPE.ANY, 0, ENT_TYPE.MONS_CAVEMAN) 26 | 27 | -- Tame all turkeys 28 | set_post_entity_spawn(function(entity) 29 | entity:tame(true) 30 | end, SPAWN_TYPE.ANY, 0, ENT_TYPE.MOUNT_TURKEY) 31 | 32 | -- Throw bombs instead of whipping 33 | set_pre_entity_spawn(function(ent_type, x, y, l, overlay) 34 | return spawn(ENT_TYPE.ITEM_BOMB, x, y, l, 0, 0) 35 | end, SPAWN_TYPE.SYSTEMIC, 0, ENT_TYPE.ITEM_WHIP) 36 | -------------------------------------------------------------------------------- /src/injector/cmd_line.cpp: -------------------------------------------------------------------------------- 1 | #include "cmd_line.h" 2 | 3 | #include // for count_if 4 | #include // for char_traits 5 | #include // for move 6 | #include // for min, find_if 7 | 8 | CmdLineParser::CmdLineParser(int argc, char** argv) 9 | : m_CmdLine(argv, argv + argc){}; 10 | 11 | std::vector CmdLineParser::Get(std::string_view arg, has_args_tag) const 12 | { 13 | auto it = std::find_if( 14 | m_CmdLine.begin(), 15 | m_CmdLine.end(), 16 | [arg](std::string_view cmd_line_arg) 17 | { return cmd_line_arg.size() > 2 && cmd_line_arg.at(0) == '-' && cmd_line_arg.at(1) == '-' && cmd_line_arg.substr(2) == arg; }); 18 | 19 | std::vector params; 20 | if (it != m_CmdLine.end()) 21 | { 22 | ++it; 23 | for (; it != m_CmdLine.end(); ++it) 24 | { 25 | if (it->size() > 2 && it->at(0) == '-' && it->at(1) == '-') 26 | { 27 | // Next arg 28 | break; 29 | } 30 | params.push_back(*it); 31 | } 32 | } 33 | return params; 34 | } 35 | 36 | bool CmdLineParser::Get(std::string_view arg) const 37 | { 38 | return std::count_if( 39 | m_CmdLine.begin(), 40 | m_CmdLine.end(), 41 | [arg](std::string_view cmd_line_arg) 42 | { return cmd_line_arg.size() > 2 && cmd_line_arg[0] == '-' && cmd_line_arg[1] == '-' && cmd_line_arg.substr(2) == arg; }); 43 | } 44 | -------------------------------------------------------------------------------- /docs/game_data/vanilla_sound_params.txt: -------------------------------------------------------------------------------- 1 | 0: VANILLA_SOUND_PARAM.POS_SCREEN_X 2 | 1: VANILLA_SOUND_PARAM.DIST_CENTER_X 3 | 2: VANILLA_SOUND_PARAM.DIST_CENTER_Y 4 | 3: VANILLA_SOUND_PARAM.DIST_Z 5 | 4: VANILLA_SOUND_PARAM.DIST_PLAYER 6 | 5: VANILLA_SOUND_PARAM.SUBMERGED 7 | 6: VANILLA_SOUND_PARAM.LIQUID_STREAM 8 | 7: VANILLA_SOUND_PARAM.LIQUID_INTENSITY 9 | 8: VANILLA_SOUND_PARAM.VALUE 10 | 9: VANILLA_SOUND_PARAM.GHOST 11 | 10: VANILLA_SOUND_PARAM.TRIGGER 12 | 11: VANILLA_SOUND_PARAM.ANGER_PROXIMITY 13 | 12: VANILLA_SOUND_PARAM.ANGER_STATE 14 | 13: VANILLA_SOUND_PARAM.TORCH_PROXIMITY 15 | 14: VANILLA_SOUND_PARAM.COLLISION_MATERIAL 16 | 15: VANILLA_SOUND_PARAM.VELOCITY 17 | 16: VANILLA_SOUND_PARAM.LIGHTNESS 18 | 17: VANILLA_SOUND_PARAM.SIZE 19 | 18: VANILLA_SOUND_PARAM.TYPE_ID 20 | 19: VANILLA_SOUND_PARAM.MONSTER_ID 21 | 20: VANILLA_SOUND_PARAM.PLAYER_ACTIVITY 22 | 21: VANILLA_SOUND_PARAM.PLAYER_LIFE 23 | 22: VANILLA_SOUND_PARAM.PLAYER_DEPTH 24 | 23: VANILLA_SOUND_PARAM.FIRST_RUN 25 | 24: VANILLA_SOUND_PARAM.CAM_DEPTH 26 | 25: VANILLA_SOUND_PARAM.RESTLESS_DEAD 27 | 26: VANILLA_SOUND_PARAM.SPECIAL_MACHINE 28 | 27: VANILLA_SOUND_PARAM.POISONED 29 | 28: VANILLA_SOUND_PARAM.CURSED 30 | 29: VANILLA_SOUND_PARAM.PLAYER_CONTROLLED 31 | 30: VANILLA_SOUND_PARAM.PLAYER_CHARACTER 32 | 31: VANILLA_SOUND_PARAM.PAGE 33 | 32: VANILLA_SOUND_PARAM.DM_STATE 34 | 33: VANILLA_SOUND_PARAM.FAST_FORWARD 35 | 34: VANILLA_SOUND_PARAM.CURRENT_THEME 36 | 35: VANILLA_SOUND_PARAM.CURRENT_LEVEL 37 | 36: VANILLA_SOUND_PARAM.CURRENT_SHOP_TYPE 38 | 37: VANILLA_SOUND_PARAM.CURRENT_LAYER2 39 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/theme_vtable_lua.cpp: -------------------------------------------------------------------------------- 1 | #include "theme_vtable_lua.hpp" 2 | 3 | #include "heap_base.hpp" // for HeapBase 4 | 5 | namespace NThemeVTables 6 | { 7 | ThemeVTable& get_theme_info_vtable(sol::state& lua) 8 | { 9 | static ThemeVTable theme_vtable(lua, lua["ThemeInfo"], "THEME_OVERRIDE"); 10 | return theme_vtable; 11 | } 12 | 13 | void register_usertypes(sol::state& lua) 14 | { 15 | static auto& theme_vtable = get_theme_info_vtable(lua); 16 | 17 | HookHandler::set_hook_dtor_impl( 18 | [](std::uint32_t theme_id, std::function fun) 19 | { 20 | auto level_gen = HeapBase::get_main().level_gen(); 21 | ThemeInfo* theme = level_gen->themes[theme_id - 1]; 22 | std::uint32_t callback_id = theme_vtable.reserve_callback_id(theme); 23 | theme_vtable.set_pre( 24 | theme, 25 | callback_id, 26 | [fun = std::move(fun)](ThemeInfo* theme_inner) 27 | { fun(theme_inner->get_theme_id()); return false; }); 28 | return callback_id; 29 | }); 30 | HookHandler::set_unhook_impl( 31 | [](std::uint32_t callback_id, std::uint32_t theme_id) 32 | { 33 | auto level_gen = HeapBase::get_main().level_gen(); 34 | ThemeInfo* theme = level_gen->themes[theme_id - 1]; 35 | theme_vtable.unhook(theme, callback_id); 36 | }); 37 | } 38 | }; // namespace NThemeVTables 39 | -------------------------------------------------------------------------------- /examples/inventory.lua: -------------------------------------------------------------------------------- 1 | meta.name = "Inventory Example" 2 | meta.version = "0.1" 3 | meta.description = "Examples for using player inventory." 4 | meta.author = "jeremyhay" 5 | 6 | local names = {} 7 | for i,v in pairs(ENT_TYPE) do 8 | names[v] = i 9 | end 10 | 11 | -- display the items and the powerups in the player's inventory 12 | set_callback(function(draw_ctx) 13 | if players and players[1] then 14 | x, y, l = get_position(players[1].uid) 15 | -- you may want to mask only the items and logical (powerups) 16 | -- here so you don't get any unwanted FX 17 | -- note that ITEM_WHIP shows up while you are whipping 18 | local ANY_ITEM = 0 19 | local items = entity_get_items_by(players[1].uid, ANY_ITEM, MASK.ITEM | MASK.LOGICAL) 20 | local str = "" 21 | for i, uid in ipairs(items) do 22 | local entity = get_entity(uid) 23 | local type = get_type(entity.type.id) 24 | str = str .. " " .. names[entity.type.id] 25 | end 26 | sx, sy = screen_position(x, y-1) 27 | draw_ctx:draw_text(sx, sy, 0, str, rgba(255, 255, 255, 255)) 28 | end 29 | end, ON.GUIFRAME) 30 | 31 | 32 | -- Auto drop broken arrows 33 | set_callback(function(draw_ctx) 34 | if players and players[1] then 35 | local items = entity_get_items_by(players[1].uid, ENT_TYPE.ITEM_BROKEN_ARROW, 0) 36 | if (#items > 0) then 37 | for i, uid in ipairs(items) do 38 | entity_remove_item(players[1].uid, uid) 39 | end 40 | end 41 | end 42 | end, ON.GAMEFRAME) 43 | -------------------------------------------------------------------------------- /.github/workflows/clang_format_verify.yml: -------------------------------------------------------------------------------- 1 | name: Clang-Format Lint 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | workflow_dispatch: 9 | inputs: 10 | do_formatting: 11 | description: "Run clang-format inplace formatting and commit the result" 12 | required: true 13 | default: "false" 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: DoozyX/clang-format-lint-action@v0.16.2 21 | if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.do_formatting != 'true' }} 22 | with: 23 | source: "src" 24 | extensions: "inl,h,hpp,cpp" 25 | exclude: "./src/game_api/lua_libs" 26 | clangFormatVersion: 16 27 | 28 | - uses: DoozyX/clang-format-lint-action@v0.16.2 29 | if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.do_formatting == 'true' }} 30 | with: 31 | source: "src" 32 | extensions: "inl,h,hpp,cpp" 33 | exclude: "./src/game_api/lua_libs" 34 | clangFormatVersion: 16 35 | inplace: true 36 | - uses: EndBug/add-and-commit@v4 37 | if: ${{ success() && github.event_name == 'workflow_dispatch' && github.event.inputs.do_formatting == 'true' }} 38 | with: 39 | author_name: Clang-Format Bot 40 | author_email: fake.user@github.com 41 | message: "Automated clang-format changes" 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | -------------------------------------------------------------------------------- /examples/custom_texture.lua: -------------------------------------------------------------------------------- 1 | meta = { 2 | name = "Custom Texture", 3 | version = "WIP", 4 | description = "Shows how to add and use custom textures", 5 | author = "Malacath" 6 | } 7 | 8 | -- load a textured just like "floor_cave.png" 9 | local bad_texture_id = nil 10 | do 11 | local texture_def = get_texture_definition(TEXTURE.DATA_TEXTURES_FLOOR_CAVE_0) 12 | texture_def.texture_path = "floor_puke.png" 13 | bad_texture_id = define_texture(texture_def) 14 | end 15 | 16 | -- replace random floor tiles with the new texture 17 | -- this looks like shit, but it proves that both textures still exist 18 | set_post_tile_code_callback(function(x, y, l) 19 | local aabb = AABB:new():offset(x, y):extrude(0.45) 20 | local ents = get_entities_overlapping_hitbox(0, MASK.FLOOR, aabb, l); 21 | for _, uid in pairs(ents) do 22 | if math.random() > 0.5 then 23 | local ent = get_entity(uid) 24 | ent:set_texture(bad_texture_id) 25 | end 26 | end 27 | end, "floor") 28 | 29 | -- adds a trail to the player, note that this changes the texture for all particles of that type 30 | -- so it's actually a bad idea to do this 31 | function create_particles() 32 | for _, player in pairs(players) do 33 | local particle_type = get_particle_type(PARTICLEEMITTER.WITCHDOCTORSKULL_TRAIL) 34 | particle_type:set_texture(player:get_texture()) 35 | generate_particles(PARTICLEEMITTER.WITCHDOCTORSKULL_TRAIL, player.uid) 36 | end 37 | end 38 | set_callback(create_particles, ON.CAMP) 39 | set_callback(create_particles, ON.LEVEL) 40 | set_callback(create_particles, ON.TRANSITION) -------------------------------------------------------------------------------- /src/game_api/drops.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for size_t 4 | #include // for uint32_t, uint8_t, int32_t 5 | #include // for string, allocator 6 | #include // for vector 7 | 8 | #include "aliases.hpp" // for ENT_TYPE 9 | #include "virtual_table.hpp" // for VTABLE_OFFSET 10 | 11 | struct DropEntry 12 | { 13 | std::string caption; 14 | std::string_view pattern; 15 | VTABLE_OFFSET vtable_offset; // some patterns are not found in the vtables, use NONE to look for pattern in entire exe 16 | uint32_t vtable_rel_offset; 17 | uint8_t value_offset = 0; // the offset of the value to be replaced within the pattern 18 | uint8_t vtable_occurrence = 1; // when a value occurs more than once in the same virtual table function, choose how many times to replace 19 | size_t offsets[3] = {0}; 20 | }; 21 | 22 | struct DropChanceEntry 23 | { 24 | std::string caption; 25 | std::string_view pattern; 26 | VTABLE_OFFSET vtable_offset; 27 | uint32_t vtable_rel_offset; 28 | uint8_t chance_sizeof = 4; 29 | size_t offset = 0; 30 | }; 31 | using DROPCHANCE = int32_t; 32 | void set_drop_chance(DROPCHANCE dropchance_id, uint32_t new_drop_chance); 33 | using DROP = int32_t; 34 | void replace_drop(DROP drop_id, ENT_TYPE new_drop_entity_type); 35 | void replace_drop_by_name(std::string_view name, ENT_TYPE new_drop_entity_type); 36 | 37 | extern std::vector drop_entries; 38 | 39 | extern std::vector dropchance_entries; 40 | 41 | // #define PERFORM_DROPS_TEST 42 | #ifdef PERFORM_DROPS_TEST 43 | void test_drops(); 44 | #endif 45 | -------------------------------------------------------------------------------- /docs/game_data/spawn_chances.txt: -------------------------------------------------------------------------------- 1 | ARROWTRAP_CHANCE: 0 2 | TOTEMTRAP_CHANCE: 1 3 | PUSHBLOCK_CHANCE: 2 4 | SNAP_TRAP_CHANCE: 3 5 | JUNGLE_SPEAR_TRAP_CHANCE: 4 6 | SPIKE_BALL_CHANCE: 5 7 | CHAIN_BLOCKS_CHANCE: 6 8 | CRUSHER_TRAP_CHANCE: 7 9 | LIONTRAP_CHANCE: 8 10 | LASERTRAP_CHANCE: 9 11 | SPARKTRAP_CHANCE: 10 12 | BIGSPEARTRAP_CHANCE: 11 13 | STICKYTRAP_CHANCE: 12 14 | SKULLDROP_CHANCE: 13 15 | EGGSAC_CHANCE: 14 16 | MINISTER_CHANCE: 15 17 | BEEHIVE_CHANCE: 16 18 | LEPRECHAUN_CHANCE: 17 19 | SPRINGTRAP: 73 20 | SNAKE: 220 21 | SPIDER: 221 22 | HANGSPIDER: 222 23 | GIANTSPIDER: 223 24 | BAT: 224 25 | CAVEMAN: 225 26 | HORNEDLIZARD: 230 27 | MOLE: 231 28 | MANTRAP: 233 29 | TIKIMAN: 234 30 | WITCHDOCTOR: 235 31 | MOSQUITO: 237 32 | MONKEY: 238 33 | ROBOT: 240 34 | FIREBUG: 241 35 | IMP: 243 36 | LAVAMANDER: 244 37 | VAMPIRE: 245 38 | CROCMAN: 247 39 | COBRA: 248 40 | SORCERESS: 250 41 | CAT: 251 42 | NECROMANCER: 252 43 | JIANGSHI: 260 44 | FEMALE_JIANGSHI: 261 45 | FISH: 262 46 | OCTOPUS: 263 47 | HERMITCRAB: 264 48 | UFO: 266 49 | YETI: 268 50 | OLMITE: 276 51 | BEE: 278 52 | FROG: 283 53 | FIREFROG: 284 54 | TADPOLE: 287 55 | GIANTFLY: 288 56 | LEPRECHAUN: 310 57 | CRABMAN: 311 58 | CRITTERDUNGBEETLE: 331 59 | CRITTERBUTTERFLY: 332 60 | CRITTERSNAIL: 333 61 | CRITTERFISH: 334 62 | CRITTERANCHOVY: 335 63 | CRITTERCRAB: 336 64 | CRITTERLOCUST: 337 65 | CRITTERPENGUIN: 338 66 | CRITTERFIREFLY: 339 67 | CRITTERDRONE: 340 68 | CRITTERSLIME: 341 69 | LANDMINE: 439 70 | RED_SKELETON: 440 71 | WALLTORCH: 441 72 | LITWALLTORCH: 442 73 | ELEVATOR: 443 74 | ADD_GOLD_BAR: 444 75 | ADD_GOLD_BARS: 445 76 | DIAMOND: 446 77 | EMERALD: 447 78 | SAPPHIRE: 448 79 | RUBY: 449 80 | -------------------------------------------------------------------------------- /src/game_api/console.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for size_t 4 | #include // for uint32_t 5 | #include // for deque 6 | #include // for function 7 | #include // for unique_ptr 8 | #include // for string 9 | #include // for string_view 10 | #include // for vector 11 | 12 | struct ScriptMessage; 13 | 14 | class SpelunkyConsole 15 | { 16 | public: 17 | SpelunkyConsole(class SoundManager* sound_manager); 18 | ~SpelunkyConsole(); 19 | 20 | void loop_messages(std::function message_fun) const; 21 | std::deque consume_messages(); 22 | std::vector consume_requires(); 23 | 24 | bool is_enabled(); 25 | bool is_toggled(); 26 | 27 | void draw(struct ImDrawList* dl); 28 | void render_options(); 29 | 30 | void toggle(); 31 | 32 | std::string execute(std::string code, bool raw = false); 33 | 34 | bool has_new_history() const; 35 | void set_max_history_size(size_t max_history); 36 | void save_history(std::string_view path); 37 | void load_history(std::string_view path); 38 | void push_history(std::string history_item, std::vector result_item); 39 | 40 | std::string dump_api(); 41 | 42 | class LuaConsole* get_impl() 43 | { 44 | return m_Impl.get(); 45 | } 46 | 47 | void set_selected_uid(uint32_t uid); 48 | unsigned int get_input_lines(); 49 | void set_geometry(float x, float y, float w, float h); 50 | void set_alt_keys(bool enable); 51 | 52 | private: 53 | std::unique_ptr m_Impl; 54 | }; 55 | -------------------------------------------------------------------------------- /cmake/link_sys_library.cmake: -------------------------------------------------------------------------------- 1 | # stolen from https://stackoverflow.com/questions/52135983/cmake-target-link-libraries-include-as-system-to-suppress-compiler-warnings 2 | # modified to allow for linking binaries, e.g. Shlwapi 3 | function(target_link_libraries_system target) 4 | set(options PRIVATE PUBLIC INTERFACE) 5 | cmake_parse_arguments(TLLS "${options}" "" "" ${ARGN}) 6 | 7 | foreach(op ${options}) 8 | if(TLLS_${op}) 9 | set(scope ${op}) 10 | endif() 11 | endforeach(op) 12 | 13 | set(libs ${TLLS_UNPARSED_ARGUMENTS}) 14 | 15 | foreach(lib ${libs}) 16 | if(TARGET ${lib}) 17 | get_target_property(lib_include_dirs ${lib} INTERFACE_INCLUDE_DIRECTORIES) 18 | 19 | if(lib_include_dirs) 20 | if(scope) 21 | target_include_directories(${target} SYSTEM ${scope} ${lib_include_dirs}) 22 | else() 23 | target_include_directories(${target} SYSTEM PRIVATE ${lib_include_dirs}) 24 | endif() 25 | else() 26 | message("Warning: ${lib} doesn't set INTERFACE_INCLUDE_DIRECTORIES. No include_directories set.") 27 | endif() 28 | 29 | if(scope) 30 | target_link_libraries(${target} ${scope} ${lib}) 31 | else() 32 | target_link_libraries(${target} ${lib}) 33 | endif() 34 | else() 35 | if(scope) 36 | target_link_libraries(${target} ${scope} ${lib}) 37 | else() 38 | target_link_libraries(${target} ${lib}) 39 | endif() 40 | endif() 41 | endforeach() 42 | endfunction(target_link_libraries_system) -------------------------------------------------------------------------------- /src/injected/decode_audio_file.cpp: -------------------------------------------------------------------------------- 1 | #include "decode_audio_file.hpp" 2 | 3 | #include // for byte 4 | #include // for memcpy 5 | #include // for make_unique, unique_ptr 6 | #include // for move 7 | #include // for vector 8 | 9 | #if defined(__clang__) 10 | #pragma clang diagnostic push 11 | #pragma clang diagnostic ignored "-Wall" 12 | #pragma clang diagnostic ignored "-Wextra" 13 | #pragma clang diagnostic ignored "-Wshadow-all" 14 | #else 15 | #pragma warning(push, 0) 16 | #endif 17 | 18 | #include // for uint8_t 19 | #include // for AudioData, ConvertFromFloat32, GetF... 20 | 21 | #if defined(__clang__) 22 | #pragma clang diagnostic pop 23 | #else 24 | #pragma warning(pop) 25 | #endif 26 | 27 | DecodedAudioBuffer LoadAudioFile(const char* file_path) 28 | { 29 | nqr::AudioData decoded_data; 30 | nqr::NyquistIO loader; 31 | loader.Load(&decoded_data, file_path); 32 | 33 | const auto data_size = decoded_data.samples.size() * (GetFormatBitsPerSample(decoded_data.sourceFormat) / 8); 34 | auto data = std::make_unique(data_size + 32); // 16 bytes padding front and back 35 | if (decoded_data.sourceFormat == nqr::PCM_FLT) 36 | { 37 | memcpy(data.get() + 16, decoded_data.samples.data(), data_size); 38 | } 39 | else 40 | { 41 | nqr::ConvertFromFloat32((std::uint8_t*)data.get() + 16, decoded_data.samples.data(), decoded_data.samples.size(), decoded_data.sourceFormat); 42 | } 43 | return DecodedAudioBuffer{ 44 | decoded_data.channelCount, decoded_data.sampleRate, static_cast(decoded_data.sourceFormat - 1), std::move(data), data_size}; 45 | } 46 | -------------------------------------------------------------------------------- /src/injected/ui.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include // for true_type, is_invocable_r_v 7 | #include 8 | 9 | template 10 | requires(std::is_function_v) 11 | struct is_invocable_as : std::false_type 12 | { 13 | }; 14 | template 15 | struct is_invocable_as : std::is_invocable_r 16 | { 17 | }; 18 | template 19 | inline constexpr auto is_invocable_as_v = is_invocable_as::value; 20 | 21 | template 22 | requires is_invocable_as_v 23 | struct OnScopeExit 24 | { 25 | OnScopeExit(FunT&& fun) 26 | : Fun{std::forward(fun)} 27 | { 28 | } 29 | ~OnScopeExit() 30 | { 31 | Fun(); 32 | } 33 | FunT Fun; 34 | }; 35 | 36 | #define OL_CONCAT_IMPL(x, y) x##y 37 | #define OL_CONCAT(x, y) OL_CONCAT_IMPL(x, y) 38 | #define ON_SCOPE_EXIT(expr) \ 39 | OnScopeExit OL_CONCAT(on_scope_exit_, __LINE__) \ 40 | { \ 41 | [&]() { expr; } \ 42 | } 43 | 44 | const int OL_KEY_CTRL = 0x100; 45 | const int OL_KEY_SHIFT = 0x200; 46 | const int OL_KEY_ALT = 0x800; 47 | const int OL_BUTTON_MOUSE = 0x400; 48 | const int OL_MOUSE_WHEEL = 0x10; 49 | const int OL_WHEEL_DOWN = 0x11; 50 | const int OL_WHEEL_UP = 0x12; 51 | 52 | struct EntityItem; 53 | 54 | void create_box(std::vector items); 55 | void init_ui(struct ImGuiContext* ctx); 56 | void reload_enabled_scripts(); 57 | -------------------------------------------------------------------------------- /docs/examples/set_storage_layer.lua: -------------------------------------------------------------------------------- 1 | -- Sets the right layer when using the vanilla tile code if waddler is still happy, 2 | -- otherwise spawns the floor to the left of this tile. 3 | -- Manually spawning FLOOR_STORAGE pre-tilecode doesn't seem to work as expected, 4 | -- so we destroy it post-tilecode. 5 | set_post_tile_code_callback(function(x, y, layer) 6 | if not test_flag(state.quest_flags, 10) then 7 | -- Just set the layer and let the vanilla tilecode handle the floor 8 | set_storage_layer(layer) 9 | else 10 | local floor = get_entity(get_grid_entity_at(x, y, layer)) 11 | if floor then 12 | floor:destroy() 13 | end 14 | if get_grid_entity_at(x - 1, y, layer) ~= -1 then 15 | local left = get_entity(get_grid_entity_at(x - 1, y, layer)) 16 | spawn_grid_entity(left.type.id, x, y, layer) 17 | end 18 | end 19 | end, "storage_floor") 20 | 21 | -- This fixes a bug in the game that breaks storage on transition. 22 | -- The old storage_uid is not cleared after every level for some reason. 23 | set_callback(function() 24 | state.storage_uid = -1 25 | end, ON.TRANSITION) 26 | 27 | -- Having a waddler is completely optional for storage, 28 | -- but this makes a nice waddler room if he still likes you. 29 | define_tile_code("waddler") 30 | set_pre_tile_code_callback(function(x, y, layer) 31 | if not test_flag(state.quest_flags, 10) then 32 | local uid = spawn_roomowner(ENT_TYPE.MONS_STORAGEGUY, x + 0.5, y, layer, ROOM_TEMPLATE.WADDLER) 33 | set_on_kill(uid, function() 34 | -- Disable current level storage if you kill waddler 35 | state.storage_uid = -1 36 | end) 37 | end 38 | return true 39 | end, "waddler") 40 | -------------------------------------------------------------------------------- /examples/illumination.lua: -------------------------------------------------------------------------------- 1 | meta.name = "Illumination" 2 | meta.version = "WIP" 3 | meta.description = "Demonstrates how to create custom illumination" 4 | meta.author = "Zappatic" 5 | 6 | -- This script creates two light emitters at the start of each level, a red one that follows the player around 7 | -- and a blue one that stays stationary, wherever the player spawned. 8 | 9 | static_light_emitter = nil 10 | following_light_emitter = nil 11 | 12 | set_callback(function() 13 | -- Create a light emitter linked to a uid (so it follows it around) 14 | following_light_emitter = create_illumination(Color:red(), 10.0, players[1].uid) 15 | -- 2.0 to not to be abstracted by the level illumination, you probably want different value for backlayer 16 | following_light_emitter.brightness = 2.0 17 | 18 | -- Creates a light emitter at a stationary position 19 | x, y, layer = get_position(players[1].uid) 20 | static_light_emitter = create_illumination(Color:blue(), 10.0, x, y) 21 | -- 2.0 to not to be abstracted by the level illumination, you probably want different value for backlayer 22 | static_light_emitter.brightness = 2.0 23 | end, ON.LEVEL) 24 | 25 | set_callback(function() 26 | -- need to reset at the end of a level, so we don't access it as it's no longer valid 27 | static_light_emitter = nil 28 | following_light_emitter = nil 29 | end, ON.PRE_LEVEL_DESTRUCTION) 30 | 31 | 32 | set_callback(function() 33 | if following_light_emitter ~= nil then 34 | -- In order for light emitters not to fade out, you have to "refresh" them each frame. If you do not do this, 35 | -- the brightness will be reduced with -0.05 per frame. 36 | refresh_illumination(following_light_emitter) 37 | end 38 | if static_light_emitter ~= nil then 39 | refresh_illumination(static_light_emitter) 40 | end 41 | end, ON.PRE_UPDATE) 42 | -------------------------------------------------------------------------------- /examples/guns.lua: -------------------------------------------------------------------------------- 1 | meta.name = "Gun test" 2 | meta.version = "WIP" 3 | meta.description = "Testing guns." 4 | meta.author = "Dregu" 5 | 6 | -- get info on the item the player is holding on every frame 7 | set_callback(function(draw_ctx) 8 | for i, player in ipairs(players) do 9 | 10 | -- are we holding something 11 | if player.holding_uid > -1 then 12 | gun = get_entity(player.holding_uid) 13 | 14 | -- check if it's actually a gun 15 | if gun.type.id == ENT_TYPE.ITEM_SHOTGUN or gun.type.id == ENT_TYPE.ITEM_WEBGUN or gun.type.id == 16 | ENT_TYPE.ITEM_CLONEGUN or gun.type.id == ENT_TYPE.ITEM_PLASMACANNON then 17 | 18 | --format string 19 | text_display = F"Player:{i}, Gun cooldown:{gun.cooldown}, shots:{gun.shots}, shots2:{gun.shots2}" 20 | 21 | --if webgun, add the extra info 22 | if gun.type.id == ENT_TYPE.ITEM_WEBGUN then 23 | text_display = F"{text_display}, chamber:{gun.in_chamber}" 24 | end 25 | text_display = F"{text_display}, frame:{gun.animation_frame}" 26 | 27 | -- print the gun variables on screen 28 | draw_ctx:draw_text(-0.67, (9 - i) / 10.0, 24, text_display, rgba(255, 0, 255, 255)) 29 | 30 | -- make all guns super fast and infinite 31 | if gun.cooldown > 30 then 32 | gun.cooldown = 15 33 | end 34 | gun.shots = 0 35 | gun.shots2 = 0 36 | 37 | -- reset clone gun to full, or it will loop through the whole sprite sheet 38 | if gun.type.id == ENT_TYPE.ITEM_CLONEGUN then 39 | gun.animation_frame = 0x98 40 | end 41 | end 42 | end 43 | end 44 | end, ON.GUIFRAME) 45 | -------------------------------------------------------------------------------- /src/game_api/illumination.cpp: -------------------------------------------------------------------------------- 1 | #include "illumination.hpp" 2 | 3 | #include 4 | 5 | #include "color.hpp" // for Color 6 | #include "entity.hpp" // for Entity 7 | #include "math.hpp" // for Vec2 8 | #include "search.hpp" // for get_address 9 | #include "state.hpp" // for get_state_ptr 10 | 11 | Illumination* create_illumination(Vec2 pos, Color col, LIGHT_TYPE type, float size, uint8_t light_flags, int32_t uid, LAYER layer) 12 | { 13 | static size_t offset = get_address("generate_illumination"); 14 | 15 | if (offset != 0) 16 | { 17 | auto state = get_state_ptr(); 18 | 19 | typedef Illumination* create_illumination_func(LightSources*, Vec2*, Color, LIGHT_TYPE, float, uint8_t light_flags, int32_t uid, uint8_t layer); 20 | static create_illumination_func* cif = (create_illumination_func*)(offset); 21 | // enum_to_layer here does not use offset which you could argue should be used, since this function is comparable with spawn type function 22 | auto emitted_light = cif(state->lightsources, &pos, std::move(col), type, size, light_flags, uid, enum_to_layer(layer)); 23 | return emitted_light; 24 | } 25 | return nullptr; 26 | } 27 | 28 | Illumination* create_illumination(Color color, float size, float x, float y) 29 | { 30 | return create_illumination(Vec2{x, y}, std::move(color), LIGHT_TYPE::NONE, size, 0x20, -1, LAYER::FRONT); 31 | } 32 | 33 | Illumination* create_illumination(Color color, float size, int32_t uid) 34 | { 35 | auto entity = get_entity_ptr(uid); 36 | if (entity != nullptr) 37 | { 38 | return create_illumination(entity->abs_position(), std::move(color), LIGHT_TYPE::FOLLOW_ENTITY, size, 0x20, uid, (LAYER)entity->layer); 39 | } 40 | return nullptr; 41 | } 42 | 43 | void refresh_illumination(Illumination* illumination) 44 | { 45 | illumination->timer = HeapBase::get().frame_count(); 46 | } 47 | -------------------------------------------------------------------------------- /src/game_api/containers/game_allocator.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for size_t, ptrdiff_t 4 | #include // for operator new 5 | 6 | [[nodiscard]] void* game_malloc(std::size_t size); 7 | void game_free(void* mem); 8 | 9 | // This is an allocator that always uses the malloc/free implementations that the game provides 10 | // Thus it avoids CRT-mismatch while debugging and should be used in stl-containers that the 11 | // game creates/destroys but we want to modify anyways 12 | template 13 | struct game_allocator 14 | { 15 | game_allocator() = default; 16 | 17 | typedef std::size_t size_type; 18 | typedef std::ptrdiff_t difference_type; 19 | typedef T* pointer; 20 | typedef const T* const_pointer; 21 | typedef T& reference; 22 | typedef const T& const_reference; 23 | typedef T value_type; 24 | 25 | template 26 | struct rebind 27 | { 28 | typedef game_allocator other; 29 | }; 30 | template 31 | game_allocator(const game_allocator&) 32 | { 33 | } 34 | 35 | pointer address(reference x) const 36 | { 37 | return &x; 38 | } 39 | const_pointer address(const_reference x) const 40 | { 41 | return &x; 42 | } 43 | size_type max_size() const throw() 44 | { 45 | return size_type(-1) / sizeof(value_type); 46 | } 47 | 48 | pointer allocate(size_type n, [[maybe_unused]] void* hint = nullptr) 49 | { 50 | return static_cast(game_malloc(n * sizeof(T))); 51 | } 52 | 53 | void deallocate(pointer p, [[maybe_unused]] size_type n) 54 | { 55 | game_free(p); 56 | } 57 | 58 | template 59 | void construct(U* const p, Args&&... args) 60 | { 61 | new (static_cast(p)) U(std::forward(args)...); 62 | } 63 | 64 | template 65 | void destroy(U* const p) 66 | { 67 | p->~U(); 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /.github/workflows/continous_integration.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | name: Continuous Integration ${{ matrix.target_name }} ${{ matrix.toolset_name }} ${{ matrix.build_type }} 12 | runs-on: windows-latest 13 | strategy: 14 | matrix: 15 | build_type: [Debug, Release] 16 | toolset_name: [MSVC, Ninja] 17 | build_dll: [false, true] 18 | include: 19 | - toolset_name: MSVC 20 | toolset: -A x64 -T v143 21 | - toolset_name: Ninja 22 | toolset: -G"Ninja Multi-Config" 23 | cmake_opts: -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ 24 | - build_dll: false 25 | additional_opts: -DBUILD_OVERLUNKY=ON -DBUILD_INFO_DUMP=ON -DBUILD_SPEL2_DLL=OFF 26 | target_name: overlunky 27 | - build_dll: true 28 | additional_opts: -DBUILD_OVERLUNKY=OFF -DBUILD_INFO_DUMP=OFF -DBUILD_SPEL2_DLL=ON 29 | target_name: spel2.dll 30 | 31 | steps: 32 | - uses: llvm/actions/install-ninja@main 33 | 34 | - name: Install llvm 17 35 | if: matrix.toolset_name == 'Ninja' 36 | run: choco install llvm --version 17.0.6 --allow-downgrade -y 37 | 38 | - uses: actions/checkout@v2 39 | with: 40 | fetch-depth: 1 41 | submodules: true 42 | 43 | - name: Remove Strawberry Perl from PATH 44 | run: | 45 | $env:PATH = $env:PATH -replace "C:\\Strawberry\\c\\bin;", "" 46 | "PATH=$env:PATH" | Out-File -FilePath $env:GITHUB_ENV -Append 47 | 48 | - name: Configure 49 | run: | 50 | mkdir build 51 | cd build 52 | cmake .. -Wno-dev ${{ matrix.toolset }} ${{ matrix.additional_opts }} ${{ matrix.cmake_opts }} 53 | 54 | - name: Build 55 | run: | 56 | cd build 57 | cmake --build . --config ${{ matrix.build_type }} 58 | -------------------------------------------------------------------------------- /.github/workflows/whip-build.yml: -------------------------------------------------------------------------------- 1 | name: Whip Build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | 12 | runs-on: windows-latest 13 | steps: 14 | - uses: llvm/actions/install-ninja@main 15 | 16 | - name: Install llvm 17 17 | run: choco install llvm --version 17.0.6 --allow-downgrade -y 18 | 19 | - uses: actions/checkout@v2 20 | with: 21 | fetch-depth: 1 22 | submodules: true 23 | 24 | - name: Remove Strawberry Perl from PATH 25 | run: | 26 | $env:PATH = $env:PATH -replace "C:\\Strawberry\\c\\bin;", "" 27 | "PATH=$env:PATH" | Out-File -FilePath $env:GITHUB_ENV -Append 28 | 29 | - name: Get tags for release notes 30 | shell: bash 31 | run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* 32 | 33 | - name: Build 34 | run: | 35 | mkdir build 36 | cd build 37 | cmake .. -DBUILD_INFO_DUMP=OFF -DBUILD_SPEL2_DLL=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -GNinja 38 | cmake --build . --config Release 39 | 40 | - name: Create artifacts 41 | run: | 42 | mkdir Overlunky 43 | move build\bin\Overlunky.dll Overlunky\Overlunky.dll 44 | move build\bin\Overlunky.exe Overlunky\Overlunky.exe 45 | move README.md Overlunky\README.txt 46 | move examples Overlunky\Scripts 47 | 7z a Overlunky_WHIP.zip Overlunky\ 48 | 49 | - name: Create WHIP release 50 | uses: marvinpinto/action-automatic-releases@v1.2.0 51 | env: 52 | ACTIONS_ALLOW_UNSECURE_COMMANDS: "true" 53 | with: 54 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 55 | automatic_release_tag: "whip" 56 | prerelease: false 57 | title: "WHIP Build" 58 | files: | 59 | Overlunky_WHIP.zip 60 | Overlunky/Overlunky.exe 61 | Overlunky/Overlunky.dll 62 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/flags_lua.cpp: -------------------------------------------------------------------------------- 1 | #include "flags_lua.hpp" 2 | 3 | #include // for state 4 | #include // for get 5 | 6 | namespace NEntityFlags 7 | { 8 | void register_usertypes(sol::state& lua) 9 | { 10 | lua.create_named_table( 11 | "ENT_FLAG", 12 | "INVISIBLE", 13 | 1, 14 | "INDESTRUCTIBLE_OR_SPECIAL_FLOOR", 15 | 2, 16 | "SOLID", 17 | 3, 18 | "PASSES_THROUGH_OBJECTS", 19 | 4, 20 | "PASSES_THROUGH_EVERYTHING", 21 | 5, 22 | "TAKE_NO_DAMAGE", 23 | 6, 24 | "THROWABLE_OR_KNOCKBACKABLE", 25 | 7, 26 | "IS_PLATFORM", 27 | 8, 28 | "CLIMBABLE", 29 | 9, 30 | "NO_GRAVITY", 31 | 10, 32 | "INTERACT_WITH_WATER", 33 | 11, 34 | "STUNNABLE", 35 | 12, 36 | "COLLIDES_WALLS", 37 | 13, 38 | "INTERACT_WITH_SEMISOLIDS", 39 | 14, 40 | "CAN_BE_STOMPED", 41 | 15, 42 | "POWER_STOMPS", 43 | 16, 44 | "FACING_LEFT", 45 | 17, 46 | "PICKUPABLE", 47 | 18, 48 | "USABLE_ITEM", 49 | 19, 50 | "ENABLE_BUTTON_PROMPT", 51 | 20, 52 | "INTERACT_WITH_WEBS", 53 | 21, 54 | "LOCKED", 55 | 22, 56 | "SHOP_ITEM", 57 | 23, 58 | "SHOP_FLOOR", 59 | 24, 60 | "PASSES_THROUGH_PLAYER", 61 | 25, 62 | "PAUSE_AI_AND_PHYSICS", 63 | 28, 64 | "DEAD", 65 | 29, 66 | "HAS_BACKITEM", 67 | 32); 68 | lua.create_named_table( 69 | "ENT_MORE_FLAG", 70 | "HIRED_HAND_REVIVED", 71 | 2, 72 | "SWIMMING", 73 | 11, 74 | "HIT_GROUND", 75 | 12, 76 | "HIT_WALL", 77 | 13, 78 | "FALLING", 79 | 14, 80 | "CURSED_EFFECT", 81 | 15, 82 | "ELIXIR_BUFF", 83 | 16, 84 | "DISABLE_INPUT", 85 | 17); 86 | } 87 | }; // namespace NEntityFlags 88 | -------------------------------------------------------------------------------- /src/game_api/containers/custom_allocator.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for size_t, ptrdiff_t 4 | #include // for operator new 5 | 6 | [[nodiscard]] void* custom_malloc(std::size_t size); 7 | void custom_free(void* mem); 8 | 9 | // This is an allocator that always uses the MemHeap implementations that the game provides 10 | // Thus it crashes in stl-containers that the game creates/destroys but we want to modify anyways 11 | template 12 | struct custom_allocator 13 | { 14 | custom_allocator() = default; 15 | 16 | typedef std::size_t size_type; 17 | typedef std::ptrdiff_t difference_type; 18 | typedef T* pointer; 19 | typedef const T* const_pointer; 20 | typedef T& reference; 21 | typedef const T& const_reference; 22 | typedef T value_type; 23 | 24 | template 25 | struct rebind 26 | { 27 | typedef custom_allocator other; 28 | }; 29 | template 30 | custom_allocator(const custom_allocator&) 31 | { 32 | } 33 | 34 | pointer address(reference x) const 35 | { 36 | return &x; 37 | } 38 | const_pointer address(const_reference x) const 39 | { 40 | return &x; 41 | } 42 | size_type max_size() const throw() 43 | { 44 | return size_type(-1) / sizeof(value_type); 45 | } 46 | 47 | pointer allocate(size_type n, [[maybe_unused]] void* hint = nullptr) 48 | { 49 | return static_cast(custom_malloc(n * sizeof(T))); 50 | } 51 | 52 | void deallocate(pointer p, [[maybe_unused]] size_type n) 53 | { 54 | custom_free(p); 55 | } 56 | 57 | void construct(pointer p, const T& val) 58 | { 59 | new (static_cast(p)) T(val); 60 | } 61 | 62 | template 63 | void construct(U* const p, Args&&... args) 64 | { 65 | new (static_cast(p)) U(std::forward(args)...); 66 | } 67 | 68 | template 69 | void destroy(U* const p) 70 | { 71 | p->~U(); 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /src/game_api/texture.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for array 4 | #include // for uint32_t 5 | #include // for optional 6 | #include // for string 7 | #include // for string_view 8 | 9 | #include "aliases.hpp" // for TEXTURE 10 | 11 | struct Texture 12 | { 13 | TEXTURE id; 14 | uint32_t padding{0}; 15 | const char** name; 16 | std::uint32_t width; 17 | std::uint32_t height; 18 | std::uint32_t num_tiles_width; 19 | std::uint32_t num_tiles_height; 20 | float offset_x_weird_math; 21 | float offset_y_weird_math; 22 | float tile_width_fraction; 23 | float tile_height_fraction; 24 | float tile_width_minus_one_fraction; 25 | float tile_height_minus_one_fraction; 26 | float one_over_width; 27 | float one_over_height; 28 | }; 29 | 30 | struct Textures 31 | { 32 | std::uint32_t num_textures; 33 | std::array textures; 34 | std::array texture_map; 35 | }; 36 | 37 | struct TextureDefinition 38 | { 39 | std::string texture_path; 40 | uint32_t width; 41 | uint32_t height; 42 | uint32_t tile_width; 43 | uint32_t tile_height; 44 | uint32_t sub_image_offset_x{0}; 45 | uint32_t sub_image_offset_y{0}; 46 | uint32_t sub_image_width{0}; 47 | uint32_t sub_image_height{0}; 48 | }; 49 | 50 | Textures* get_textures(); 51 | TextureDefinition get_texture_definition(TEXTURE texture_id); 52 | Texture* get_texture(TEXTURE texture_id); 53 | TEXTURE define_texture(TextureDefinition data); 54 | std::optional get_texture(TextureDefinition data); 55 | std::optional get_texture(std::string_view texture_name); 56 | void reload_texture(const char* texture_name); // Does a lookup for the right texture to reload 57 | void reload_texture(const char** texture_name); // Reloads the texture directly 58 | bool replace_texture(TEXTURE vanilla_id, TEXTURE custom_id); 59 | void reset_texture(TEXTURE vanilla_id); 60 | bool replace_texture_and_heart_color(TEXTURE vanilla_id, TEXTURE custom_id); 61 | -------------------------------------------------------------------------------- /src/game_api/game_patches.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "aliases.hpp" 7 | 8 | void patch_orbs_limit(); 9 | void patch_olmec_kill_crash(); 10 | void patch_liquid_OOB(); 11 | void set_skip_olmec_cutscene(bool skip); 12 | void patch_tiamat_kill_crash(); 13 | void set_skip_tiamat_cutscene(bool skip); 14 | void patch_ushabti_error(); 15 | void patch_entering_closed_door_crash(); 16 | 17 | void modify_sparktraps(float angle_increment = 0.015, float distance = 3.0); 18 | float* get_sparktraps_parameters_ptr(); // for UI 19 | void activate_sparktraps_hack(bool activate); 20 | void set_storage_layer(LAYER layer); 21 | void set_kapala_blood_threshold(uint8_t threshold); 22 | void set_kapala_hud_icon(int8_t icon_index); 23 | void set_olmec_phase_y_level(uint8_t phase, float y); 24 | void force_olmec_phase_0(bool b); 25 | void set_ghost_spawn_times(uint32_t normal = 10800, uint32_t cursed = 9000); 26 | void set_time_ghost_enabled(bool b); 27 | void set_time_jelly_enabled(bool b); 28 | void set_camp_camera_bounds_enabled(bool b); 29 | void set_explosion_mask(int32_t mask); 30 | void set_max_rope_length(uint8_t length); 31 | void change_sunchallenge_spawns(std::vector ent_types); 32 | void change_diceshop_prizes(std::vector ent_types); 33 | void change_altar_damage_spawns(std::vector ent_types); 34 | void change_waddler_drop(std::vector ent_types); 35 | void modify_ankh_health_gain(uint8_t max_health, uint8_t beat_add_health); 36 | void change_poison_timer(int16_t frames); 37 | bool disable_floor_embeds(bool disable); 38 | void set_cursepot_ghost_enabled(bool enable); 39 | void set_ending_unlock(ENT_TYPE type); 40 | void activate_tiamat_position_hack(bool activate); 41 | void activate_crush_elevator_hack(bool activate); 42 | void activate_hundun_hack(bool activate); 43 | void set_boss_door_control_enabled(bool enable); 44 | void set_level_logic_enabled(bool enable); 45 | void set_camera_layer_control_enabled(bool enable); 46 | void set_start_level_paused(bool enable); 47 | void set_liquid_layer(LAYER l); 48 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/entities_decorations_lua.cpp: -------------------------------------------------------------------------------- 1 | #include "entities_decorations_lua.hpp" 2 | 3 | #include // for max 4 | #include // for operator new 5 | #include // for proxy_key_t, state, basic_table_... 6 | #include // for operator==, allocator 7 | #include // for get 8 | #include // for move, declval 9 | #include // for min, max 10 | 11 | #include "entities_decorations.hpp" // for PalaceSign, CrossBeam, DecoRegen... 12 | #include "entity.hpp" // for Entity 13 | #include "illumination.hpp" // IWYU pragma: keep 14 | 15 | namespace NEntitiesDecorations 16 | { 17 | void register_usertypes(sol::state& lua) 18 | { 19 | lua["Entity"]["as_crossbeam"] = &Entity::as; 20 | lua["Entity"]["as_destructiblebg"] = &Entity::as; 21 | lua["Entity"]["as_palacesign"] = &Entity::as; 22 | lua["Entity"]["as_decoregeneratingblock"] = &Entity::as; 23 | 24 | lua.new_usertype( 25 | "CrossBeam", 26 | "attached_to_side_uid", 27 | &CrossBeam::attached_to_side_uid, 28 | "attached_to_top_uid", 29 | &CrossBeam::attached_to_top_uid, 30 | sol::base_classes, 31 | sol::bases()); 32 | 33 | lua.new_usertype( 34 | "DestructibleBG", 35 | sol::base_classes, 36 | sol::bases()); 37 | 38 | lua.new_usertype( 39 | "PalaceSign", 40 | "sound", 41 | &PalaceSign::sound, 42 | "illumination", 43 | &PalaceSign::illumination, 44 | "arrow_illumination", 45 | &PalaceSign::arrow_illumination, 46 | "arrow_change_timer", 47 | &PalaceSign::arrow_change_timer, 48 | sol::base_classes, 49 | sol::bases()); 50 | 51 | lua.new_usertype( 52 | "DecoRegeneratingBlock", 53 | sol::base_classes, 54 | sol::bases()); 55 | } 56 | }; // namespace NEntitiesDecorations 57 | -------------------------------------------------------------------------------- /src/injector/cmd_line.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for from_chars, from_chars_result 4 | #include // for operator new 5 | #include // for string_view 6 | #include // for allocator, vector 7 | 8 | class CmdLineParser 9 | { 10 | public: 11 | CmdLineParser(int argc, char** argv); 12 | 13 | struct has_args_tag 14 | { 15 | }; 16 | inline static constexpr has_args_tag has_args{}; 17 | std::vector Get(std::string_view arg, has_args_tag) const; 18 | bool Get(std::string_view arg) const; 19 | 20 | private: 21 | std::vector m_CmdLine; 22 | }; 23 | 24 | template 25 | T GetCmdLineParam(const CmdLineParser& parser, std::string_view arg, const T& default_value); 26 | template <> 27 | inline std::vector 28 | GetCmdLineParam>(const CmdLineParser& parser, std::string_view arg, const std::vector& default_value) 29 | { 30 | const auto ret = parser.Get(arg, CmdLineParser::has_args); 31 | return ret.empty() ? default_value : ret; 32 | } 33 | template <> 34 | inline std::string_view GetCmdLineParam(const CmdLineParser& parser, std::string_view arg, const std::string_view& default_value) 35 | { 36 | const auto ret = parser.Get(arg, CmdLineParser::has_args); 37 | return ret.empty() ? default_value : ret.front(); 38 | } 39 | template <> 40 | inline int GetCmdLineParam(const CmdLineParser& parser, std::string_view arg, const int& default_value) 41 | { 42 | int ret; 43 | std::string_view param = GetCmdLineParam(parser, arg, "none"); 44 | std::from_chars_result char_conv_result = std::from_chars(param.data(), param.data() + param.size(), ret); 45 | if (char_conv_result.ptr == arg.data()) 46 | { 47 | return default_value; 48 | } 49 | return ret; 50 | } 51 | template <> 52 | inline bool GetCmdLineParam(const CmdLineParser& parser, std::string_view arg, const bool& default_value) 53 | { 54 | return default_value || parser.Get(arg); 55 | } 56 | -------------------------------------------------------------------------------- /examples/options2.lua: -------------------------------------------------------------------------------- 1 | meta.name = 'Options test 2' 2 | meta.version = 'WIP' 3 | meta.description = 'Setting all the options in a single callback for total control, with save and load' 4 | meta.author = 'Dregu' 5 | 6 | -- init defaults 7 | options = { 8 | name = 'Ana Spelunky', 9 | shoe_size = 9.5, 10 | checkboxes = { 11 | a = true, 12 | b = false, 13 | c = false, 14 | d = false, 15 | e = true 16 | }, 17 | age = 10, 18 | debug = true, 19 | } 20 | 21 | -- save options to disk 22 | set_callback(function(ctx) 23 | ctx:save(json.encode(options)) 24 | end, ON.SAVE) 25 | 26 | -- load options from disk 27 | set_callback(function(ctx) 28 | local options_json = ctx:load() 29 | if options_json ~= '' then 30 | options = json.decode(options_json) 31 | end 32 | end, ON.LOAD) 33 | 34 | -- do everything in one callback 35 | register_option_callback('', options, function(ctx) 36 | -- now we're in complete control of the order 37 | options.name = ctx:win_input_text('Name', options.name) 38 | options.shoe_size = ctx:win_slider_float('Shoe size (US)', options.shoe_size, 1, 20) 39 | ctx:win_section('Random checkboxes', function() 40 | options.checkboxes.a = ctx:win_check('A', options.checkboxes.a) 41 | options.checkboxes.b = ctx:win_check('B', options.checkboxes.b) 42 | options.checkboxes.c = ctx:win_check('C', options.checkboxes.c) 43 | options.checkboxes.d = ctx:win_check('D', options.checkboxes.d) 44 | options.checkboxes.e = ctx:win_check('E', options.checkboxes.e) 45 | ctx:win_separator() 46 | end) 47 | options.age = ctx:win_input_int('Age', options.age) 48 | options.debug = ctx:win_check('Debug window', options.debug) 49 | if ctx:win_button('Save options') then 50 | save_script() 51 | end 52 | 53 | if options.debug then 54 | ctx:window('Your options are', 0, 0, 0, 0, true, function() 55 | ctx:win_text(inspect(options)) 56 | end) 57 | end 58 | 59 | return options -- actually pointless, we already edited the only copy 60 | end) 61 | -------------------------------------------------------------------------------- /examples/olmec.lua: -------------------------------------------------------------------------------- 1 | meta.name = "Olmec" 2 | meta.version = "WIP" 3 | meta.description = "Displays Olmec timings" 4 | meta.author = "Zappatic" 5 | 6 | set_callback(function(draw_ctx) 7 | olmecs = get_entities_by_type(ENT_TYPE.ACTIVEFLOOR_OLMEC) 8 | x = -0.95 9 | for i, olmec_uid in ipairs(olmecs) do 10 | olmec = get_entity(olmec_uid) 11 | y = -0.50 12 | white = rgba(255, 255, 255, 255) 13 | 14 | attack_phase = "stomp" 15 | if olmec.attack_phase == 1 then attack_phase = "bombs" 16 | elseif olmec.attack_phase == 2 then attack_phase = "stomp+ufos" 17 | elseif olmec.attack_phase == 3 then attack_phase = "in lava" end 18 | 19 | draw_ctx:draw_text(x, y, 0, "Olmec attack phase: " .. attack_phase, white) 20 | y = y - 0.05 21 | draw_ctx:draw_text(x, y, 0, "Attack timer: " .. tostring(olmec.attack_timer), white) 22 | y = y - 0.05 23 | draw_ctx:draw_text(x, y, 0, "Jump timer: " .. tostring(olmec.jump_timer), white) 24 | y = y - 0.05 25 | draw_ctx:draw_text(x, y, 0, "AI timer: " .. tostring(olmec.ai_timer), white) 26 | y = y - 0.05 27 | 28 | move_direction = "down" 29 | if olmec.move_direction < 0 then move_direction = "left" 30 | elseif olmec.move_direction > 0 then move_direction = "right" end 31 | draw_ctx:draw_text(x, y, 0, "Move direction: " .. move_direction, white) 32 | y = y - 0.05 33 | 34 | if olmec.attack_phase == 1 then 35 | draw_text(x, y, 0, "Bomb salvos left: " .. olmec.phase1_amount_of_bomb_salvos, white) 36 | y = y - 0.05 37 | draw_ctx:draw_text(x, y, 0, "Broken floaters: " .. olmec:broken_floaters(), white) 38 | y = y - 0.05 39 | draw_ctx:draw_text(x, y, 0, "Unknown attack state: " .. olmec.unknown_attack_state, white) 40 | y = y - 0.05 41 | elseif olmec.attack_phase == 2 then 42 | draw_ctx:draw_text(x, y, 0, "Unknown attack state: " .. olmec.unknown_attack_state, white) 43 | y = y - 0.05 44 | end 45 | x = x + 0.4 46 | end 47 | end, ON.GUIFRAME) 48 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/color_lua.cpp: -------------------------------------------------------------------------------- 1 | #include "color_lua.hpp" 2 | 3 | #include 4 | #include 5 | 6 | #include "aliases.hpp" 7 | #include "color.hpp" 8 | #include "script/sol_helper.hpp" // for self_return 9 | 10 | namespace NColor 11 | { 12 | void register_usertypes(sol::state& lua) 13 | { 14 | /// Converts a color to int to be used in drawing functions. Use values from `0..255`. 15 | lua["rgba"] = [](int r, int g, int b, int a) -> uColor 16 | { 17 | return (uColor)(a << 24) + (b << 16) + (g << 8) + (r); 18 | }; 19 | 20 | lua["get_color"] = get_color; 21 | 22 | auto color_type = lua.new_usertype("Color", sol::constructors)>{}, // 23 | sol::meta_function::equal_to, 24 | &Color::operator==); 25 | 26 | color_type["r"] = &Color::r; 27 | color_type["g"] = &Color::g; 28 | color_type["b"] = &Color::b; 29 | color_type["a"] = &Color::a; 30 | color_type["white"] = &Color::white; 31 | color_type["silver"] = &Color::silver; 32 | color_type["gray"] = &Color::gray; 33 | color_type["black"] = &Color::black; 34 | color_type["red"] = &Color::red; 35 | color_type["maroon"] = &Color::maroon; 36 | color_type["yellow"] = &Color::yellow; 37 | color_type["olive"] = &Color::olive; 38 | color_type["lime"] = &Color::lime; 39 | color_type["green"] = &Color::green; 40 | color_type["aqua"] = &Color::aqua; 41 | color_type["teal"] = &Color::teal; 42 | color_type["blue"] = &Color::blue; 43 | color_type["navy"] = &Color::navy; 44 | color_type["fuchsia"] = &Color::fuchsia; 45 | color_type["purple"] = &Color::purple; 46 | color_type["get_rgba"] = &Color::get_rgba; 47 | color_type["set_rgba"] = self_return<&Color::set_rgba>(); 48 | color_type["get_ucolor"] = &Color::get_ucolor; 49 | color_type["set_ucolor"] = self_return<&Color::set_ucolor>(); 50 | color_type["set"] = self_return<&Color::set>(); 51 | } 52 | } // namespace NColor 53 | -------------------------------------------------------------------------------- /examples/rando/projectile.lua: -------------------------------------------------------------------------------- 1 | local module = {} 2 | projectile_cbs = {} 3 | 4 | proj_from = {ENT_TYPE.ITEM_BULLET, ENT_TYPE.ITEM_FREEZERAYSHOT, ENT_TYPE.ITEM_CLONEGUNSHOT, ENT_TYPE.ITEM_UFO_LASER_SHOT, 5 | ENT_TYPE.ITEM_LAMASSU_LASER_SHOT, ENT_TYPE.ITEM_SORCERESS_DAGGER_SHOT, ENT_TYPE.ITEM_LASERTRAP_SHOT, 6 | ENT_TYPE.ITEM_SCEPTER_PLAYERSHOT, ENT_TYPE.ITEM_FIREBALL, ENT_TYPE.ITEM_HUNDUN_FIREBALL, ENT_TYPE.ITEM_ACIDSPIT, 7 | ENT_TYPE.ITEM_INKSPIT, ENT_TYPE.ITEM_WOODEN_ARROW} 8 | proj_to = {ENT_TYPE.ITEM_ROCK, ENT_TYPE.ITEM_EGGPLANT, ENT_TYPE.ITEM_BULLET, ENT_TYPE.ITEM_UFO_LASER_SHOT, 9 | ENT_TYPE.ITEM_LAMASSU_LASER_SHOT, ENT_TYPE.ITEM_SORCERESS_DAGGER_SHOT, ENT_TYPE.ITEM_LASERTRAP_SHOT, 10 | ENT_TYPE.ITEM_FIREBALL, ENT_TYPE.ITEM_HUNDUN_FIREBALL, ENT_TYPE.ITEM_ACIDSPIT, ENT_TYPE.ITEM_INKSPIT, 11 | ENT_TYPE.ITEM_WOODEN_ARROW, ENT_TYPE.ITEM_NUGGET_SMALL, ENT_TYPE.ITEM_SKULL} 12 | projectile_done = {} 13 | 14 | function projectile_replaced(id) 15 | for i, v in pairs(projectile_done) do 16 | if v == id then 17 | return true 18 | end 19 | end 20 | return false 21 | end 22 | 23 | function replace_projectiles() 24 | ents = get_entities_by_type(proj_from) 25 | for i, ent in ipairs(ents) do 26 | if not projectile_replaced(ent) then 27 | x, y, l = get_position(ent) 28 | e = get_entity(ent) 29 | vx = e.velocityx 30 | vy = e.velocityy 31 | kill_entity(ent) 32 | newent = proj_to[math.random(#proj_to)] 33 | newid = spawn(newent, x, y, l, vx, vy) 34 | projectile_done[#projectile_done + 1] = newid 35 | end 36 | end 37 | end 38 | 39 | function module.start() 40 | projectile_cbs[#projectile_cbs+1] = set_callback(function() 41 | projectile_done = {} 42 | end, ON.LEVEL) 43 | 44 | projectile_cbs[#projectile_cbs+1] = set_callback(function() 45 | replace_projectiles() 46 | end, ON.FRAME) 47 | end 48 | 49 | function module.stop() 50 | for i,v in ipairs(projectile_cbs) do 51 | clear_callback(v) 52 | end 53 | projectile_cbs = {} 54 | end 55 | 56 | return module 57 | -------------------------------------------------------------------------------- /examples/barrymod.lua: -------------------------------------------------------------------------------- 1 | meta.name = 'Barrymod' 2 | meta.version = '1.0' 3 | meta.description = 'Creates checkpoints and restarts the current level on death like nothing happened.' 4 | meta.author = 'Dregu' 5 | 6 | register_option_bool('save_early', 'Multiverse of Madness Mode', 7 | 'Rerolls the level generation on death,\nbut keeps quest state and inventory.', 8 | false) 9 | 10 | register_option_callback('buttons', nil, function(ctx) 11 | if ctx:win_button('Quick Save') then save_state(1) end 12 | ctx:win_inline() 13 | if ctx:win_button('Quick Load') then load_state(1) end 14 | end) 15 | 16 | function save_early() 17 | return options.save_early and state.theme ~= THEME.OLMEC --typical olmec crashing stuff again 18 | end 19 | 20 | set_callback(function() 21 | if not save_early() then 22 | save_state(1) 23 | end 24 | for _, p in pairs(players) do 25 | set_on_player_instagib(p.uid, function(e) 26 | -- can't load_state directly here, cause we're still in the middle of an update 27 | restart = true 28 | end) 29 | end 30 | end, ON.LEVEL) 31 | 32 | set_callback(function() 33 | if save_early() then 34 | -- for whatever prng related reason, loading a save created at this point will reroll the level rng, which is a neat I guess 35 | save_state(1) 36 | end 37 | end, ON.PRE_LEVEL_GENERATION) 38 | 39 | set_callback(function() 40 | if restart then 41 | restart = nil 42 | -- load the save state we made earlier, after updates to not mess with the running state 43 | load_state(1) 44 | end 45 | end, ON.POST_UPDATE) 46 | 47 | set_callback(function() 48 | if state.screen ~= SCREEN.TRANSITION then return end 49 | local tile = get_entity(get_grid_entity_at(6, 121, LAYER.FRONT)) 50 | if tile then 51 | tile:remove() 52 | tile = get_entity(get_grid_entity_at(6, 120, LAYER.FRONT)) 53 | if tile then 54 | tile:decorate_internal() 55 | end 56 | else 57 | tile = get_entity(spawn_grid_entity(ENT_TYPE.FLOOR_GENERIC, 6, 121, LAYER.FRONT)) 58 | tile:decorate_internal() 59 | end 60 | end, ON.POST_LEVEL_GENERATION) 61 | -------------------------------------------------------------------------------- /examples/checkpoint.lua: -------------------------------------------------------------------------------- 1 | meta.name = 'Ankh Checkpoint' 2 | meta.version = 'WIP' 3 | meta.description = 'If you die with an ankh, you get revived where you picked it up and get a new ankh. Only works in the front layer!' 4 | meta.author = 'Dregu' 5 | 6 | cp = {0, 0, 0} 7 | ankhs = {} 8 | revive = -1 9 | died = 0 10 | 11 | function dist(ax, ay, bx, by) 12 | return math.sqrt((bx - ax)^2 + (by - ay)^2) 13 | end 14 | 15 | set_callback(function() 16 | set_timeout(function() 17 | set_interval(function() 18 | for i, v in ipairs(get_entities_by_type(ENT_TYPE.ITEM_PICKUP_ANKH)) do 19 | ent = get_entity(v) 20 | ent.flags = set_flag(ent.flags, 10) 21 | x, y, l = get_position(v) 22 | ankhs[v] = {x, y + 0.1, l} 23 | end 24 | 25 | for i, v in pairs(ankhs) do 26 | ent = get_entity(i) 27 | if ent == nil or ent.type.id ~= ENT_TYPE.ITEM_PICKUP_ANKH then 28 | px, py, pl = get_position(players[1].uid) 29 | if dist(px, py, v[1], v[2]) < 1 and get_frame() > died+60 then 30 | cp = v 31 | toast('Checkpoint!') 32 | end 33 | ankhs[i] = nil 34 | end 35 | end 36 | 37 | if entity_has_item_type(players[1].uid, ENT_TYPE.ITEM_POWERUP_ANKH) then 38 | if players[1].health == 0 then 39 | died = get_frame() 40 | set_timeout(function() 41 | move_entity(players[1].uid, cp[1], cp[2], LAYER.FRONT, 0, 0) 42 | end, 3) 43 | if revive == -1 then 44 | revive = set_timeout(function() 45 | spawn_entity_over(ENT_TYPE.ITEM_PICKUP_ANKH, players[1].uid, 0, 0) 46 | revive = -1 47 | end, 4) 48 | end 49 | end 50 | end 51 | end, 1) 52 | end, 15) 53 | x, y, l = get_position(players[1].uid) 54 | cp = {x, y, l} 55 | ankhs = {} 56 | died = 0 57 | end, ON.LEVEL) 58 | -------------------------------------------------------------------------------- /src/game_api/prng.cpp: -------------------------------------------------------------------------------- 1 | #include "prng.hpp" 2 | 3 | void PRNG::seed(int64_t seed) 4 | { 5 | auto next_pair = [useed = static_cast(seed)]() mutable 6 | { 7 | // advance state 8 | useed = (uint64_t((useed & 0xffffffff) == 0) - (useed & 0xffffffff)) * -0x61939c2f98956567; 9 | useed = (((useed >> 0x1c) ^ useed) >> 0x17) ^ useed; 10 | 11 | // generate next pair 12 | PRNG::prng_pair useed_pair; 13 | useed_pair.first = useed * -0x61939c2f98956567; 14 | useed_pair.second = (useed * -0x7cc4ab2b38000000 | useed_pair.first >> 0x25) * -0x61939c2f98956567; 15 | useed_pair.first = (useed_pair.first >> 0x1c ^ useed_pair.first) >> 0x17 ^ useed_pair.first; 16 | return useed_pair; 17 | }; 18 | 19 | for (auto& pair : pairs) 20 | { 21 | pair = next_pair(); 22 | } 23 | } 24 | 25 | PRNG::prng_pair PRNG::get_and_advance(PRNG_CLASS type) 26 | { 27 | prng_pair& pair = pairs[type]; 28 | prng_pair copy = pair; 29 | 30 | const std::uint64_t lower = pair.first; 31 | const std::uint64_t upper = pair.second; 32 | 33 | const std::uint64_t rest = upper - lower; 34 | 35 | pair = { 36 | static_cast(static_cast(upper) * -0x2c7cc17fb0b3a8b5), 37 | pair.second = rest * 0x8000000 | rest >> 0x25, 38 | }; 39 | 40 | return copy; 41 | } 42 | 43 | std::optional PRNG::internal_random_int(std::int64_t min, std::int64_t size, PRNG_CLASS type) 44 | { 45 | if (size <= min) 46 | { 47 | return std::nullopt; 48 | } 49 | 50 | static auto wrap = [](std::int64_t val, std::int64_t _min, std::int64_t _max) 51 | { 52 | const auto diff = _max - _min; 53 | 54 | if (val < _min) 55 | val += diff * ((_min - val) / diff + 1); 56 | 57 | return _min + (val - _min) % diff; 58 | }; 59 | 60 | prng_pair pair = get_and_advance(type); 61 | 62 | // Technically not a uniform distribution, but we have 64bit to map to a range that is many orders of magnitude smaller 63 | // So in the grand scheme this is close enough to a uniform distribution 64 | return wrap(static_cast(pair.first), min, size); 65 | } 66 | -------------------------------------------------------------------------------- /src/game_api/savestate.cpp: -------------------------------------------------------------------------------- 1 | #include "savestate.hpp" 2 | 3 | #include "memory.hpp" // for write_mem_prot, write_mem_recoverable 4 | #include "online.hpp" // for Online 5 | #include "script/events.hpp" // for pre_load_state 6 | #include "state.hpp" // for StateMemory 7 | 8 | void SaveState::backup_main(int slot_to) 9 | { 10 | if (pre_save_state(slot_to, get_save_state(slot_to))) 11 | return; 12 | 13 | auto base_from = HeapBase::get_main(); 14 | auto base_to = HeapBase::get(static_cast(slot_to - 1)); 15 | 16 | pre_copy_state_event(base_from, base_to); 17 | base_from.copy_to(base_to); 18 | post_save_state(slot_to, base_to.state()); 19 | } 20 | 21 | void SaveState::restore_main(int slot_from) 22 | { 23 | if (pre_load_state(slot_from, get_save_state(slot_from))) 24 | return; 25 | 26 | auto base_from = HeapBase::get(static_cast(slot_from - 1)); 27 | auto base_to = HeapBase::get_main(); 28 | 29 | pre_copy_state_event(base_from, base_to); 30 | base_from.copy_to(base_to); 31 | post_load_state(slot_from, base_from.state()); 32 | } 33 | 34 | StateMemory* get_save_state(int slot) 35 | { 36 | auto state = HeapBase::get(static_cast(slot - 1)).state(); 37 | if (state->screen) 38 | return state; 39 | return nullptr; 40 | } 41 | 42 | void invalidate_save_slots() 43 | { 44 | if (get_online()->is_active()) 45 | return; 46 | for (int i = 1; i <= 4; ++i) 47 | { 48 | auto state = get_save_state(i); 49 | if (state) 50 | state->screen = 0; 51 | } 52 | } 53 | 54 | void SaveState::load() 55 | { 56 | if (base.is_null()) 57 | return; 58 | 59 | if (slot != -1 && base.state()->screen == 0) 60 | return; 61 | 62 | auto state = base.state(); 63 | if (pre_load_state(-1, state)) 64 | return; 65 | base.copy_to(HeapBase::get_main()); 66 | post_load_state(-1, state); 67 | } 68 | 69 | void SaveState::save() 70 | { 71 | if (base.is_null()) 72 | return; 73 | 74 | auto state = base.state(); 75 | if (pre_save_state(-1, state)) 76 | return; 77 | HeapBase::get_main().copy_to(base); 78 | post_save_state(-1, state); 79 | } 80 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.17) 2 | project(overlunky) 3 | 4 | set(CMAKE_CXX_STANDARD 20) 5 | 6 | include(cmake/clang-format.cmake) 7 | setup_format_target(format_overlunky) 8 | 9 | set(IWYU_MAPPING_FILE ${CMAKE_SOURCE_DIR}/overlunky.3rdparty.headers.imp) 10 | set(POST_IWYU_FORMATTING_TARGET format_overlunky_changes) 11 | include(cmake/include-what-you-use.cmake) 12 | 13 | include(cmake/link_sys_library.cmake) 14 | 15 | string(COMPARE EQUAL "Clang" "${CMAKE_CXX_COMPILER_ID}" CLANG) 16 | 17 | if(NOT DEFINED CMAKE_RUNTIME_OUTPUT_DIRECTORY) 18 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ../../bin) 19 | endif() 20 | 21 | option(BUILD_OVERLUNKY CACHE ON) 22 | option(BUILD_INFO_DUMP CACHE ON) 23 | option(BUILD_SPEL2_DLL CACHE OFF) 24 | OPTION(OVERLUNKY_UNITY_BUILD OFF) 25 | 26 | function(setup_ol_target TARGET_NAME) 27 | setup_iwyu(${TARGET_NAME}) 28 | 29 | if(OVERLUNKY_UNITY_BUILD) 30 | set_property( 31 | TARGET ${TARGET_NAME} 32 | PROPERTY UNITY_BUILD ON) 33 | endif() 34 | 35 | if(OVERLUNKY_LINK_TIME_OPT) 36 | set_property( 37 | TARGET ${TARGET_NAME} 38 | PROPERTY INTERPROCEDURAL_OPTIMIZATION ON) 39 | endif() 40 | endfunction() 41 | 42 | add_compile_definitions(_ITERATOR_DEBUG_LEVEL=0) 43 | add_compile_definitions(NOMINMAX) 44 | add_compile_definitions(WIN32_LEAN_AND_MEAN) 45 | 46 | # Fix MSVC 19.40 crash with mutex due to spelunky using an old redist (mscvp140.dll) 47 | # Related links: https://github.com/microsoft/STL/releases/tag/vs-2022-17.10 | https://github.com/actions/runner-images/issues/10004 48 | add_compile_definitions(_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR) 49 | 50 | # Temp fix for the new MSVC tools requiring minimum clang 18, for the time being we're staying on clang 17 51 | add_compile_definitions(_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH) 52 | 53 | if(BUILD_OVERLUNKY) 54 | add_compile_definitions(SPEL2_EDITABLE_SCRIPTS) 55 | add_compile_definitions(SPEL2_EXTRA_ANNOYING_SCRIPT_ERRORS) 56 | endif() 57 | 58 | add_subdirectory(src) 59 | 60 | if(MSVC) 61 | add_definitions(/bigobj) 62 | 63 | if(BUILD_OVERLUNKY) 64 | set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT injector) 65 | endif() 66 | endif() 67 | -------------------------------------------------------------------------------- /src/game_api/script/usertypes/drops_lua.cpp: -------------------------------------------------------------------------------- 1 | #include "drops_lua.hpp" 2 | 3 | #include // for size_t 4 | #include // for operator new 5 | #include // for proxy_key_t, global_table, state, table_proxy 6 | #include // for get 7 | #include // for move 8 | #include // for max, min 9 | #include // for vector 10 | 11 | #include "drops.hpp" // for drop_entries, dropchance_entries, replace_drop 12 | 13 | namespace NDrops 14 | { 15 | void register_usertypes(sol::state& lua) 16 | { 17 | /// Alters the drop chance for the provided monster-item combination (use e.g. set_drop_chance(DROPCHANCE.MOLE_MATTOCK, 10) for a 1 in 10 chance) 18 | /// Use `-1` as dropchance_id to reset all to default 19 | lua["set_drop_chance"] = set_drop_chance; 20 | /// Changes a particular drop, e.g. what Van Horsing throws at you (use e.g. replace_drop(DROP.VAN_HORSING_DIAMOND, ENT_TYPE.ITEM_PLASMACANNON)) 21 | /// Use `0` as type to reset this drop to default, use `-1` as drop_id to reset all to default 22 | /// Check all the available drops [here](https://github.com/spelunky-fyi/overlunky/blob/main/src/game_api/drops.cpp) 23 | lua["replace_drop"] = replace_drop; 24 | 25 | lua.create_named_table("DROPCHANCE" 26 | //, "BONEBLOCK_SKELETONKEY", 0 27 | //, "", ...see__[drops.cpp](https://github.com/spelunky-fyi/overlunky/blob/main/src/game_api/drops.cpp\]__for__a__list__of__possible__dropchances... 28 | //, "YETI_PITCHERSMITT", 10 29 | ); 30 | for (size_t x = 0; x < dropchance_entries.size(); ++x) 31 | { 32 | lua["DROPCHANCE"][dropchance_entries.at(x).caption] = x; 33 | } 34 | 35 | lua.create_named_table("DROP" 36 | //, "ALTAR_DICE_CLIMBINGGLOVES", 0 37 | //, "", ...see__[drops.cpp](https://github.com/spelunky-fyi/overlunky/blob/main/src/game_api/drops.cpp\]__for__a__list__of__possible__drops... 38 | //, "YETI_PITCHERSMITT", 85 39 | ); 40 | for (size_t x = 0; x < drop_entries.size(); ++x) 41 | { 42 | lua["DROP"][drop_entries.at(x).caption] = x; 43 | } 44 | } 45 | }; // namespace NDrops 46 | -------------------------------------------------------------------------------- /examples/room_visualizer.lua: -------------------------------------------------------------------------------- 1 | meta.name = "Room visualizer" 2 | meta.version = "WIP" 3 | meta.description = "Shows shop zones, shop floors and other special room things." 4 | meta.author = "Dregu" 5 | 6 | set_callback(function(ctx) 7 | local l = state.camera_layer 8 | for rx=0,state.width-1 do 9 | for ry=0,state.height-1 do 10 | local rt = get_room_template(rx, ry, l) 11 | local rn = get_room_template_name(rt) 12 | if rt == 86 or rt == 87 or rt == 90 or rt == 45 or string.match(rn, "shop") or string.match(rn, "challenge_entrance") or string.match(rn, "vault") then 13 | local x = rx * 10 + 7.5 14 | local y = 122.5 - ry * 8 - 4 15 | local left, right = x, x 16 | local top, bottom = y+0.01, y-0.01 17 | while is_inside_shop_zone(left, y, l) do left = left - 0.1 end 18 | while is_inside_shop_zone(x, top, l) do top = top + 0.1 end 19 | while is_inside_shop_zone(right, y, l) do right = right + 0.1 end 20 | while is_inside_shop_zone(x, bottom, l) do bottom = bottom - 0.1 end 21 | local box = AABB:new(left, top, right, bottom) 22 | local sbox = screen_aabb(box) 23 | ctx:draw_rect_filled(sbox, 0, 0x7000ff00) 24 | 25 | for x=rx*10+3,rx*10+12,1 do 26 | for y=122-ry*8,122-ry*8-7,-1 do 27 | local color = 0x60000000 28 | if is_inside_active_shop_room(x, y, l) then color = color | 0x00ff0000 end 29 | local floor = get_grid_entity_at(x, y, l) 30 | box = AABB:new(x-0.5, y+0.5, x+0.5, y-0.5) 31 | sbox = screen_aabb(box) 32 | if floor ~= -1 then 33 | local ent = get_entity(floor) 34 | if test_flag(ent.flags, ENT_FLAG.SHOP_FLOOR) then 35 | color = color | 0x400000ff 36 | end 37 | end 38 | ctx:draw_rect_filled(sbox, 0, color) 39 | end 40 | end 41 | end 42 | end 43 | end 44 | end, ON.GUIFRAME) 45 | -------------------------------------------------------------------------------- /src/game_api/illumination.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "aliases.hpp" 7 | 8 | struct Color; 9 | struct Vec2; 10 | 11 | enum class LIGHT_TYPE : uint8_t 12 | { 13 | NONE = 0x0, 14 | FOLLOW_CAMERA = 0x1, 15 | FOLLOW_ENTITY = 0x2, 16 | ROOM_LIGHT = 0x4, 17 | }; 18 | 19 | struct LightParams // it's probably just Color 20 | { 21 | float red; // default = 1.0 (can go over 1.0 for oversaturation) 22 | float green; 23 | float blue; 24 | float size; 25 | 26 | /// Returns LightParams as Color, note that size = alpha 27 | Color* as_color() 28 | { 29 | return reinterpret_cast(this); 30 | }; 31 | }; 32 | 33 | struct Illumination 34 | { 35 | union 36 | { 37 | /// Table of light1, light2, ... etc. 38 | std::array lights; 39 | struct 40 | { 41 | LightParams light1; 42 | LightParams light2; 43 | LightParams light3; 44 | /// It's rendered on objects around, not as an actual bright spot 45 | LightParams light4; 46 | }; 47 | }; 48 | float brightness; 49 | float brightness_multiplier; 50 | float light_pos_x; 51 | float light_pos_y; 52 | float offset_x; 53 | float offset_y; 54 | float distortion; 55 | int32_t entity_uid; 56 | uint32_t timer; 57 | union 58 | { 59 | /// see [flags.hpp](https://github.com/spelunky-fyi/overlunky/blob/main/src/game_api/flags.hpp) illumination_flags 60 | uint32_t flags; 61 | struct 62 | { 63 | uint8_t light_flags; // not exposed since flags already is so no reason to essentially expose this again, even thou flags is technically not correct 64 | 65 | LIGHT_TYPE type_flags; 66 | uint8_t layer; 67 | bool enabled; 68 | }; 69 | }; 70 | }; 71 | 72 | [[nodiscard]] Illumination* create_illumination(Vec2 pos, Color color, LIGHT_TYPE type, float size, uint8_t flags, int32_t uid, LAYER layer); 73 | [[nodiscard]] Illumination* create_illumination(Color color, float size, float x, float y); 74 | [[nodiscard]] Illumination* create_illumination(Color color, float size, int32_t uid); 75 | void refresh_illumination(Illumination* illumination); 76 | -------------------------------------------------------------------------------- /docs/examples/EntityDB.md: -------------------------------------------------------------------------------- 1 | > When cloning an entity type, remember to save it in the script for as long as you need it. Otherwise the memory will be freed immediately, which eventually leads to a crash when used or overwritten by other stuff: 2 | 3 | ```lua 4 | -- Create a special fast snake type with weird animation 5 | special_snake = EntityDB:new(ENT_TYPE.MONS_SNAKE) 6 | special_snake.max_speed = 1 7 | special_snake.acceleration = 2 8 | special_snake.animations[2].num_tiles = 1 9 | 10 | set_post_entity_spawn(function(snake) 11 | -- 50% chance to make snakes special 12 | if prng:random_chance(2, PRNG_CLASS.PROCEDURAL_SPAWNS) then 13 | -- Assign custom type 14 | snake.type = special_snake 15 | -- This is only really needed if types are changed during the level 16 | snake.current_animation = special_snake.animations[2] 17 | end 18 | end, SPAWN_TYPE.ANY, MASK.MONSTER, ENT_TYPE.MONS_SNAKE) 19 | ``` 20 | 21 | > You can also use Entity.user_data to store the custom type: 22 | 23 | ```lua 24 | -- Custom player who is buffed a bit every level 25 | set_callback(function() 26 | -- Doing this to include HH 27 | for i,v in ipairs(get_entities_by_mask(MASK.PLAYER)) do 28 | local player = get_entity(v) 29 | 30 | -- Create new custom type on the first level, based on the original type 31 | if not player.user_data then 32 | player.user_data = {} 33 | player.user_data.type = EntityDB:new(player.type.id) 34 | end 35 | 36 | -- Set the player entity type to the custom type every level 37 | player.type = player.user_data.type 38 | 39 | -- Buff the player every subsequent level 40 | if state.level_count > 0 then 41 | player.type.max_speed = player.type.max_speed * 1.1 42 | player.type.acceleration = player.type.acceleration * 1.1 43 | player.type.jump = player.type.jump * 1.1 44 | end 45 | end 46 | end, ON.POST_LEVEL_GENERATION) 47 | ``` 48 | 49 | > Illegal bad example, don't do this: 50 | 51 | ```lua 52 | set_callback(function() 53 | -- Nobody owns the new type and the memory is freed immediately, eventually leading to a crash 54 | players[1].type = EntityDB:new(players[1].type) 55 | players[1].type.max_speed = 2 56 | end, ON.POST_LEVEL_GENERATION) 57 | ``` 58 | -------------------------------------------------------------------------------- /src/game_api/file_api.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for size_t 4 | #include // for malloc 5 | #include // for ID3D11ShaderResourceView 6 | #include // for string 7 | #include // string_view 8 | 9 | using AllocFun = decltype(malloc); 10 | 11 | struct FileInfo 12 | { 13 | void* Data{nullptr}; 14 | int _member_1{0}; 15 | int DataSize{0}; 16 | int AllocationSize{0}; 17 | int _member_4{0}; 18 | }; 19 | using LoadFileCallback = FileInfo*(const char* file_path, AllocFun alloc_fun); 20 | using ReadFromFileOrig = void(const char* file, void** out_data, size_t* out_data_size); 21 | using ReadFromFileCallback = void(const char* file, void** out_data, size_t* out_data_size, AllocFun alloc_fun, ReadFromFileOrig* orig); 22 | using WriteToFileOrig = void(const char* backup_file, const char* file, void* data, size_t data_size); 23 | using WriteToFileCallback = void(const char* backup_file, const char* file, void* data, size_t data_size, WriteToFileOrig* orig); 24 | using GetImageFilePathCallback = std::string(std::string root_path, std::string relative_path); 25 | using MakeSavePathCallback = std::string (*)(std::string_view script_path, std::string_view script_name); 26 | 27 | FileInfo* load_file_as_dds_if_image(const char* file_path, AllocFun alloc_fun); 28 | 29 | void register_on_load_file(LoadFileCallback on_load_file); 30 | void register_on_read_from_file(ReadFromFileCallback on_read_from_file); 31 | void register_on_write_to_file(WriteToFileCallback on_write_to_file); 32 | void register_get_image_file_path(GetImageFilePathCallback get_image_file_path); 33 | void register_make_save_path(MakeSavePathCallback make_save_path_callback); 34 | 35 | std::string get_image_file_path(std::string root_path, std::string relative_path); 36 | 37 | bool create_d3d11_texture_from_file(const char* filename, struct ID3D11ShaderResourceView** out_srv, int* out_width, int* out_height, int crop_x = 0, int crop_y = 0, int crop_w = 0, int crop_h = 0); 38 | bool create_d3d11_texture_from_memory(const unsigned char* buf, const unsigned int buf_size, ID3D11ShaderResourceView** out_srv, int* out_width, int* out_height); 39 | bool get_image_size_from_file(const char* filename, int* out_width, int* out_height); 40 | 41 | std::string hash_path(std::string_view path); 42 | void clear_cache(std::string_view path = ""); 43 | -------------------------------------------------------------------------------- /examples/skipintro.lua: -------------------------------------------------------------------------------- 1 | meta.name = 'Skip intro' 2 | meta.description = 'Jump straight to 1-1 from intro or title screen.' 3 | meta.author = 'Dregu' 4 | meta.version = '1.2' 5 | 6 | --[[ Overlunky Pro Tip: Put 7 | autorun_scripts = ["skipintro.lua"] 8 | in your overlunky.ini for this to make any sense... 9 | 10 | Quick start with "Q" is baked into Overlunky now, but maybe you can use this with Playlunky. ]] 11 | 12 | local function skip() 13 | -- Jump to 1-1 from any screen before menu 14 | if state.screen >= SCREEN.LOGO and state.screen < SCREEN.MENU then 15 | 16 | -- init single player: 17 | state.items.player_select[1].activated = true 18 | state.items.player_count = 1 19 | -- get character from last played game: 20 | state.items.player_select[1].character = ENT_TYPE.CHAR_ANA_SPELUNKY + savegame.players[1] 21 | state.items.player_select[1].texture = TEXTURE.DATA_TEXTURES_CHAR_YELLOW_0 + savegame.players[1] 22 | 23 | -- init state: 24 | state.screen_next = SCREEN.LEVEL 25 | state.world_start = 1 26 | state.level_start = 1 27 | state.theme_start = THEME.DWELLING 28 | state.world_next = 1 29 | state.level_next = 1 30 | state.theme_next = THEME.DWELLING 31 | state.quest_flags = QUEST_FLAG.RESET 32 | state.loading = 1 33 | 34 | -- fix for character select screen (when going directly from death screen): 35 | state.screen_character_select.available_mine_entrances = 4; 36 | -- can also be set to SCREEN.LEVEL 37 | -- then going to character select (from death screen) and choosing the character will bring you back to the game directly 38 | state.screen_character_select.next_screen_to_load = SCREEN.CAMP; 39 | 40 | -- disable the [title screen -> menu] animation: 41 | game_manager.screen_menu.loaded_once = true 42 | 43 | -- set controller to first input device for player one and menu 44 | -- id 0 usually is keyboard, without this, you will need to press jump button first to register controller 45 | if game_manager.game_props.input_index[1] == -1 then 46 | game_manager.game_props.input_index[1] = 0 47 | end 48 | if game_manager.game_props.input_index[5] == -1 then 49 | game_manager.game_props.input_index[5] = 0 50 | end 51 | end 52 | end 53 | 54 | set_callback(skip, ON.SCREEN) 55 | 56 | -- only for overlunky loading the script from the UI 57 | skip() 58 | -------------------------------------------------------------------------------- /src/game_api/ghidra_byte_string.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for size_t 4 | #include // for length_error, runtime_error 5 | #include // for string_view 6 | 7 | #include "tokenize.h" // for Tokenize 8 | 9 | template 10 | struct GhidraByteString 11 | { 12 | inline static constexpr std::size_t M = (N + 1) / 3; 13 | char cpp_byte_string[M]{}; 14 | 15 | constexpr GhidraByteString(char const (&str)[N]) 16 | { 17 | std::size_t i{}; 18 | for (const auto substr : Tokenize<' '>{str}) 19 | { 20 | if (substr.size() != 2) 21 | { 22 | throw std::length_error{"GhidraByteString must be constructed from a sequence of 2-char long strings."}; 23 | } 24 | else if (substr == "..") 25 | { 26 | cpp_byte_string[i] = '*'; 27 | } 28 | else 29 | { 30 | cpp_byte_string[i] = from_string(substr); 31 | } 32 | 33 | i++; 34 | } 35 | }; 36 | constexpr std::size_t size() const 37 | { 38 | return M; 39 | } 40 | 41 | private: 42 | static constexpr bool is_digit(char c) 43 | { 44 | return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'); 45 | } 46 | static constexpr char tolower(const char c) 47 | { 48 | return (c >= 'A' && c <= 'F') ? c + ('a' - 'A') : c; 49 | } 50 | static constexpr char to_byte(char c) 51 | { 52 | return (c >= '0' && c <= '9') 53 | ? c - '0' 54 | : c - 'a' + 10; 55 | } 56 | 57 | static constexpr char from_string(std::string_view str) 58 | { 59 | const char first = tolower(str[0]); 60 | const char second = tolower(str[1]); 61 | if (!is_digit(first) || !is_digit(second)) 62 | { 63 | throw std::runtime_error{"Not a digit"}; 64 | } 65 | 66 | const char value = (to_byte(first) << 4) | to_byte(second); 67 | return value; 68 | } 69 | }; 70 | 71 | template 72 | constexpr auto operator"" _gh() 73 | { 74 | return std::string_view{Str.cpp_byte_string, Str.size()}; 75 | } 76 | 77 | #ifndef _MSC_VER 78 | static_assert("0F 0f af 00 12 22 .. .. 12 .."_gh == "\x0F\x0f\xaf\x00\x12\x22**\x12*"sv); 79 | #endif 80 | -------------------------------------------------------------------------------- /src/game_api/script/script_impl.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for path 4 | #include // for string 5 | 6 | #include "lua_backend.hpp" // for LuaBackend 7 | #include "script.hpp" // for ScriptMeta 8 | 9 | class LuaConsole; 10 | class SoundManager; 11 | 12 | class ScriptImpl : public LockableLuaBackend 13 | { 14 | public: 15 | #ifdef SPEL2_EDITABLE_SCRIPTS 16 | std::string code; 17 | #else 18 | std::string code; 19 | #endif 20 | 21 | bool changed = true; 22 | bool enabled = true; 23 | ScriptMeta meta = {"", "", "", "", "", "", "", "", "", false}; 24 | std::filesystem::path script_folder; 25 | 26 | ScriptImpl(std::string script, std::string file, SoundManager* sound_manager, LuaConsole* con, bool enable = true); 27 | virtual ~ScriptImpl() override 28 | { 29 | set_enabled(false); 30 | } 31 | 32 | std::string script_id(); 33 | 34 | virtual bool reset() override; 35 | virtual bool pre_update() override 36 | { 37 | if (changed) 38 | { 39 | result = ""; 40 | changed = false; 41 | if (!reset()) 42 | { 43 | return false; 44 | } 45 | } 46 | return true; 47 | } 48 | 49 | virtual void set_enabled(bool enabled) override; 50 | virtual bool get_enabled() const override 51 | { 52 | return enabled; 53 | } 54 | virtual bool get_unsafe() const override 55 | { 56 | return meta.unsafe; 57 | } 58 | virtual const char* get_name() const override 59 | { 60 | return meta.stem.c_str(); 61 | } 62 | virtual const char* get_id() const override 63 | { 64 | return meta.id.c_str(); 65 | } 66 | virtual const char* get_version() const override 67 | { 68 | return meta.version.c_str(); 69 | } 70 | virtual const char* get_path() const override 71 | { 72 | return meta.file.c_str(); 73 | } 74 | virtual const char* get_root() const override 75 | { 76 | return meta.path.c_str(); 77 | } 78 | virtual const std::filesystem::path& get_root_path() const override 79 | { 80 | return script_folder; 81 | } 82 | 83 | std::string execute(std::string str, bool raw = false); 84 | sol::protected_function_result execute_raw(std::string str); 85 | }; 86 | -------------------------------------------------------------------------------- /src/game_api/savestate.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // for uint32_t, int8_t 4 | 5 | #include "heap_base.hpp" // for HeapBase 6 | 7 | struct StateMemory; 8 | struct PRNG; 9 | 10 | class SaveState 11 | { 12 | public: 13 | /// Create a new temporary SaveState/clone of the main level state. Unlike save_state slots that are preallocated by the game anyway, these will use 32MiB a pop and aren't freed automatically, so make sure to clear them or reuse the same one to save memory. The garbage collector will eventually clear the SaveStates you don't have a handle to any more though. 14 | SaveState() 15 | : base(reinterpret_cast(malloc(8ull * 0x400000))) 16 | { 17 | save(); 18 | } 19 | /// NoDoc 20 | SaveState(uint8_t index) 21 | : base(HeapBase::get(index)){}; 22 | ~SaveState() 23 | { 24 | clear(); 25 | } 26 | /// Get the pre-allocated by the game save slot 1-4. Call as `SaveState.get(slot)` 27 | static SaveState get(int save_slot) 28 | { 29 | int8_t index = static_cast(save_slot - 1); 30 | SaveState save_from_slot(index); 31 | save_from_slot.slot = index; 32 | return save_from_slot; 33 | } 34 | 35 | /// Access the StateMemory inside a SaveState 36 | StateMemory* get_state() const 37 | { 38 | return base.state(); 39 | } 40 | /// Get the current frame from the SaveState, equivelent to the [get_frame](#Get_frame) global function that returns the frame from the "loaded in state" 41 | uint32_t get_frame() const 42 | { 43 | return base.frame_count(); 44 | } 45 | /// Access the PRNG inside a SaveState 46 | PRNG* get_prng() const 47 | { 48 | return base.prng(); 49 | } 50 | 51 | /// Load a SaveState 52 | void load(); 53 | 54 | /// Save over a previously allocated SaveState 55 | void save(); 56 | 57 | /// Delete the SaveState and free the memory. The SaveState can't be used after this. 58 | void clear() 59 | { 60 | if (slot != -1) 61 | return; 62 | 63 | base.free(); 64 | } 65 | static void backup_main(int slot_to); 66 | static void restore_main(int from_slot); 67 | 68 | private: 69 | HeapBase base; 70 | int8_t slot{-1}; 71 | }; 72 | 73 | StateMemory* get_save_state(int slot); 74 | void invalidate_save_slots(); 75 | -------------------------------------------------------------------------------- /examples/duat_from_any_altar.lua: -------------------------------------------------------------------------------- 1 | meta.name = "Duat from any altar self-sacrifice" 2 | meta.version = "WIP" 3 | meta.description = "Sacrificing yourself on any altar with an ankh transitions you to Duat" 4 | meta.author = "Zappatic" 5 | 6 | 7 | transition_started = false 8 | original_health = 4 9 | 10 | function transition_to_duat() 11 | -- When transitioning, make sure we set the health back, otherwise we immediately die in Duat 12 | -- The player is dead though, so we set it directly in the player inventory 13 | state.items.player_inventory[1].health = original_health 14 | 15 | state.screen_next = SCREEN.TRANSITION 16 | state.world_next = 4 17 | state.level_next = 4 18 | state.theme_next = THEME.DUAT 19 | state.ingame = 1 20 | state.fade_timer = 18 21 | state.fade_length = 18 22 | state.loading_black_screen_timer = 0 23 | state:force_current_theme(THEME.CITY_OF_GOLD) -- this makes it so we "come from" COG, which renders the correct limbo transition 24 | state.loading = 1 25 | transition_started = false 26 | end 27 | 28 | set_callback(function() 29 | local altars = get_entities_by_type(ENT_TYPE.FLOOR_ALTAR) 30 | for i, altar_uid in ipairs(altars) do 31 | -- Collision 2 on FLOOR_ALTAR is what does all the sacrificing stuff, so we do our own stuff just before that 32 | set_pre_collision2(altar_uid, function(self, collision_entity) 33 | if transition_started == false and collision_entity.uid == players[1].uid and players[1].state == 18 and players[1]:has_powerup(ENT_TYPE.ITEM_POWERUP_ANKH) then 34 | transition_started = true 35 | 36 | local powerups = players[1]:get_powerups() 37 | for j, powerup_uid in ipairs(powerups) do 38 | local powerup = get_entity(powerup_uid) 39 | if powerup ~= nil and powerup.type.id == ENT_TYPE.ITEM_POWERUP_ANKH then 40 | powerup.move_state = 0 -- prevent the ankh from showing 41 | break 42 | end 43 | end 44 | players[1]:remove_powerup(ENT_TYPE.ITEM_POWERUP_ANKH) 45 | 46 | original_health = players[1].health -- save health for restoration later 47 | set_timeout(transition_to_duat, 100) -- transition after 100 frames (just like the game does) 48 | end 49 | end) 50 | end 51 | end, ON.LEVEL) 52 | -------------------------------------------------------------------------------- /src/game_api/steam_api.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | using FEAT = uint8_t; 4 | 5 | constexpr std::array g_AchievementNames = { 6 | "THE_FULL_SPELUNKY", 7 | "YOU_GOT_THIS", 8 | "FEELS_GOOD", 9 | "SKILLS_IMPROVING", 10 | "PERSISTENT", 11 | "JOURNEYMAN", 12 | "IRONMAN", 13 | "SPEEDLUNKY", 14 | "LOW_SCORER", 15 | "PILGRIM", 16 | "MASTER", 17 | "AWAKENED", 18 | "EXCAVATOR", 19 | "TORCHBEARER", 20 | "SURVIVOR", 21 | "MILLIONAIRE", 22 | "SEEN_A_LOT", 23 | "SEEN_IT_ALL", 24 | "MAMAS_LITTLE_HELPER", 25 | "MAMAS_BIG_HELPER", 26 | "TRACK_STAR", 27 | "ARENA_CHAMPION", 28 | "TURKEY_WHISPERER", 29 | "SUPPORT_A_LOCAL_BUSINESS", 30 | "VIP", 31 | "SHADOW_SHOPPER", 32 | "LEGENDARY", 33 | "HER_FAVORITE", 34 | "DIVINE_RIGHT", 35 | "A_SECOND_CHANCE", 36 | "CHOSEN_ONE", 37 | "PARENTHOOD", 38 | }; 39 | 40 | constexpr std::array g_AllAchievements = { 41 | "6E0C60E83AC07309", 42 | "B03193B1D35645AC", 43 | "81B12D01899DD911", 44 | "C356BD3F920AA007", 45 | "CD2247AB88095173", 46 | "3080F6BD3EFFD04E", 47 | "0D471B6E7F899BC9", 48 | "381FE256BB6A3D93", 49 | "7039963F98FB58A5", 50 | "5C97977A1C41E1D8", 51 | "12BB5BD07F56194C", 52 | "EA488CC02AD233FD", 53 | "1BFD11B72624F4C6", 54 | "887316D012E74D3B", 55 | "5B7F2E4EAEC18E51", 56 | "BB2966DD89D2C3E9", 57 | "31F3186C15C42794", 58 | "E93DBDD33881A338", 59 | "3DF7CAAF05559953", 60 | "B8604E694E6449F3", 61 | "84D574F017DC65B9", 62 | "7D7B995A1ED5E7A7", 63 | "468F80D65DD09F9E", 64 | "37801BFF5481B550", 65 | "7EFF7F7E6B9D813F", 66 | "ECBEF23A87A0737A", 67 | "B7EFFD56C8457082", 68 | "061E03E6CA94CA71", 69 | "4F080C487BB27C26", 70 | "112E2F91AC19A57A", 71 | "710891CB8FE6D822", 72 | "C999E58F1EF15759", 73 | }; 74 | 75 | class ISteamUserStats* get_steam_user_stats(); 76 | void enable_steam_achievements(); 77 | void disable_steam_achievements(); 78 | void reset_all_steam_achievements(); 79 | void change_feat(FEAT feat, bool hidden, std::u16string_view name, std::u16string_view description); 80 | std::tuple get_feat(FEAT feat); 81 | bool get_feat_hidden(FEAT feat); 82 | void set_feat_hidden(FEAT feat, bool hidden); 83 | void init_achievement_hooks(); 84 | bool get_steam_achievement(const char* achievement_id, bool* achieved); 85 | bool set_steam_achievement(const char* achievement_id, bool achieved); 86 | -------------------------------------------------------------------------------- /examples/custom_feats.lua: -------------------------------------------------------------------------------- 1 | meta.name = 'Custom Feats' 2 | meta.version = 'WIP' 3 | meta.description = 'Replace a few Feats with our own and create hooks for them.' 4 | meta.author = 'Dregu' 5 | 6 | -- Keep custom feats in a table, unlock some by default for some reason 7 | -- We could just leave this empty though, cause we default to false later 8 | feats = { 9 | [FEAT.THE_FULL_SPELUNKY] = false, 10 | [2] = false, 11 | [32] = true 12 | } 13 | 14 | -- Unlock a feat in our table and toast for it 15 | function perform(feat) 16 | done, hidden, name, desc = get_feat(feat) 17 | if feats[feat] == false then 18 | toast(F"{name}\n{desc}") 19 | end 20 | feats[feat] = true 21 | prinspect("Unlocked custom feat", feat, name) 22 | end 23 | 24 | -- Change "Parenthood" and make it visible 25 | change_feat(FEAT.PARENTHOOD, false, "I am the eggman", "This is impossible to get, but you already have it") 26 | 27 | -- Change the first feat and make it hidden 28 | change_feat(1, true, "I am the walrus", "Play as Guy Spelunky from the hit game Spelunky") 29 | set_callback(function() 30 | if players[1].type.id == ENT_TYPE.CHAR_GUY_SPELUNKY then perform(1) end 31 | end, ON.LEVEL) 32 | 33 | -- One more 34 | change_feat(2, false, "Borscht", "Die to lava") 35 | set_callback(function(id) 36 | if id == hash_to_stringid(0x9c821452) then 37 | perform(2) 38 | -- Reveal the first feat only after getting this one 39 | set_feat_hidden(1, false) 40 | end -- MELTED 41 | end, ON.DEATH_MESSAGE) 42 | 43 | -- Return feat status from our table when asked, defaulting to false if not found 44 | set_callback(function(feat) 45 | return feats[feat] or false 46 | 47 | -- this would default to vanilla behaviour for feats missing from the table, 48 | -- if you only want to override some of them 49 | --return feats[feat] 50 | end, ON.PRE_GET_FEAT) 51 | 52 | -- Block vanilla feats, although they wouldn't show up on the Feats page, 53 | -- they would still trigger Steam achievements if not blocked otherwise 54 | set_callback(function(feat) 55 | prinspect("Tried to unlock a vanilla feat", enum_get_name(FEAT, feat)) 56 | return true 57 | 58 | -- this would default to vanilla behaviour for feats missing from the table, 59 | -- if you only want to override some of them 60 | --if feats[feat] ~= nil then return true end 61 | end, ON.PRE_SET_FEAT) 62 | 63 | -- Clear the feats our mod doesn't use. 64 | for i=3,31 do 65 | change_feat(i, false, "", "") 66 | end 67 | -------------------------------------------------------------------------------- /src/game_api/entities_backgrounds.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "entity.hpp" 4 | #include "sound_manager.hpp" 5 | 6 | struct Illumination; 7 | 8 | class BGBackLayerDoor : public Entity 9 | { 10 | public: 11 | Illumination* illumination1; 12 | Illumination* illumination2; 13 | }; 14 | 15 | class BGSurfaceStar : public Entity 16 | { 17 | public: 18 | int32_t blink_timer; // why negative? 19 | float relative_x; 20 | float relative_y; 21 | int32_t unknown_padding; 22 | }; 23 | 24 | class BGRelativeElement : public Entity 25 | { 26 | public: 27 | float relative_x; 28 | float relative_y; 29 | }; 30 | 31 | class BGSurfaceLayer : public BGRelativeElement 32 | { 33 | public: 34 | float relative_offset_x; 35 | float relative_offset_y; 36 | }; 37 | 38 | class BGEggshipRoom : public Entity 39 | { 40 | public: 41 | SoundMeta* sound; 42 | Entity* fx_shell; 43 | Entity* fx_door; 44 | Entity* platform_left; 45 | Entity* platform_middle; 46 | Entity* platform_right; 47 | bool player_in; 48 | }; 49 | 50 | class BGMovingStar : public BGSurfaceStar 51 | { 52 | public: 53 | /// Can make it rise if set to negative 54 | float falling_speed; 55 | }; 56 | 57 | class BGTutorialSign : public Entity 58 | { 59 | public: 60 | bool is_shown; 61 | }; 62 | 63 | class BGShootingStar : public BGRelativeElement 64 | { 65 | public: 66 | float x_increment; 67 | float y_increment; 68 | int16_t timer; 69 | int16_t max_timer; 70 | /// Gets smaller as the timer gets close to the max_timer 71 | float size; 72 | float light_size; // UNSURE if you make it the same as size it starts to flicker, making this bigger increases the size as well 73 | }; 74 | 75 | class BGShopEntrance : public Entity 76 | { 77 | public: 78 | bool on_entering; 79 | }; 80 | 81 | class BGFloatingDebris : public BGSurfaceLayer 82 | { 83 | public: 84 | /// Distance it travels up and down from spawn position 85 | float distance; 86 | float speed; 87 | float sine_angle; 88 | }; 89 | 90 | class BGShopKeeperPrime : public Entity 91 | { 92 | public: 93 | float normal_y; 94 | float sine_pos; 95 | int16_t bubbles_timer; 96 | bool bubble_spawn_trigger; 97 | int8_t unknown_padding; 98 | int16_t bubble_spawn_delay; // normally it's just 0, 1 or 2, but you can set it to some value and then when using bubble_spawn_trigger it will count down and then spawn bubbles 99 | }; 100 | --------------------------------------------------------------------------------