diff --git a/src/game_api/game_patches.cpp b/src/game_api/game_patches.cpp new file mode 100644 index 000000000..22f09730f --- /dev/null +++ b/src/game_api/game_patches.cpp @@ -0,0 +1,207 @@ +#include "game_patches.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "entity.hpp" +#include "layer.hpp" +#include "memory.hpp" +#include "movable.hpp" +#include "search.hpp" +#include "state.hpp" +#include "state_structs.hpp" +#include "virtual_table.hpp" + +void patch_orbs_limit() +{ + /* + * The idea: we nuke jump instruction and some nop's after it + * in new code we check if the number of orbs is greater than 3 + * if yes we set it as 3 (2 becouse of 0 - 2 range) + * we jump back to the place of oryginal jump + */ + static bool once = false; + if (once) + return; + + auto memory = Memory::get(); + const auto function_offset = get_virtual_function_address(VTABLE_OFFSET::ITEM_FLOATING_ORB, (uint32_t)VIRT_FUNC::ENTITY_KILL); + // finding exit of the loop that counts orbs, after jump there is unused code so we have enogh space for a our patch + auto instance = find_inst(memory.exe(), "\xF3\x75"sv, function_offset, function_offset + 0x622, "patch_orbs_limit"); + auto offset = memory.at_exe(instance) + 3; + + if (instance == 0) // not found, don't crash the game (ex. PL + OV calling this) + return; + + auto loop_exit_jump = memory_read(offset + 1); + + // set r8b to 2 if it's more than 2 + std::string_view new_code{ + "\x41\x80\xf8\x02"sv // cmp r8b,0x2 + "\x72\x03"sv // jb + "\x41\xb0\x02"sv}; // mov r8b,0x2 + + patch_and_redirect(offset, 8, new_code, true, offset + 2 + loop_exit_jump); + once = true; +} + +void clear_cutscene_behavior() +{ + StateMemory* state = State::get().ptr_local(); + if (state == nullptr) + state = State::get().ptr_main(); + + // loop thru entities mask 7 + const auto entities_map = &state->layers[0]->entities_by_mask; + for (uint8_t mask = 1; mask < 7; mask <<= 1) + { + auto it = entities_map->find(mask); + if (it == entities_map->end()) + continue; + for (auto entity : it->second.entities()) + { + auto mov = entity->as(); + if (mov->ic8 != nullptr) + { + mov->ic8->~CutsceneBehavior(); + mov->ic8 = nullptr; + } + } + } + // memory leak, ignore this, it's only temporary as proof of concept + state->entity_lookup->unknown1[2].ent_list = nullptr; + state->entity_lookup->unknown1[2].cap = 0; + state->entity_lookup->unknown1[3].size = 0; + state->entity_lookup->unknown3 = state->entity_lookup->unknown4; +} + +void patch_olmec_kill_crash() +{ + /* + * The idea: do what's necessary xd + */ + static bool once = false; + if (once) + return; + + const auto offset = get_address("olmec_lookup_crash"); + constexpr auto code_to_move = 7; + auto memory = Memory::get(); + size_t return_addr; + { + // find address to escape to + + auto rva = offset - memory.exe_ptr; + // there are two jump that performe long jump, at the end, of it, the is 'mov rax,qword ptr ds:[rdi]', then find jump that's jumps over that code and create sound meta call + // this is actually unique pattern + auto jump_out_lookup = find_inst(memory.exe(), "\x48\x8B\x45\x50\x48\x83\x78\x60\x00"sv, rva, std::nullopt, "patch_olmec_kill_crash"); + if (jump_out_lookup == 0) + return; + + // could probably just make static offset from this stuff + auto jump_offset_offset = memory.at_exe(jump_out_lookup + 10); // 4 (lookup instruction size) + 1 (jump instruction) + auto jump_offset = memory_read(jump_offset_offset); + return_addr = jump_offset_offset + 1 + jump_offset; // +1 to get address after the jump + } + { + // patch the cutscene + + const auto function_offset = get_virtual_function_address(VTABLE_OFFSET::THEME_OLMEC, 24); // spawn_effects + // find the jump out of olmec lookup loop + auto jump_out_lookup = find_inst(memory.exe(), "\x48\x03\x58\x28"sv, function_offset, function_offset + 0x51C, "patch_olmec_kill_crash"); + if (jump_out_lookup == 0) + return; + + // find first jump (skips the whole funciton) + auto end_function_jump = find_inst(memory.exe(), "\x0F\x84"sv, function_offset, function_offset + 0xC3, "patch_olmec_kill_crash"); + if (end_function_jump == 0) + return; + + auto end_loop_jump = memory.at_exe(jump_out_lookup + 9); // +9 to skip the pattern and some other stuff + auto jump_addr = memory.at_exe(end_function_jump); + auto addr_to_jump_to = jump_addr + 6 + memory_read(jump_addr + 2); + std::string clear_ic8_code = fmt::format( + "\x48\xb8{}" // movabs RAX, &clear_cutscene_behavior + "\xff\xd0"sv, // call RAX + to_le_bytes((size_t)&clear_cutscene_behavior)); + + /* The idea: + * replace end of the loop jump with jump to the new code + * call our own function to clear all the cutscene behaviors + * jump to the end of the function + * hopefully there isn't something important that we're skipping + */ + + patch_and_redirect(end_loop_jump, 5, clear_ic8_code, true, addr_to_jump_to); + } + + /* The idea: + * if it's not the end of the array it's looking for olmec, jump back to the oryginal code via jump in `new_code` + * if it's end of the array (no olmec found) jump to return_addr + * the place for return_addr was kind of choosen by feel, as the code is complicated + * the whole point of the patched code is to find olmec and check it's faze, maybe for the music? + */ + + std::string_view new_code{ + "\x0f\x85\x00\x00\x00\x00"sv}; // jne (offset needs to be updated after we know the address) + + auto new_code_addr = patch_and_redirect(offset, code_to_move, new_code, false, return_addr); + if (new_code_addr == 0) + return; + + new_code_addr += code_to_move; + int32_t rel = static_cast(offset + code_to_move - (new_code_addr + 6)); // +6 after the jump + write_mem_prot(new_code_addr + 2, rel, true); + once = true; +} + +void patch_liquid_OOB() +{ + /* + * The idea: + * there is a loop thru all liquid entities + * if liquid is out of bounds (coordinate below 0) we essentially simulate `continue;` behavior + */ + + static bool once = false; + if (once) + return; + + const auto offset = get_address("liquid_OOB_crash"); + size_t continue_addr; + { + // find address to continue the loop + auto memory = Memory::get(); + auto rva = offset - memory.exe_ptr; + // first `ja` loop + auto jump_out_lookup = find_inst(memory.exe(), "\x0F\x87"sv, rva, rva + 0x7D, "patch_liquid_OOB"); + if (jump_out_lookup == 0) + return; + + auto jump_out_addr = memory.at_exe(jump_out_lookup); + auto jump_offset = memory_read(jump_out_addr + 2); // should be negative + continue_addr = jump_out_addr + 6 + jump_offset; // 6 size of the jump instruction + } + + constexpr auto code_to_move = 5; + + std::string_view new_code{ + "\x48\x83\xFD\x00"sv // cmp ebp,0x0 (ebp = y) + "\x0f\x8C\x00\x00\x00\x00"sv // jl (offset needs to be updated after we know the address) + "\x48\x83\xfa\x00" // cmp rdx,0x0 (rdx = x) + "\x0f\x8C\x00\x00\x00\x00"sv}; // jl (same offset as before) + + auto new_code_addr = patch_and_redirect(offset, code_to_move, new_code); + new_code_addr += code_to_move; + + int32_t rel = static_cast(continue_addr - (new_code_addr + 10)); // +10 after the jump + write_mem_prot(new_code_addr + 6, rel, true); + write_mem_prot(new_code_addr + 16, rel - 10, true); + + once = true; +} diff --git a/src/game_api/game_patches.hpp b/src/game_api/game_patches.hpp new file mode 100644 index 000000000..686083db5 --- /dev/null +++ b/src/game_api/game_patches.hpp @@ -0,0 +1,5 @@ +#pragma once + +void patch_orbs_limit(); +void patch_olmec_kill_crash(); +void patch_liquid_OOB(); diff --git a/src/game_api/movable.hpp b/src/game_api/movable.hpp index d4877eab1..889a6073e 100644 --- a/src/game_api/movable.hpp +++ b/src/game_api/movable.hpp @@ -15,7 +15,7 @@ class CutsceneBehavior public: virtual ~CutsceneBehavior(){}; virtual void update() = 0; - // no more virtuals, it's possible that different classes have some extra variables as well + // no more virtuals, it's possible that different sub classes have some extra variables as well }; class Movable : public Entity diff --git a/src/game_api/rpc.cpp b/src/game_api/rpc.cpp index 260d7cd07..eefce27ec 100644 --- a/src/game_api/rpc.cpp +++ b/src/game_api/rpc.cpp @@ -1880,187 +1880,3 @@ void set_ending_unlock(ENT_TYPE type) recover_mem("ending_unlock"); } } - -void patch_orbs_limit() -{ - /* - * The idea: we nuke jump instruction and some nop's after it - * in new code we check if the number of orbs is greater than 3 - * if yes we set it as 3 (2 becouse of 0 - 2 range) - * we jump back to the place of oryginal jump - */ - static bool once = false; - if (once) - return; - - auto memory = Memory::get(); - const auto function_offset = get_virtual_function_address(VTABLE_OFFSET::ITEM_FLOATING_ORB, (uint32_t)VIRT_FUNC::ENTITY_KILL); - // finding exit of the loop that counts orbs, after jump there is unused code so we have enogh space for a our patch - auto instance = find_inst(memory.exe(), "\xF3\x75"sv, function_offset, function_offset + 0x622, "patch_orbs_limit"); - auto offset = memory.at_exe(instance) + 3; - - if (instance == 0) // not found, don't crash the game (ex. PL + OV calling this) - return; - - auto loop_exit_jump = memory_read(offset + 1); - - // set r8b to 2 if it's more than 2 - std::string_view new_code{ - "\x41\x80\xf8\x02"sv // cmp r8b,0x2 - "\x72\x03"sv // jb - "\x41\xb0\x02"sv}; // mov r8b,0x2 - - patch_and_redirect(offset, 8, new_code, true, offset + 2 + loop_exit_jump); - once = true; -} - -void clear_cutscene_behavior() -{ - auto state = State::get().ptr_local(); - if (state == nullptr) - state = State::get().ptr_main(); - - // loop thru entities mask 7 - const auto entities_map = &state->layers[0]->entities_by_mask; - for (uint8_t mask = 1; mask < 7; mask <<= 1) - { - auto it = entities_map->find(mask); - if (it == entities_map->end()) - continue; - for (auto entity : it->second.entities()) - { - auto mov = entity->as(); - if (mov->ic8 != nullptr) - { - mov->ic8->~CutsceneBehavior(); - mov->ic8 = nullptr; - } - } - } -} - -void patch_olmec_kill_crash() -{ - /* - * The idea: do what's necessary xd - */ - static bool once = false; - if (once) - return; - - const auto offset = get_address("olmec_lookup_crash"); - constexpr auto code_to_move = 7; - auto memory = Memory::get(); - size_t return_addr; - { - // find address to escape to - - auto rva = offset - memory.exe_ptr; - // there are two jump that performe long jump, at the end, of it, the is 'mov rax,qword ptr ds:[rdi]', then find jump that's jumps over that code and create sound meta call - // this is actually unique pattern - auto jump_out_lookup = find_inst(memory.exe(), "\x48\x8B\x45\x50\x48\x83\x78\x60\x00"sv, rva, std::nullopt, "patch_olmec_kill_crash"); - if (jump_out_lookup == 0) - return; - - // could probably just make static offset from this stuff - auto jump_offset_offset = memory.at_exe(jump_out_lookup + 10); // 4 (lookup instruction size) + 1 (jump instruction) - auto jump_offset = memory_read(jump_offset_offset); - return_addr = jump_offset_offset + 1 + jump_offset; // +1 to get address after the jump - } - { - // patch the cutscene - - const auto function_offset = get_virtual_function_address(VTABLE_OFFSET::THEME_OLMEC, 24); // spawn_effects - // find the jump out of olmec lookup loop - auto jump_out_lookup = find_inst(memory.exe(), "\x48\x03\x58\x28"sv, function_offset, function_offset + 0x51C, "patch_olmec_kill_crash"); - if (jump_out_lookup == 0) - return; - - // find first jump (skips the whole funciton) - auto end_function_jump = find_inst(memory.exe(), "\x0F\x84"sv, function_offset, function_offset + 0xC3, "patch_olmec_kill_crash"); - if (end_function_jump == 0) - return; - - auto end_loop_jump = memory.at_exe(jump_out_lookup + 9); // +9 to skip the pattern and some other stuff - auto jump_addr = memory.at_exe(end_function_jump); - auto addr_to_jump_to = jump_addr + 6 + memory_read(jump_addr + 2); - std::string clear_ic8_code = fmt::format( - "\x48\xb8{}" // movabs RAX, &clear_cutscene_behavior - "\xff\xd0"sv, // call RAX - to_le_bytes((size_t)&clear_cutscene_behavior)); - - /* The idea: - * replace end of the loop jump with jump to the new code - * call our own function to clear all the cutscene behaviors - * jump to the end of the function - * hopefully there isn't something important that we're skipping - */ - - patch_and_redirect(end_loop_jump, 5, clear_ic8_code, true, addr_to_jump_to); - } - - /* The idea: - * if it's not the end of the array it's looking for olmec, jump back to the oryginal code via jump in `new_code` - * if it's end of the array (no olmec found) jump to return_addr - * the place for return_addr was kind of choosen by feel, as the code is complicated - * the whole point of the patched code is to find olmec and check it's faze, maybe for the music? - */ - - std::string_view new_code{ - "\x0f\x85\x00\x00\x00\x00"sv}; // jne (offset needs to be updated after we know the address) - - auto new_code_addr = patch_and_redirect(offset, code_to_move, new_code, false, return_addr); - if (new_code_addr == 0) - return; - - new_code_addr += code_to_move; - int32_t rel = static_cast(offset + code_to_move - (new_code_addr + 6)); // +6 after the jump - write_mem_prot(new_code_addr + 2, rel, true); - once = true; -} - -void patch_liquid_OOB() -{ - /* - * The idea: - * there is a loop thru all liquid entities - * if liquid is out of bounds (coordinate below 0) we essentially simulate `continue;` behavior - */ - - static bool once = false; - if (once) - return; - - const auto offset = get_address("liquid_OOB_crash"); - size_t continue_addr; - { - // find address to continue the loop - auto memory = Memory::get(); - auto rva = offset - memory.exe_ptr; - // first `ja` loop - auto jump_out_lookup = find_inst(memory.exe(), "\x0F\x87"sv, rva, rva + 0x7D, "patch_liquid_OOB"); - if (jump_out_lookup == 0) - return; - - auto jump_out_addr = memory.at_exe(jump_out_lookup); - auto jump_offset = memory_read(jump_out_addr + 2); // should be negative - continue_addr = jump_out_addr + 6 + jump_offset; // 6 size of the jump instruction - } - - constexpr auto code_to_move = 5; - - std::string_view new_code{ - "\x48\x83\xFD\x00"sv // cmp ebp,0x0 (ebp = y) - "\x0f\x8C\x00\x00\x00\x00"sv // jl (offset needs to be updated after we know the address) - "\x48\x83\xfa\x00" // cmp rdx,0x0 (rdx = x) - "\x0f\x8C\x00\x00\x00\x00"sv}; // jl (same offset as before) - - auto new_code_addr = patch_and_redirect(offset, code_to_move, new_code); - new_code_addr += code_to_move; - - int32_t rel = static_cast(continue_addr - (new_code_addr + 10)); // +10 after the jump - write_mem_prot(new_code_addr + 6, rel, true); - write_mem_prot(new_code_addr + 16, rel - 10, true); - - once = true; -} diff --git a/src/game_api/rpc.hpp b/src/game_api/rpc.hpp index 64cba7ca6..55a419269 100644 --- a/src/game_api/rpc.hpp +++ b/src/game_api/rpc.hpp @@ -137,6 +137,3 @@ void call_death_screen(); void save_progress(); void set_level_string(std::u16string_view text); void set_ending_unlock(ENT_TYPE type); -void patch_orbs_limit(); -void patch_olmec_kill_crash(); -void patch_liquid_OOB(); diff --git a/src/game_api/script/lua_vm.cpp b/src/game_api/script/lua_vm.cpp index 2029aa8bf..0eb6e0e43 100644 --- a/src/game_api/script/lua_vm.cpp +++ b/src/game_api/script/lua_vm.cpp @@ -2014,12 +2014,6 @@ end return AABB(ax + index * w + 0.02f * f, ay, ax + index * w + w - 0.02f * f, ay - h); }; - lua["patch_orbs_limit"] = patch_orbs_limit; - - lua["patch_olmec_kill_crash"] = patch_olmec_kill_crash; - - lua["patch_liquid_OOB"] = patch_liquid_OOB; - lua.create_named_table("INPUTS", "NONE", 0, "JUMP", 1, "WHIP", 2, "BOMB", 4, "ROPE", 8, "RUN", 16, "DOOR", 32, "MENU", 64, "JOURNAL", 128, "LEFT", 256, "RIGHT", 512, "UP", 1024, "DOWN", 2048); lua.create_named_table( diff --git a/src/game_api/state.cpp b/src/game_api/state.cpp index 301e62b26..67f778488 100644 --- a/src/game_api/state.cpp +++ b/src/game_api/state.cpp @@ -13,6 +13,7 @@ #include "entity.hpp" // for to_id, Entity, HookWithId, EntityDB #include "entity_hooks_info.hpp" // for Player #include "game_manager.hpp" // for get_game_manager, GameManager, SaveR... +#include "game_patches.hpp" // #include "items.hpp" // for Items, SelectPlayerSlot #include "level_api.hpp" // for LevelGenSystem, LevelGenSystem::(ano... #include "logger.h" // for DEBUG @@ -284,6 +285,11 @@ State& State::get() hook_godmode_functions(); strings_init(); init_state_update_hook(); + + // game patches + patch_orbs_limit(); + patch_olmec_kill_crash(); + patch_liquid_OOB(); } get_is_init() = true;