mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-02 06:40:42 -07:00
37218e7fe0
As noted in #567, a filesystem path that contains non-ascii will break a lot if using a clang toolchain. Luckily, fmtlib has a lossy utf8 convert when you use it to print a path (after including `fmt/std.h`). The vast majority of this diff is just removing `.string()` from paths inside loggings calls. There are some callsites I _didn't_ touch, mainly the options, because it would be an ABI break to change those to be wide strings and I cbf looking into settings upgrades. There are also some spots (avs mountpath remapping, for example) where the path is guaranteed to be ascii, so I didn't modify them. ImGui doesn't appear to easily support wide strings (I mean, surely it does, but I'm not gonna look too far into it) so I mostly just left those alone too, with a few spots modified to re-use fmtlib's lossy utf8. Some of the changes are basically never gonna be hit IRL, like who would put a file with a non-ascii _extension_ along with their modules? But the diff is (I hope) pretty easy to validate as OK. Testing has been somewhat minimal, I fired up the GCC build of spice2x in a dodgy folder name, got mojibake (running via wine in linux so take that as you will), ran the unmodified clang spice and crashed the same way the reporter did. After modification, I get the exact same mojibake so I assume if the terminal enjoys utf8 it'll display OK. Claude (only used for review) thinks the commit is fine but is annoyed that I use `fmt::detail` in the appdata censoring, which is part of the private API; personally I don't care because it's pretty stable.
369 lines
11 KiB
C++
369 lines
11 KiB
C++
#include "avshook.h"
|
|
|
|
#include <cassert>
|
|
#include <mutex>
|
|
#include <map>
|
|
#include <optional>
|
|
#include <external/robin_hood.h>
|
|
|
|
#include "avs/core.h"
|
|
#include "avs/ea3.h"
|
|
#include "avs/game.h"
|
|
#include "util/detour.h"
|
|
#include "util/fileutils.h"
|
|
#include "util/logging.h"
|
|
#include "util/utils.h"
|
|
|
|
#ifdef min
|
|
#undef min
|
|
#endif
|
|
|
|
static bool FAKE_FILE_OPEN = false;
|
|
|
|
static std::map<std::string, std::string> ROM_FILE_MAP;
|
|
static std::string *ROM_FILE_CONTENTS = nullptr;
|
|
|
|
static std::mutex lstat_cache_mutex;
|
|
static robin_hood::unordered_map<std::string, std::pair<int, struct avs::core::avs_stat>> lstat_cache;
|
|
|
|
namespace hooks::avs::config {
|
|
bool DISABLE_VFS_DRIVE_REDIRECTION = false;
|
|
bool DISABLE_CACHE = false;
|
|
bool LOG = false;
|
|
};
|
|
|
|
using namespace hooks::avs;
|
|
|
|
#define WRAP_DEBUG_FMT(format, ...) \
|
|
if (config::LOG) { \
|
|
log_misc("avshook", "{}: " format " = 0x{:x}", __FUNCTION__, __VA_ARGS__, static_cast<unsigned>(value)); \
|
|
}
|
|
#define AVS_HOOK(f) hook_function(::avs::core::IMPORT_NAMES.f, #f, &::avs::core::f, f)
|
|
|
|
template<typename T>
|
|
static void hook_function(const char *source_name, const char *target_name, T **source, T *target) {
|
|
if (!detour::trampoline_try(avs::core::DLL_NAME.c_str(), source_name, target, source)) {
|
|
log_warning("avshook", "could not hook {} ({})", target_name, source_name);
|
|
}
|
|
}
|
|
|
|
static inline bool is_fake_fd(avs::core::avs_file_t fd) {
|
|
return FAKE_FILE_OPEN && fd == 1337;
|
|
}
|
|
|
|
static bool is_dest_file(const char *name) {
|
|
static std::string path_dest = fmt::format("/dev/raw/{}.dest", avs::game::DEST[0]);
|
|
static std::string path_bin = fmt::format("/dev/raw/{}.bin", avs::game::DEST[0]);
|
|
|
|
return !_stricmp(name, path_dest.c_str()) || !_stricmp(name, path_bin.c_str());
|
|
}
|
|
|
|
static bool is_dest_file(const char *name, uint16_t mode) {
|
|
return mode == 1 && is_dest_file(name);
|
|
}
|
|
|
|
static bool is_dest_spec_ea3_config(const char *name) {
|
|
static std::string path = fmt::format("/prop/ea3-config-{}{}.xml", avs::game::DEST[0], avs::game::SPEC[0]);
|
|
|
|
return !_stricmp(name, path.c_str());
|
|
}
|
|
|
|
static bool is_spam_file(const char *file) {
|
|
static const char *spam_prefixes[] = {
|
|
"/mnt/bm2d/ngp",
|
|
"/afp",
|
|
"/dev/nvram/pm_eco.xml",
|
|
"/dev/nvram/pm_gamesys.xml",
|
|
"/dev/nvram/pm_clock.xml",
|
|
};
|
|
|
|
for (auto &spam : spam_prefixes) {
|
|
if (string_begins_with(file, spam)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool is_ldj_cacheable_file(const char *file) {
|
|
static const char *spam_prefixes[] = {
|
|
// iidx33: profile background customization, spams on every frame during card entry
|
|
"/data/graphic/entry_card",
|
|
// iidx32+: movie thumbnails, spams on every frame during song select
|
|
"/data/graphic/thumbnail/",
|
|
"/data/graphic/movie_thumbnail/",
|
|
};
|
|
|
|
for (auto &spam : spam_prefixes) {
|
|
if (string_begins_with(file, spam)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static int cached_avs_fs_lstat(const char *name, struct avs::core::avs_stat *st) {
|
|
assert(name != nullptr);
|
|
assert(st != nullptr);
|
|
|
|
const std::string filename(name);
|
|
int value = 0;
|
|
std::lock_guard<std::mutex> lock(lstat_cache_mutex);
|
|
|
|
// if this was already seen, return cached result
|
|
if (lstat_cache.contains(filename)) {
|
|
const auto &result = lstat_cache.at(filename);
|
|
value = result.first;
|
|
memcpy(st, &result.second, sizeof(*st));
|
|
WRAP_DEBUG_FMT(
|
|
"(cached) file={}, a={}, m={}, c={}, size={}, u={}",
|
|
name,
|
|
st->st_atime,
|
|
st->st_mtime,
|
|
st->st_ctime,
|
|
st->filesize,
|
|
st->unk1);
|
|
return value;
|
|
}
|
|
|
|
// if not, call the original avs function
|
|
// this returns 1 on success with valid values in st
|
|
// return 0 on failure, with filesize set to 0
|
|
// maybe this could be used for future optimization
|
|
value = avs::core::avs_fs_lstat(name, st);
|
|
WRAP_DEBUG_FMT(
|
|
"file={} a={}, m={}, c={}, size={}, u={}",
|
|
name,
|
|
st->st_atime,
|
|
st->st_mtime,
|
|
st->st_ctime,
|
|
st->filesize,
|
|
st->unk1);
|
|
|
|
// and cache it if there is room (prevent unconstrained growth)
|
|
if (lstat_cache.size() <= 4096) {
|
|
// c++ was a mistake
|
|
lstat_cache.emplace(
|
|
std::piecewise_construct,
|
|
std::forward_as_tuple(filename),
|
|
std::forward_as_tuple(value, *st));
|
|
}
|
|
|
|
// done
|
|
return value;
|
|
}
|
|
|
|
static int avs_fs_fstat(avs::core::avs_file_t fd, struct avs::core::avs_stat *st) {
|
|
if (is_fake_fd(fd) && ROM_FILE_CONTENTS) {
|
|
if (st) {
|
|
st->filesize = static_cast<uint32_t>(ROM_FILE_CONTENTS->length());
|
|
st->padding.st_dev = 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
return avs::core::avs_fs_fstat(fd, st);
|
|
}
|
|
|
|
static int avs_fs_lstat(const char *name, struct avs::core::avs_stat *st) {
|
|
if (name == nullptr) {
|
|
return avs::core::avs_fs_lstat(name, st);
|
|
}
|
|
|
|
if (is_dest_file(name) || is_dest_spec_ea3_config(name)) {
|
|
if (st) {
|
|
st->filesize = 0;
|
|
st->padding.st_dev = 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
if (!hooks::avs::config::DISABLE_CACHE && (st != nullptr) &&
|
|
avs::game::is_model("LDJ") && is_ldj_cacheable_file(name)) {
|
|
return cached_avs_fs_lstat(name, st);
|
|
}
|
|
|
|
const auto value = avs::core::avs_fs_lstat(name, st);
|
|
if (!is_spam_file(name)) {
|
|
WRAP_DEBUG_FMT("name: {}", name);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
static avs::core::avs_file_t avs_fs_open(const char *name, uint16_t mode, int flags) {
|
|
if (name == nullptr) {
|
|
return avs::core::avs_fs_open(name, mode, flags);
|
|
}
|
|
|
|
if (!FAKE_FILE_OPEN && (is_dest_file(name, mode) || ROM_FILE_MAP.contains(name))) {
|
|
FAKE_FILE_OPEN = true;
|
|
|
|
if (ROM_FILE_MAP.contains(name)) {
|
|
ROM_FILE_CONTENTS = &ROM_FILE_MAP.at(name);
|
|
}
|
|
|
|
log_info("avshook", "opening fake file '{}'", name);
|
|
|
|
return 1337;
|
|
}
|
|
|
|
auto value = avs::core::avs_fs_open(name, mode, flags);
|
|
|
|
if (!is_spam_file(name)) {
|
|
WRAP_DEBUG_FMT("name: {} mode: {} flags: {}", name, mode, flags);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
static void avs_fs_close(avs::core::avs_file_t fd) {
|
|
if (is_fake_fd(fd)) {
|
|
FAKE_FILE_OPEN = false;
|
|
ROM_FILE_CONTENTS = nullptr;
|
|
|
|
log_info("hooks::avs", "closing fake fd");
|
|
} else {
|
|
avs::core::avs_fs_close(fd);
|
|
}
|
|
}
|
|
|
|
static int avs_fs_copy(const char *sname, const char *dname) {
|
|
if (sname == nullptr || dname == nullptr) {
|
|
return avs::core::avs_fs_copy(sname, dname);
|
|
}
|
|
|
|
auto value = avs::core::avs_fs_copy(sname, dname);
|
|
WRAP_DEBUG_FMT("sname: {} dname {}", sname, dname);
|
|
|
|
return value;
|
|
}
|
|
|
|
static avs::core::avs_file_t avs_fs_opendir(const char *path) {
|
|
if (path == nullptr) {
|
|
return avs::core::avs_fs_opendir(path);
|
|
}
|
|
|
|
auto value = avs::core::avs_fs_opendir(path);
|
|
WRAP_DEBUG_FMT("path: {}", path);
|
|
|
|
return value;
|
|
}
|
|
|
|
static int avs_fs_mount(const char *mountpoint, const char *fsroot, const char *fstype, void *data) {
|
|
// avs redirection breaks ongaku paradise
|
|
if (mountpoint == nullptr ||
|
|
fsroot == nullptr ||
|
|
fstype == nullptr ||
|
|
avs::game::is_model("JC9")) {
|
|
return avs::core::avs_fs_mount(mountpoint, fsroot, fstype, data);
|
|
}
|
|
|
|
std::optional<std::string> new_fs_root = std::nullopt;
|
|
|
|
if (_stricmp(mountpoint, "/mnt/ea3-config.xml") == 0 && is_dest_spec_ea3_config(fsroot)) {
|
|
new_fs_root = fmt::format("/{}", avs::ea3::CFG_PATH);
|
|
}
|
|
|
|
// remap drive mounts to `dev/vfs/drive_x` where x is the drive letter
|
|
if (!config::DISABLE_VFS_DRIVE_REDIRECTION &&
|
|
(_strnicmp(fsroot, "d:", 2) == 0 ||
|
|
_strnicmp(fsroot, "e:", 2) == 0 ||
|
|
_strnicmp(fsroot, "f:", 2) == 0) &&
|
|
_stricmp(fstype, "fs") == 0)
|
|
{
|
|
// sub path is everything after the drive and colon characters
|
|
const char drive_letter[2] {
|
|
static_cast<char>(std::tolower(static_cast<unsigned char>(fsroot[0]))),
|
|
'\0',
|
|
};
|
|
const auto separator = fsroot[2] == '/' ? "" : "/";
|
|
const auto sub_path = &fsroot[2];
|
|
const std::filesystem::path mapped_path = fmt::format(
|
|
"dev/vfs/drive_{}{}{}",
|
|
drive_letter,
|
|
separator,
|
|
sub_path);
|
|
|
|
// create the mapped directory path
|
|
std::error_code err;
|
|
std::filesystem::create_directories(mapped_path, err);
|
|
|
|
if (err) {
|
|
log_warning("hooks::avs", "failed to create '{}': {}", mapped_path, err.message());
|
|
} else {
|
|
|
|
// if this is the `e:\`, then create the special directories
|
|
if (drive_letter[0] == 'e' &&
|
|
(sub_path[0] == '/' || sub_path[0] == '\\') &&
|
|
sub_path[1] == '\0')
|
|
{
|
|
fileutils::dir_create_log("hooks::avs", mapped_path / "tmp");
|
|
fileutils::dir_create_log("hooks::avs", mapped_path / "up");
|
|
}
|
|
|
|
log_misc("hooks::avs", "source directory '{}' remapped to '{}'",
|
|
fsroot,
|
|
mapped_path);
|
|
}
|
|
|
|
new_fs_root = mapped_path.string();
|
|
}
|
|
|
|
auto fs_root_data = new_fs_root.has_value() ? new_fs_root->c_str() : fsroot;
|
|
auto value = avs::core::avs_fs_mount(mountpoint, fs_root_data, fstype, data);
|
|
|
|
WRAP_DEBUG_FMT("mountpoint: {}, fsroot: {}, fstype: {}", mountpoint, fs_root_data, fstype);
|
|
|
|
return value;
|
|
}
|
|
|
|
static size_t avs_fs_read(avs::core::avs_file_t fd, uint8_t *data, uint32_t data_size) {
|
|
if (is_fake_fd(fd) && ROM_FILE_CONTENTS) {
|
|
const auto size = std::min(static_cast<size_t>(data_size), ROM_FILE_CONTENTS->length());
|
|
|
|
memcpy(data, ROM_FILE_CONTENTS->c_str(), size);
|
|
|
|
return size;
|
|
}
|
|
|
|
return avs::core::avs_fs_read(fd, data, data_size);
|
|
}
|
|
|
|
static int property_file_write(avs::core::property_ptr prop, const char* path) {
|
|
if (prop == nullptr || path == nullptr) {
|
|
return avs::core::property_file_write(prop, path);
|
|
}
|
|
|
|
// resort anthem dumps eacoin to /dev/nvram/eacoin AND /eacoin.xml
|
|
// it never reads /eacoin.xml, so it's probably a development leftover
|
|
if (avs::game::is_model("JDZ") && _stricmp(path, "/eacoin.xml") == 0) {
|
|
return 0;
|
|
}
|
|
|
|
auto value = avs::core::property_file_write(prop, path);
|
|
|
|
WRAP_DEBUG_FMT("path: {}", path);
|
|
|
|
return value;
|
|
}
|
|
|
|
void hooks::avs::init() {
|
|
log_info("hooks::avs", "initializing");
|
|
|
|
AVS_HOOK(avs_fs_fstat);
|
|
AVS_HOOK(avs_fs_lstat);
|
|
AVS_HOOK(avs_fs_open);
|
|
AVS_HOOK(avs_fs_close);
|
|
AVS_HOOK(avs_fs_copy);
|
|
AVS_HOOK(avs_fs_opendir);
|
|
AVS_HOOK(avs_fs_mount);
|
|
AVS_HOOK(avs_fs_read);
|
|
AVS_HOOK(property_file_write);
|
|
}
|
|
|
|
void hooks::avs::set_rom(const char *path, const char *contents) {
|
|
ROM_FILE_MAP.emplace(path, contents);
|
|
}
|