From e8b555563089b72d30003204e4f203de280c138a Mon Sep 17 00:00:00 2001 From: Nekotekina Date: Tue, 11 Sep 2018 19:02:19 +0300 Subject: [PATCH] Rewrite vfs::get and vfs::mount Preprocess . and .. correctly Don't use recursive locking Also use std::string_view Fix format system for std::string and std::string_view Fix fmt::merge for std::string_view --- Utilities/File.cpp | 28 +++ Utilities/StrFmt.cpp | 22 +-- Utilities/StrUtil.h | 5 +- rpcs3/Emu/Cell/lv2/sys_fs.cpp | 151 +++++++------- rpcs3/Emu/System.cpp | 23 ++- rpcs3/Emu/VFS.cpp | 250 +++++++++++++++++++----- rpcs3/Emu/VFS.h | 12 +- rpcs3/rpcs3qt/trophy_manager_dialog.cpp | 5 +- rpcs3/stdafx.h | 1 + 9 files changed, 346 insertions(+), 151 deletions(-) diff --git a/Utilities/File.cpp b/Utilities/File.cpp index 762a8bf56d..62d5a856e3 100644 --- a/Utilities/File.cpp +++ b/Utilities/File.cpp @@ -513,6 +513,13 @@ bool fs::create_path(const std::string& path) bool fs::remove_dir(const std::string& path) { + if (path.empty()) + { + // Don't allow removing empty path (TODO) + g_tls_error = fs::error::noent; + return false; + } + if (auto device = get_virtual_device(path)) { return device->remove_dir(path); @@ -539,6 +546,13 @@ bool fs::remove_dir(const std::string& path) bool fs::rename(const std::string& from, const std::string& to, bool overwrite) { + if (from.empty() || to.empty()) + { + // Don't allow opening empty path (TODO) + g_tls_error = fs::error::noent; + return false; + } + const auto device = get_virtual_device(from); if (device != get_virtual_device(to)) @@ -786,6 +800,13 @@ void fs::file::xfail() const fs::file::file(const std::string& path, bs_t mode) { + if (path.empty()) + { + // Don't allow opening empty path (TODO) + g_tls_error = fs::error::noent; + return; + } + if (auto device = get_virtual_device(path)) { if (auto&& _file = device->open(path, mode)) @@ -1176,6 +1197,13 @@ void fs::dir::xnull() const bool fs::dir::open(const std::string& path) { + if (path.empty()) + { + // Don't allow opening empty path (TODO) + g_tls_error = fs::error::noent; + return false; + } + if (auto device = get_virtual_device(path)) { if (auto&& _dir = device->open_dir(path)) diff --git a/Utilities/StrFmt.cpp b/Utilities/StrFmt.cpp index 724c8b494d..7aff3bd877 100644 --- a/Utilities/StrFmt.cpp +++ b/Utilities/StrFmt.cpp @@ -4,6 +4,7 @@ #include "cfmt.h" #include +#include #ifdef _WIN32 #include @@ -73,32 +74,31 @@ void fmt_class_string::format(std::string& out, u64 arg) void fmt_class_string::format(std::string& out, u64 arg) { - if (arg) - { - fmt::append(out, "%p", reinterpret_cast(static_cast(arg))); - } - else - { - out += "(NULL)"; - } + fmt::append(out, "%p", arg); } void fmt_class_string::format(std::string& out, u64 arg) { if (arg) { - out += reinterpret_cast(static_cast(arg)); + out += reinterpret_cast(arg); } else { - out += "(NULL)"; + out += "(NULLSTR)"; } } template <> void fmt_class_string::format(std::string& out, u64 arg) { - out += get_object(arg).c_str(); // TODO? + out += get_object(arg); +} + +template <> +void fmt_class_string::format(std::string& out, u64 arg) +{ + out += get_object(arg); } template <> diff --git a/Utilities/StrUtil.h b/Utilities/StrUtil.h index 86669d0f16..526e767560 100644 --- a/Utilities/StrUtil.h +++ b/Utilities/StrUtil.h @@ -5,6 +5,7 @@ #include #include #include +#include // Copy null-terminated string from std::string to char array with truncation template @@ -100,10 +101,10 @@ namespace fmt auto end = source.end(); for (--end; it != end; ++it) { - result += *it + separator; + result += std::string{*it} + separator; } - return result + source.back(); + return result + std::string{source.back()}; } template diff --git a/rpcs3/Emu/Cell/lv2/sys_fs.cpp b/rpcs3/Emu/Cell/lv2/sys_fs.cpp index e03a40233a..924e74c0dc 100644 --- a/rpcs3/Emu/Cell/lv2/sys_fs.cpp +++ b/rpcs3/Emu/Cell/lv2/sys_fs.cpp @@ -198,7 +198,13 @@ error_code sys_fs_open(vm::cptr path, s32 flags, vm::ptr fd, s32 mode if (!path[0]) return CELL_ENOENT; - const std::string& local_path = vfs::get(path.get_ptr()); + const std::string_view vpath = path.get_ptr(); + const std::string local_path = vfs::get(vpath); + + if (vpath.find_first_not_of('/') == -1) + { + return {CELL_EISDIR, path}; + } if (local_path.empty()) { @@ -207,7 +213,7 @@ error_code sys_fs_open(vm::cptr path, s32 flags, vm::ptr fd, s32 mode // TODO: other checks for path - if (local_path == "/" || fs::is_dir(local_path)) + if (fs::is_dir(local_path)) { return {CELL_EISDIR, path}; } @@ -462,7 +468,14 @@ error_code sys_fs_opendir(vm::cptr path, vm::ptr fd) if (!path[0]) return CELL_ENOENT; - const std::string& local_path = vfs::get(path.get_ptr()); + const std::string_view vpath = path.get_ptr(); + const std::string local_path = vfs::get(vpath); + + if (vpath.find_first_not_of('/') == -1) + { + // TODO: open root + return {CELL_EPERM, path}; + } if (local_path.empty()) { @@ -471,11 +484,6 @@ error_code sys_fs_opendir(vm::cptr path, vm::ptr fd) // TODO: other checks for path - if (local_path == "/") - { - return {CELL_EPERM, path}; - } - if (fs::is_file(local_path)) { return {CELL_ENOTDIR, path}; @@ -559,7 +567,14 @@ error_code sys_fs_stat(vm::cptr path, vm::ptr sb) if (!path[0]) return CELL_ENOENT; - const std::string local_path = vfs::get(path.get_ptr()); + const std::string_view vpath = path.get_ptr(); + const std::string local_path = vfs::get(vpath); + + if (vpath.find_first_not_of('/') == -1) + { + *sb = {CELL_FS_S_IFDIR | 0444}; + return CELL_OK; + } if (local_path.empty()) { @@ -568,12 +583,6 @@ error_code sys_fs_stat(vm::cptr path, vm::ptr sb) fs::stat_t info{}; - if (local_path == "/") - { - sb->mode = CELL_FS_S_IFDIR | 0444; - return CELL_OK; - } - if (!fs::stat(local_path, info)) { switch (auto error = fs::g_tls_error) @@ -671,18 +680,19 @@ error_code sys_fs_mkdir(vm::cptr path, s32 mode) if (!path[0]) return CELL_ENOENT; - const std::string local_path = vfs::get(path.get_ptr()); + const std::string_view vpath = path.get_ptr(); + const std::string local_path = vfs::get(vpath); + + if (vpath.find_first_not_of('/') == -1) + { + return {CELL_EEXIST, path}; + } if (local_path.empty()) { return {CELL_ENOTMOUNTED, path}; } - if (local_path == "/") - { - return {CELL_EEXIST, path}; - } - if (!fs::create_path(local_path)) { switch (auto error = fs::g_tls_error) @@ -703,19 +713,22 @@ error_code sys_fs_rename(vm::cptr from, vm::cptr to) { sys_fs.warning("sys_fs_rename(from=%s, to=%s)", from, to); - const std::string local_from = vfs::get(from.get_ptr()); - const std::string local_to = vfs::get(to.get_ptr()); + const std::string_view vfrom = from.get_ptr(); + const std::string local_from = vfs::get(vfrom); + + const std::string_view vto = to.get_ptr(); + const std::string local_to = vfs::get(vto); + + if (vfrom.find_first_not_of('/') == -1 || vto.find_first_not_of('/') == -1) + { + return CELL_EPERM; + } if (local_from.empty() || local_to.empty()) { return CELL_ENOTMOUNTED; } - if (local_to == "/" || local_from == "/") - { - return CELL_EPERM; - } - if (!fs::rename(local_from, local_to, false)) { switch (auto error = fs::g_tls_error) @@ -742,18 +755,19 @@ error_code sys_fs_rmdir(vm::cptr path) if (!path[0]) return CELL_ENOENT; - const std::string local_path = vfs::get(path.get_ptr()); + const std::string_view vpath = path.get_ptr(); + const std::string local_path = vfs::get(vpath); + + if (vpath.find_first_not_of('/') == -1) + { + return {CELL_EPERM, path}; + } if (local_path.empty()) { return {CELL_ENOTMOUNTED, path}; } - if (local_path == "/") - { - return {CELL_EPERM, path}; - } - if (!fs::remove_dir(local_path)) { switch (auto error = fs::g_tls_error) @@ -780,18 +794,19 @@ error_code sys_fs_unlink(vm::cptr path) if (!path[0]) return CELL_ENOENT; - const std::string local_path = vfs::get(path.get_ptr()); + const std::string_view vpath = path.get_ptr(); + const std::string local_path = vfs::get(vpath); + + if (vpath.find_first_not_of('/') == -1) + { + return {CELL_EISDIR, path}; + } if (local_path.empty()) { return {CELL_ENOTMOUNTED, path}; } - if (local_path == "/") - { - return {CELL_EISDIR, path}; - } - if (!fs::remove_file(local_path)) { switch (auto error = fs::g_tls_error) @@ -922,16 +937,17 @@ error_code sys_fs_fcntl(u32 fd, u32 op, vm::ptr _arg, u32 _size) { const auto arg = vm::static_ptr_cast(_arg); - const std::string local_path = vfs::get(arg->path.get_ptr()); + const std::string_view vpath = arg->path.get_ptr(); + const std::string local_path = vfs::get(vpath); + + if (vpath.find_first_not_of('/') == -1) + { + return {CELL_EPERM, vpath}; + } if (local_path.empty()) { - return {CELL_ENOTMOUNTED, arg->path}; - } - - if (local_path == "/") - { - return {CELL_EPERM, arg->path}; + return {CELL_ENOTMOUNTED, vpath}; } fs::device_stat info; @@ -939,7 +955,7 @@ error_code sys_fs_fcntl(u32 fd, u32 op, vm::ptr _arg, u32 _size) { switch (auto error = fs::g_tls_error) { - case fs::error::noent: return {CELL_ENOENT, arg->path}; + case fs::error::noent: return {CELL_ENOENT, vpath}; default: sys_fs.error("sys_fs_fcntl(0xc0000002): unknown error %s", error); } @@ -1300,18 +1316,19 @@ error_code sys_fs_truncate(vm::cptr path, u64 size) if (!path[0]) return CELL_ENOENT; - const std::string local_path = vfs::get(path.get_ptr()); + const std::string_view vpath = path.get_ptr(); + const std::string local_path = vfs::get(vpath); + + if (vpath.find_first_not_of('/') == -1) + { + return {CELL_EISDIR, path}; + } if (local_path.empty()) { return {CELL_ENOTMOUNTED, path}; } - if (local_path == "/") - { - return {CELL_EISDIR, path}; - } - if (!fs::truncate_file(local_path, size)) { switch (auto error = fs::g_tls_error) @@ -1398,18 +1415,19 @@ error_code sys_fs_disk_free(vm::cptr path, vm::ptr total_free, vm::pt if (!path[0]) return CELL_EINVAL; - const std::string local_path = vfs::get(path.get_ptr()); + const std::string_view vpath = path.get_ptr(); + const std::string local_path = vfs::get(vpath); + + if (vpath.find_first_not_of('/') == -1) + { + return {CELL_EPERM, path}; + } if (local_path.empty()) { return {CELL_ENOTMOUNTED, path}; } - if (local_path == "/") - { - return {CELL_EPERM, path}; - } - fs::device_stat info; if (!fs::statfs(local_path, info)) { @@ -1439,18 +1457,19 @@ error_code sys_fs_utime(vm::cptr path, vm::cptr timep) if (!path[0]) return CELL_ENOENT; - const std::string local_path = vfs::get(path.get_ptr()); + const std::string_view vpath = path.get_ptr(); + const std::string local_path = vfs::get(vpath); + + if (vpath.find_first_not_of('/') == -1) + { + return {CELL_EISDIR, path}; + } if (local_path.empty()) { return {CELL_ENOTMOUNTED, path}; } - if (local_path == "/") - { - return {CELL_EISDIR, path}; - } - if (!fs::utime(local_path, timep->actime, timep->modtime)) { switch (auto error = fs::g_tls_error) diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index 151010c3b7..f499238c35 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -708,13 +708,12 @@ void Emulator::Load(bool add_only) std::string bdvd_dir = g_cfg.vfs.dev_bdvd; // Mount default relative path to non-existent directory - vfs::mount("", fs::get_config_dir() + "delete_this_dir/"); - vfs::mount("dev_hdd0", fmt::replace_all(g_cfg.vfs.dev_hdd0, "$(EmulatorDir)", emu_dir)); - vfs::mount("dev_hdd1", fmt::replace_all(g_cfg.vfs.dev_hdd1, "$(EmulatorDir)", emu_dir)); - vfs::mount("dev_flash", g_cfg.vfs.get_dev_flash()); - vfs::mount("dev_usb", fmt::replace_all(g_cfg.vfs.dev_usb000, "$(EmulatorDir)", emu_dir)); - vfs::mount("dev_usb000", fmt::replace_all(g_cfg.vfs.dev_usb000, "$(EmulatorDir)", emu_dir)); - vfs::mount("app_home", home_dir.empty() ? elf_dir + '/' : fmt::replace_all(home_dir, "$(EmulatorDir)", emu_dir)); + vfs::mount("/dev_hdd0", fmt::replace_all(g_cfg.vfs.dev_hdd0, "$(EmulatorDir)", emu_dir)); + vfs::mount("/dev_hdd1", fmt::replace_all(g_cfg.vfs.dev_hdd1, "$(EmulatorDir)", emu_dir)); + vfs::mount("/dev_flash", g_cfg.vfs.get_dev_flash()); + vfs::mount("/dev_usb", fmt::replace_all(g_cfg.vfs.dev_usb000, "$(EmulatorDir)", emu_dir)); + vfs::mount("/dev_usb000", fmt::replace_all(g_cfg.vfs.dev_usb000, "$(EmulatorDir)", emu_dir)); + vfs::mount("/app_home", home_dir.empty() ? elf_dir + '/' : fmt::replace_all(home_dir, "$(EmulatorDir)", emu_dir)); // Special boot mode (directory scan) if (fs::is_dir(m_path)) @@ -903,7 +902,7 @@ void Emulator::Load(bool add_only) { fs::file sfb_file; - vfs::mount("dev_bdvd", bdvd_dir); + vfs::mount("/dev_bdvd", bdvd_dir); LOG_NOTICE(LOADER, "Disc: %s", vfs::get("/dev_bdvd")); if (!sfb_file.open(vfs::get("/dev_bdvd/PS3_DISC.SFB")) || sfb_file.size() < 4 || sfb_file.read() != ".SFB"_u32) @@ -964,7 +963,7 @@ void Emulator::Load(bool add_only) } else if (m_cat == "DG" && from_hdd0_game) { - vfs::mount("dev_bdvd/PS3_GAME", hdd0_game + m_path.substr(hdd0_game.size(), 10)); + vfs::mount("/dev_bdvd/PS3_GAME", hdd0_game + m_path.substr(hdd0_game.size(), 10)); LOG_NOTICE(LOADER, "Game: %s", vfs::get("/dev_bdvd/PS3_GAME")); } else if (disc.empty()) @@ -975,7 +974,7 @@ void Emulator::Load(bool add_only) else { bdvd_dir = disc; - vfs::mount("dev_bdvd", bdvd_dir); + vfs::mount("/dev_bdvd", bdvd_dir); LOG_NOTICE(LOADER, "Disk: %s", vfs::get("/dev_bdvd")); } @@ -1062,10 +1061,10 @@ void Emulator::Load(bool add_only) return m_path = hdd0_boot, Load(); } - // Mount /host_root/ if necessary + // Mount /host_root/ if necessary (special value) if (g_cfg.vfs.host_root) { - vfs::mount("host_root", {}); + vfs::mount("/host_root", "/"); } // Open SELF or ELF diff --git a/rpcs3/Emu/VFS.cpp b/rpcs3/Emu/VFS.cpp index 0368613838..309c3455d6 100644 --- a/rpcs3/Emu/VFS.cpp +++ b/rpcs3/Emu/VFS.cpp @@ -2,88 +2,234 @@ #include "IdManager.h" #include "VFS.h" -#include +#include "Utilities/mutex.h" +#include "Utilities/StrUtil.h" + +struct vfs_directory +{ + // Real path (empty if root or not exists) + std::string path; + + // Virtual subdirectories (vector because only vector allows incomplete types) + std::vector> dirs; +}; struct vfs_manager { shared_mutex mutex; - // Device name -> Real path - std::unordered_map mounted; + // VFS root + vfs_directory root; }; -const std::regex s_regex_ps3("^/+(.*?)(?:$|/)(.*)", std::regex::optimize); - -bool vfs::mount(const std::string& dev_name, const std::string& path) +bool vfs::mount(std::string_view vpath, std::string_view path) { const auto table = fxm::get_always(); - safe_writer_lock lock(table->mutex); + std::lock_guard lock(table->mutex); - return table->mounted.emplace(dev_name, path).second; + if (vpath.empty()) + { + // Empty relative path, should set relative path base; unsupported + return false; + } + + for (std::vector list{&table->root};;) + { + // Skip one or more '/' + const auto pos = vpath.find_first_not_of('/'); + + if (pos == 0) + { + // Mounting relative path is not supported + return false; + } + + if (pos == -1) + { + // Mounting completed + list.back()->path = path; + return true; + } + + // Get fragment name + const auto name = vpath.substr(pos, vpath.find_first_of('/', pos) - pos); + vpath.remove_prefix(name.size() + pos); + + if (name == ".") + { + // Keep current + continue; + } + + if (name == "..") + { + // Root parent is root + if (list.size() == 1) + { + continue; + } + + // Go back one level + list.resize(list.size() - 1); + continue; + } + + // Find or add + const auto last = list.back(); + + for (auto& dir : last->dirs) + { + if (dir.first == name) + { + list.push_back(&dir.second); + break; + } + } + + if (last == list.back()) + { + // Add new entry + list.push_back(&last->dirs.emplace_back(name, vfs_directory{}).second); + } + } } -std::string vfs::get(const std::string& vpath, const std::string* prev, std::size_t pos) +std::string vfs::get(std::string_view vpath, std::vector* out_dir) { const auto table = fxm::get_always(); - safe_reader_lock lock(table->mutex); + reader_lock lock(table->mutex); - std::smatch match; + // Resulting path fragments: decoded ones + std::vector result; + result.reserve(vpath.size() / 2); - if (!std::regex_match(vpath.begin() + pos, vpath.end(), match, s_regex_ps3)) + // Mounted path + std::string_view result_base; + + if (vpath.empty()) { - const auto found = table->mounted.find(""); + // Empty relative path (reuse further return) + vpath = "."; + } - if (found == table->mounted.end()) + for (std::vector list{&table->root};;) + { + // Skip one or more '/' + const auto pos = vpath.find_first_not_of('/'); + + if (pos == 0) { - LOG_WARNING(GENERAL, "vfs::get(): no default directory: %s", vpath); - return {}; + // Relative path: point to non-existent location + return fs::get_config_dir() + "delete_this_dir.../delete_this..."; } - return found->second + vfs::escape(vpath); - } - - if (match.length(1) + pos == 0) - { - return "/"; - } - - std::string dev; - - if (prev) - { - dev += *prev; - dev += '/'; - } - - dev += match.str(1); - - const auto found = table->mounted.find(dev); - - if (found == table->mounted.end()) - { - if (match.length(2)) + if (pos == -1) { - return vfs::get(vpath, &dev, pos + match.position(1) + match.length(1)); + // Absolute path: finalize + for (auto it = list.rbegin(), rend = list.rend(); it != rend; it++) + { + if (auto* dir = *it; dir && !dir->path.empty()) + { + // Save latest valid mount path + result_base = dir->path; + + // Erase unnecessary path fragments + result.erase(result.begin(), result.begin() + (std::distance(it, rend) - 1)); + + // Extract mounted subdirectories (TODO) + if (out_dir) + { + for (auto& pair : dir->dirs) + { + if (!pair.second.path.empty()) + { + out_dir->emplace_back(pair.first); + } + } + } + + break; + } + } + + if (!vpath.empty()) + { + // Finalize path with '/' + result.emplace_back(""); + } + + break; } - LOG_WARNING(GENERAL, "vfs::get(): device not found: %s", vpath); + // Get fragment name + const auto name = vpath.substr(pos, vpath.find_first_of('/', pos) - pos); + vpath.remove_prefix(name.size() + pos); + + // Process special directories + if (name == ".") + { + // Keep current + continue; + } + + if (name == "..") + { + // Root parent is root + if (list.size() == 1) + { + continue; + } + + // Go back one level + list.resize(list.size() - 1); + result.resize(result.size() - 1); + continue; + } + + const auto last = list.back(); + list.push_back(nullptr); + + result.push_back(name); + + if (!last) + { + continue; + } + + for (auto& dir : last->dirs) + { + if (dir.first == name) + { + list.back() = &dir.second; + break; + } + } + + if (last->path == "/") + { + if (vpath.empty()) + { + return {}; + } + + // Handle /host_root (not escaped, not processed) + return std::string{vpath.substr(1)}; + } + } + + if (result_base.empty()) + { + // Not mounted return {}; } - if (found->second.empty()) - { - // Don't escape /host_root (TODO) - return match.str(2); - } - - // Escape and concatenate - return found->second + vfs::escape(match.str(2)); + // Escape and merge path fragments + return std::string{result_base} + vfs::escape(fmt::merge(result, "/")); } - -std::string vfs::escape(const std::string& path) +std::string vfs::escape(std::string_view path) { std::string result; result.reserve(path.size()); @@ -175,7 +321,7 @@ std::string vfs::escape(const std::string& path) return result; } -std::string vfs::unescape(const std::string& path) +std::string vfs::unescape(std::string_view path) { std::string result; result.reserve(path.size()); diff --git a/rpcs3/Emu/VFS.h b/rpcs3/Emu/VFS.h index ca6fe7e64f..b4dc298e55 100644 --- a/rpcs3/Emu/VFS.h +++ b/rpcs3/Emu/VFS.h @@ -1,18 +1,20 @@ #pragma once +#include #include +#include namespace vfs { // Mount VFS device - bool mount(const std::string& dev_name, const std::string& path); + bool mount(std::string_view vpath, std::string_view path); - // Convert VFS path to fs path - std::string get(const std::string& vpath, const std::string* = nullptr, std::size_t = 0); + // Convert VFS path to fs path, optionally listing directories mounted in it + std::string get(std::string_view vpath, std::vector* out_dir = nullptr); // Escape VFS path by replacing non-portable characters with surrogates - std::string escape(const std::string& path); + std::string escape(std::string_view path); // Invert escape operation - std::string unescape(const std::string& path); + std::string unescape(std::string_view path); } diff --git a/rpcs3/rpcs3qt/trophy_manager_dialog.cpp b/rpcs3/rpcs3qt/trophy_manager_dialog.cpp index e800d1520b..d9564cc4fc 100644 --- a/rpcs3/rpcs3qt/trophy_manager_dialog.cpp +++ b/rpcs3/rpcs3qt/trophy_manager_dialog.cpp @@ -55,8 +55,7 @@ trophy_manager_dialog::trophy_manager_dialog(std::shared_ptr gui_s m_show_platinum_trophies = m_gui_settings->GetValue(gui::tr_show_platinum).toBool(); // HACK: dev_hdd0 must be mounted for vfs to work for loading trophies. - vfs::mount("dev_hdd0", Emu.GetHddDir()); - + vfs::mount("/dev_hdd0", Emu.GetHddDir()); // Get the currently selected user's trophy path. m_trophy_dir = "/dev_hdd0/home/" + Emu.GetUsr() + "/trophy/"; @@ -577,7 +576,7 @@ void trophy_manager_dialog::ShowContextMenu(const QPoint& loc) QString path = qstr(m_trophies_db[db_ind]->path); QDesktopServices::openUrl(QUrl("file:///" + path)); }); - + menu->addAction(show_trophy_dir); menu->exec(globalPos); } diff --git a/rpcs3/stdafx.h b/rpcs3/stdafx.h index 0c40854b0c..7174d8d6a8 100644 --- a/rpcs3/stdafx.h +++ b/rpcs3/stdafx.h @@ -43,5 +43,6 @@ namespace std { inline namespace literals { inline namespace chrono_literals {}} #include #include #include +#include using namespace std::literals;