#include "stdafx.h" #include "Emu/localized_string.h" #include "Emu/System.h" #include "Emu/system_utils.hpp" #include "Emu/VFS.h" #include "Emu/IdManager.h" #include "Emu/Cell/ErrorCodes.h" #include "Emu/Cell/PPUModule.h" #include "Emu/Cell/lv2/sys_fs.h" #include "Emu/Cell/lv2/sys_sync.h" #include "cellSysutil.h" #include "cellMsgDialog.h" #include "cellGame.h" #include "Loader/PSF.h" #include "Utilities/StrUtil.h" #include "util/init_mutex.hpp" #include "util/asm.hpp" #include "Crypto/utils.h" #include LOG_CHANNEL(cellGame); vm::gvar g_stat_get; vm::gvar g_stat_set; vm::gvar g_file_param; vm::gvar g_cb_result; stx::init_lock acquire_lock(stx::init_mutex& mtx, ppu_thread* ppu = nullptr); stx::access_lock acquire_access_lock(stx::init_mutex& mtx, ppu_thread* ppu = nullptr); stx::reset_lock acquire_reset_lock(stx::init_mutex& mtx, ppu_thread* ppu = nullptr); template<> void fmt_class_string::format(std::string& out, u64 arg) { format_enum(out, arg, [](auto error) { switch (error) { STR_CASE(CELL_GAME_ERROR_NOTFOUND); STR_CASE(CELL_GAME_ERROR_BROKEN); STR_CASE(CELL_GAME_ERROR_INTERNAL); STR_CASE(CELL_GAME_ERROR_PARAM); STR_CASE(CELL_GAME_ERROR_NOAPP); STR_CASE(CELL_GAME_ERROR_ACCESS_ERROR); STR_CASE(CELL_GAME_ERROR_NOSPACE); STR_CASE(CELL_GAME_ERROR_NOTSUPPORTED); STR_CASE(CELL_GAME_ERROR_FAILURE); STR_CASE(CELL_GAME_ERROR_BUSY); STR_CASE(CELL_GAME_ERROR_IN_SHUTDOWN); STR_CASE(CELL_GAME_ERROR_INVALID_ID); STR_CASE(CELL_GAME_ERROR_EXIST); STR_CASE(CELL_GAME_ERROR_NOTPATCH); STR_CASE(CELL_GAME_ERROR_INVALID_THEME_FILE); STR_CASE(CELL_GAME_ERROR_BOOTPATH); } return unknown; }); } template<> void fmt_class_string::format(std::string& out, u64 arg) { format_enum(out, arg, [](auto error) { switch (error) { STR_CASE(CELL_GAMEDATA_ERROR_CBRESULT); STR_CASE(CELL_GAMEDATA_ERROR_ACCESS_ERROR); STR_CASE(CELL_GAMEDATA_ERROR_INTERNAL); STR_CASE(CELL_GAMEDATA_ERROR_PARAM); STR_CASE(CELL_GAMEDATA_ERROR_NOSPACE); STR_CASE(CELL_GAMEDATA_ERROR_BROKEN); STR_CASE(CELL_GAMEDATA_ERROR_FAILURE); } return unknown; }); } template<> void fmt_class_string::format(std::string& out, u64 arg) { format_enum(out, arg, [](auto error) { switch (error) { STR_CASE(CELL_DISCGAME_ERROR_INTERNAL); STR_CASE(CELL_DISCGAME_ERROR_NOT_DISCBOOT); STR_CASE(CELL_DISCGAME_ERROR_PARAM); } return unknown; }); } template<> void fmt_class_string::format(std::string& out, u64 arg) { format_enum(out, arg, [](auto error) { switch (error) { STR_CASE(CELL_HDDGAME_ERROR_CBRESULT); STR_CASE(CELL_HDDGAME_ERROR_ACCESS_ERROR); STR_CASE(CELL_HDDGAME_ERROR_INTERNAL); STR_CASE(CELL_HDDGAME_ERROR_PARAM); STR_CASE(CELL_HDDGAME_ERROR_NOSPACE); STR_CASE(CELL_HDDGAME_ERROR_BROKEN); STR_CASE(CELL_HDDGAME_ERROR_FAILURE); } return unknown; }); } // If dir is empty: // contentInfo = "/dev_bdvd/PS3_GAME" // usrdir = "/dev_bdvd/PS3_GAME/USRDIR" // Temporary content directory (dir is not empty): // contentInfo = "/dev_hdd0/game/_GDATA_" + time_since_epoch // usrdir = "/dev_hdd0/game/_GDATA_" + time_since_epoch + "/USRDIR" // Normal content directory (dir is not empty): // contentInfo = "/dev_hdd0/game/" + dir // usrdir = "/dev_hdd0/game/" + dir + "/USRDIR" struct content_permission final { // Content directory name or path std::string dir; // SFO file psf::registry sfo; // Temporary directory path std::string temp; stx::init_mutex init; enum class check_mode { not_set, game_data, patch, hdd_game, disc_game }; atomic_t can_create = 0; atomic_t exists = false; atomic_t mode = check_mode::not_set; content_permission() = default; content_permission(const content_permission&) = delete; content_permission& operator=(const content_permission&) = delete; void reset() { dir.clear(); sfo.clear(); temp.clear(); can_create = 0; exists = false; mode = check_mode::not_set; } ~content_permission() { bool success = false; fs::g_tls_error = fs::error::ok; if (temp.size() <= 1 || fs::remove_all(temp)) { success = true; } if (!success) { cellGame.fatal("Failed to clean directory '%s' (%s)", temp, fs::g_tls_error); } } }; template<> void fmt_class_string::format(std::string& out, u64 arg) { format_enum(out, arg, [](auto error) { switch (error) { STR_CASE(content_permission::check_mode::not_set); STR_CASE(content_permission::check_mode::game_data); STR_CASE(content_permission::check_mode::patch); STR_CASE(content_permission::check_mode::hdd_game); STR_CASE(content_permission::check_mode::disc_game); } return unknown; }); } template<> void fmt_class_string::format(std::string& out, u64 arg) { format_enum(out, arg, [](auto error) { switch (error) { STR_CASE(disc_change_manager::eject_state::unknown); STR_CASE(disc_change_manager::eject_state::inserted); STR_CASE(disc_change_manager::eject_state::ejected); STR_CASE(disc_change_manager::eject_state::busy); } return unknown; }); } static bool check_system_ver(vm::cptr systemVersion) { // Only allow something like "04.8300". // The disassembly shows that "04.83" would also be considered valid, but the initial strlen check makes this void. return ( systemVersion && std::strlen(systemVersion.get_ptr()) == 7 && std::isdigit(systemVersion[0]) && std::isdigit(systemVersion[1]) && systemVersion[2] == '.' && std::isdigit(systemVersion[3]) && std::isdigit(systemVersion[4]) && std::isdigit(systemVersion[5]) && std::isdigit(systemVersion[6]) ); } disc_change_manager::disc_change_manager() { Emu.GetCallbacks().enable_disc_eject(false); Emu.GetCallbacks().enable_disc_insert(false); } disc_change_manager::~disc_change_manager() { Emu.GetCallbacks().enable_disc_eject(false); Emu.GetCallbacks().enable_disc_insert(false); } error_code disc_change_manager::register_callbacks(vm::ptr func_eject, vm::ptr func_insert) { std::lock_guard lock(mtx); eject_callback = func_eject; insert_callback = func_insert; const bool is_disc_mounted = fs::is_dir(vfs::get("/dev_bdvd/PS3_GAME")); if (state == eject_state::unknown) { state = is_disc_mounted ? eject_state::inserted : eject_state::ejected; } Emu.GetCallbacks().enable_disc_eject(!!func_eject && is_disc_mounted); Emu.GetCallbacks().enable_disc_insert(!!func_insert && !is_disc_mounted); return CELL_OK; } error_code disc_change_manager::unregister_callbacks() { const auto unregister = [this]() -> void { eject_callback = vm::null; insert_callback = vm::null; Emu.GetCallbacks().enable_disc_eject(false); Emu.GetCallbacks().enable_disc_insert(false); }; if (is_inserting) { // NOTE: The insert_callback is known to call cellGameUnregisterDiscChangeCallback. // So we keep it out of the mutex lock until it proves to be an issue. unregister(); } else { std::lock_guard lock(mtx); unregister(); } return CELL_OK; } void disc_change_manager::eject_disc() { cellGame.notice("Ejecting disc..."); std::lock_guard lock(mtx); if (state != eject_state::inserted) { cellGame.fatal("Can not eject disc in the current state. (state=%s)", state.load()); return; } state = eject_state::busy; Emu.GetCallbacks().enable_disc_eject(false); ensure(eject_callback); sysutil_register_cb([](ppu_thread& cb_ppu) -> s32 { auto& dcm = g_fxo->get(); std::lock_guard lock(dcm.mtx); cellGame.notice("Executing eject_callback..."); dcm.eject_callback(cb_ppu); ensure(vfs::unmount("/dev_bdvd")); ensure(vfs::unmount("/dev_ps2disc")); dcm.state = eject_state::ejected; // Re-enable disc insertion only if the callback is still registered Emu.GetCallbacks().enable_disc_insert(!!dcm.insert_callback); return CELL_OK; }); } void disc_change_manager::insert_disc(u32 disc_type, std::string title_id) { cellGame.notice("Inserting disc..."); std::lock_guard lock(mtx); if (state != eject_state::ejected) { cellGame.fatal("Can not insert disc in the current state. (state=%s)", state.load()); return; } state = eject_state::busy; Emu.GetCallbacks().enable_disc_insert(false); ensure(insert_callback); is_inserting = true; sysutil_register_cb([disc_type, title_id = std::move(title_id)](ppu_thread& cb_ppu) -> s32 { auto& dcm = g_fxo->get(); std::lock_guard lock(dcm.mtx); if (disc_type == CELL_GAME_DISCTYPE_PS3) { vm::var _title_id = vm::make_str(title_id); cellGame.notice("Executing insert_callback for title '%s' with disc_type %d...", _title_id.get_ptr(), disc_type); dcm.insert_callback(cb_ppu, disc_type, _title_id); } else { cellGame.notice("Executing insert_callback with disc_type %d...", disc_type); dcm.insert_callback(cb_ppu, disc_type, vm::null); } dcm.state = eject_state::inserted; // Re-enable disc ejection only if the callback is still registered Emu.GetCallbacks().enable_disc_eject(!!dcm.eject_callback); dcm.is_inserting = false; return CELL_OK; }); } extern void lv2_sleep(u64 timeout, ppu_thread* ppu = nullptr) { if (!ppu) { ppu = ensure(cpu_thread::get_current()); } if (!timeout) { return; } const bool had_wait = ppu->state.test_and_set(cpu_flag::wait); lv2_obj::sleep(*ppu); lv2_obj::wait_timeout(timeout); ppu->check_state(); if (had_wait) { ppu->state += cpu_flag::wait; } } error_code cellHddGameCheck(ppu_thread& ppu, u32 version, vm::cptr dirName, u32 errDialog, vm::ptr funcStat, u32 container) { cellGame.warning("cellHddGameCheck(version=%d, dirName=%s, errDialog=%d, funcStat=*0x%x, container=%d)", version, dirName, errDialog, funcStat, container); if (version != CELL_GAMEDATA_VERSION_CURRENT || !dirName || !funcStat || sysutil_check_name_string(dirName.get_ptr(), 1, CELL_GAME_DIRNAME_SIZE) != 0) { return CELL_HDDGAME_ERROR_PARAM; } std::string game_dir = dirName.get_ptr(); // TODO: Find error code ensure(game_dir.size() == 9); const std::string dir = "/dev_hdd0/game/" + game_dir; auto [sfo, psf_error] = psf::load(vfs::get(dir + "/PARAM.SFO")); const u32 new_data = psf_error == psf::error::stream ? CELL_GAMEDATA_ISNEWDATA_YES : CELL_GAMEDATA_ISNEWDATA_NO; if (!new_data) { const auto cat = psf::get_string(sfo, "CATEGORY", ""); if (!psf::is_cat_hdd(cat)) { return { CELL_GAMEDATA_ERROR_BROKEN, "CATEGORY='%s'", cat }; } } const std::string usrdir = dir + "/USRDIR"; auto& get = g_stat_get; auto& set = g_stat_set; auto& result = g_cb_result; std::memset(get.get_ptr(), 0, sizeof(*get)); std::memset(set.get_ptr(), 0, sizeof(*set)); std::memset(result.get_ptr(), 0, sizeof(*result)); const std::string local_dir = vfs::get(dir); // 40 GB - 256 kilobytes. The reasoning is that many games take this number and multiply it by 1024, to get the amount of bytes. With 40GB exactly, // this will result in an overflow, and the size would be 0, preventing the game from running. By reducing 256 kilobytes, we make sure that even // after said overflow, the number would still be high enough to contain the game's data. get->hddFreeSizeKB = 40 * 1024 * 1024 - 256; get->isNewData = CELL_HDDGAME_ISNEWDATA_EXIST; get->sysSizeKB = 0; // TODO get->st_atime_ = 0; // TODO get->st_ctime_ = 0; // TODO get->st_mtime_ = 0; // TODO get->sizeKB = CELL_HDDGAME_SIZEKB_NOTCALC; strcpy_trunc(get->contentInfoPath, dir); strcpy_trunc(get->gameDataPath, usrdir); std::memset(g_file_param.get_ptr(), 0, sizeof(*g_file_param)); set->setParam = g_file_param; if (!fs::is_dir(local_dir)) { get->isNewData = CELL_HDDGAME_ISNEWDATA_NODIR; get->getParam = {}; } else { // TODO: Is cellHddGameCheck really responsible for writing the information in get->getParam ? (If not, delete this else) const psf::registry psf = psf::load_object(local_dir + "/PARAM.SFO"); // Some following fields may be zero in old FW 1.00 version PARAM.SFO if (psf.contains("PARENTAL_LEVEL")) get->getParam.parentalLevel = ::at32(psf, "PARENTAL_LEVEL").as_integer(); if (psf.contains("ATTRIBUTE")) get->getParam.attribute = ::at32(psf, "ATTRIBUTE").as_integer(); if (psf.contains("RESOLUTION")) get->getParam.resolution = ::at32(psf, "RESOLUTION").as_integer(); if (psf.contains("SOUND_FORMAT")) get->getParam.soundFormat = ::at32(psf, "SOUND_FORMAT").as_integer(); if (psf.contains("TITLE")) strcpy_trunc(get->getParam.title, ::at32(psf, "TITLE").as_string()); if (psf.contains("APP_VER")) strcpy_trunc(get->getParam.dataVersion, ::at32(psf, "APP_VER").as_string()); if (psf.contains("TITLE_ID")) strcpy_trunc(get->getParam.titleId, ::at32(psf, "TITLE_ID").as_string()); for (u32 i = 0; i < CELL_HDDGAME_SYSP_LANGUAGE_NUM; i++) { strcpy_trunc(get->getParam.titleLang[i], psf::get_string(psf, fmt::format("TITLE_%02d", i))); } } // TODO ? lv2_sleep(5000, &ppu); funcStat(ppu, result, get, set); std::string error_msg; switch (result->result) { case CELL_HDDGAME_CBRESULT_OK: { // Game confirmed that it wants to create directory const auto setParam = set->setParam; lv2_sleep(2000, &ppu); if (new_data) { if (!setParam) { return CELL_GAMEDATA_ERROR_PARAM; } if (!fs::create_path(vfs::get(usrdir))) { return {CELL_GAME_ERROR_ACCESS_ERROR, usrdir}; } } // Nuked until correctly reversed engineered // if (setParam) // { // if (new_data) // { // psf::assign(sfo, "CATEGORY", psf::string(3, "HG")); // } // psf::assign(sfo, "TITLE_ID", psf::string(TITLEID_SFO_ENTRY_SIZE, setParam->titleId)); // psf::assign(sfo, "TITLE", psf::string(CELL_GAME_SYSP_TITLE_SIZE, setParam->title)); // psf::assign(sfo, "VERSION", psf::string(CELL_GAME_SYSP_VERSION_SIZE, setParam->dataVersion)); // psf::assign(sfo, "PARENTAL_LEVEL", +setParam->parentalLevel); // psf::assign(sfo, "RESOLUTION", +setParam->resolution); // psf::assign(sfo, "SOUND_FORMAT", +setParam->soundFormat); // for (u32 i = 0; i < CELL_HDDGAME_SYSP_LANGUAGE_NUM; i++) // { // if (!setParam->titleLang[i][0]) // { // continue; // } // psf::assign(sfo, fmt::format("TITLE_%02d", i), psf::string(CELL_GAME_SYSP_TITLE_SIZE, setParam->titleLang[i])); // } // psf::save_object(fs::file(vfs::get(dir + "/PARAM.SFO"), fs::rewrite), sfo); // } return CELL_OK; } case CELL_HDDGAME_CBRESULT_OK_CANCEL: cellGame.warning("cellHddGameCheck(): callback returned CELL_HDDGAME_CBRESULT_OK_CANCEL"); return CELL_OK; case CELL_HDDGAME_CBRESULT_ERR_NOSPACE: cellGame.error("cellHddGameCheck(): callback returned CELL_HDDGAME_CBRESULT_ERR_NOSPACE. Space Needed: %d KB", result->errNeedSizeKB); error_msg = get_localized_string(localized_string_id::CELL_HDD_GAME_CHECK_NOSPACE, fmt::format("%d", result->errNeedSizeKB).c_str()); break; case CELL_HDDGAME_CBRESULT_ERR_BROKEN: cellGame.error("cellHddGameCheck(): callback returned CELL_HDDGAME_CBRESULT_ERR_BROKEN"); error_msg = get_localized_string(localized_string_id::CELL_HDD_GAME_CHECK_BROKEN, game_dir.c_str()); break; case CELL_HDDGAME_CBRESULT_ERR_NODATA: cellGame.error("cellHddGameCheck(): callback returned CELL_HDDGAME_CBRESULT_ERR_NODATA"); error_msg = get_localized_string(localized_string_id::CELL_HDD_GAME_CHECK_NODATA, game_dir.c_str()); break; case CELL_HDDGAME_CBRESULT_ERR_INVALID: cellGame.error("cellHddGameCheck(): callback returned CELL_HDDGAME_CBRESULT_ERR_INVALID. Error message: %s", result->invalidMsg); error_msg = get_localized_string(localized_string_id::CELL_HDD_GAME_CHECK_INVALID, fmt::format("%s", result->invalidMsg).c_str()); break; default: cellGame.error("cellHddGameCheck(): callback returned unknown error (code=0x%x). Error message: %s", result->invalidMsg); error_msg = get_localized_string(localized_string_id::CELL_HDD_GAME_CHECK_INVALID, fmt::format("%s", result->invalidMsg).c_str()); break; } if (errDialog == CELL_GAMEDATA_ERRDIALOG_ALWAYS) // Maybe != CELL_GAMEDATA_ERRDIALOG_NONE { // Yield before a blocking dialog is being spawned lv2_obj::sleep(ppu); // Get user confirmation by opening a blocking dialog error_code res = open_msg_dialog(true, CELL_MSGDIALOG_TYPE_SE_TYPE_ERROR | CELL_MSGDIALOG_TYPE_BUTTON_TYPE_OK | CELL_MSGDIALOG_TYPE_DISABLE_CANCEL_ON, vm::make_str(error_msg), msg_dialog_source::_cellGame); // Reschedule after a blocking dialog returns if (ppu.check_state()) { return 0; } if (res != CELL_OK) { return CELL_GAMEDATA_ERROR_INTERNAL; } } else { lv2_sleep(2000, &ppu); } return CELL_HDDGAME_ERROR_CBRESULT; } error_code cellHddGameCheck2(ppu_thread& ppu, u32 version, vm::cptr dirName, u32 errDialog, vm::ptr funcStat, u32 container) { cellGame.trace("cellHddGameCheck2()"); // Identical function return cellHddGameCheck(ppu, version, dirName, errDialog, funcStat, container); } error_code cellHddGameGetSizeKB(ppu_thread& ppu, vm::ptr size) { ppu.state += cpu_flag::wait; cellGame.warning("cellHddGameGetSizeKB(size=*0x%x)", size); if (!size) { return CELL_HDDGAME_ERROR_PARAM; } lv2_obj::sleep(ppu); const u64 start_sleep = ppu.start_time; const std::string local_dir = vfs::get(Emu.GetDir()); const auto dirsz = fs::get_dir_size(local_dir, 1024); // This function is very slow by nature // TODO: Check if after first use the result is being cached so the sleep can be reduced in this case lv2_sleep(utils::sub_saturate(dirsz == umax ? 2000 : 200000, get_guest_system_time() - start_sleep), &ppu); if (dirsz == umax) { const auto error = fs::g_tls_error; if (fs::exists(local_dir)) { cellGame.error("cellHddGameGetSizeKB(): Unknown failure on calculating directory '%s' size (%s)", local_dir, error); } return CELL_HDDGAME_ERROR_FAILURE; } ppu.check_state(); *size = ::narrow(dirsz / 1024); return CELL_OK; } error_code cellHddGameSetSystemVer(vm::cptr systemVersion) { cellGame.todo("cellHddGameSetSystemVer(systemVersion=%s)", systemVersion); if (!check_system_ver(systemVersion)) { return CELL_HDDGAME_ERROR_PARAM; } return CELL_OK; } error_code cellHddGameExitBroken() { cellGame.warning("cellHddGameExitBroken()"); return open_exit_dialog(get_localized_string(localized_string_id::CELL_HDD_GAME_EXIT_BROKEN), true, msg_dialog_source::_cellGame); } error_code cellGameDataGetSizeKB(ppu_thread& ppu, vm::ptr size) { ppu.state += cpu_flag::wait; cellGame.warning("cellGameDataGetSizeKB(size=*0x%x)", size); if (!size) { return CELL_GAMEDATA_ERROR_PARAM; } lv2_obj::sleep(ppu); const u64 start_sleep = ppu.start_time; const std::string local_dir = vfs::get(Emu.GetDir()); const auto dirsz = fs::get_dir_size(local_dir, 1024); // This function is very slow by nature // TODO: Check if after first use the result is being cached so the sleep can be reduced in this case lv2_sleep(utils::sub_saturate(dirsz == umax ? 2000 : 200000, get_guest_system_time() - start_sleep), &ppu); if (dirsz == umax) { const auto error = fs::g_tls_error; if (fs::exists(local_dir)) { cellGame.error("cellGameDataGetSizeKB(): Unknown failure on calculating directory '%s' size (%s)", local_dir, error); } return CELL_GAMEDATA_ERROR_FAILURE; } ppu.check_state(); *size = ::narrow(dirsz / 1024); return CELL_OK; } error_code cellGameDataSetSystemVer(vm::cptr systemVersion) { cellGame.todo("cellGameDataSetSystemVer(systemVersion=%s)", systemVersion); if (!check_system_ver(systemVersion)) { return CELL_GAMEDATA_ERROR_PARAM; } return CELL_OK; } error_code cellGameDataExitBroken() { cellGame.warning("cellGameDataExitBroken()"); return open_exit_dialog(get_localized_string(localized_string_id::CELL_GAME_DATA_EXIT_BROKEN), true, msg_dialog_source::_cellGame); } error_code cellGameBootCheck(vm::ptr type, vm::ptr attributes, vm::ptr size, vm::ptr dirName) { cellGame.warning("cellGameBootCheck(type=*0x%x, attributes=*0x%x, size=*0x%x, dirName=*0x%x)", type, attributes, size, dirName); if (!type || !attributes) { return CELL_GAME_ERROR_PARAM; } auto& perm = g_fxo->get(); lv2_sleep(500); const auto init = acquire_lock(perm.init); if (!init) { return CELL_GAME_ERROR_BUSY; } std::string dir; psf::registry sfo; const std::string& cat = Emu.GetFakeCat(); u32 _type{}; if (cat == "DG") { perm.mode = content_permission::check_mode::disc_game; _type = CELL_GAME_GAMETYPE_DISC; *attributes = 0; // TODO // TODO: dirName might be a read only string when BootCheck is called on a disc game. (e.g. Ben 10 Ultimate Alien: Cosmic Destruction) sfo = psf::load_object(vfs::get("/dev_bdvd/PS3_GAME/PARAM.SFO")); } else if (cat == "GD") { perm.mode = content_permission::check_mode::patch; _type = CELL_GAME_GAMETYPE_DISC; *attributes = CELL_GAME_ATTRIBUTE_PATCH; // TODO sfo = psf::load_object(vfs::get(Emu.GetDir() + "PARAM.SFO")); } else { perm.mode = content_permission::check_mode::hdd_game; _type = CELL_GAME_GAMETYPE_HDD; *attributes = 0; // TODO sfo = psf::load_object(vfs::get(Emu.GetDir() + "PARAM.SFO")); dir = fmt::trim(Emu.GetDir().substr(fs::get_parent_dir_view(Emu.GetDir()).size() + 1), fs::delim); } *type = _type; if (size) { // TODO: Use the free space of the computer's HDD where RPCS3 is being run. size->hddFreeSizeKB = 40 * 1024 * 1024 - 256; // Read explanation in cellHddGameCheck // TODO: Calculate data size for HG and DG games, if necessary. size->sizeKB = CELL_GAME_SIZEKB_NOTCALC; size->sysSizeKB = 4; } if (_type == u32{CELL_GAME_GAMETYPE_HDD} && dirName) { ensure(dir.size() < CELL_GAME_DIRNAME_SIZE); strcpy_trunc(*dirName, dir); } perm.dir = std::move(dir); perm.sfo = std::move(sfo); perm.exists = true; return CELL_OK; } error_code cellGamePatchCheck(vm::ptr size, vm::ptr reserved) { cellGame.warning("cellGamePatchCheck(size=*0x%x, reserved=*0x%x)", size, reserved); lv2_sleep(5000); if (Emu.GetCat() != "GD") { return CELL_GAME_ERROR_NOTPATCH; } psf::registry sfo = psf::load_object(vfs::get(Emu.GetDir() + "PARAM.SFO")); auto& perm = g_fxo->get(); const auto init = acquire_lock(perm.init); if (!init) { return CELL_GAME_ERROR_BUSY; } if (size) { // TODO: Use the free space of the computer's HDD where RPCS3 is being run. size->hddFreeSizeKB = 40 * 1024 * 1024 - 256; // Read explanation in cellHddGameCheck // TODO: Calculate data size for patch data, if necessary. size->sizeKB = CELL_GAME_SIZEKB_NOTCALC; size->sysSizeKB = 0; // TODO } perm.mode = content_permission::check_mode::patch; perm.dir = Emu.GetTitleID(); perm.sfo = std::move(sfo); perm.exists = true; return CELL_OK; } error_code cellGameDataCheck(u32 type, vm::cptr dirName, vm::ptr size) { cellGame.warning("cellGameDataCheck(type=%d, dirName=%s, size=*0x%x)", type, dirName, size); if ((type - 1) >= 3 || (type != CELL_GAME_GAMETYPE_DISC && !dirName)) { return {CELL_GAME_ERROR_PARAM, type}; } std::string name; if (type != CELL_GAME_GAMETYPE_DISC) { name = dirName.get_ptr(); } const std::string dir = type == CELL_GAME_GAMETYPE_DISC ? "/dev_bdvd/PS3_GAME"s : "/dev_hdd0/game/" + name; // TODO: not sure what should be checked there auto& perm = g_fxo->get(); auto init = acquire_lock(perm.init); if (!init) { lv2_sleep(300); return CELL_GAME_ERROR_BUSY; } // This function is incredibly slow, slower for DISC type and even if the game/disc data does not exist // Null size does not change it lv2_sleep(type == CELL_GAME_GAMETYPE_DISC ? 300000 : 120000); auto [sfo, psf_error] = psf::load(vfs::get(dir + "/PARAM.SFO")); if (const std::string_view cat = psf::get_string(sfo, "CATEGORY"); [&]() { switch (type) { case CELL_GAME_GAMETYPE_HDD: return !psf::is_cat_hdd(cat); case CELL_GAME_GAMETYPE_GAMEDATA: return cat != "GD"sv; case CELL_GAME_GAMETYPE_DISC: return cat != "DG"sv; default: fmt::throw_exception("Unreachable"); } }()) { if (psf_error != psf::error::stream) { init.cancel(); return {CELL_GAME_ERROR_BROKEN, "psf::error='%s', type='%d' CATEGORY='%s'", psf_error, type, cat}; } } if (size) { // TODO: Use the free space of the computer's HDD where RPCS3 is being run. size->hddFreeSizeKB = 40 * 1024 * 1024 - 256; // Read explanation in cellHddGameCheck // TODO: Calculate data size for game data, if necessary. size->sizeKB = sfo.empty() ? 0 : CELL_GAME_SIZEKB_NOTCALC; size->sysSizeKB = 0; // TODO } perm.dir = std::move(name); perm.can_create = type == CELL_GAME_GAMETYPE_GAMEDATA; perm.mode = content_permission::check_mode::game_data; if (sfo.empty()) { cellGame.warning("cellGameDataCheck(): directory '%s' not found", dir); return not_an_error(CELL_GAME_RET_NONE); } perm.exists = true; perm.sfo = std::move(sfo); return CELL_OK; } error_code cellGameContentPermit(ppu_thread& ppu, vm::ptr contentInfoPath, vm::ptr usrdirPath) { cellGame.warning("cellGameContentPermit(contentInfoPath=*0x%x, usrdirPath=*0x%x)", contentInfoPath, usrdirPath); if (!contentInfoPath || !usrdirPath) { return CELL_GAME_ERROR_PARAM; } auto& perm = g_fxo->get(); const auto init = acquire_reset_lock(perm.init); if (!init) { return CELL_GAME_ERROR_FAILURE; } const std::string dir = perm.dir.empty() ? "/dev_bdvd/PS3_GAME"s : "/dev_hdd0/game/" + perm.dir; if (perm.temp.empty() && !perm.exists) { perm.reset(); strcpy_trunc(*contentInfoPath, ""); strcpy_trunc(*usrdirPath, ""); return CELL_OK; } lv2_obj::sleep(ppu); const u64 start_sleep = ppu.start_time; if (!perm.temp.empty()) { std::vector> lv2_files; const std::string real_dir = vfs::get(dir) + "/"; std::lock_guard lock(g_mp_sys_dev_hdd0.mutex); // Create PARAM.SFO fs::pending_file temp(perm.temp + "/PARAM.SFO"); temp.file.write(psf::save_object(perm.sfo)); ensure(temp.commit()); idm::select([&](u32 id, lv2_file& file) { if (file.mp != &g_mp_sys_dev_hdd0) { return; } if (real_dir.starts_with(file.real_path)) { if (!file.file) { return; } if (file.flags & CELL_FS_O_ACCMODE) { // Synchronize outside IDM lock scope lv2_files.emplace_back(ensure(idm::get_unlocked(id))); } } }); for (auto& file : lv2_files) { // For atomicity file->file.sync(); } // Make temporary directory persistent (atomically) if (vfs::host::rename(perm.temp, real_dir, &g_mp_sys_dev_hdd0, false, false)) { cellGame.success("cellGameContentPermit(): directory '%s' has been created", dir); // Prevent cleanup perm.temp.clear(); } else { cellGame.error("cellGameContentPermit(): failed to initialize directory '%s' (%s)", dir, fs::g_tls_error); } } else if (perm.can_create) { // Update PARAM.SFO fs::pending_file temp(vfs::get(dir + "/PARAM.SFO")); temp.file.write(psf::save_object(perm.sfo)); ensure(temp.commit()); } // This function is very slow by nature lv2_sleep(utils::sub_saturate(!perm.temp.empty() || perm.can_create ? 200000 : 2000, get_guest_system_time() - start_sleep), &ppu); // Cleanup perm.reset(); strcpy_trunc(*contentInfoPath, dir); strcpy_trunc(*usrdirPath, dir + "/USRDIR"); return CELL_OK; } error_code cellGameDataCheckCreate2(ppu_thread& ppu, u32 version, vm::cptr dirName, u32 errDialog, vm::ptr funcStat, u32 container) { cellGame.success("cellGameDataCheckCreate2(version=0x%x, dirName=%s, errDialog=0x%x, funcStat=*0x%x, container=%d)", version, dirName, errDialog, funcStat, container); //older sdk. it might not care about game type. if (version != CELL_GAMEDATA_VERSION_CURRENT || !funcStat || !dirName || sysutil_check_name_string(dirName.get_ptr(), 1, CELL_GAME_DIRNAME_SIZE) != 0) { return CELL_GAMEDATA_ERROR_PARAM; } const std::string game_dir = dirName.get_ptr(); const std::string dir = "/dev_hdd0/game/"s + game_dir; auto [sfo, psf_error] = psf::load(vfs::get(dir + "/PARAM.SFO")); const u32 new_data = psf_error == psf::error::stream ? CELL_GAMEDATA_ISNEWDATA_YES : CELL_GAMEDATA_ISNEWDATA_NO; if (!new_data) { const auto cat = psf::get_string(sfo, "CATEGORY", ""); if (cat != "GD" && cat != "DG") { return CELL_GAMEDATA_ERROR_BROKEN; } } const std::string usrdir = dir + "/USRDIR"; auto& cbResult = g_cb_result; auto& cbGet = g_stat_get; auto& cbSet = g_stat_set; std::memset(cbGet.get_ptr(), 0, sizeof(*cbGet)); std::memset(cbSet.get_ptr(), 0, sizeof(*cbSet)); std::memset(cbResult.get_ptr(), 0, sizeof(*cbResult)); cbGet->isNewData = new_data; // TODO: Use the free space of the computer's HDD where RPCS3 is being run. cbGet->hddFreeSizeKB = 40 * 1024 * 1024 - 256; // Read explanation in cellHddGameCheck strcpy_trunc(cbGet->contentInfoPath, dir); strcpy_trunc(cbGet->gameDataPath, usrdir); // TODO: set correct time cbGet->st_atime_ = 0; cbGet->st_ctime_ = 0; cbGet->st_mtime_ = 0; // TODO: calculate data size, if necessary cbGet->sizeKB = CELL_GAMEDATA_SIZEKB_NOTCALC; cbGet->sysSizeKB = 0; // TODO cbGet->getParam.attribute = CELL_GAMEDATA_ATTR_NORMAL; cbGet->getParam.parentalLevel = psf::get_integer(sfo, "PARENTAL_LEVEL", 0); strcpy_trunc(cbGet->getParam.dataVersion, psf::get_string(sfo, "APP_VER", psf::get_string(sfo, "VERSION", ""))); // Old games do not have APP_VER key strcpy_trunc(cbGet->getParam.titleId, psf::get_string(sfo, "TITLE_ID", "")); strcpy_trunc(cbGet->getParam.title, psf::get_string(sfo, "TITLE", "")); for (u32 i = 0; i < CELL_HDDGAME_SYSP_LANGUAGE_NUM; i++) { strcpy_trunc(cbGet->getParam.titleLang[i], psf::get_string(sfo, fmt::format("TITLE_%02d", i))); } lv2_sleep(5000, &ppu); funcStat(ppu, cbResult, cbGet, cbSet); std::string error_msg; switch (cbResult->result) { case CELL_GAMEDATA_CBRESULT_OK_CANCEL: { cellGame.warning("cellGameDataCheckCreate2(): callback returned CELL_GAMEDATA_CBRESULT_OK_CANCEL"); return CELL_OK; } case CELL_GAMEDATA_CBRESULT_OK: { // Game confirmed that it wants to create directory const auto setParam = cbSet->setParam; lv2_sleep(2000, &ppu); if (new_data) { if (!setParam) { return CELL_GAMEDATA_ERROR_PARAM; } if (!fs::create_path(vfs::get(usrdir))) { return {CELL_GAME_ERROR_ACCESS_ERROR, usrdir}; } } if (setParam) { if (new_data) { psf::assign(sfo, "CATEGORY", psf::string(3, "GD")); } psf::assign(sfo, "TITLE_ID", psf::string(TITLEID_SFO_ENTRY_SIZE, setParam->titleId, true)); psf::assign(sfo, "TITLE", psf::string(CELL_GAME_SYSP_TITLE_SIZE, setParam->title)); psf::assign(sfo, "VERSION", psf::string(CELL_GAME_SYSP_VERSION_SIZE, setParam->dataVersion)); psf::assign(sfo, "PARENTAL_LEVEL", +setParam->parentalLevel); for (u32 i = 0; i < CELL_HDDGAME_SYSP_LANGUAGE_NUM; i++) { if (!setParam->titleLang[i][0]) { continue; } psf::assign(sfo, fmt::format("TITLE_%02d", i), psf::string(CELL_GAME_SYSP_TITLE_SIZE, setParam->titleLang[i])); } if (!psf::check_registry(sfo)) { // This results in CELL_OK, broken SFO and CELL_GAMEDATA_ERROR_BROKEN on the next load // Avoid creation for now cellGame.error("Broken SFO paramters: %s", sfo); return CELL_OK; } fs::pending_file temp(vfs::get(dir + "/PARAM.SFO")); temp.file.write(psf::save_object(sfo)); ensure(temp.commit()); } return CELL_OK; } case CELL_GAMEDATA_CBRESULT_ERR_NOSPACE: cellGame.error("cellGameDataCheckCreate2(): callback returned CELL_GAMEDATA_CBRESULT_ERR_NOSPACE. Space Needed: %d KB", cbResult->errNeedSizeKB); error_msg = get_localized_string(localized_string_id::CELL_GAMEDATA_CHECK_NOSPACE, fmt::format("%d", cbResult->errNeedSizeKB).c_str()); break; case CELL_GAMEDATA_CBRESULT_ERR_BROKEN: cellGame.error("cellGameDataCheckCreate2(): callback returned CELL_GAMEDATA_CBRESULT_ERR_BROKEN"); error_msg = get_localized_string(localized_string_id::CELL_GAMEDATA_CHECK_BROKEN, game_dir.c_str()); break; case CELL_GAMEDATA_CBRESULT_ERR_NODATA: cellGame.error("cellGameDataCheckCreate2(): callback returned CELL_GAMEDATA_CBRESULT_ERR_NODATA"); error_msg = get_localized_string(localized_string_id::CELL_GAMEDATA_CHECK_NODATA, game_dir.c_str()); break; case CELL_GAMEDATA_CBRESULT_ERR_INVALID: cellGame.error("cellGameDataCheckCreate2(): callback returned CELL_GAMEDATA_CBRESULT_ERR_INVALID. Error message: %s", cbResult->invalidMsg); error_msg = get_localized_string(localized_string_id::CELL_GAMEDATA_CHECK_INVALID, fmt::format("%s", cbResult->invalidMsg).c_str()); break; default: cellGame.error("cellGameDataCheckCreate2(): callback returned unknown error (code=0x%x). Error message: %s", cbResult->invalidMsg); error_msg = get_localized_string(localized_string_id::CELL_GAMEDATA_CHECK_INVALID, fmt::format("%s", cbResult->invalidMsg).c_str()); break; } if (errDialog == CELL_GAMEDATA_ERRDIALOG_ALWAYS) { // Yield before a blocking dialog is being spawned lv2_obj::sleep(ppu); // Get user confirmation by opening a blocking dialog error_code res = open_msg_dialog(true, CELL_MSGDIALOG_TYPE_SE_TYPE_ERROR | CELL_MSGDIALOG_TYPE_BUTTON_TYPE_OK | CELL_MSGDIALOG_TYPE_DISABLE_CANCEL_ON, vm::make_str(error_msg), msg_dialog_source::_cellGame); // Reschedule after a blocking dialog returns if (ppu.check_state()) { return 0; } if (res != CELL_OK) { return CELL_GAMEDATA_ERROR_INTERNAL; } } else { lv2_sleep(2000, &ppu); } return CELL_GAMEDATA_ERROR_CBRESULT; } error_code cellGameDataCheckCreate(ppu_thread& ppu, u32 version, vm::cptr dirName, u32 errDialog, vm::ptr funcStat, u32 container) { cellGame.warning("cellGameDataCheckCreate(version=0x%x, dirName=%s, errDialog=0x%x, funcStat=*0x%x, container=%d)", version, dirName, errDialog, funcStat, container); // TODO: almost identical, the only difference is that this function will always calculate the size of game data return cellGameDataCheckCreate2(ppu, version, dirName, errDialog, funcStat, container); } error_code cellGameCreateGameData(vm::ptr init, vm::ptr tmp_contentInfoPath, vm::ptr tmp_usrdirPath) { cellGame.success("cellGameCreateGameData(init=*0x%x, tmp_contentInfoPath=*0x%x, tmp_usrdirPath=*0x%x)", init, tmp_contentInfoPath, tmp_usrdirPath); if (!init) { return CELL_GAME_ERROR_PARAM; } auto& perm = g_fxo->get(); const auto _init = acquire_access_lock(perm.init); lv2_sleep(2000); if (!_init || perm.dir.empty()) { return CELL_GAME_ERROR_FAILURE; } if (!perm.can_create) { return CELL_GAME_ERROR_NOTSUPPORTED; } if (perm.exists) { return CELL_GAME_ERROR_EXIST; } // Account for for filesystem operations lv2_sleep(50'000); std::string dirname = "_GDATA_" + std::to_string(steady_clock::now().time_since_epoch().count()); std::string tmp_contentInfo = "/dev_hdd0/game/" + dirname; std::string tmp_usrdir = "/dev_hdd0/game/" + dirname + "/USRDIR"; if (!fs::create_dir(vfs::get(tmp_contentInfo))) { cellGame.error("cellGameCreateGameData(): failed to create directory '%s' (%s)", tmp_contentInfo, fs::g_tls_error); return CELL_GAME_ERROR_ACCESS_ERROR; // ??? } // cellGameContentPermit should then move files in non-temporary location and return their non-temporary displacement if (tmp_contentInfoPath) strcpy_trunc(*tmp_contentInfoPath, tmp_contentInfo); if (!fs::create_dir(vfs::get(tmp_usrdir))) { cellGame.error("cellGameCreateGameData(): failed to create directory '%s' (%s)", tmp_usrdir, fs::g_tls_error); return CELL_GAME_ERROR_ACCESS_ERROR; // ??? } if (tmp_usrdirPath) strcpy_trunc(*tmp_usrdirPath, tmp_usrdir); perm.temp = vfs::get(tmp_contentInfo); cellGame.success("cellGameCreateGameData(): temporary directory '%s' has been created", tmp_contentInfo); // Initial PARAM.SFO parameters (overwrite) perm.sfo = { { "CATEGORY", psf::string(3, "GD") }, { "TITLE_ID", psf::string(TITLEID_SFO_ENTRY_SIZE, init->titleId) }, { "TITLE", psf::string(CELL_GAME_SYSP_TITLE_SIZE, init->title) }, { "VERSION", psf::string(CELL_GAME_SYSP_VERSION_SIZE, init->version) }, }; return CELL_OK; } error_code cellGameDeleteGameData(vm::cptr dirName) { cellGame.warning("cellGameDeleteGameData(dirName=%s)", dirName); if (!dirName) { return CELL_GAME_ERROR_PARAM; } const std::string name = dirName.get_ptr(); const std::string dir = vfs::get("/dev_hdd0/game/"s + name); auto& perm = g_fxo->get(); auto remove_gd = [&]() -> error_code { if (Emu.GetCat() == "GD" && Emu.GetDir().substr(Emu.GetDir().find_last_of('/') + 1) == vfs::escape(name)) { // Boot patch cannot delete its own directory return CELL_GAME_ERROR_NOTSUPPORTED; } const auto [sfo, psf_error] = psf::load(dir + "/PARAM.SFO"); if (psf::get_string(sfo, "CATEGORY") != "GD" && psf_error != psf::error::stream) { return {CELL_GAME_ERROR_NOTSUPPORTED, psf_error}; } if (sfo.empty()) { // Nothing to remove return CELL_GAME_ERROR_NOTFOUND; } if (auto id = psf::get_string(sfo, "TITLE_ID"); !id.empty() && id != Emu.GetTitleID()) { cellGame.error("cellGameDeleteGameData(%s): Attempts to delete GameData with TITLE ID which does not match the program's (%s)", id, Emu.GetTitleID()); } // Actually remove game data if (!vfs::host::remove_all(dir, rpcs3::utils::get_hdd0_dir(), &g_mp_sys_dev_hdd0, true)) { return {CELL_GAME_ERROR_ACCESS_ERROR, dir}; } return CELL_OK; }; while (true) { // Obtain exclusive lock and cancel init auto _init = perm.init.init(); if (!_init) { // Or access it if (auto access = acquire_access_lock(perm.init); access) { // Cannot remove it when it is accessed by cellGameDataCheck // If it is HG data then resort to remove_gd for ERROR_BROKEN if (perm.dir == name && perm.can_create) { return CELL_GAME_ERROR_NOTSUPPORTED; } return remove_gd(); } else { // Reacquire lock continue; } } auto err = remove_gd(); _init.cancel(); return err; } } error_code cellGameGetParamInt(s32 id, vm::ptr value) { cellGame.warning("cellGameGetParamInt(id=%d, value=*0x%x)", id, value); if (!value) { return CELL_GAME_ERROR_PARAM; } lv2_sleep(2000); auto& perm = g_fxo->get(); const auto init = acquire_access_lock(perm.init); if (!init) { return CELL_GAME_ERROR_FAILURE; } std::string key; switch(id) { case CELL_GAME_PARAMID_PARENTAL_LEVEL: key = "PARENTAL_LEVEL"; break; case CELL_GAME_PARAMID_RESOLUTION: key = "RESOLUTION"; break; case CELL_GAME_PARAMID_SOUND_FORMAT: key = "SOUND_FORMAT"; break; default: { return CELL_GAME_ERROR_INVALID_ID; } } if (!perm.sfo.count(key)) { // TODO: Check if special values need to be set here cellGame.warning("cellGameGetParamInt(): id=%d was not found", id); } *value = psf::get_integer(perm.sfo, key, 0); return CELL_OK; } // String key flags enum class strkey_flag : u32 { get_game_data, // reading is allowed for game data PARAM.SFO set_game_data, // writing is allowed for game data PARAM.SFO get_other, // reading is allowed for other types of PARAM.SFO //set_other, // writing is allowed for other types of PARAM.SFO (not possible) __bitset_enum_max }; struct string_key_info { public: string_key_info() = default; string_key_info(std::string_view _name, u32 _max_size, bs_t _flags) : name(_name), max_size(_max_size), flags(_flags) {} std::string_view name; u32 max_size = 0; inline bool is_supported(bool is_setter, content_permission::check_mode mode) const { switch (mode) { case content_permission::check_mode::game_data: case content_permission::check_mode::patch: // TODO: it's unclear if patch mode should also support these flags { return !!(flags & (is_setter ? strkey_flag::set_game_data : strkey_flag::get_game_data)); } case content_permission::check_mode::hdd_game: case content_permission::check_mode::disc_game: { return !is_setter && (flags & (strkey_flag::get_other)); } case content_permission::check_mode::not_set: { fmt::throw_exception("This should never happen!"); } } return false; // Fixes some VS warning } private: bs_t flags{}; // allowed operations }; static string_key_info get_param_string_key(s32 id) { switch (id) { case CELL_GAME_PARAMID_TITLE: return string_key_info("TITLE", CELL_GAME_SYSP_TITLE_SIZE, strkey_flag::get_game_data + strkey_flag::get_other + strkey_flag::set_game_data); case CELL_GAME_PARAMID_TITLE_DEFAULT: return string_key_info("TITLE", CELL_GAME_SYSP_TITLE_SIZE, strkey_flag::get_game_data + strkey_flag::get_other + strkey_flag::set_game_data); case CELL_GAME_PARAMID_TITLE_JAPANESE: return string_key_info("TITLE_00", CELL_GAME_SYSP_TITLE_SIZE, strkey_flag::get_game_data + strkey_flag::set_game_data); case CELL_GAME_PARAMID_TITLE_ENGLISH: return string_key_info("TITLE_01", CELL_GAME_SYSP_TITLE_SIZE, strkey_flag::get_game_data + strkey_flag::set_game_data); case CELL_GAME_PARAMID_TITLE_FRENCH: return string_key_info("TITLE_02", CELL_GAME_SYSP_TITLE_SIZE, strkey_flag::get_game_data + strkey_flag::set_game_data); case CELL_GAME_PARAMID_TITLE_SPANISH: return string_key_info("TITLE_03", CELL_GAME_SYSP_TITLE_SIZE, strkey_flag::get_game_data + strkey_flag::set_game_data); case CELL_GAME_PARAMID_TITLE_GERMAN: return string_key_info("TITLE_04", CELL_GAME_SYSP_TITLE_SIZE, strkey_flag::get_game_data + strkey_flag::set_game_data); case CELL_GAME_PARAMID_TITLE_ITALIAN: return string_key_info("TITLE_05", CELL_GAME_SYSP_TITLE_SIZE, strkey_flag::get_game_data + strkey_flag::set_game_data); case CELL_GAME_PARAMID_TITLE_DUTCH: return string_key_info("TITLE_06", CELL_GAME_SYSP_TITLE_SIZE, strkey_flag::get_game_data + strkey_flag::set_game_data); case CELL_GAME_PARAMID_TITLE_PORTUGUESE: return string_key_info("TITLE_07", CELL_GAME_SYSP_TITLE_SIZE, strkey_flag::get_game_data + strkey_flag::set_game_data); case CELL_GAME_PARAMID_TITLE_RUSSIAN: return string_key_info("TITLE_08", CELL_GAME_SYSP_TITLE_SIZE, strkey_flag::get_game_data + strkey_flag::set_game_data); case CELL_GAME_PARAMID_TITLE_KOREAN: return string_key_info("TITLE_09", CELL_GAME_SYSP_TITLE_SIZE, strkey_flag::get_game_data + strkey_flag::set_game_data); case CELL_GAME_PARAMID_TITLE_CHINESE_T: return string_key_info("TITLE_10", CELL_GAME_SYSP_TITLE_SIZE, strkey_flag::get_game_data + strkey_flag::set_game_data); case CELL_GAME_PARAMID_TITLE_CHINESE_S: return string_key_info("TITLE_11", CELL_GAME_SYSP_TITLE_SIZE, strkey_flag::get_game_data + strkey_flag::set_game_data); case CELL_GAME_PARAMID_TITLE_FINNISH: return string_key_info("TITLE_12", CELL_GAME_SYSP_TITLE_SIZE, strkey_flag::get_game_data + strkey_flag::set_game_data); case CELL_GAME_PARAMID_TITLE_SWEDISH: return string_key_info("TITLE_13", CELL_GAME_SYSP_TITLE_SIZE, strkey_flag::get_game_data + strkey_flag::set_game_data); case CELL_GAME_PARAMID_TITLE_DANISH: return string_key_info("TITLE_14", CELL_GAME_SYSP_TITLE_SIZE, strkey_flag::get_game_data + strkey_flag::set_game_data); case CELL_GAME_PARAMID_TITLE_NORWEGIAN: return string_key_info("TITLE_15", CELL_GAME_SYSP_TITLE_SIZE, strkey_flag::get_game_data + strkey_flag::set_game_data); case CELL_GAME_PARAMID_TITLE_POLISH: return string_key_info("TITLE_16", CELL_GAME_SYSP_TITLE_SIZE, strkey_flag::get_game_data + strkey_flag::set_game_data); case CELL_GAME_PARAMID_TITLE_PORTUGUESE_BRAZIL: return string_key_info("TITLE_17", CELL_GAME_SYSP_TITLE_SIZE, strkey_flag::get_game_data + strkey_flag::set_game_data); case CELL_GAME_PARAMID_TITLE_ENGLISH_UK: return string_key_info("TITLE_18", CELL_GAME_SYSP_TITLE_SIZE, strkey_flag::get_game_data + strkey_flag::set_game_data); case CELL_GAME_PARAMID_TITLE_TURKISH: return string_key_info("TITLE_19", CELL_GAME_SYSP_TITLE_SIZE, strkey_flag::get_game_data + strkey_flag::set_game_data); case CELL_GAME_PARAMID_TITLE_ID: return string_key_info("TITLE_ID", CELL_GAME_SYSP_TITLEID_SIZE, strkey_flag::get_game_data + strkey_flag::get_other); case CELL_GAME_PARAMID_VERSION: return string_key_info("VERSION", CELL_GAME_SYSP_VERSION_SIZE, strkey_flag::get_game_data); case CELL_GAME_PARAMID_PS3_SYSTEM_VER: return string_key_info("PS3_SYSTEM_VER", CELL_GAME_SYSP_PS3_SYSTEM_VER_SIZE, {}); // TODO case CELL_GAME_PARAMID_APP_VER: return string_key_info("APP_VER", CELL_GAME_SYSP_APP_VER_SIZE, strkey_flag::get_game_data + strkey_flag::get_other); } return {}; } error_code cellGameGetParamString(s32 id, vm::ptr buf, u32 bufsize) { cellGame.warning("cellGameGetParamString(id=%d, buf=*0x%x, bufsize=%d)", id, buf, bufsize); if (!buf || bufsize == 0) { return CELL_GAME_ERROR_PARAM; } auto& perm = g_fxo->get(); lv2_sleep(2000); const auto init = acquire_access_lock(perm.init); if (!init || perm.mode == content_permission::check_mode::not_set) { return CELL_GAME_ERROR_FAILURE; } const auto key = get_param_string_key(id); if (key.name.empty()) { return CELL_GAME_ERROR_INVALID_ID; } if (!key.is_supported(false, perm.mode)) { // TODO: this error is possibly only returned during debug mode return { CELL_GAME_ERROR_NOTSUPPORTED, "id %d is not supported in the current check mode: %s", id, perm.mode.load() }; } const auto value = psf::get_string(perm.sfo, key.name); if (value.empty() && !perm.sfo.count(std::string(key.name))) { // TODO: Check if special values need to be set here cellGame.warning("cellGameGetParamString(): id=%d was not found", id); } std::span dst(buf.get_ptr(), bufsize); strcpy_trunc(dst, value); return CELL_OK; } error_code cellGameSetParamString(s32 id, vm::cptr buf) { cellGame.warning("cellGameSetParamString(id=%d, buf=*0x%x %s)", id, buf, buf); if (!buf) { return CELL_GAME_ERROR_PARAM; } lv2_sleep(2000); auto& perm = g_fxo->get(); const auto init = acquire_access_lock(perm.init); if (!init || perm.mode == content_permission::check_mode::not_set) { return CELL_GAME_ERROR_FAILURE; } const auto key = get_param_string_key(id); if (key.name.empty()) { return CELL_GAME_ERROR_INVALID_ID; } if (!perm.can_create || !key.is_supported(true, perm.mode)) { return CELL_GAME_ERROR_NOTSUPPORTED; } psf::assign(perm.sfo, key.name, psf::string(key.max_size, buf.get_ptr())); return CELL_OK; } error_code cellGameGetSizeKB(ppu_thread& ppu, vm::ptr size) { cellGame.warning("cellGameGetSizeKB(size=*0x%x)", size); if (!size) { return CELL_GAME_ERROR_PARAM; } // Always reset to 0 at start *size = 0; ppu.state += cpu_flag::wait; auto& perm = g_fxo->get(); const auto init = acquire_access_lock(perm.init); if (!init) { return CELL_GAME_ERROR_FAILURE; } lv2_obj::sleep(ppu); const u64 start_sleep = ppu.start_time; const std::string local_dir = !perm.temp.empty() ? perm.temp : vfs::get("/dev_hdd0/game/" + perm.dir); const auto dirsz = fs::get_dir_size(local_dir, 1024); // This function is very slow by nature // TODO: Check if after first use the result is being cached so the sleep can be reduced in this case lv2_sleep(utils::sub_saturate(dirsz == umax ? 1000 : 200000, get_guest_system_time() - start_sleep), &ppu); if (dirsz == umax) { const auto error = fs::g_tls_error; if (!fs::exists(local_dir)) { return CELL_OK; } else { cellGame.error("cellGameGetSizeKb(): Unknown failure on calculating directory size '%s' (%s)", local_dir, error); return CELL_GAME_ERROR_ACCESS_ERROR; } } ppu.check_state(); *size = ::narrow(dirsz / 1024); return CELL_OK; } error_code cellGameGetDiscContentInfoUpdatePath(vm::ptr updatePath) { cellGame.todo("cellGameGetDiscContentInfoUpdatePath(updatePath=*0x%x)", updatePath); if (!updatePath) { return CELL_GAME_ERROR_PARAM; } return CELL_OK; } error_code cellGameGetLocalWebContentPath(vm::ptr contentPath) { cellGame.todo("cellGameGetLocalWebContentPath(contentPath=*0x%x)", contentPath); if (!contentPath) { return CELL_GAME_ERROR_PARAM; } return CELL_OK; } error_code cellGameContentErrorDialog(s32 type, s32 errNeedSizeKB, vm::cptr dirName) { cellGame.warning("cellGameContentErrorDialog(type=%d, errNeedSizeKB=%d, dirName=%s)", type, errNeedSizeKB, dirName); std::string error_msg; switch (type) { case CELL_GAME_ERRDIALOG_BROKEN_GAMEDATA: // Game data is corrupted. The application will continue. error_msg = get_localized_string(localized_string_id::CELL_GAME_ERROR_BROKEN_GAMEDATA); break; case CELL_GAME_ERRDIALOG_BROKEN_HDDGAME: // HDD boot game is corrupted. The application will continue. error_msg = get_localized_string(localized_string_id::CELL_GAME_ERROR_BROKEN_HDDGAME); break; case CELL_GAME_ERRDIALOG_NOSPACE: // Not enough available space. The application will continue. error_msg = get_localized_string(localized_string_id::CELL_GAME_ERROR_NOSPACE, fmt::format("%d", errNeedSizeKB).c_str()); break; case CELL_GAME_ERRDIALOG_BROKEN_EXIT_GAMEDATA: // Game data is corrupted. The application will be terminated. error_msg = get_localized_string(localized_string_id::CELL_GAME_ERROR_BROKEN_EXIT_GAMEDATA); break; case CELL_GAME_ERRDIALOG_BROKEN_EXIT_HDDGAME: // HDD boot game is corrupted. The application will be terminated. error_msg = get_localized_string(localized_string_id::CELL_GAME_ERROR_BROKEN_EXIT_HDDGAME); break; case CELL_GAME_ERRDIALOG_NOSPACE_EXIT: // Not enough available space. The application will be terminated. error_msg = get_localized_string(localized_string_id::CELL_GAME_ERROR_NOSPACE_EXIT, fmt::format("%d", errNeedSizeKB).c_str()); break; default: return CELL_GAME_ERROR_PARAM; } if (dirName) { if (!memchr(dirName.get_ptr(), '\0', CELL_GAME_DIRNAME_SIZE)) { return CELL_GAME_ERROR_PARAM; } error_msg += '\n'; error_msg += get_localized_string(localized_string_id::CELL_GAME_ERROR_DIR_NAME, fmt::format("%s", dirName).c_str()); } return open_exit_dialog(error_msg, type > CELL_GAME_ERRDIALOG_NOSPACE, msg_dialog_source::_cellGame); } error_code cellGameThemeInstall(vm::cptr usrdirPath, vm::cptr fileName, u32 option) { cellGame.todo("cellGameThemeInstall(usrdirPath=%s, fileName=%s, option=0x%x)", usrdirPath, fileName, option); if (!usrdirPath || !fileName || !memchr(usrdirPath.get_ptr(), '\0', CELL_GAME_PATH_MAX) || option > CELL_GAME_THEME_OPTION_APPLY) { return CELL_GAME_ERROR_PARAM; } const std::string src_path = vfs::get(fmt::format("%s/%s", usrdirPath, fileName)); // Use hash to get a hopefully unique filename std::string hash; if (fs::file theme = fs::file(src_path)) { u32 magic{}; if (src_path.ends_with(".p3t") || !theme.read(magic) || magic != "P3TF"_u32) { return CELL_GAME_ERROR_INVALID_THEME_FILE; } hash = sha256_get_hash(theme.to_string().c_str(), theme.size(), true); } else { return CELL_GAME_ERROR_NOTFOUND; } const std::string dst_path = vfs::get(fmt::format("/dev_hdd0/theme/%s_%s.p3t", Emu.GetTitleID(), hash)); // TODO: this is renamed with some other scheme if (fs::is_file(dst_path)) { cellGame.notice("cellGameThemeInstall: theme already installed: '%s'", dst_path); } else { cellGame.notice("cellGameThemeInstall: copying theme from '%s' to '%s'", src_path, dst_path); if (!fs::copy_file(src_path, dst_path, false)) // TODO: new file is write protected { cellGame.error("cellGameThemeInstall: failed to copy theme from '%s' to '%s' (error=%s)", src_path, dst_path, fs::g_tls_error); return CELL_GAME_ERROR_ACCESS_ERROR; } } if (false && !fs::remove_file(src_path)) // TODO: disabled for now { cellGame.error("cellGameThemeInstall: failed to remove source theme from '%s' (error=%s)", src_path, fs::g_tls_error); } if (option == CELL_GAME_THEME_OPTION_APPLY) { // TODO: apply new theme } return CELL_OK; } error_code cellGameThemeInstallFromBuffer(ppu_thread& ppu, u32 fileSize, u32 bufSize, vm::ptr buf, vm::ptr func, u32 option) { cellGame.todo("cellGameThemeInstallFromBuffer(fileSize=%d, bufSize=%d, buf=*0x%x, func=*0x%x, option=0x%x)", fileSize, bufSize, buf, func, option); if (!buf || !fileSize || (fileSize > bufSize && !func) || bufSize < CELL_GAME_THEMEINSTALL_BUFSIZE_MIN || option > CELL_GAME_THEME_OPTION_APPLY) { return CELL_GAME_ERROR_PARAM; } const std::string hash = sha256_get_hash(reinterpret_cast(buf.get_ptr()), fileSize, true); const std::string dst_path = vfs::get(fmt::format("/dev_hdd0/theme/%s_%s.p3t", Emu.GetTitleID(), hash)); // TODO: this is renamed with some scheme if (fs::file theme = fs::file(dst_path, fs::write_new + fs::isfile)) // TODO: new file is write protected { const u32 magic = *reinterpret_cast(buf.get_ptr()); if (magic != "P3TF"_u32) { return CELL_GAME_ERROR_INVALID_THEME_FILE; } if (func && bufSize < fileSize) { cellGame.notice("cellGameThemeInstallFromBuffer: writing theme with func callback to '%s'", dst_path); for (u32 file_offset = 0; file_offset < fileSize;) { const u32 read_size = std::min(bufSize, fileSize - file_offset); cellGame.notice("cellGameThemeInstallFromBuffer: writing %d bytes at pos %d", read_size, file_offset); if (theme.write(reinterpret_cast(buf.get_ptr()) + file_offset, read_size) != read_size) { cellGame.error("cellGameThemeInstallFromBuffer: failed to write to destination file '%s' (error=%s)", dst_path, fs::g_tls_error); if (fs::g_tls_error == fs::error::nospace) { return CELL_GAME_ERROR_NOSPACE; } return CELL_GAME_ERROR_ACCESS_ERROR; } file_offset += read_size; // Report status with callback cellGame.notice("cellGameThemeInstallFromBuffer: func(fileOffset=%d, readSize=%d, buf=0x%x)", file_offset, read_size, buf); const s32 result = func(ppu, file_offset, read_size, buf); if (result == CELL_GAME_RET_CANCEL) // same as CELL_GAME_CBRESULT_CANCEL { cellGame.notice("cellGameThemeInstallFromBuffer: theme installation was cancelled"); return not_an_error(CELL_GAME_RET_CANCEL); } } } else { cellGame.notice("cellGameThemeInstallFromBuffer: writing theme to '%s'", dst_path); if (theme.write(buf.get_ptr(), fileSize) != fileSize) { cellGame.error("cellGameThemeInstallFromBuffer: failed to write to destination file '%s' (error=%s)", dst_path, fs::g_tls_error); if (fs::g_tls_error == fs::error::nospace) { return CELL_GAME_ERROR_NOSPACE; } return CELL_GAME_ERROR_ACCESS_ERROR; } } } else if (fs::g_tls_error == fs::error::exist) // Do not overwrite files, but continue. { cellGame.notice("cellGameThemeInstallFromBuffer: theme already installed: '%s'", dst_path); } else { cellGame.error("cellGameThemeInstallFromBuffer: failed to open destination file '%s' (error=%s)", dst_path, fs::g_tls_error); return CELL_GAME_ERROR_ACCESS_ERROR; } if (option == CELL_GAME_THEME_OPTION_APPLY) { // TODO: apply new theme } return CELL_OK; } error_code cellDiscGameGetBootDiscInfo(vm::ptr getParam) { cellGame.warning("cellDiscGameGetBootDiscInfo(getParam=*0x%x)", getParam); if (!getParam) { return CELL_DISCGAME_ERROR_PARAM; } // Always sets 0 at first dword write_to_ptr(getParam->titleId, 0); lv2_sleep(2000); // This is also called by non-disc games, see NPUB90029 static const std::string dir = "/dev_bdvd/PS3_GAME"s; if (!fs::is_dir(vfs::get(dir))) { return CELL_DISCGAME_ERROR_NOT_DISCBOOT; } const psf::registry psf = psf::load_object(vfs::get(dir + "/PARAM.SFO")); if (psf.contains("PARENTAL_LEVEL")) getParam->parentalLevel = ::at32(psf, "PARENTAL_LEVEL").as_integer(); if (psf.contains("TITLE_ID")) strcpy_trunc(getParam->titleId, ::at32(psf, "TITLE_ID").as_string()); return CELL_OK; } error_code cellDiscGameRegisterDiscChangeCallback(vm::ptr funcEject, vm::ptr funcInsert) { cellGame.warning("cellDiscGameRegisterDiscChangeCallback(funcEject=*0x%x, funcInsert=*0x%x)", funcEject, funcInsert); return g_fxo->get().register_callbacks(funcEject, funcInsert); } error_code cellDiscGameUnregisterDiscChangeCallback() { cellGame.warning("cellDiscGameUnregisterDiscChangeCallback()"); return g_fxo->get().unregister_callbacks(); } error_code cellGameRegisterDiscChangeCallback(vm::ptr funcEject, vm::ptr funcInsert) { cellGame.warning("cellGameRegisterDiscChangeCallback(funcEject=*0x%x, funcInsert=*0x%x)", funcEject, funcInsert); return g_fxo->get().register_callbacks(funcEject, funcInsert); } error_code cellGameUnregisterDiscChangeCallback() { cellGame.warning("cellGameUnregisterDiscChangeCallback()"); return g_fxo->get().unregister_callbacks(); } void cellSysutil_GameData_init() { REG_FUNC(cellSysutil, cellHddGameCheck); REG_FUNC(cellSysutil, cellHddGameCheck2); REG_FUNC(cellSysutil, cellHddGameGetSizeKB); REG_FUNC(cellSysutil, cellHddGameSetSystemVer); REG_FUNC(cellSysutil, cellHddGameExitBroken); REG_FUNC(cellSysutil, cellGameDataGetSizeKB); REG_FUNC(cellSysutil, cellGameDataSetSystemVer); REG_FUNC(cellSysutil, cellGameDataExitBroken); REG_FUNC(cellSysutil, cellGameDataCheckCreate); REG_FUNC(cellSysutil, cellGameDataCheckCreate2); REG_FUNC(cellSysutil, cellDiscGameGetBootDiscInfo); REG_FUNC(cellSysutil, cellDiscGameRegisterDiscChangeCallback); REG_FUNC(cellSysutil, cellDiscGameUnregisterDiscChangeCallback); REG_FUNC(cellSysutil, cellGameRegisterDiscChangeCallback); REG_FUNC(cellSysutil, cellGameUnregisterDiscChangeCallback); } DECLARE(ppu_module_manager::cellGame)("cellGame", []() { REG_FUNC(cellGame, cellGameBootCheck); REG_FUNC(cellGame, cellGamePatchCheck); REG_FUNC(cellGame, cellGameDataCheck); REG_FUNC(cellGame, cellGameContentPermit); REG_FUNC(cellGame, cellGameCreateGameData); REG_FUNC(cellGame, cellGameDeleteGameData); REG_FUNC(cellGame, cellGameGetParamInt); REG_FUNC(cellGame, cellGameGetParamString); REG_FUNC(cellGame, cellGameSetParamString); REG_FUNC(cellGame, cellGameGetSizeKB); REG_FUNC(cellGame, cellGameGetDiscContentInfoUpdatePath); REG_FUNC(cellGame, cellGameGetLocalWebContentPath); REG_FUNC(cellGame, cellGameContentErrorDialog); REG_FUNC(cellGame, cellGameThemeInstall); REG_FUNC(cellGame, cellGameThemeInstallFromBuffer); REG_VAR(cellGame, g_stat_get).flag(MFF_HIDDEN); REG_VAR(cellGame, g_stat_set).flag(MFF_HIDDEN); REG_VAR(cellGame, g_file_param).flag(MFF_HIDDEN); REG_VAR(cellGame, g_cb_result).flag(MFF_HIDDEN); });