Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions SharpPluginLoader.Core/InternalCalls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,7 @@ public static bool TimelineTrack(string label, Span<float> 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";
}
}
16 changes: 2 additions & 14 deletions SharpPluginLoader.Core/Memory/AddressRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");

Expand All @@ -48,20 +42,14 @@ 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.");
}

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
{
Expand Down
235 changes: 123 additions & 112 deletions mhw-cs-plugin-loader/AddressRepository.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,138 +14,149 @@

#include "picosha2/picosha2.h"

std::unordered_map<std::string, uintptr_t> scan_for_address_records(json records_json);
std::string get_game_revision();
static std::unordered_map<std::string, uintptr_t> scan_for_address_records(json records_json);

void AddressRepository::initialize() {
// Load address records json from the default chunk
std::shared_ptr<Chunk> default_chunk = std::make_shared<Chunk>(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<unsigned char> 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<std::chrono::milliseconds>(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<Chunk> default_chunk = std::make_shared<Chunk>(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<unsigned char> 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<std::chrono::milliseconds>(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];
}

/// <summary>
/// Scans for the patterns in the provided JSON object in parallel.
/// </summary>
std::unordered_map<std::string, uintptr_t> scan_for_address_records(json records_json) {
std::mutex map_lock;
std::unordered_map<std::string, uintptr_t> 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<std::string, uintptr_t> 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<i32*>(func + 7);
const uintptr_t offset_base = func + 11;

const char* version = *reinterpret_cast<const char**>(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<i32*>(func + 7);
const uintptr_t offset_base = func + 11;
const char* version = *reinterpret_cast<const char**>(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;
}

62 changes: 35 additions & 27 deletions mhw-cs-plugin-loader/AddressRepository.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,43 @@
class AddressRepository
{
public:
AddressRepository(): m_address_records() {}

/// <summary>
/// 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.
/// </summary>
void initialize();

/// <summary>
/// Gets the address for the given pattern name.
///
/// Returns 0 if not found.
/// </summary>
uintptr_t get(const std::string& name);
AddressRepository(): m_address_records() {}

/// <summary>
/// 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.
/// </summary>
void initialize();

/// <summary>
/// Get the game revision and cache it for future calls.
///
/// Returns "unknown" on error, never null.
/// </summary>
const char* get_game_revision();

/// <summary>
/// Gets the address for the given pattern name.
///
/// Returns 0 if not found.
/// </summary>
uintptr_t get(const std::string& name);

private:
/// <summary>
/// Writes the currently resolved address records to the on-disk cache file.
/// </summary>
void write_cache(const std::string& game_version, const std::string& address_records_file_hash);

/// <summary>
/// Restores the resolved address cache from disk.
///
/// Returns true if successful.
/// </summary>
bool restore_cache(const std::string& game_version, const std::string& address_records_file_hash);
/// <summary>
/// Writes the currently resolved address records to the on-disk cache file.
/// </summary>
void write_cache(const std::string& game_version, const std::string& address_records_file_hash);

/// <summary>
/// Restores the resolved address cache from disk.
///
/// Returns true if successful.
/// </summary>
bool restore_cache(const std::string& game_version, const std::string& address_records_file_hash);

private:
std::unordered_map<std::string, uintptr_t> m_address_records;
const char* m_game_revision = nullptr;
static constexpr const char* UNKNOWN_REVISION = "unknown";
std::unordered_map<std::string, uintptr_t> m_address_records;
};

2 changes: 1 addition & 1 deletion mhw-cs-plugin-loader/CoreClr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
1 change: 0 additions & 1 deletion mhw-cs-plugin-loader/CoreClr.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,3 @@ class CoreClr {

std::vector<InternalCall> m_internal_calls{};
};

Loading
Loading