Skip to content
This repository has been archived by the owner on Jul 19, 2024. It is now read-only.

Feat/wwise file location resolver hook #166

Merged
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,4 @@ jobs:
with:
name: 'modengine-${{ github.sha }}'
path: '${{ env.CMAKE_BUILD_DIR }}/ModEngine-*-win64.*' # FIXME gtierney: run-cmake isn't respecting CMAKE_INSTALL_PREFIX
if: ${{ github.event_name == 'push' }}
if: ${{ github.event_name == 'push' }}
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ add_library(modengine2 SHARED
modengine/ext/debug_menu/ds3/_DS3GameProperties.asm
modengine/ext/mod_loader/archive_file_overrides.cpp
modengine/ext/mod_loader/mod_loader_extension.cpp
modengine/ext/mod_loader/wwise_file_overrides.cpp
modengine/ext/profiling/profiling_extension.cpp
modengine/ext/profiling/profiler_trampoline.asm
modengine/ext/profiling/main_loop.cpp
Expand Down
4 changes: 1 addition & 3 deletions src/modengine/ext/mod_loader/archive_file_overrides.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ namespace modengine::ext {
// the easiest and most robust way to do this. It also has the bonus of allowing files that don't go through the asset system
// (like the data archives and some of the sounds) to be overridable.

namespace fs = std::filesystem;

concurrency::concurrent_unordered_map<std::wstring, std::optional<std::filesystem::path>> archive_override_paths;
concurrency::concurrent_unordered_map<std::wstring, std::optional<std::filesystem::path>> file_override_paths;

Expand Down Expand Up @@ -241,4 +239,4 @@ void* __cdecl virtual_to_archive_path_eldenring(DLString<modengine::GameType::SE
return res;
}

}
}
7 changes: 6 additions & 1 deletion src/modengine/ext/mod_loader/archive_file_overrides.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
#include "gametypes/dantelion/dlstring.h"

#include <set>
#include <filesystem>
#include <concurrent_vector.h>

namespace modengine::ext {

namespace fs = std::filesystem;

// String type used in DS2/DS3
typedef struct
{
Expand Down Expand Up @@ -43,4 +46,6 @@ HANDLE WINAPI tCreateFileW(LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwSh
LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile);

}
std::optional<fs::path> find_override_file(const fs::path& game_path);

}
5 changes: 5 additions & 0 deletions src/modengine/ext/mod_loader/mod_loader_extension.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "mod_loader_extension.h"
#include "archive_file_overrides.h"
#include "wwise_file_overrides.h"

#include "modengine/util/hex_string.h"
#include "modengine/util/platform.h"
Expand All @@ -18,6 +19,8 @@ auto loose_params_aob_2 = util::hex_string("0F 85 C5 00 00 00 48 8D 4C 24 28");
auto virtual_to_archive_path_er_aob = util::hex_aob("e8 ?? ?? ?? ?? 48 83 7b 20 08 48 8d 4b 08 72 03 48 8b 09 4c 8b 4b 18 41 b8 05 00 00 00 4d 3b c8");
auto virtual_to_archive_path_ac6_aob = util::hex_aob("cf e8 ?? ?? ?? ?? 48 83 7b 20 08 48 8d 4b 08 72 03 48 8b 09 4c 8b 4b 18 41 b8 05 00 00 00 4d 3b c8");

auto ak_file_location_resolver_open_aob = util::hex_aob("4c 89 74 24 28 48 8b 84 24 90 00 00 00 48 89 44 24 20 4c 8b ce 45 8b c4 49 8b d7 48 8b cd e8 ?? ?? ?? ?? 8b d8");

static fs::path primary_mod_path(const Settings& settings)
{
return settings.modengine_local_path();
Expand Down Expand Up @@ -60,6 +63,8 @@ void ModLoaderExtension::on_attach()
register_hook(DS3, &hooked_virtual_to_archive_path_ds3, util::rva2addr(0x7d660), virtual_to_archive_path_ds3);
register_hook(ELDEN_RING, &hooked_virtual_to_archive_path_eldenring, virtual_to_archive_path_er_aob, 0x0, virtual_to_archive_path_eldenring, SCAN_CALL_INST);
register_hook(ARMORED_CORE_6, &hooked_virtual_to_archive_path_eldenring, virtual_to_archive_path_ac6_aob, 0x1, virtual_to_archive_path_eldenring, SCAN_CALL_INST);
register_hook(ELDEN_RING, &hooked_ak_file_location_resolver_open, ak_file_location_resolver_open_aob, 0x1E, ak_file_location_resolver_open, SCAN_CALL_INST);
register_hook(ARMORED_CORE_6, &hooked_ak_file_location_resolver_open, ak_file_location_resolver_open_aob, 0x1E, ak_file_location_resolver_open, SCAN_CALL_INST);

auto config = get_config<ModLoaderConfig>();
for (const auto& mod : config.mods) {
Expand Down
106 changes: 106 additions & 0 deletions src/modengine/ext/mod_loader/wwise_file_overrides.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#include <filesystem>

#include "wwise_file_overrides.h"
#include "archive_file_overrides.h"

namespace modengine::ext {

// This hook the IAkFileLocationResolver::open() method. It takes in a path
// and a so-called openMode. This openMode parameter is of type AkOpenMode
// which is an enum with 4 states: Read, Write, WriteOverwrite and ReadWrite.
// Passing in any of these 4 invariants will cause this function to yield a
// FileOperator used for sourcing the file bytes. FromSoftware added another
// possible state to AKOpenMode, decimal 9. which will make this fn yield an
// EBLFileOperator, this implementation does all its fetching from the BDTs.
// EBLFileOperator does not use the usual virtual path lookup that ME2 already
// hooks. Hence we need this hook here.
//
// In order to selectively make it read from disk again this hook checks if an
// override file exists and sets the openMode back from 9 to Read and replaces
// the virtual path parameter string with an absolute path.
//
// This is a messy one though:
// Figuring out if there is an override isn't straight forward. The hooked
// function gets invoked with a virtual path string ex: `sd:/50846376.wem`.
// Wwise uses subdirectories for localized content, meaning that the "simple"
// WEM example makes Wwise look in `/50846376.wem` but also in
// `enus/50846376.wem` (or `ja/50846376.wem` for AC6 which has `ja` as an extra
// locale for some audio and BNKs). To make matters even worse, Elden Ring
// specifically sorts the WEMs into a subdir (`wem/`) and then another subdir
// based on the first two digits of the WEM. So above example will spawn
// lookups in `/wem/50/50846376.wem` and `enus/wem/50/50846376.wem`. In order
// to figure out if there is an override we will need to look in multiple
// directories per request. Luckily, aside from boot, this routine is called
// quite infrequently.
//
// Also, worth pointing out that not all paths passed to this hook will have
// the `sd:/` prefix. So we cannot get away with the usual prefix / rewrite
// trick and will have to allocate a completely new string.

namespace fs = std::filesystem;

using namespace spdlog;

const wchar_t* prefixes[3] = {
L"sd/",
L"sd/enus/",
L"sd/ja/",
};

std::optional<fs::path> check_paths(const std::wstring filename) {
for (auto prefix: prefixes) {
if (auto override = find_override_file(prefix+filename)) {
return override;
}
}

return {};
}

std::optional<fs::path> find_override(const std::wstring filename)
{
// Check wem/<first to digits of filename>/<filename> too since ER uses
// this format
if (filename.ends_with(L".wem")) {
auto wem_path = L"wem/" + filename.substr(0,2) + L"/" + filename;
auto wem_path_result = check_paths(wem_path);

if (wem_path_result.has_value()) {
return wem_path_result;
}
}

return check_paths(filename);
}

std::optional<std::wstring> normalize_filename(const std::wstring path) {
if (path.starts_with(L"sd:/")) {
return std::wstring(path.substr(4));
}

return {};
}

ScannedHook<decltype(&ak_file_location_resolver_open)> hooked_ak_file_location_resolver_open;

void* __cdecl ak_file_location_resolver_open(UINT64 p1, wchar_t* path, AKOpenMode openMode, UINT64 p4, UINT64 p5, UINT64 p6)
{
std::wstring lookup(path);
debug(L"sd.bhd entry requested: {}", lookup);

auto normalized = normalize_filename(lookup);
if (!normalized.has_value()) {
return hooked_ak_file_location_resolver_open.original(p1, path, openMode, p4, p5, p6);
}

auto filename = normalized.value();
auto override = find_override(filename);
if (!override.has_value()) {
return hooked_ak_file_location_resolver_open.original(p1, path, openMode, p4, p5, p6);
}

auto override_path_string = override.value().wstring();
return hooked_ak_file_location_resolver_open.original(p1, override_path_string.data(), AKOpenMode::READ, p4, p5, p6);
}

}
21 changes: 21 additions & 0 deletions src/modengine/ext/mod_loader/wwise_file_overrides.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#pragma once

#include "modengine/hook_set.h"

namespace modengine::ext {

enum AKOpenMode : uint32_t {
READ = 0,
WRITE = 1,
WRITE_OVERWRITE = 2,
READ_WRITE = 3,

// Custom mode specific to From Software's implementation
READ_EBL = 9,
};

void* __cdecl ak_file_location_resolver_open(UINT64 p1, wchar_t* path, AKOpenMode openMode, UINT64 p4, UINT64 p5, UINT64 p6);

extern ScannedHook<decltype(&ak_file_location_resolver_open)> hooked_ak_file_location_resolver_open;

}
Loading