diff --git a/SharpPluginLoader.Core/InternalCalls.cs b/SharpPluginLoader.Core/InternalCalls.cs index 95831bd..fd199e7 100644 --- a/SharpPluginLoader.Core/InternalCalls.cs +++ b/SharpPluginLoader.Core/InternalCalls.cs @@ -125,10 +125,7 @@ public static bool TimelineTrack(string label, Span keyFrames, out int se public static nint GetRepositoryAddress(string name) => GetRepositoryAddressPtr(name); - public static string GetGameRevision() - { - var revision = GetGameRevisionPtr(); - return revision == null ? string.Empty : new string(revision); - } + public static string GetGameRevision() => new string(GetGameRevisionPtr()); + public const string UnknownRevision = "unknown"; } } diff --git a/SharpPluginLoader.Core/Memory/AddressRepository.cs b/SharpPluginLoader.Core/Memory/AddressRepository.cs index ac07032..d1408de 100644 --- a/SharpPluginLoader.Core/Memory/AddressRepository.cs +++ b/SharpPluginLoader.Core/Memory/AddressRepository.cs @@ -31,13 +31,7 @@ private static void LoadPluginRecords() ?? throw new Exception("Failed to deserialize plugin records cache"); var gameVersion = InternalCalls.GetGameRevision(); - if (string.IsNullOrEmpty(gameVersion)) - { - Log.Error("Failed to get game revision"); - return; - } - - if (pluginCache.Version == gameVersion) + if (gameVersion != InternalCalls.UnknownRevision && pluginCache.Version == gameVersion) { Log.Debug("[Core] Restoring from plugin record cache."); @@ -48,7 +42,7 @@ private static void LoadPluginRecords() return; } - + // Actual scanning will be performed by the plugins themselves. Log.Debug("[Core] No valid plugin record cache found. Performing first-time scan."); } @@ -56,12 +50,6 @@ private static void LoadPluginRecords() public static void SavePluginRecords() { var gameVersion = InternalCalls.GetGameRevision(); - if (string.IsNullOrEmpty(gameVersion)) - { - Log.Error("Failed to get game revision"); - return; - } - var cacheJson = JsonSerializer.Serialize( new PluginRecordCacheJson { diff --git a/mhw-cs-plugin-loader/AddressRepository.cpp b/mhw-cs-plugin-loader/AddressRepository.cpp index 7123480..c38085a 100644 --- a/mhw-cs-plugin-loader/AddressRepository.cpp +++ b/mhw-cs-plugin-loader/AddressRepository.cpp @@ -14,138 +14,149 @@ #include "picosha2/picosha2.h" -std::unordered_map scan_for_address_records(json records_json); -std::string get_game_revision(); +static std::unordered_map scan_for_address_records(json records_json); void AddressRepository::initialize() { - // Load address records json from the default chunk - std::shared_ptr default_chunk = std::make_shared(config::SPL_DEFAULT_CHUNK_PATH); - auto address_records = default_chunk.get()->get_file("/Resources/AddressRecords.json"); - auto& contents_raw = address_records->Contents; - std::string contents(contents_raw.begin(), contents_raw.end()); - - // Parse the json. - json records_json = json::parse(contents, nullptr, false); - assert(!records_json.is_discarded()); - - // Get game version/revision and hash of the current address records json file. - std::string game_revision = get_game_revision(); - if (game_revision.empty()) { - dlog::debug("[AddressRepo] Failed to get game revision to validate address repository cache. Cache will be disregarded."); - } - - std::vector hash(picosha2::k_digest_size); - picosha2::hash256(contents_raw.begin(), contents_raw.end(), hash.begin(), hash.end()); - std::string address_records_file_hash = picosha2::bytes_to_hex_string(hash.begin(), hash.end()); - - dlog::debug("[AddressRepo] Attempting to initialize address repository for game revision: {}", game_revision); - - // Attempt to load file from disk - if (std::filesystem::exists(config::SPL_ADDRESS_REPOSITORY_CACHE_PATH) && !game_revision.empty()) { - if (this->restore_cache(game_revision, address_records_file_hash)) { - dlog::debug("[AddressRepo] Restored from address record cache."); - return; - } - } - - // Either the cache file doesn't exist, or the version/file hash didn't match. - // So we AOB scan in cache. - dlog::debug("[AddressRepo] No valid address record cache found. Performing first-time scan."); - - // Scan for the address records - auto pattern_scan_start_time = std::chrono::steady_clock::now(); - m_address_records = scan_for_address_records(records_json); - auto pattern_scan_end_time = std::chrono::steady_clock::now(); - - dlog::debug( - "[AddressRepo] Scanning for addresses took: {}ms", - std::chrono::duration_cast(pattern_scan_end_time - pattern_scan_start_time).count() - ); - - // Write cache file. - this->write_cache(game_revision, address_records_file_hash); - dlog::debug("[AddressRepo] Wrote cache file to disk."); - + // Load address records json from the default chunk. + std::shared_ptr default_chunk = std::make_shared(config::SPL_DEFAULT_CHUNK_PATH); + auto address_records = default_chunk.get()->get_file("/Resources/AddressRecords.json"); + auto& contents_raw = address_records->Contents; + std::string contents(contents_raw.begin(), contents_raw.end()); + + // Parse the json. + json records_json = json::parse(contents, nullptr, false); + assert(!records_json.is_discarded()); + + // Get game version/revision and hash of the current address records json file. + std::string game_revision = std::string(get_game_revision()); + const bool unknown_revision = game_revision == std::string(UNKNOWN_REVISION); + if (unknown_revision) { + dlog::debug("[AddressRepo] Failed to get game revision to validate address repository cache. Cache will be disregarded."); + } + + std::vector hash(picosha2::k_digest_size); + picosha2::hash256(contents_raw.begin(), contents_raw.end(), hash.begin(), hash.end()); + std::string address_records_file_hash = picosha2::bytes_to_hex_string(hash.begin(), hash.end()); + + dlog::debug("[AddressRepo] Attempting to initialize address repository for game revision: {}", game_revision); + + // Attempt to load file from disk. + if (std::filesystem::exists(config::SPL_ADDRESS_REPOSITORY_CACHE_PATH) && !unknown_revision) { + if (this->restore_cache(game_revision, address_records_file_hash)) { + dlog::debug("[AddressRepo] Restored from address record cache."); + return; + } + } + + // Either the cache file doesn't exist, or the version/file hash didn't match. So we AOB scan in cache. + dlog::debug("[AddressRepo] No valid address record cache found. Performing first-time scan."); + + // Scan for the address records. + auto pattern_scan_start_time = std::chrono::steady_clock::now(); + m_address_records = scan_for_address_records(records_json); + auto pattern_scan_end_time = std::chrono::steady_clock::now(); + + dlog::debug( + "[AddressRepo] Scanning for addresses took: {}ms", + std::chrono::duration_cast(pattern_scan_end_time - pattern_scan_start_time).count() + ); + + // Write cache file. If revision is unknown, this will never be reused. + this->write_cache(game_revision, address_records_file_hash); + dlog::debug("[AddressRepo] Wrote cache file to disk."); } void AddressRepository::write_cache(const std::string& game_version, const std::string& address_records_file_hash) { - std::ofstream file(config::SPL_ADDRESS_REPOSITORY_CACHE_PATH); - if (file.is_open()) - { - json data; - data["Version"] = game_version; - data["AddressRecordFileHash"] = address_records_file_hash; - data["Addresses"] = m_address_records; - file << std::setw(4) << data << "\n"; - file.close(); - } + std::ofstream file(config::SPL_ADDRESS_REPOSITORY_CACHE_PATH); + if (file.is_open()) { + json data; + data["Version"] = game_version; + data["AddressRecordFileHash"] = address_records_file_hash; + data["Addresses"] = m_address_records; + file << std::setw(4) << data << "\n"; + file.close(); + } } bool AddressRepository::restore_cache(const std::string& game_version, const std::string& address_records_file_hash) { - std::ifstream file(config::SPL_ADDRESS_REPOSITORY_CACHE_PATH); - json address_cache_json = json::parse(file); - std::string cache_version = address_cache_json["Version"]; - std::string cache_address_record_file_hash = address_cache_json["AddressRecordFileHash"]; - if (cache_version == game_version && cache_address_record_file_hash == address_records_file_hash) { - auto cached_addresses = address_cache_json["Addresses"]; - m_address_records = cached_addresses; - return true; - } - - return false; + std::ifstream file(config::SPL_ADDRESS_REPOSITORY_CACHE_PATH); + json address_cache_json = json::parse(file, nullptr, false); + bool cache_json_valid = !address_cache_json.is_discarded() && + address_cache_json.contains("Version") && + address_cache_json.contains("AddressRecordFileHash") && + address_cache_json.contains("Addresses"); + if (cache_json_valid) { + std::string cache_version = address_cache_json["Version"]; + std::string cache_address_record_file_hash = address_cache_json["AddressRecordFileHash"]; + if (cache_version == game_version && cache_address_record_file_hash == address_records_file_hash) { + auto cached_addresses = address_cache_json["Addresses"]; + m_address_records = cached_addresses; + return true; + } + } + + return false; } uintptr_t AddressRepository::get(const std::string& name) { - if (!m_address_records.contains(name)) - return 0; + if (!m_address_records.contains(name)) + return 0; - return m_address_records[name]; + return m_address_records[name]; } /// /// Scans for the patterns in the provided JSON object in parallel. /// std::unordered_map scan_for_address_records(json records_json) { - std::mutex map_lock; - std::unordered_map resolved_addresses; - std::for_each(std::execution::par, records_json.begin(), records_json.end(), [&map_lock, &resolved_addresses](const json& o) { - std::string name = o["Name"]; - std::string pattern = o["Pattern"]; - int64_t offset = o["Offset"]; - - uintptr_t address = PatternScanner::find_first(Pattern::from_string(pattern)); - if (address == 0) { - dlog::error("[AddressRepo] Failed to find address for: {}", name); - return; - } - address += offset; - - // lock map and insert the scan result. - { - std::lock_guard lock(map_lock); - resolved_addresses[name] = address; - } - }); - return resolved_addresses; + std::mutex map_lock; + std::unordered_map resolved_addresses; + std::for_each(std::execution::par, records_json.begin(), records_json.end(), [&map_lock, &resolved_addresses](const json& o) { + std::string name = o["Name"]; + std::string pattern = o["Pattern"]; + int64_t offset = o["Offset"]; + + uintptr_t address = PatternScanner::find_first(Pattern::from_string(pattern)); + if (address == 0) { + dlog::error("[AddressRepo] Failed to find address for: {}", name); + return; + } + address += offset; + + // lock map and insert the scan result. + { + std::lock_guard lock(map_lock); + resolved_addresses[name] = address; + } + }); + return resolved_addresses; } -// TODO: Essentially a duplicate of the same function in NativePluginFramework, -// should be moved somewhere general. -std::string get_game_revision() { - const auto pattern = Pattern::from_string("48 83 EC 48 48 8B 05 ? ? ? ? 4C 8D 0D ? ? ? ? BA 0A 00 00 00"); - const auto func = PatternScanner::find_first(pattern); - - if (func == 0) { - dlog::error("[AddressRepo] Failed to find game revision function"); - return std::string(); - } - - const auto constant_offset = *reinterpret_cast(func + 7); - const uintptr_t offset_base = func + 11; - - const char* version = *reinterpret_cast(offset_base + constant_offset); - return version == nullptr ? std::string() : std::string(version); +const char* AddressRepository::get_game_revision() { + if (m_game_revision == nullptr) { + const auto func = PatternScanner::find_first( + Pattern::from_string("48 83 EC 48 48 8B 05 ? ? ? ? 4C 8D 0D ? ? ? ? BA 0A 00 00 00") + ); + + if (func == 0) { + dlog::error("[AddressRepo] Failed to find game revision function"); + m_game_revision = UNKNOWN_REVISION; + } else { + const auto constant_offset = *reinterpret_cast(func + 7); + const uintptr_t offset_base = func + 11; + const char* version = *reinterpret_cast(offset_base + constant_offset); + if (!version) { + dlog::error("[AddressRepo] Failed to get game revision"); + m_game_revision = UNKNOWN_REVISION; + } else { + m_game_revision = version; + } + } + + dlog::debug("[AddressRepo] Game revision: {}", m_game_revision); + } + + return m_game_revision; } - diff --git a/mhw-cs-plugin-loader/AddressRepository.h b/mhw-cs-plugin-loader/AddressRepository.h index a96ec0f..1a6b81b 100644 --- a/mhw-cs-plugin-loader/AddressRepository.h +++ b/mhw-cs-plugin-loader/AddressRepository.h @@ -6,35 +6,43 @@ class AddressRepository { public: - AddressRepository(): m_address_records() {} - - /// - /// Loads all the patterns from the address repo JSON (in filechunk) and resolves them. - /// If a valid cache is on disk, it will use that instead of pattern scanning for the addresses. - /// - void initialize(); - - /// - /// Gets the address for the given pattern name. - /// - /// Returns 0 if not found. - /// - uintptr_t get(const std::string& name); + AddressRepository(): m_address_records() {} + + /// + /// Loads all the patterns from the address repo JSON (in filechunk) and resolves them. + /// If a valid cache is on disk, it will use that instead of pattern scanning for the addresses. + /// + void initialize(); + + /// + /// Get the game revision and cache it for future calls. + /// + /// Returns "unknown" on error, never null. + /// + const char* get_game_revision(); + + /// + /// Gets the address for the given pattern name. + /// + /// Returns 0 if not found. + /// + uintptr_t get(const std::string& name); private: - /// - /// Writes the currently resolved address records to the on-disk cache file. - /// - void write_cache(const std::string& game_version, const std::string& address_records_file_hash); - - /// - /// Restores the resolved address cache from disk. - /// - /// Returns true if successful. - /// - bool restore_cache(const std::string& game_version, const std::string& address_records_file_hash); + /// + /// Writes the currently resolved address records to the on-disk cache file. + /// + void write_cache(const std::string& game_version, const std::string& address_records_file_hash); + + /// + /// Restores the resolved address cache from disk. + /// + /// Returns true if successful. + /// + bool restore_cache(const std::string& game_version, const std::string& address_records_file_hash); private: - std::unordered_map m_address_records; + const char* m_game_revision = nullptr; + static constexpr const char* UNKNOWN_REVISION = "unknown"; + std::unordered_map m_address_records; }; - diff --git a/mhw-cs-plugin-loader/CoreClr.cpp b/mhw-cs-plugin-loader/CoreClr.cpp index f29faa3..8b43a04 100644 --- a/mhw-cs-plugin-loader/CoreClr.cpp +++ b/mhw-cs-plugin-loader/CoreClr.cpp @@ -154,6 +154,6 @@ void* CoreClr::get_method_internal(std::wstring_view assembly, const std::wstrin dlog::debug(L"Failed to get function pointer for {}.{}: {}", type, method, hr); return nullptr; } - + return function_pointer; } diff --git a/mhw-cs-plugin-loader/CoreClr.h b/mhw-cs-plugin-loader/CoreClr.h index 2294985..27530ae 100644 --- a/mhw-cs-plugin-loader/CoreClr.h +++ b/mhw-cs-plugin-loader/CoreClr.h @@ -69,4 +69,3 @@ class CoreClr { std::vector m_internal_calls{}; }; - diff --git a/mhw-cs-plugin-loader/CoreModule.h b/mhw-cs-plugin-loader/CoreModule.h index 821a552..04f7847 100644 --- a/mhw-cs-plugin-loader/CoreModule.h +++ b/mhw-cs-plugin-loader/CoreModule.h @@ -19,4 +19,3 @@ class CoreModule final : public NativeModule { safetyhook::InlineHook m_main_update_hook; }; - diff --git a/mhw-cs-plugin-loader/NativePluginFramework.cpp b/mhw-cs-plugin-loader/NativePluginFramework.cpp index 7c9adba..bee7e82 100644 --- a/mhw-cs-plugin-loader/NativePluginFramework.cpp +++ b/mhw-cs-plugin-loader/NativePluginFramework.cpp @@ -48,23 +48,5 @@ uintptr_t NativePluginFramework::get_repository_address(const char* name) { } const char* NativePluginFramework::get_game_revision() { - if (s_instance->m_game_revision != nullptr) { - return s_instance->m_game_revision; - } - - const auto pattern = Pattern::from_string("48 83 EC 48 48 8B 05 ? ? ? ? 4C 8D 0D ? ? ? ? BA 0A 00 00 00"); - const auto func = PatternScanner::find_first(pattern); - - if (func == 0) { - dlog::error("Failed to find game revision function"); - return nullptr; - } - - const auto constant_offset = *reinterpret_cast(func + 7); - const uintptr_t offset_base = func + 11; - s_instance->m_game_revision = *reinterpret_cast(offset_base + constant_offset); - - dlog::debug("Game revision: {}", s_instance->m_game_revision); - - return s_instance->m_game_revision; + return s_instance->m_address_repository->get_game_revision(); } diff --git a/mhw-cs-plugin-loader/NativePluginFramework.h b/mhw-cs-plugin-loader/NativePluginFramework.h index 93c768f..82b7d9b 100644 --- a/mhw-cs-plugin-loader/NativePluginFramework.h +++ b/mhw-cs-plugin-loader/NativePluginFramework.h @@ -39,7 +39,6 @@ class NativePluginFramework { private: std::vector> m_modules; ManagedFunctionPointers m_managed_functions; - const char* m_game_revision = nullptr; AddressRepository* m_address_repository = nullptr; static inline NativePluginFramework* s_instance = nullptr; diff --git a/mhw-cs-plugin-loader/PatternScan.cpp b/mhw-cs-plugin-loader/PatternScan.cpp index d8b17db..b082302 100644 --- a/mhw-cs-plugin-loader/PatternScan.cpp +++ b/mhw-cs-plugin-loader/PatternScan.cpp @@ -18,8 +18,7 @@ Pattern Pattern::from_string(const std::string& pattern) { if (byte == "?" || byte == "??") { bytes.push_back({ true }); - } - else { + } else { bytes.push_back({ .Value = (u8)std::stoul(byte, nullptr, 16) }); } } @@ -50,7 +49,7 @@ std::vector PatternScanner::scan(const Pattern& pattern) { while (addr < end_addr) { MEMORY_BASIC_INFORMATION mbi; - if (!VirtualQuery(addr, &mbi, sizeof(mbi)) || + if (!VirtualQuery(addr, &mbi, sizeof(mbi)) || mbi.State != MEM_COMMIT || mbi.Protect & PAGE_GUARD) { break; diff --git a/mhw-cs-plugin-loader/PatternScan.h b/mhw-cs-plugin-loader/PatternScan.h index b1a09bb..718febb 100644 --- a/mhw-cs-plugin-loader/PatternScan.h +++ b/mhw-cs-plugin-loader/PatternScan.h @@ -31,5 +31,3 @@ class PatternScanner { static std::vector scan(const Pattern& pattern); static uintptr_t find_first(const Pattern& pattern); }; - -