#include #include #include #include #include #include #include "string_format.h" #include "AppConfig.h" #include "../PS2VM_Preferences.h" #include "Log.h" #include "Iop_McServ.h" #include "Iop_PathUtils.h" #include "Iop_Sysmem.h" #include "Iop_SifCmd.h" #include "Iop_SifManPs2.h" #include "IopBios.h" #include "StdStreamUtils.h" #include "StringUtils.h" #include "MIPSAssembler.h" #include "FilesystemUtils.h" using namespace Iop; #define CLUSTER_SIZE 0x400 #define LOG_NAME ("iop_mcserv") #define MODULE_NAME "mcserv" #define MODULE_VERSION 0x101 #define CUSTOM_STARTREADFAST 0x666 #define CUSTOM_PROCEEDREADFAST 0x667 #define CUSTOM_FINISHREADFAST 0x668 #define SEPARATOR_CHAR '/' #define CMD_DELAY_DEFAULT 100000 #define STATE_MEMCARDS_FILE ("iop_mcserv/memcards.xml") #define STATE_MEMCARDS_NODE "Memorycards" #define STATE_MEMCARDS_CARDNODE "Memorycard" #define STATE_MEMCARDS_CARDNODE_PORTATTRIBUTE ("Port") #define STATE_MEMCARDS_CARDNODE_KNOWNATTRIBUTE ("Known") #define MC_FILE_ATTR_FOLDER (MC_FILE_0400 | MC_FILE_ATTR_EXISTS | MC_FILE_ATTR_SUBDIR | MC_FILE_ATTR_READABLE | MC_FILE_ATTR_WRITEABLE | MC_FILE_ATTR_EXECUTABLE) // clang-format off const char* CMcServ::m_mcPathPreference[MAX_PORTS] = { PREF_PS2_MC0_DIRECTORY, PREF_PS2_MC1_DIRECTORY, }; // clang-format on CMcServ::CMcServ(CIopBios& bios, CSifMan& sifMan, CSifCmd& sifCmd, CSysmem& sysMem, uint8* ram) : m_bios(bios) , m_sifMan(sifMan) , m_sifCmd(sifCmd) , m_sysMem(sysMem) , m_ram(ram) { m_moduleDataAddr = m_sysMem.AllocateMemory(sizeof(MODULEDATA), 0, 0); sifMan.RegisterModule(MODULE_ID, this); BuildCustomCode(); SetModuleVersion(1000); } void CMcServ::SetModuleVersion(unsigned int) { //We don't really care about the version here. //SetModuleVersion is called when IOP is reset, make sure we also reset the state of this module. //Calling Init doesn't reset the state of known memory cards. //It's probably only reset when the module is reloaded. for(bool& knownMemoryCard : m_knownMemoryCards) { knownMemoryCard = false; } } const char* CMcServ::GetMcPathPreference(unsigned int port) { return m_mcPathPreference[port]; } std::string CMcServ::EncodeMcName(const std::string& inputName) { std::string result; for(size_t i = 0; i < inputName.size(); i++) { auto inputChar = inputName[i]; if(inputChar == 0) break; //Encode problematic characters (for Windows) if((inputChar == ':') || (inputChar == '\\')) { result += string_format("%%%02X", inputChar); } else { result += inputChar; } } return result; } std::string CMcServ::DecodeMcName(const std::string& inputName) { std::string result; for(size_t i = 0; i < inputName.size(); i++) { auto inputChar = inputName[i]; if(inputChar == '%') { int decodedChar = 0; FRAMEWORK_MAYBE_UNUSED int scanCount = sscanf(inputName.c_str() + i, "%%%02X", &decodedChar); assert(scanCount == 1); result += decodedChar; i += 2; } else { result += inputChar; } } return result; } std::string CMcServ::MakeAbsolutePath(const std::string& inputPath) { auto frags = StringUtils::Split(inputPath, '/', true); std::vector newFrags; for(const auto& frag : frags) { if(frag.empty()) continue; if(frag == ".") { continue; } if(frag == "..") { if(newFrags.empty()) { //Going up too much, don't bother continue; } newFrags.pop_back(); } else { newFrags.push_back(frag); } } auto outputPath = std::string(); for(const auto& frag : newFrags) { if(frag.empty()) continue; outputPath += "/"; outputPath += frag; } return outputPath; } std::string CMcServ::GetId() const { return MODULE_NAME; } std::string CMcServ::GetFunctionName(unsigned int) const { return "unknown"; } void CMcServ::CountTicks(uint32 ticks, CSifMan* sifMan) { auto moduleData = reinterpret_cast(m_ram + m_moduleDataAddr); if(moduleData->pendingCommand == CMD_ID_NONE) return; moduleData->pendingCommandDelay -= std::min(moduleData->pendingCommandDelay, ticks); if(moduleData->pendingCommandDelay == 0) { sifMan->SendCallReply(MODULE_ID, nullptr); moduleData->pendingCommand = CMD_ID_NONE; } } void CMcServ::Invoke(CMIPS& context, unsigned int functionId) { switch(functionId) { case CUSTOM_STARTREADFAST: StartReadFast(context); break; case CUSTOM_PROCEEDREADFAST: ProceedReadFast(context); break; case CUSTOM_FINISHREADFAST: FinishReadFast(context); break; default: CLog::GetInstance().Warn(LOG_NAME, "Unknown module method invoked (%d).\r\n", functionId); break; } } bool CMcServ::Invoke(uint32 method, uint32* args, uint32 argsSize, uint32* ret, uint32 retSize, uint8* ram) { bool isDirectCall = (method & CMD_FLAG_DIRECT) != 0; method &= ~CMD_FLAG_MASK; switch(method) { case CMD_ID_GETINFO: case 0x78: // Many games seem to be sensitive to the delay response of this function: //- Nights Into Dreams (issues 2 Syncs very close to each other, infinite loop if GetInfo is instantenous) //- Melty Blood Actress Again //- Baroque //- Naruto Shippuden: Ultimate Ninja 5 (if GetInfo doesn't return quickly enough, MC thread is killed and game will hang) GetInfo(args, argsSize, ret, retSize, ram); break; case CMD_ID_OPEN: case 0x71: // Operation Winback 2 expects a delay here, otherwise it hangs trying to read or write a save file Open(args, argsSize, ret, retSize, ram); break; case CMD_ID_CLOSE: case 0x72: Close(args, argsSize, ret, retSize, ram); break; case CMD_ID_SEEK: Seek(args, argsSize, ret, retSize, ram); break; case CMD_ID_READ: case 0x73: // Operation Winback 2 expects a delay here, otherwise it hangs trying to read a save file Read(args, argsSize, ret, retSize, ram); break; case CMD_ID_WRITE: case 0x74: // Operation Winback 2 expects a delay here, otherwise it hangs trying to write a save file Write(args, argsSize, ret, retSize, ram); break; case 0x0A: case 0x7A: Flush(args, argsSize, ret, retSize, ram); break; case CMD_ID_CHDIR: ChDir(args, argsSize, ret, retSize, ram); break; case CMD_ID_GETDIR: case 0x76: //Used by homebrew (ex.: ps2infones) GetDir(args, argsSize, ret, retSize, ram); break; case CMD_ID_SETFILEINFO: case 0x7C: SetFileInfo(args, argsSize, ret, retSize, ram); break; case CMD_ID_DELETE: case 0x79: Delete(args, argsSize, ret, retSize, ram); break; case CMD_ID_GETENTSPACE: GetEntSpace(args, argsSize, ret, retSize, ram); break; case CMD_ID_SETTHREADPRIORITY: SetThreadPriority(args, argsSize, ret, retSize, ram); break; case 0x15: GetSlotMax(args, argsSize, ret, retSize, ram); break; case 0x16: return ReadFast(args, argsSize, ret, retSize, ram); break; case 0x1B: WriteFast(args, argsSize, ret, retSize, ram); break; case 0xFE: case 0x70: Init(args, argsSize, ret, retSize, ram); break; default: CLog::GetInstance().Warn(LOG_NAME, "Unknown RPC method invoked (0x%08X).\r\n", method); return true; } if(!isDirectCall) { // Delay all commands a bit (except ReadFast, which has a different mecanism) // Direct calls also don't require SIF RPC replies // Fixes games which receive the rpc response before they are ready to receive them auto moduleData = reinterpret_cast(m_ram + m_moduleDataAddr); assert(moduleData->pendingCommand == CMD_ID_NONE); moduleData->pendingCommand = method; moduleData->pendingCommandDelay = CMD_DELAY_DEFAULT; } return false; } void CMcServ::BuildCustomCode() { auto moduleData = reinterpret_cast(m_ram + m_moduleDataAddr); auto exportTable = reinterpret_cast(moduleData->trampoline); *(exportTable++) = 0x41E00000; *(exportTable++) = 0; *(exportTable++) = MODULE_VERSION; strcpy(reinterpret_cast(exportTable), MODULE_NAME); exportTable += (strlen(MODULE_NAME) + 3) / 4; { CMIPSAssembler assembler(exportTable); uint32 codeBase = (reinterpret_cast(exportTable) - m_ram); m_startReadFastAddr = codeBase + (assembler.GetProgramSize() * 4); assembler.JR(CMIPS::RA); assembler.ADDIU(CMIPS::R0, CMIPS::R0, CUSTOM_STARTREADFAST); m_proceedReadFastAddr = codeBase + (assembler.GetProgramSize() * 4); assembler.JR(CMIPS::RA); assembler.ADDIU(CMIPS::R0, CMIPS::R0, CUSTOM_PROCEEDREADFAST); m_finishReadFastAddr = codeBase + (assembler.GetProgramSize() * 4); assembler.JR(CMIPS::RA); assembler.ADDIU(CMIPS::R0, CMIPS::R0, CUSTOM_FINISHREADFAST); m_readFastAddr = codeBase + AssembleReadFast(assembler); exportTable += assembler.GetProgramSize(); } assert((reinterpret_cast(exportTable) - moduleData->trampoline) <= MODULEDATA::TRAMPOLINE_SIZE); } uint32 CMcServ::AssembleReadFast(CMIPSAssembler& assembler) { //Extra stack alloc for SifCallRpc static const int16 stackAlloc = 0x100; uint32 result = assembler.GetProgramSize() * 4; auto readNextLabel = assembler.CreateLabel(); assembler.ADDIU(CMIPS::SP, CMIPS::SP, -stackAlloc); assembler.SW(CMIPS::RA, 0xFC, CMIPS::SP); assembler.SW(CMIPS::S0, 0xF8, CMIPS::SP); assembler.LI(CMIPS::S0, m_moduleDataAddr); assembler.JAL(m_startReadFastAddr); assembler.NOP(); assembler.MarkLabel(readNextLabel); assembler.JAL(m_proceedReadFastAddr); assembler.NOP(); assembler.LW(CMIPS::A0, offsetof(MODULEDATA, readFastSize), CMIPS::S0); assembler.BNE(CMIPS::A0, CMIPS::R0, readNextLabel); assembler.NOP(); assembler.JAL(m_finishReadFastAddr); assembler.NOP(); assembler.LW(CMIPS::S0, 0xF8, CMIPS::SP); assembler.LW(CMIPS::RA, 0xFC, CMIPS::SP); assembler.JR(CMIPS::RA); assembler.ADDIU(CMIPS::SP, CMIPS::SP, stackAlloc); return result; } void CMcServ::GetInfo(uint32* args, uint32 argsSize, uint32* ret, uint32 retSize, uint8* ram) { assert(argsSize >= 0x1C); //The layout of this can actually vary according to the version of the //MCSERV module currently loaded uint32 port = args[1]; uint32 slot = args[2]; bool wantFormatted = args[3] != 0; bool wantFreeSpace = args[4] != 0; bool wantType = args[5] != 0; uint32* retBuffer = reinterpret_cast(&ram[args[7]]); CLog::GetInstance().Print(LOG_NAME, "GetInfo(port = %i, slot = %i, wantType = %i, wantFreeSpace = %i, wantFormatted = %i, retBuffer = 0x%08X);\r\n", port, slot, wantType, wantFreeSpace, wantFormatted, args[7]); if(HandleInvalidPortOrSlot(port, slot, ret)) { return; } if(wantType) { retBuffer[0x00] = 2; //2 -> PS2 memory card } if(wantFreeSpace) { retBuffer[0x01] = 0x2000; //Number of clusters, cluster size = 1024 bytes } if(wantFormatted) { retBuffer[0x24] = 1; } if(port >= MAX_PORTS) { assert(0); ret[0] = -2; return; } bool isKnownCard = m_knownMemoryCards[port]; m_knownMemoryCards[port] = true; //Return values // 0 if same card as previous call // -1 if new formatted card // -2 if new unformatted card //> -2 on error ret[0] = isKnownCard ? 0 : -1; } void CMcServ::Open(uint32* args, uint32 argsSize, uint32* ret, uint32 retSize, uint8* ram) { assert(argsSize >= 0x414); CMD* cmd = reinterpret_cast(args); CLog::GetInstance().Print(LOG_NAME, "Open(port = %i, slot = %i, flags = %i, name = '%s');\r\n", cmd->port, cmd->slot, cmd->flags, cmd->name); if(HandleInvalidPortOrSlot(cmd->port, cmd->slot, ret)) { return; } auto name = EncodeMcName(cmd->name); fs::path filePath; try { filePath = GetHostFilePath(cmd->port, cmd->slot, name.c_str()); } catch(const std::exception& exception) { CLog::GetInstance().Warn(LOG_NAME, "Error while executing Open: %s.\r\n", exception.what()); ret[0] = -1; return; } if(cmd->flags == 0x40) { //Directory only? uint32 result = -1; try { if(fs::exists(filePath)) { result = RET_NO_ENTRY; } else { fs::create_directory(filePath); result = 0; } } catch(...) { } ret[0] = result; return; } else { if(cmd->flags & OPEN_FLAG_CREAT) { if(!fs::exists(filePath)) { //Create file if it doesn't exist try { Framework::CreateOutputStdStream(filePath.native()); } catch(...) { //Might fail in some conditions (ex.: if file is to be created in a directory that doesn't exist). ret[0] = RET_NO_ENTRY; return; } } } if(cmd->flags & OPEN_FLAG_TRUNC) { if(fs::exists(filePath)) { //Create file (discard contents) if it exists Framework::CreateOutputStdStream(filePath.native()); } } //At this point, we assume that the file has been created or truncated try { auto file = Framework::CreateUpdateExistingStdStream(filePath.native()); uint32 handle = GenerateHandle(); if(handle == -1) { //Exhausted all file handles throw std::exception(); } m_files[handle] = std::move(file); ret[0] = handle; } catch(...) { //Not existing file? ret[0] = RET_NO_ENTRY; return; } } } void CMcServ::Close(uint32* args, uint32 argsSize, uint32* ret, uint32 retSize, uint8* ram) { auto cmd = reinterpret_cast(args); CLog::GetInstance().Print(LOG_NAME, "Close(handle = %i);\r\n", cmd->handle); auto file = GetFileFromHandle(cmd->handle); if(file == nullptr) { ret[0] = -1; assert(0); return; } file->Clear(); ret[0] = 0; } void CMcServ::Seek(uint32* args, uint32 argsSize, uint32* ret, uint32 retSize, uint8* ram) { FILECMD* cmd = reinterpret_cast(args); CLog::GetInstance().Print(LOG_NAME, "Seek(handle = %i, offset = 0x%08X, origin = 0x%08X);\r\n", cmd->handle, cmd->offset, cmd->origin); auto file = GetFileFromHandle(cmd->handle); if(file == nullptr) { ret[0] = -1; assert(0); return; } Framework::STREAM_SEEK_DIRECTION origin = Framework::STREAM_SEEK_SET; switch(cmd->origin) { case 0: origin = Framework::STREAM_SEEK_SET; break; case 1: origin = Framework::STREAM_SEEK_CUR; break; case 2: origin = Framework::STREAM_SEEK_END; break; default: assert(0); break; } file->Seek(static_cast(cmd->offset), origin); ret[0] = static_cast(file->Tell()); } void CMcServ::Read(uint32* args, uint32 argsSize, uint32* ret, uint32 retSize, uint8* ram) { FILECMD* cmd = reinterpret_cast(args); CLog::GetInstance().Print(LOG_NAME, "Read(handle = %i, size = 0x%08X, bufferAddress = 0x%08X, paramAddress = 0x%08X);\r\n", cmd->handle, cmd->size, cmd->bufferAddress, cmd->paramAddress); if(cmd->paramAddress != 0) { //This param buffer is used in the callback after calling this method. //It seems to be used to ferry some data to reduce the number of transfers between IOP and EE. reinterpret_cast(&ram[cmd->paramAddress])[0] = 0; reinterpret_cast(&ram[cmd->paramAddress])[1] = 0; } auto file = GetFileFromHandle(cmd->handle); if(!file) { CLog::GetInstance().Warn(LOG_NAME, "Warning. Attempted to read from an invalid fd (%d).\r\n", cmd->handle); ret[0] = RET_PERMISSION_DENIED; return; } assert(cmd->bufferAddress != 0); void* dst = &ram[cmd->bufferAddress]; if(file->IsEOF()) { ret[0] = 0; } else { ret[0] = static_cast(file->Read(dst, cmd->size)); //Sync pointer for games that write after read without seeking or flushing file->Seek(file->Tell(), Framework::STREAM_SEEK_SET); } } void CMcServ::Write(uint32* args, uint32 argsSize, uint32* ret, uint32 retSize, uint8* ram) { FILECMD* cmd = reinterpret_cast(args); CLog::GetInstance().Print(LOG_NAME, "Write(handle = %i, nSize = 0x%08X, bufferAddress = 0x%08X, origin = 0x%08X);\r\n", cmd->handle, cmd->size, cmd->bufferAddress, cmd->origin); auto file = GetFileFromHandle(cmd->handle); if(file == nullptr) { ret[0] = RET_PERMISSION_DENIED; assert(0); return; } const void* dst = &ram[cmd->bufferAddress]; uint32 result = 0; //Write "origin" bytes from "data" field first if(cmd->origin != 0) { file->Write(cmd->data, cmd->origin); result += cmd->origin; } result += static_cast(file->Write(dst, cmd->size)); ret[0] = result; //Force flushing for games that read after write without seeking or flushing file->Flush(); } void CMcServ::Flush(uint32* args, uint32 argsSize, uint32* ret, uint32 retSize, uint8* ram) { FILECMD* cmd = reinterpret_cast(args); CLog::GetInstance().Print(LOG_NAME, "Flush(handle = %d);\r\n", cmd->handle); auto file = GetFileFromHandle(cmd->handle); if(file == nullptr) { ret[0] = -1; assert(0); return; } file->Flush(); ret[0] = 0; } void CMcServ::ChDir(uint32* args, uint32 argsSize, uint32* ret, uint32 retSize, uint8* ram) { assert(argsSize >= 0x414); CMD* cmd = reinterpret_cast(args); CLog::GetInstance().Print(LOG_NAME, "ChDir(port = %i, slot = %i, tableAddress = 0x%08X, name = '%s');\r\n", cmd->port, cmd->slot, cmd->tableAddress, cmd->name); if(HandleInvalidPortOrSlot(cmd->port, cmd->slot, ret)) { return; } uint32 result = -1; auto& currentDirectory = m_currentDirectory[cmd->port]; //Write out current directory if(cmd->tableAddress != 0) { //Make sure we return '/' even if the current directory is empty, needed by Silent Hill 3 auto curDir = currentDirectory.empty() ? std::string(1, SEPARATOR_CHAR) : currentDirectory; const size_t maxCurDirSize = 256; char* currentDirOut = reinterpret_cast(ram + cmd->tableAddress); strncpy(currentDirOut, curDir.c_str(), maxCurDirSize - 1); } try { std::string newCurrentDirectory; std::string requestedDirectory = EncodeMcName(cmd->name); if(!requestedDirectory.empty() && (requestedDirectory[0] == SEPARATOR_CHAR)) { if(requestedDirectory.length() != 1) { newCurrentDirectory = requestedDirectory; } else { //Clear if only separator char newCurrentDirectory.clear(); } } else { newCurrentDirectory = currentDirectory + SEPARATOR_CHAR + requestedDirectory; } //Some games (EA games) will try to ChDir('..') from the MC's root //Kim Possible: What's the Switch will also attempt this and rely on the result //to consider other MC operations to be successes. newCurrentDirectory = MakeAbsolutePath(newCurrentDirectory); auto mcPath = CAppConfig::GetInstance().GetPreferencePath(m_mcPathPreference[cmd->port]); auto hostPath = Iop::PathUtils::MakeHostPath(mcPath, newCurrentDirectory.c_str()); if(!Iop::PathUtils::IsInsideBasePath(mcPath, hostPath)) { //This shouldn't happen, but fail just in case. assert(false); result = RET_NO_ENTRY; } else if(fs::exists(hostPath) && fs::is_directory(hostPath)) { currentDirectory = newCurrentDirectory; result = 0; } else { //Not found (I guess) result = RET_NO_ENTRY; } } catch(const std::exception& exception) { CLog::GetInstance().Warn(LOG_NAME, "Error while executing ChDir: %s.\r\n", exception.what()); } ret[0] = result; } void CMcServ::GetDir(uint32* args, uint32 argsSize, uint32* ret, uint32 retSize, uint8* ram) { uint32 result = 0; assert(argsSize >= 0x414); auto cmd = reinterpret_cast(args); CLog::GetInstance().Print(LOG_NAME, "GetDir(port = %i, slot = %i, flags = %i, maxEntries = %i, tableAddress = 0x%08X, name = '%s');\r\n", cmd->port, cmd->slot, cmd->flags, cmd->maxEntries, cmd->tableAddress, cmd->name); if(HandleInvalidPortOrSlot(cmd->port, cmd->slot, ret)) { return; } try { if(cmd->flags == 0) { m_pathFinder.Reset(); auto mcPath = CAppConfig::GetInstance().GetPreferencePath(m_mcPathPreference[cmd->port]); if(cmd->name[0] != SEPARATOR_CHAR) { mcPath = Iop::PathUtils::MakeHostPath(mcPath, m_currentDirectory[cmd->port].c_str()); } mcPath = fs::absolute(mcPath); if(!fs::exists(mcPath)) { //Directory doesn't exist ret[0] = RET_NO_ENTRY; return; } auto searchPath = Iop::PathUtils::MakeHostPath(mcPath, cmd->name); searchPath.remove_filename(); if(!fs::exists(searchPath)) { //Specified directory doesn't exist, this is an error ret[0] = RET_NO_ENTRY; return; } assert(*mcPath.string().rbegin() != '/'); m_pathFinder.Search(mcPath, cmd->name); } auto entries = (cmd->maxEntries > 0) ? reinterpret_cast(&ram[cmd->tableAddress]) : nullptr; result = m_pathFinder.Read(entries, cmd->maxEntries); } catch(const std::exception& exception) { CLog::GetInstance().Warn(LOG_NAME, "Error while executing GetDir: %s.\r\n", exception.what()); } ret[0] = result; } void CMcServ::SetFileInfo(uint32* args, uint32 argsSize, uint32* ret, uint32 retSize, uint8* ram) { auto cmd = reinterpret_cast(args); CLog::GetInstance().Print(LOG_NAME, "SetFileInfo(port = %i, slot = %i, flags = %i, name = '%s');\r\n", cmd->port, cmd->slot, cmd->flags, cmd->name); if(HandleInvalidPortOrSlot(cmd->port, cmd->slot, ret)) { return; } auto entry = reinterpret_cast(ram + cmd->tableAddress); auto flags = cmd->flags; if(flags & MC_FILE_ATTR_FILE) { auto filePath1 = GetHostFilePath(cmd->port, cmd->slot, cmd->name); auto filePath2 = GetHostFilePath(cmd->port, cmd->slot, cmd->name); filePath2.replace_filename(reinterpret_cast(entry->name)); if(filePath1 != filePath2) { try { if(!fs::exists(filePath1)) { ret[0] = RET_NO_ENTRY; return; } fs::rename(filePath1, filePath2); } catch(...) { ret[0] = -1; return; } } } flags &= ~MC_FILE_ATTR_FILE; if(flags != 0) { // TODO: We only support file renaming for the moment CLog::GetInstance().Warn(LOG_NAME, "Setting unknown file attribute flag %i\r\n", cmd->flags); } ret[0] = 0; } void CMcServ::Delete(uint32* args, uint32 argsSize, uint32* ret, uint32 retSize, uint8* ram) { auto cmd = reinterpret_cast(args); CLog::GetInstance().Print(LOG_NAME, "Delete(port = %d, slot = %d, name = '%s');\r\n", cmd->port, cmd->slot, cmd->name); if(HandleInvalidPortOrSlot(cmd->port, cmd->slot, ret)) { return; } try { auto filePath = GetHostFilePath(cmd->port, cmd->slot, cmd->name); if(fs::exists(filePath)) { fs::remove(filePath); ret[0] = 0; } else { ret[0] = RET_NO_ENTRY; } } catch(const fs::filesystem_error& exception) { CLog::GetInstance().Warn(LOG_NAME, "Error while executing Delete: %s.\r\n", exception.what()); auto errorCode = exception.code(); if(errorCode == std::errc::directory_not_empty) { //Musashi Samurai Legends will try to delete a directory when overwriting a save ret[0] = RET_NOT_EMPTY; } else { ret[0] = -1; } } catch(const std::exception& exception) { CLog::GetInstance().Warn(LOG_NAME, "Error while executing Delete: %s.\r\n", exception.what()); ret[0] = -1; } } void CMcServ::GetEntSpace(uint32* args, uint32 argsSize, uint32* ret, uint32 retSize, uint8* ram) { auto cmd = reinterpret_cast(args); CLog::GetInstance().Print(LOG_NAME, "GetEntSpace(port = %i, slot = %i, flags = %i, name = '%s');\r\n", cmd->port, cmd->slot, cmd->flags, cmd->name); if(HandleInvalidPortOrSlot(cmd->port, cmd->slot, ret)) { return; } auto mcPath = CAppConfig::GetInstance().GetPreferencePath(m_mcPathPreference[cmd->port]); auto savePath = Iop::PathUtils::MakeHostPath(mcPath, cmd->name); try { if(fs::exists(savePath) && fs::is_directory(savePath)) { // Arbitrarity number, allows Drakengard to detect MC ret[0] = 0xFE; } else { ret[0] = RET_NO_ENTRY; } } catch(const std::exception& exception) { CLog::GetInstance().Warn(LOG_NAME, "Error while executing GetEntSpace: %s.\r\n", exception.what()); ret[0] = -1; } } void CMcServ::SetThreadPriority(uint32* args, uint32 argsSize, uint32* ret, uint32 retSize, uint8* ram) { auto cmd = reinterpret_cast(args); uint32 priority = *reinterpret_cast(cmd->name); CLog::GetInstance().Print(LOG_NAME, "SetThreadPriority(priority = %d);\r\n", priority); //We don't really care about thread priority here, just report a success ret[0] = 0; } void CMcServ::GetSlotMax(uint32* args, uint32 argsSize, uint32* ret, uint32 retSize, uint8* ram) { int port = args[1]; CLog::GetInstance().Print(LOG_NAME, "GetSlotMax(port = %i);\r\n", port); ret[0] = MAX_SLOTS; } bool CMcServ::ReadFast(uint32* args, uint32 argsSize, uint32* ret, uint32 retSize, uint8* ram) { //Based on mcemu code: https://github.com/ifcaro/Open-PS2-Loader/blob/master/modules/mcemu/mcemu_rpc.c auto cmd = reinterpret_cast(args); CLog::GetInstance().Print(LOG_NAME, "ReadFast(handle = %d, size = 0x%08X, bufferAddress = 0x%08X, paramAddress = 0x%08X);\r\n", cmd->handle, cmd->size, cmd->bufferAddress, cmd->paramAddress); auto file = GetFileFromHandle(cmd->handle); if(file == nullptr) { ret[0] = -1; return true; } //Returns the amount of bytes read assert(cmd->size >= file->GetRemainingLength()); ret[0] = cmd->size; auto moduleData = reinterpret_cast(m_ram + m_moduleDataAddr); moduleData->readFastHandle = cmd->handle; moduleData->readFastSize = cmd->size; moduleData->readFastBufferAddress = cmd->bufferAddress; m_bios.TriggerCallback(m_readFastAddr); return false; } void CMcServ::WriteFast(uint32* args, uint32 argsSize, uint32* ret, uint32 retSize, uint8* ram) { FILECMD* cmd = reinterpret_cast(args); CLog::GetInstance().Print(LOG_NAME, "WriteFast(handle = %d, size = 0x%08X, bufferAddress = 0x%08X, paramAddress = 0x%08X);\r\n", cmd->handle, cmd->size, cmd->bufferAddress, cmd->paramAddress); auto file = GetFileFromHandle(cmd->handle); if(file == nullptr) { ret[0] = RET_PERMISSION_DENIED; return; } const void* dst = &ram[cmd->bufferAddress]; uint32 result = 0; result += static_cast(file->Write(dst, cmd->size)); ret[0] = result; } void CMcServ::Init(uint32* args, uint32 argsSize, uint32* ret, uint32 retSize, uint8* ram) { assert(argsSize == 0x30); assert(retSize == 0x0C); ret[0] = 0x00000000; ret[1] = 0x0000020A; //mcserv version ret[2] = 0x0000020E; //mcman version CLog::GetInstance().Print(LOG_NAME, "Init();\r\n"); } bool CMcServ::HandleInvalidPortOrSlot(uint32 port, uint32 slot, uint32* ret) { if(port >= MAX_PORTS) { CLog::GetInstance().Warn(LOG_NAME, "Called mc function with invalid port %d\r\n", port); ret[0] = -1; return true; } if(slot >= MAX_SLOTS) { //Just warn if an invalid slot is specified. Should not be a big deal since we never use that parameter //in our functions. It shouldn't also matter since we don't support multitap. //Note that some games specify 'slot = 1' while never checking GetSlotMax: //- Dragon Quest 8 CLog::GetInstance().Warn(LOG_NAME, "Called mc function with invalid slot %d\r\n", slot); } return false; } void CMcServ::StartReadFast(CMIPS& context) { auto moduleData = reinterpret_cast(m_ram + m_moduleDataAddr); if(!moduleData->initialized) { context.m_State.nGPR[CMIPS::A0].nV0 = m_moduleDataAddr + offsetof(MODULEDATA, rpcClientData); context.m_State.nGPR[CMIPS::A1].nV0 = MODULE_ID; context.m_State.nGPR[CMIPS::A2].nV0 = 0; //Wait mode m_sifCmd.SifBindRpc(context); moduleData->initialized = true; } } void CMcServ::ProceedReadFast(CMIPS& context) { auto moduleData = reinterpret_cast(m_ram + m_moduleDataAddr); auto file = GetFileFromHandle(moduleData->readFastHandle); assert(file); uint32 readSize = std::min(moduleData->readFastSize, CLUSTER_SIZE); uint8 cluster[CLUSTER_SIZE]; FRAMEWORK_MAYBE_UNUSED uint32 amountRead = file->Read(cluster, readSize); assert(amountRead == readSize); moduleData->readFastSize -= readSize; if(auto sifManPs2 = dynamic_cast(&m_sifMan)) { auto eeRam = sifManPs2->GetEeRam(); memcpy(eeRam + moduleData->readFastBufferAddress, cluster, readSize); } reinterpret_cast(moduleData->rpcBuffer)[3] = readSize; context.m_State.nGPR[CMIPS::A0].nV0 = m_moduleDataAddr + offsetof(MODULEDATA, rpcClientData); context.m_State.nGPR[CMIPS::A1].nV0 = 2; context.m_State.nGPR[CMIPS::A2].nV0 = 0; context.m_State.nGPR[CMIPS::A3].nV0 = m_moduleDataAddr + offsetof(MODULEDATA, rpcBuffer); context.m_pMemoryMap->SetWord(context.m_State.nGPR[CMIPS::SP].nV0 + 0x10, MODULEDATA::RPC_BUFFER_SIZE); context.m_pMemoryMap->SetWord(context.m_State.nGPR[CMIPS::SP].nV0 + 0x14, m_moduleDataAddr + offsetof(MODULEDATA, rpcBuffer)); context.m_pMemoryMap->SetWord(context.m_State.nGPR[CMIPS::SP].nV0 + 0x18, MODULEDATA::RPC_BUFFER_SIZE); context.m_pMemoryMap->SetWord(context.m_State.nGPR[CMIPS::SP].nV0 + 0x1C, 0); context.m_pMemoryMap->SetWord(context.m_State.nGPR[CMIPS::SP].nV0 + 0x20, 0); m_sifCmd.SifCallRpc(context); } void CMcServ::FinishReadFast(CMIPS& context) { m_sifMan.SendCallReply(MODULE_ID, nullptr); } uint32 CMcServ::GenerateHandle() { for(unsigned int i = 0; i < MAX_FILES; i++) { if(m_files[i].IsEmpty()) return i; } return -1; } Framework::CStdStream* CMcServ::GetFileFromHandle(uint32 handle) { assert(handle < MAX_FILES); if(handle >= MAX_FILES) { return nullptr; } auto& file = m_files[handle]; if(file.IsEmpty()) { return nullptr; } return &file; } fs::path CMcServ::GetHostFilePath(unsigned int port, unsigned int slot, const char* path) const { auto mcPath = CAppConfig::GetInstance().GetPreferencePath(m_mcPathPreference[port]); auto nameLength = strlen(path); if(nameLength == 0) return mcPath; std::string guestPath; if(path[0] == SEPARATOR_CHAR) { guestPath = path; } else { const auto& currentDirectory = m_currentDirectory[port]; guestPath = currentDirectory + SEPARATOR_CHAR + std::string(path); } guestPath = MakeAbsolutePath(guestPath); return Iop::PathUtils::MakeHostPath(mcPath, guestPath.c_str()); } void CMcServ::LoadState(Framework::CZipArchiveReader& archive) { auto stateFile = CXmlStateFile(*archive.BeginReadFile(STATE_MEMCARDS_FILE)); auto stateNode = stateFile.GetRoot(); auto cardNodes = stateNode->SelectNodes(STATE_MEMCARDS_NODE "/" STATE_MEMCARDS_CARDNODE); int i = 0; for(auto fileNode : cardNodes) { Framework::Xml::GetAttributeIntValue(fileNode, STATE_MEMCARDS_CARDNODE_PORTATTRIBUTE, &i); Framework::Xml::GetAttributeBoolValue(fileNode, STATE_MEMCARDS_CARDNODE_KNOWNATTRIBUTE, &m_knownMemoryCards[i]); } } void CMcServ::SaveState(Framework::CZipArchiveWriter& archive) const { auto stateFile = std::make_unique(STATE_MEMCARDS_FILE, STATE_MEMCARDS_NODE); auto stateNode = stateFile->GetRoot(); for(unsigned int i = 0; i < MAX_PORTS; i++) { auto cardNode = std::make_unique(STATE_MEMCARDS_CARDNODE, true); cardNode->InsertAttribute(Framework::Xml::CreateAttributeIntValue(STATE_MEMCARDS_CARDNODE_PORTATTRIBUTE, i)); cardNode->InsertAttribute(Framework::Xml::CreateAttributeBoolValue(STATE_MEMCARDS_CARDNODE_KNOWNATTRIBUTE, m_knownMemoryCards[i])); stateNode->InsertNode(std::move(cardNode)); } archive.InsertFile(std::move(stateFile)); } ///////////////////////////////////////////// //CPathFinder Implementation ///////////////////////////////////////////// CMcServ::CPathFinder::CPathFinder() : m_index(0) { } CMcServ::CPathFinder::~CPathFinder() { } void CMcServ::CPathFinder::Reset() { m_entries.clear(); m_index = 0; } void CMcServ::CPathFinder::Search(const fs::path& basePath, const char* filter) { m_basePath = basePath; std::string filterPathString = filter; //Resolve relative paths (only when filter starts with one) if(filterPathString.find("./") == 0) { filterPathString = filterPathString.substr(1); } if(filterPathString[0] != '/') { filterPathString = "/" + filterPathString; } //Remove slash at end if((filterPathString.size() > 1) && (*filterPathString.rbegin() == '/')) { filterPathString.erase(filterPathString.end() - 1); } { std::string filterExpString = filterPathString; filterExpString = StringUtils::ReplaceAll(filterExpString, "\\", "\\\\"); filterExpString = StringUtils::ReplaceAll(filterExpString, ".", "\\."); filterExpString = StringUtils::ReplaceAll(filterExpString, "?", ".?"); filterExpString = StringUtils::ReplaceAll(filterExpString, "*", ".*"); m_filterExp = std::regex(filterExpString); } auto filterPath = fs::path(filterPathString); filterPath.remove_filename(); auto currentDirPath = filterPath / "."; auto parentDirPath = filterPath / ".."; auto currentDirPathString = currentDirPath.generic_string(); auto parentDirPathString = parentDirPath.generic_string(); if(std::regex_match(currentDirPathString, m_filterExp)) { ENTRY entry; memset(&entry, 0, sizeof(entry)); strcpy(reinterpret_cast(entry.name), "."); entry.size = 0; entry.attributes = MC_FILE_ATTR_FOLDER; m_entries.push_back(entry); } if(std::regex_match(parentDirPathString, m_filterExp)) { ENTRY entry; memset(&entry, 0, sizeof(entry)); strcpy(reinterpret_cast(entry.name), ".."); entry.size = 0; entry.attributes = MC_FILE_ATTR_FOLDER; m_entries.push_back(entry); } SearchRecurse(m_basePath); } unsigned int CMcServ::CPathFinder::Read(ENTRY* entry, unsigned int size) { assert(m_index <= m_entries.size()); unsigned int remaining = m_entries.size() - m_index; unsigned int readCount = std::min(remaining, size); if(entry != nullptr) { for(unsigned int i = 0; i < readCount; i++) { entry[i] = m_entries[i + m_index]; } } m_index += readCount; return readCount; } void CMcServ::CPathFinder::SearchRecurse(const fs::path& path) { bool found = false; for(const auto& element : fs::directory_iterator(path)) { const auto& relativePath(element.path()); std::string relativePathString(relativePath.generic_string()); //"Extract" a more appropriate relative path from the memory card point of view relativePathString.erase(0, m_basePath.generic_string().size()); //Attempt to match this against the filter if(std::regex_match(relativePathString, m_filterExp)) { //Fill in the information ENTRY entry; memset(&entry, 0, sizeof(entry)); auto entryName = DecodeMcName(relativePath.filename().string()); strncpy(reinterpret_cast(entry.name), entryName.c_str(), 0x1F); entry.name[0x1F] = 0; if(element.is_directory()) { entry.size = CountEntries(element); entry.attributes = MC_FILE_ATTR_FOLDER; } else { entry.size = static_cast(element.file_size()); entry.attributes = MC_FILE_0400 | MC_FILE_ATTR_EXISTS | MC_FILE_ATTR_CLOSED | MC_FILE_ATTR_FILE | MC_FILE_ATTR_READABLE | MC_FILE_ATTR_WRITEABLE | MC_FILE_ATTR_EXECUTABLE; } //Fill in modification date info { auto changeSystemTime = Framework::ConvertFsTimeToSystemTime(element.last_write_time()); auto localChangeDate = std::localtime(&changeSystemTime); entry.modificationTime.second = localChangeDate->tm_sec; entry.modificationTime.minute = localChangeDate->tm_min; entry.modificationTime.hour = localChangeDate->tm_hour; entry.modificationTime.day = localChangeDate->tm_mday; entry.modificationTime.month = localChangeDate->tm_mon; entry.modificationTime.year = localChangeDate->tm_year + 1900; } //std::filesystem doesn't provide a way to get creation time, so just make it the same as modification date entry.creationTime = entry.modificationTime; m_entries.push_back(entry); found = true; } if(element.is_directory() && !found) { SearchRecurse(element); } } } uint32 CMcServ::CPathFinder::CountEntries(const fs::path& path) { uint32 entryCount = 0; assert(fs::is_directory(path)); for(FRAMEWORK_MAYBE_UNUSED const auto& entry : fs::directory_iterator(path)) { entryCount++; } return entryCount; }