diff --git a/CMakePresets.json b/CMakePresets.json index 718359f..9d44578 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -144,7 +144,7 @@ "name": "_release", "hidden": true, "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" + "CMAKE_BUILD_TYPE": "RelWithDebInfo" } }, { diff --git a/common_cmake b/common_cmake index 0b1c32d..17528aa 160000 --- a/common_cmake +++ b/common_cmake @@ -1 +1 @@ -Subproject commit 0b1c32de1ff98a7851f798d6b092edd1e89e5eba +Subproject commit 17528aa7461deea26f55b2d7dc9ede720feee4c3 diff --git a/src/unrealsdk/game/bl1/antidebug.cpp b/src/unrealsdk/game/bl1/antidebug.cpp index c8db461..7753063 100644 --- a/src/unrealsdk/game/bl1/antidebug.cpp +++ b/src/unrealsdk/game/bl1/antidebug.cpp @@ -1,8 +1,3 @@ - -// - NOTE - -// Copied from bl2/antidebug.cpp -// - #include "unrealsdk/pch.h" #include "unrealsdk/game/bl1/bl1.h" diff --git a/src/unrealsdk/game/bl1/bl1.cpp b/src/unrealsdk/game/bl1/bl1.cpp index c5e20dd..e824f5a 100644 --- a/src/unrealsdk/game/bl1/bl1.cpp +++ b/src/unrealsdk/game/bl1/bl1.cpp @@ -14,6 +14,8 @@ using namespace unrealsdk::unreal; namespace unrealsdk::game { void BL1Hook::hook(void) { + wait_for_steam_drm(); + hook_antidebug(); hook_process_event(); @@ -41,6 +43,11 @@ void BL1Hook::post_init(void) { namespace { +#if defined(__MINGW32__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wattributes" // thiscall on non-class +#endif + // FFrame::Step is inlined, so instead we manually re-implement it using GNatives. const constinit Pattern<11> GNATIVES_SIG{ "8B 14 95 {????????}" // mov edx, [edx*4+01F942C0] @@ -49,9 +56,13 @@ const constinit Pattern<11> GNATIVES_SIG{ }; // NOLINTNEXTLINE(modernize-use-using) -typedef void(__stdcall* fframe_step_func)(FFrame*, void*); +typedef void(__thiscall* fframe_step_func)(UObject*, FFrame*, void*); fframe_step_func** fframe_step_gnatives; +#if defined(__MINGW32__) +#pragma GCC diagnostic pop +#endif + } // namespace void BL1Hook::find_fframe_step(void) { @@ -59,8 +70,8 @@ void BL1Hook::find_fframe_step(void) { LOG(MISC, "GNatives: {:p}", reinterpret_cast(fframe_step_gnatives)); } -void BL1Hook::fframe_step(unreal::FFrame* frame, unreal::UObject* /*obj*/, void* param) const { - ((*fframe_step_gnatives)[*frame->Code++])(frame, param); +void BL1Hook::fframe_step(FFrame* frame, UObject* obj, void* param) const { + ((*fframe_step_gnatives)[*frame->Code++])(obj, frame, param); } #pragma region FName::Init diff --git a/src/unrealsdk/game/bl1/bl1.h b/src/unrealsdk/game/bl1/bl1.h index 3fe259c..1724a20 100644 --- a/src/unrealsdk/game/bl1/bl1.h +++ b/src/unrealsdk/game/bl1/bl1.h @@ -12,6 +12,11 @@ namespace unrealsdk::game { class BL1Hook : public AbstractHook { protected: + /** + * @brief Blocking waits for the steam drm to finish unpacking the executable. + */ + static void wait_for_steam_drm(void); + /** * @brief Finds `FName::Init`, and sets up such that `fname_init` may be called. */ @@ -126,10 +131,11 @@ class BL1Hook : public AbstractHook { template <> struct GameTraits { - static constexpr auto NAME = "Borderlands"; + static constexpr auto NAME = "Borderlands 1"; static bool matches_executable(std::string_view executable) { - return executable == "Borderlands.exe" || executable == "borderlands.exe"; + return executable.starts_with("Borderlands.exe") + || executable.starts_with("borderlands.exe"); } }; diff --git a/src/unrealsdk/game/bl1/steamdrm.cpp b/src/unrealsdk/game/bl1/steamdrm.cpp new file mode 100644 index 0000000..13d7ff7 --- /dev/null +++ b/src/unrealsdk/game/bl1/steamdrm.cpp @@ -0,0 +1,110 @@ +#include "unrealsdk/pch.h" +#include "unrealsdk/game/bl1/bl1.h" +#include "unrealsdk/memory.h" +#include "unrealsdk/utils.h" + +#if UNREALSDK_FLAVOUR == UNREALSDK_FLAVOUR_WILLOW && !defined(UNREALSDK_IMPORTING) + +using namespace unrealsdk::utils; +using namespace unrealsdk::memory; + +namespace unrealsdk::game { + +namespace { + +// This is ___tmainCRTStartup, so expect it's very stable +const constinit Pattern<14> UNPACKED_ENTRY_SIG{"6A 58 68 ?? ?? ?? ?? E8 ?? ?? ?? ?? 33 DB"}; + +std::atomic ready = false; +std::mutex ready_mutex; +std::condition_variable ready_cv; + +// This is probably orders of magnitude bigger than we need, but best be safe +const constexpr std::chrono::seconds FALLBACK_DELAY{5}; + +// NOLINTBEGIN(readability-identifier-naming) + +// NOLINTNEXTLINE(modernize-use-using) - need a typedef for calling conventions in msvc +typedef void(WINAPI* GetStartupInfoA_func)(LPSTARTUPINFOA); +GetStartupInfoA_func GetStartupInfoA_ptr; + +void GetStartupInfoA_hook(LPSTARTUPINFOA lpStartupInfo) { + GetStartupInfoA_ptr(lpStartupInfo); + + static_assert(decltype(ready)::is_always_lock_free, "need to lock on checking ready flag too"); + if (ready.load()) { + return; + } + + const std::lock_guard lock{ready_mutex}; + ready.store(true); + ready_cv.notify_all(); +} + +// NOLINTEND(readability-identifier-naming) + +} // namespace + +void BL1Hook::wait_for_steam_drm(void) { + { + // Immediately suspend the other threads + const ThreadSuspender suspend{}; + + if (UNPACKED_ENTRY_SIG.sigscan_nullable() != 0) { + // If we found a match, we're already unpacked + return; + } + + LOG(MISC, "Waiting for steam drm unpack"); + + // Set up a hook for GetStartupInfoA, which is the one of the first things the unpacked + // entry function calls, which we'll use to tell once it's been unpacked + MH_STATUS status = MH_OK; + + status = MH_CreateHook(reinterpret_cast(&GetStartupInfoA), + reinterpret_cast(&GetStartupInfoA_hook), + reinterpret_cast(&GetStartupInfoA_ptr)); + if (status != MH_OK) { + LOG(ERROR, "Failed to create GetStartupInfoA hook: {:x}", + static_cast(status)); + + LOG(ERROR, "Falling back to a static delay"); + std::this_thread::sleep_for(FALLBACK_DELAY); + return; + } + + status = MH_EnableHook(reinterpret_cast(&GetStartupInfoA)); + if (status != MH_OK) { + LOG(ERROR, "Failed to enable GetStartupInfoA hook: {:x}", + static_cast(status)); + + LOG(ERROR, "Falling back to a static delay"); + std::this_thread::sleep_for(FALLBACK_DELAY); + return; + } + + // Drop out of this scope and unsuspend the other threads, let the unpacker run + } + + std::unique_lock lock(ready_mutex); + ready_cv.wait(lock, [] { return ready.load(); }); + + MH_STATUS status = MH_OK; + status = MH_DisableHook(reinterpret_cast(&GetStartupInfoA)); + if (status != MH_OK) { + LOG(ERROR, "Failed to disable GetStartupInfoA hook: {:x}", static_cast(status)); + + // If it fails, there isn't really any harm in leaving it active, just return + return; + } + + status = MH_RemoveHook(reinterpret_cast(&GetStartupInfoA)); + if (status != MH_OK) { + LOG(ERROR, "Failed to remove GetStartupInfoA hook: {:x}", static_cast(status)); + return; + } +} + +} // namespace unrealsdk::game + +#endif diff --git a/src/unrealsdk/game/tps/offsets.h b/src/unrealsdk/game/tps/offsets.h index 8e38102..944143f 100644 --- a/src/unrealsdk/game/tps/offsets.h +++ b/src/unrealsdk/game/tps/offsets.h @@ -71,7 +71,7 @@ class UClass : public UStruct { UObject* ClassDefaultObject; private: - uint8_t UnknownData01[0x14]; + uint8_t UnknownData01[0x10]; public: unreal::TArray Interfaces; diff --git a/src/unrealsdk/memory.cpp b/src/unrealsdk/memory.cpp index b294877..84e7972 100644 --- a/src/unrealsdk/memory.cpp +++ b/src/unrealsdk/memory.cpp @@ -28,6 +28,15 @@ std::pair get_exe_range(void) { auto module_length = nt_header->OptionalHeader.SizeOfImage; range = {reinterpret_cast(allocation_base), module_length}; + + if constexpr (sizeof(uintptr_t) == 4) { + LOG(MISC, "Executable memory range: {:08x}-{:08x}", range->first, + range->first + range->second); + } else { + LOG(MISC, "Executable memory range: {:012x}-{:012x}", range->first, + range->first + range->second); + } + return *range; } diff --git a/src/unrealsdk/pch.h b/src/unrealsdk/pch.h index 43259f7..6535c56 100644 --- a/src/unrealsdk/pch.h +++ b/src/unrealsdk/pch.h @@ -29,6 +29,8 @@ #define SetThreadDescription(x, y) #endif +#include + #include #ifdef __cplusplus diff --git a/src/unrealsdk/unreal/structs/fscriptdelegate.cpp b/src/unrealsdk/unreal/structs/fscriptdelegate.cpp index 9cb3b71..8275a98 100644 --- a/src/unrealsdk/unreal/structs/fscriptdelegate.cpp +++ b/src/unrealsdk/unreal/structs/fscriptdelegate.cpp @@ -94,8 +94,12 @@ void FScriptDelegate::validate_signature(const std::optional& fun reasonably simple to implement. */ { - auto func_props = func->func->properties(); - auto sig_props = signature->properties(); + auto func_props = std::ranges::filter_view(func->func->properties(), [](UProperty* prop) { + return (prop->PropertyFlags() & UProperty::PROP_FLAG_PARAM) != 0; + }); + auto sig_props = std::ranges::filter_view(signature->properties(), [](UProperty* prop) { + return (prop->PropertyFlags() & UProperty::PROP_FLAG_PARAM) != 0; + }); auto [func_diff, sig_diff] = std::ranges::mismatch( func_props, sig_props, diff --git a/src/unrealsdk/utils.cpp b/src/unrealsdk/utils.cpp index d060b52..9af332a 100644 --- a/src/unrealsdk/utils.cpp +++ b/src/unrealsdk/utils.cpp @@ -90,4 +90,57 @@ std::filesystem::path get_executable(void) { return path; } +namespace { + +/** + * @brief Suspends or resumes all other threads in the process. + * + * @param resume True if to resume, false if to suspend them. + */ +void adjust_thread_running_status(bool resume) { + HANDLE thread_snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); + if (thread_snapshot == nullptr) { + CloseHandle(thread_snapshot); + return; + } + + THREADENTRY32 te32{}; + te32.dwSize = sizeof(THREADENTRY32); + + if (Thread32First(thread_snapshot, &te32) == 0) { + CloseHandle(thread_snapshot); + return; + } + + do { + if (te32.th32OwnerProcessID != GetCurrentProcessId() + || te32.th32ThreadID == GetCurrentThreadId()) { + continue; + } + + HANDLE thread = OpenThread(THREAD_GET_CONTEXT | THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME, + 0, te32.th32ThreadID); + if (thread != nullptr) { + if (resume) { + ResumeThread(thread); + } else { + SuspendThread(thread); + } + + CloseHandle(thread); + } + } while (Thread32Next(thread_snapshot, &te32) != 0); + + CloseHandle(thread_snapshot); +} + +} // namespace + +ThreadSuspender::ThreadSuspender(void) { + adjust_thread_running_status(false); +} +ThreadSuspender::~ThreadSuspender() { + adjust_thread_running_status(true); +} + } // namespace unrealsdk::utils diff --git a/src/unrealsdk/utils.h b/src/unrealsdk/utils.h index b20a3de..1e0b531 100644 --- a/src/unrealsdk/utils.h +++ b/src/unrealsdk/utils.h @@ -131,6 +131,20 @@ struct DLLSafeCallback { R operator()(As... args) { return this->vftable->call(this, args...); } }; +/** + * @brief RAII class which suspends all other threads for it's lifespan. + */ +class ThreadSuspender { + public: + ThreadSuspender(void); + ~ThreadSuspender(); + + ThreadSuspender(const ThreadSuspender&) = delete; + ThreadSuspender(ThreadSuspender&&) = delete; + ThreadSuspender& operator=(const ThreadSuspender&) = delete; + ThreadSuspender& operator=(ThreadSuspender&&) = delete; +}; + namespace { template