From ceb9db651a7d6ff6c6851388e5038e89c47c336a Mon Sep 17 00:00:00 2001 From: kruz1337 Date: Thu, 7 Dec 2023 20:49:14 +0300 Subject: [PATCH] $fully_updated --- .gitmodules | 6 + OPL Game Installer.sln | 31 ++ OPL Game Installer/OPL Game Installer.aps | Bin 0 -> 6564 bytes OPL Game Installer/OPL Game Installer.rc | Bin 0 -> 3312 bytes OPL Game Installer/OPL Game Installer.vcxproj | 166 ++++++ .../OPL Game Installer.vcxproj.filters | 58 ++ .../OPL Game Installer.vcxproj.user | 6 + OPL Game Installer/isoparser.cpp | 104 ++++ OPL Game Installer/isoparser.h | 14 + OPL Game Installer/lib9660 | 1 + OPL Game Installer/libcurl | 1 + OPL Game Installer/main.cpp | 408 ++++++++++++++ OPL Game Installer/opl.cpp | 523 ++++++++++++++++++ OPL Game Installer/opl.h | 16 + OPL Game Installer/resource.h | 16 + OPL Game Installer/utilities.h | 152 +++++ README.md | 54 +- thumbnail.png | Bin 0 -> 54189 bytes 18 files changed, 1555 insertions(+), 1 deletion(-) create mode 100644 .gitmodules create mode 100644 OPL Game Installer.sln create mode 100644 OPL Game Installer/OPL Game Installer.aps create mode 100644 OPL Game Installer/OPL Game Installer.rc create mode 100644 OPL Game Installer/OPL Game Installer.vcxproj create mode 100644 OPL Game Installer/OPL Game Installer.vcxproj.filters create mode 100644 OPL Game Installer/OPL Game Installer.vcxproj.user create mode 100644 OPL Game Installer/isoparser.cpp create mode 100644 OPL Game Installer/isoparser.h create mode 160000 OPL Game Installer/lib9660 create mode 160000 OPL Game Installer/libcurl create mode 100644 OPL Game Installer/main.cpp create mode 100644 OPL Game Installer/opl.cpp create mode 100644 OPL Game Installer/opl.h create mode 100644 OPL Game Installer/resource.h create mode 100644 OPL Game Installer/utilities.h create mode 100644 thumbnail.png diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a5c956b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "OPL Game Installer/libcurl"] + path = OPL Game Installer/libcurl + url = https://github.com/curl/curl +[submodule "OPL Game Installer/lib9660"] + path = OPL Game Installer/lib9660 + url = https://github.com/erincandescent/lib9660 diff --git a/OPL Game Installer.sln b/OPL Game Installer.sln new file mode 100644 index 0000000..f187955 --- /dev/null +++ b/OPL Game Installer.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33815.320 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "OPL Game Installer", "OPL Game Installer\OPL Game Installer.vcxproj", "{1157B2FE-476C-425F-98BB-4712E3D3E0AC}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1157B2FE-476C-425F-98BB-4712E3D3E0AC}.Debug|x64.ActiveCfg = Debug|x64 + {1157B2FE-476C-425F-98BB-4712E3D3E0AC}.Debug|x64.Build.0 = Debug|x64 + {1157B2FE-476C-425F-98BB-4712E3D3E0AC}.Debug|x86.ActiveCfg = Debug|Win32 + {1157B2FE-476C-425F-98BB-4712E3D3E0AC}.Debug|x86.Build.0 = Debug|Win32 + {1157B2FE-476C-425F-98BB-4712E3D3E0AC}.Release|x64.ActiveCfg = Release|x64 + {1157B2FE-476C-425F-98BB-4712E3D3E0AC}.Release|x64.Build.0 = Release|x64 + {1157B2FE-476C-425F-98BB-4712E3D3E0AC}.Release|x86.ActiveCfg = Release|Win32 + {1157B2FE-476C-425F-98BB-4712E3D3E0AC}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {7A169FAA-3D76-46AE-A474-9946DBB52021} + EndGlobalSection +EndGlobal diff --git a/OPL Game Installer/OPL Game Installer.aps b/OPL Game Installer/OPL Game Installer.aps new file mode 100644 index 0000000000000000000000000000000000000000..6a65a86cf04cefebf8eb254065396cd04c0ed31d GIT binary patch literal 6564 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 t_0jZs*_sER4CDV+f{K~PE5V@0Z0RA5MhFkb + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + + + + + + + + + + + + + + + + + + + 16.0 + Win32Proj + {1157b2fe-476c-425f-98bb-4712e3d3e0ac} + FAT32Writer + 10.0 + + + + Application + true + v143 + MultiByte + + + Application + false + v143 + true + MultiByte + + + Application + true + v143 + MultiByte + + + Application + false + v143 + true + MultiByte + + + + + + + + + + + + + + + + + + + + + libcurl/debug64;$(LibraryPath) + + + libcurl/release64;$(LibraryPath) + + + libcurl/release32;$(LibraryPath) + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;_CRT_SECURE_NO_WARNINGS;CURL_STATICLIB;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + _CRT_SECURE_NO_WARNINGS;CURL_STATICLIB;WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + libcurl/include;%(AdditionalIncludeDirectories) + + + Console + true + true + true + libcurl_a.lib;%(AdditionalDependencies) + + + + + Level3 + true + _DEBUG;_CONSOLE;_CRT_SECURE_NO_WARNINGS;CURL_STATICLIB;%(PreprocessorDefinitions) + true + libcurl/include;%(AdditionalIncludeDirectories) + + + Console + true + libcurl_a.lib;%(AdditionalDependencies) + + + + + Level3 + true + true + true + _CRT_SECURE_NO_WARNINGS;CURL_STATICLIB;%(PreprocessorDefinitions) + true + libcurl/include;%(AdditionalIncludeDirectories) + + + Console + true + true + true + libcurl_a.lib;%(AdditionalDependencies) + + + + + + \ No newline at end of file diff --git a/OPL Game Installer/OPL Game Installer.vcxproj.filters b/OPL Game Installer/OPL Game Installer.vcxproj.filters new file mode 100644 index 0000000..3bbe420 --- /dev/null +++ b/OPL Game Installer/OPL Game Installer.vcxproj.filters @@ -0,0 +1,58 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Header Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Resource Files + + + + + Resource Files + + + \ No newline at end of file diff --git a/OPL Game Installer/OPL Game Installer.vcxproj.user b/OPL Game Installer/OPL Game Installer.vcxproj.user new file mode 100644 index 0000000..dc63f8a --- /dev/null +++ b/OPL Game Installer/OPL Game Installer.vcxproj.user @@ -0,0 +1,6 @@ + + + + false + + \ No newline at end of file diff --git a/OPL Game Installer/isoparser.cpp b/OPL Game Installer/isoparser.cpp new file mode 100644 index 0000000..86660cf --- /dev/null +++ b/OPL Game Installer/isoparser.cpp @@ -0,0 +1,104 @@ +#include "isoparser.h" +#include "utilities.h" + +static FILE* isof; + +static bool readsect(l9660_fs* fs, void* buf, uint32_t sector) +{ + if (_fseeki64(isof, 2048 * sector, SEEK_SET)) { + fprintf(stderr, "Reading sector %u\n", sector); + perror("fseek"); + return false; + } + + if (fread(buf, 2048, 1, isof) != 1) { + fprintf(stderr, "Reading sector %u\n", sector); + perror("fread"); + return false; + } + + return true; +} + +char* IsoParser::readIsoFile(const char* path, const char* insidePath) { + isof = fopen(path, "rb"); + if (!isof) { + printf("[-] Failed to open '%s' path.\n", path); + return nullptr; + } + + l9660_fs fs; + l9660_dir dir; + l9660_openfs(&fs, readsect); + + l9660_fs_open_root(&dir, &fs); + + l9660_file file; + l9660_openat(&file, &dir, insidePath); + + size_t totalRead = 0; + + char* content = nullptr; + + while (true) { + char buf[128]; + size_t bytesRead; + l9660_read(&file, buf, 128, &bytesRead); + + if (bytesRead == 0) { + break; + } + + content = new char[bytesRead]; + std::memcpy(content, buf, bytesRead); + totalRead += bytesRead; + } + + if (totalRead > 0) { + content[totalRead] = '\0'; + } + + fclose(isof); + + return content; +} + +char* IsoParser::parseConfig(const char* conf, const char* option) { + std::string strBuffer(conf); + + size_t found = strBuffer.find(option); + if (found == std::string::npos) { + return nullptr; + } + + std::string afterOption = strBuffer.substr(found); + + size_t endOfLine = afterOption.find_first_of("\n"); + if (endOfLine == std::string::npos) { + return nullptr; + } + + std::string removeLines = strBuffer.substr(found, afterOption.find_first_of("\n") - 1); + std::string afterProp = removeLines.substr(strlen(option) + 3); + + return stringToCharp(afterProp); +} + +char* IsoParser::getGameID(const char* isoPath) { + + char* config = IsoParser::readIsoFile(isoPath, "SYSTEM.CNF"); + if (!config) { + return nullptr; + } + + std::string boot = IsoParser::parseConfig(config, "BOOT2"); + size_t p = boot.find_first_of("\\"); + if (p == std::string::npos) { + return nullptr; + } + + std::string boot2 = boot.substr(p + 1); + std::string gameId = boot2.substr(0, boot2.length() - 2); + + return stringToCharp(gameId); +} \ No newline at end of file diff --git a/OPL Game Installer/isoparser.h b/OPL Game Installer/isoparser.h new file mode 100644 index 0000000..c40c3d3 --- /dev/null +++ b/OPL Game Installer/isoparser.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +extern "C" { +#include "lib9660/lib9660.h" +} + +namespace IsoParser { + char* readIsoFile(const char* path, const char* insidePath); + char* parseConfig(const char* conf, const char* option); + char* getGameID(const char* isoPath); +}; \ No newline at end of file diff --git a/OPL Game Installer/lib9660 b/OPL Game Installer/lib9660 new file mode 160000 index 0000000..00d0277 --- /dev/null +++ b/OPL Game Installer/lib9660 @@ -0,0 +1 @@ +Subproject commit 00d027749ab556208c2aedebe793592a70ad30d0 diff --git a/OPL Game Installer/libcurl b/OPL Game Installer/libcurl new file mode 160000 index 0000000..ca6bafc --- /dev/null +++ b/OPL Game Installer/libcurl @@ -0,0 +1 @@ +Subproject commit ca6bafce95365fadd906a533bb16e47b638c5c2a diff --git a/OPL Game Installer/main.cpp b/OPL Game Installer/main.cpp new file mode 100644 index 0000000..f19beaa --- /dev/null +++ b/OPL Game Installer/main.cpp @@ -0,0 +1,408 @@ +#include +#include + +#include "isoparser.h" +#include "opl.h" +#include "utilities.h" + +#ifdef _WIN32 +#include +#ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING +#define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004 +#endif +#define WAIT(ms) Sleep(ms) +#define MKDIR(dir) CreateDirectoryA(dir, NULL) +#define SETUP_CONSOLE() \ + do { \ + DWORD outMode = 0; \ + HANDLE stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); \ + \ + if (stdoutHandle == INVALID_HANDLE_VALUE || !GetConsoleMode(stdoutHandle, &outMode)) { \ + exit(GetLastError()); \ + } \ + \ + DWORD outModeInit = outMode; \ + outMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; \ + \ + if (!SetConsoleMode(stdoutHandle, outMode)) { \ + exit(GetLastError()); \ + } \ + } while(0) +#else +#include +#define WAIT(ms) usleep(ms * 1000) +#define MKDIR(dir) mkdir(dir, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) == 0 +#define SETUP_CONSOLE() +#endif +#define PAUSE() printf("\n\nPress any key to continue . . ."); (void)getchar(); +#define CHECK_YES_NO_INPUT(type) (__strcmpi(type, "y") == 0 || __strcmpi(type, "yes") == 0 ? 1 : (__strcmpi(type, "n") == 0 || __strcmpi(type, "no") == 0 ? 0 : -1)) +#define CLEAR() printf("\033[H\033[2J\033[3J") +#define CREATE_ASCII() printf("\x1B[31m%s\n\033[0m\t\t\n", R"( + ____ __ _ __ ____ + / __ \___ ____ ___ _____ _____/ /| |/ / / __ \___ _ __ + / /_/ / _ \/ __ `/ / / / _ \/ ___/ __| / / / / / _ | | / / + / _, _/ __/ /_/ / /_/ / __(__ / /_/ | / /_/ / __| |/ _ + /_/ |_|\___/\__, /\__,_/\___/____/\__/_/|_| /_____/\___/|___(_))") +#define CREATE_ERROR(msg, pause) CLEAR(); CREATE_ASCII(); printf(LIGHT_RED"%s" RESET, msg); if(pause) {PAUSE();} +#define SET_TITLE(title) printf("\033]0;%s\007", title); +#define RESET "\x1B[0m" +#define HIDE_CURSOR "\033[?25l" +#define SHOW_CURSOR "\033[?25h" +#define BLACK "\x1B[30m" +#define RED "\x1B[31m" +#define GREEN "\x1B[32m" +#define YELLOW "\x1B[33m" +#define BLUE "\x1B[34m" +#define MAGENTA "\x1B[35m" +#define CYAN "\x1B[36m" +#define WHITE "\x1B[37m" +#define LIGHT_BLACK "\x1B[90m" +#define LIGHT_RED "\x1B[91m" +#define LIGHT_GREEN "\x1B[92m" +#define LIGHT_YELLOW "\x1B[93m" +#define LIGHT_BLUE "\x1B[94m" +#define LIGHT_MAGENTA "\x1B[95m" +#define LIGHT_CYAN "\x1B[96m" +#define LIGHT_WHITE "\x1B[97m" +#define BG_BLACK "\x1B[40m" +#define BG_RED "\x1B[41m" +#define BG_GREEN "\x1B[42m" +#define BG_YELLOW "\x1B[43m" +#define BG_BLUE "\x1B[44m" +#define BG_MAGENTA "\x1B[45m" +#define BG_CYAN "\x1B[46m" +#define BG_WHITE "\x1B[47m" +#define BG_LIGHT_BLACK "\x1B[100m" +#define BG_LIGHT_RED "\x1B[101m" +#define BG_LIGHT_GREEN "\x1B[102m" +#define BG_LIGHT_YELLOW "\x1B[103m" +#define BG_LIGHT_BLUE "\x1B[104m" +#define BG_LIGHT_MAGENTA "\x1B[105m" +#define BG_LIGHT_CYAN "\x1B[106m" +#define BG_LIGHT_WHITE "\x1B[107m" + +const std::streamsize chunkSize = 1LL * (1024 * 1024 * 1024); // (byte) equals 1gb > number of bytes to seperate files +const std::streamsize readSize = 4LL * (1024); // (byte) equals 4096b > total number of bytes to read each +const std::streamsize maxSize = 4LL * (1024 * 1024 * 1024); // (byte) equals 4gb > fat32 supported max size + +int main(int argc, char* argv[]) { + SETUP_CONSOLE(); + CREATE_ASCII(); + SET_TITLE("OPL Game Installer - github.com/kruz1337"); + + std::string str_isoPath, str_copyFile, str_mediaType, str_outputDir, str_addCover, str_createVmc, str_sizeOfVmc; + + { + if (argc > 1) + { + if (!argv[1] || !argv[2] || !argv[3]) { + CREATE_ERROR("[-] Invalid argument type.", 0); + return 1; + } + + str_isoPath = argv[1]; + str_outputDir = argv[2]; + str_mediaType = argv[3]; + str_copyFile = findFromCharArray(argv, "--copy", argc) == -1 ? "n" : "y"; + str_addCover = findFromCharArray(argv, "--add-cover", argc) == -1 ? "n" : "y"; + + int vmcArgPos = findFromCharArray(argv, "--create-vmc", argc); + if (vmcArgPos == -1) { + str_createVmc = "n"; + } + else { + str_createVmc = "y"; + str_sizeOfVmc = argv[vmcArgPos + 1]; + } + } + } + + if (argc <= 1) { + printf("[?] Iso file path: \n> "); + std::getline(std::cin, str_isoPath); + } + + const char* isoPath = str_isoPath.c_str(); + std::ifstream input(isoPath, std::ios::binary); + if (!input.is_open()) { + CREATE_ERROR("[-] Failed to open input file.", 1); + return 1; + } + + char* gameId = IsoParser::getGameID(isoPath); + if (!gameId) + { + CREATE_ERROR("[-] Invalid iso file.", 1); + return 1; + } + + input.seekg(0, input.end); + std::streamsize isoSize = input.tellg(); + input.seekg(0, input.beg); + + int copyFile = 0; + + if (isoSize <= maxSize) { + if (argc <= 1) { + printf(YELLOW"\n[*] The file can be copied directly because the file size does not exceed the limit.\n" RESET); + printf("[?] Do you want the file copied? (y/N) [Default: y]\n> "); + std::getline(std::cin, str_copyFile); + } + + copyFile = str_copyFile == "" ? 1 : CHECK_YES_NO_INPUT(str_copyFile.c_str()); + if (copyFile == -1) { + CREATE_ERROR("[-] Invalid result type.", 1); + return 1; + } + } + + if (argc <= 1) { + printf("\n[?] Media type: (CD/DVD) [Default: DVD]\n> "); + std::getline(std::cin, str_mediaType); + } + + str_mediaType = str_mediaType == "" ? "DVD" : toUpperCase(str_mediaType); + + const char* mediaType = str_mediaType.c_str(); + if (strcmp(mediaType, "DVD") != 0 && strcmp(mediaType, "CD") != 0) { + CREATE_ERROR("[-] Invalid result type.", 1); + return 1; + } + + if (argc <= 1) { + printf("\n[?] OPL Root path: \n> "); + std::getline(std::cin, str_outputDir); + } + + const char* outputDir = str_outputDir.c_str(); + if (!isDirectory(outputDir)) { + CREATE_ERROR("[-] Failed to open root dir.", 1); + return 1; + } + + if (argc <= 1) { + printf(YELLOW "\n[*] This process need to internet connection." RESET); + printf("\n[?] Do you want to add cover image? (y/N) [Default: n]\n> "); + std::getline(std::cin, str_addCover); + } + + int addCover = str_addCover == "" ? 0 : CHECK_YES_NO_INPUT(str_addCover.c_str()); + if (addCover == -1) { + CREATE_ERROR("[-] Invalid result type.", 1); + return 1; + } + + if (argc <= 1) { + printf(YELLOW "\n[*] It doesn't delete existing memory cards, but replaces the first slot with a newly created memory card." RESET); + printf("\n[?] Do you want to create vmc for this game? (y/N) [Default: n]\n> "); + std::getline(std::cin, str_createVmc); + } + + int sizeOfVmc = 8; + int createVmc = str_createVmc == "" ? 0 : CHECK_YES_NO_INPUT(str_createVmc.c_str()); + if (createVmc == -1) { + CREATE_ERROR("[-] Invalid result type.", 1); + return 1; + } + else if (createVmc == 1) { + if (argc <= 1) { + printf(YELLOW "\n[*] Memory card size should be minimum 1 mb and maximum 1024 mb." RESET); + printf("\n[?] How much should be megabyte for memory card? [Default: 8]\n> "); + std::getline(std::cin, str_sizeOfVmc); + } + + if (str_sizeOfVmc != "") { + try { + sizeOfVmc = std::stoi(str_sizeOfVmc); + } + catch (...) { + CREATE_ERROR("[-] Invalid memory card size.", 1); + return 1; + } + } + + if ((sizeOfVmc < 1) || (sizeOfVmc > 1024)) { + CREATE_ERROR("[-] Invalid memory card size.", 1); + return 1; + } + } + + char* fileName = getFilenameByPath(isoPath, true); + char* gameName = getFilenameByPath(isoPath); + char* crcHex = decimalToHex(OPL::crc32Hex(gameName), 8); + + const char* newIsoDir = locationFix(std::string(outputDir) + "\\" + mediaType); + const char* newIsoPath = locationFix(std::string(newIsoDir) + "\\" + fileName); + const char* vmcName = locationFix(replaceStr(gameId, ".", "")); + const char* vmcDir = locationFix(std::string(outputDir) + "\\VMC"); + const char* vmcPath = locationFix((std::string(vmcDir) + "\\" + vmcName + ".bin")); + const char* cfgDir = locationFix(std::string(outputDir) + "\\CFG"); + const char* cfgPath = locationFix(std::string(cfgDir) + "\\" + gameId + ".cfg"); + const char* artDir = locationFix(std::string(outputDir) + "\\ART"); + + CLEAR(); + CREATE_ASCII(); + printf("[*] Process started.\n[*] Game title is \"%s\", copying to \"%s\" directory.\n" YELLOW"[*] Switch off the \"Check USB game fragmentation\" option in OPL settings, otherwise it will not run fragmented games.\n[*] Do not eject the disc during the process and do not interfere with the files in use.\n\n" RESET, gameName, newIsoDir); + + { + auto start = std::chrono::high_resolution_clock::now(); + std::streamsize totalReadByte = 0; + + bool writeJob_IsCompleted = false; + + auto writeJob = [&]() { + std::ofstream output; + char buffer[readSize]; + + if (copyFile == 0) { + int chunkCount = 0; + + while (!input.eof()) { + static std::streamsize partRead = chunkSize; + input.read(buffer, partRead + readSize > chunkSize ? abs(chunkSize - partRead) : readSize); + + if (partRead >= chunkSize) { + partRead = 0; + + std::string path = std::string(outputDir) + "\\ul." + crcHex + "." + gameId + "." + padStart(std::to_string(chunkCount), 2, '0'); + + output.close(); + output.open(path, std::ios::binary); + + if (!output.is_open()) { + writeJob_IsCompleted = true; + input.close(); + printf(LIGHT_RED "\n\n[-] Failed to open part path." RESET "\n[!] Process finished with errors."); + PAUSE(); + exit(1); + } + + chunkCount++; + } + + std::streamsize bytesRead = input.gcount(); + output.write(buffer, bytesRead); + totalReadByte += bytesRead; + partRead += bytesRead; + } + + int ulResult = OPL::writeUl(outputDir, gameName, gameId, mediaType, chunkCount); + if (ulResult < 0) { + writeJob_IsCompleted = true; + output.close(); + input.close(); + printf(LIGHT_RED "\n\n[-] Failed to create defragged game." RESET "\n[!] Process finished with errors."); + PAUSE(); + exit(1); + } + } + else if (copyFile == 1) { + if (!isDirectory(newIsoDir) && !MKDIR(newIsoDir)) { + writeJob_IsCompleted = true; + input.close(); + printf(LIGHT_RED "\n\n[-] Failed to create media directory." RESET "\n[!] Process finished with errors."); + PAUSE(); + exit(1); + } + + output.open(newIsoPath, std::ios::binary); + if (!output.is_open()) { + writeJob_IsCompleted = true; + input.close(); + printf(LIGHT_RED "\n\n[-] Failed to open part path." RESET "\n[!] Process finished with errors."); + PAUSE(); + exit(1); + } + + while (!input.eof()) { + input.read(buffer, readSize); + + std::streamsize bytesRead = input.gcount(); + output.write(buffer, bytesRead); + totalReadByte += bytesRead; + } + } + + input.close(); + output.close(); + writeJob_IsCompleted = true; + }; + + std::thread writeJob_Thread(writeJob); + + while (!writeJob_IsCompleted) { + double percentage = (double)totalReadByte / isoSize * 100; + auto end = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end - start); + + const char chars[] = { '\\', '|', '/', '-' }; + static int index = 0; + index = (index + 1) % 4; + + printf(LIGHT_BLACK HIDE_CURSOR "%c %llu bytes transferred in %llu ms from total of %llu bytes (%%%i completed) %c" RESET"\t\r", chars[index], totalReadByte, duration.count(), isoSize, (int)round(percentage), chars[index]); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + writeJob_Thread.join(); + + printf(SHOW_CURSOR RESET "\n"); + } + + if (createVmc == 1) { + if (!isDirectory(vmcDir) && !MKDIR(vmcDir)) { + printf(LIGHT_RED "\n[-] Failed to create virtual memory directory." RESET "\n[!] Process finished with errors."); + PAUSE(); + return 1; + } + + int vmcResult = OPL::createVmc(vmcPath, sizeOfVmc * 1024, 16); + if (vmcResult < 0) { + printf(LIGHT_RED "\n[-] Failed to create virtual memory card." RESET "\n[!] Process finished with errors."); + PAUSE(); + return 1; + } + + if (!isDirectory(cfgDir) && !MKDIR(cfgDir)) { + printf(LIGHT_RED "\n[-] Failed to create config directory." RESET "\n[!] Process finished with errors."); + PAUSE(); + return 1; + } + + bool cfgResult = OPL::writeCfg(cfgPath, "$VMC_0", vmcName); + if (!cfgResult) { + printf(LIGHT_RED "\n[-] Failed to set memory card slot." RESET "\n[!] Process finished with errors."); + PAUSE(); + return 1; + } + + printf(YELLOW"\n[*] Virtual memory card created and selected." RESET); + } + + if (addCover == 1) { + if (!isDirectory(artDir) && !MKDIR(artDir)) { + printf(LIGHT_RED "\n[-] Failed to create art directory." RESET "\n[!] Process finished with errors."); + PAUSE(); + return 1; + } + + int fetchResult = OPL::downloadArts(artDir, gameId); + if (fetchResult == -1) { + printf(LIGHT_RED "\n[-] Failed to write image data." RESET "\n[!] Process finished with errors."); + PAUSE(); + return 1; + } + else if (fetchResult == -2) { + printf(LIGHT_RED "\n[-] There is no game image on the server." RESET "\n[!] Process finished with errors."); + PAUSE(); + return 1; + } + + printf(YELLOW"\n[*] Game cover art installed." RESET); + + } + + printf("\n[+] Process succesfully finished."); + PAUSE(); + + return 0; +} \ No newline at end of file diff --git a/OPL Game Installer/opl.cpp b/OPL Game Installer/opl.cpp new file mode 100644 index 0000000..d7e4745 --- /dev/null +++ b/OPL Game Installer/opl.cpp @@ -0,0 +1,523 @@ +#include "opl.h" +#include "utilities.h" + +#include +#pragma comment(lib, "Normaliz.lib") +#pragma comment(lib, "crypt32.lib") +#pragma comment(lib, "Wldap32.lib") +#pragma comment(lib, "ws2_32.lib") + +size_t curl_write_data_callback(void* contents, size_t size, size_t nmemb, std::vector* data) { + size_t total_size = size * nmemb; + data->insert(data->end(), static_cast(contents), static_cast(contents) + total_size); + return total_size; +} + +bool OPL::fetchImage(const char* url, std::vector& buffer) { + CURL* curl = curl_easy_init(); + if (!curl) { + return false; + } + + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_data_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer); + + CURLcode res = curl_easy_perform(curl); + if (res != CURLE_OK) { + curl_easy_cleanup(curl); + return false; + } + + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + if (http_code != 200) { + curl_easy_cleanup(curl); + return false; + } + + curl_easy_cleanup(curl); + + return true; +} + +int OPL::downloadArts(const char* dir, const char* gameId) { + std::vector buffer; + + int fetched = 0; + + const char* gameArts[] = { "LGO.png", "LAB.jpg", "ICO.png", "COV.jpg", "COV.png", "COV2.jpg", "SCR_00.jpg", "SCR_01.jpg", "BG_00.jpg" }; + for (auto& art : gameArts) { + buffer.clear(); + + std::string erasedName = std::string(gameId) + "_" + replaceMultiStr(art, { "_00", "_01"}, {"", "2"}); + std::string url("https://requestx.dev/ARTS/" + std::string(gameId) + "/" + std::string(gameId) + "_" + art); + + bool result = OPL::fetchImage(url.c_str(), buffer); + if (!result) { + continue; + } + + std::ofstream img(std::string(dir) + "\\" + erasedName, std::ios::binary); + if (!img) { + return -1; + } + + img.write(reinterpret_cast(&buffer[0]), buffer.size()); + img.close(); + + fetched++; + } + + return fetched > 0 ? 0 : -2; +} + +bool OPL::writeCfg(const char* path, const char* key, const char* value) { + std::ifstream input(path); + std::ofstream output(path); + + std::string cfg = ((std::string)key + '=' + value); + + if (input) { + bool f = false; + std::string line; + + while (input >> line) + { + if (line.find(key + std::string("=")) == 0) { + line = cfg; + f = true; + } + + line += "\n"; + output << line; + } + + if (!f) { + output << cfg; + } + + return true; + } + else if (output) { + output << cfg; + } + else { + return false; + } + + input.close(); + output.close(); + + return true; +} + +//These following functions are taked from https://github.com/ps2homebrew/Open-PS2-Loader/ +int OPL::writeUl(const char* drive, const char* game_name, const char* game_id, const char* media, int parts) +{ + typedef struct + { + char name[32]; + char image[15]; + unsigned char parts; + unsigned char media; + unsigned char pad[15]; + } cfg_t; + + FILE* fh_cfg; + cfg_t cfg; + char cfg_path[256]; + int r; + +#ifdef _WIN32 + if (strlen(drive) == 1) + sprintf(cfg_path, "%s:\\ul.cfg", drive); + else + sprintf(cfg_path, "%s\\ul.cfg", drive); +#else + sprintf(cfg_path, "%s/ul.cfg", drive); +#endif + memset(&cfg, 0, sizeof(cfg_t)); + + strncpy(cfg.name, game_name, 32); + sprintf(cfg.image, "ul.%s", game_id); + cfg.parts = parts; + cfg.pad[4] = 0x08; + + if (!strcmp(media, "CD")) + cfg.media = 0x12; + else if (!strcmp(media, "DVD")) + cfg.media = 0x14; + + fh_cfg = fopen(cfg_path, "ab"); + if (!fh_cfg) + return -1; + + r = fwrite(&cfg, 1, sizeof(cfg_t), fh_cfg); + if (r != sizeof(cfg_t)) { + fclose(fh_cfg); + return -2; + } + + fclose(fh_cfg); + + return 0; +} + +int OPL::crc32Hex(const char* string) +{ + int crctab[0x400]; + int crc, table, count, byte; + + for (table = 0; table < 256; table++) { + crc = table << 24; + + for (count = 8; count > 0; count--) { + if (crc < 0) + crc = crc << 1; + else + crc = (crc << 1) ^ 0x04C11DB7; + } + crctab[255 - table] = crc; + } + + do { + byte = string[count++]; + crc = crctab[byte ^ ((crc >> 24) & 0xFF)] ^ ((crc << 8) & 0xFFFFFF00); + } while (string[count - 1] != 0); + + return crc; +} + +typedef struct +{ + unsigned char magic[28]; + unsigned char version[12]; + unsigned short pagesize; + unsigned short pages_per_cluster; + unsigned short blocksize; + unsigned short unused; + unsigned int clusters_per_card; + unsigned int alloc_offset; + unsigned int alloc_end; + unsigned int rootdir_cluster; + unsigned int backup_block1; + unsigned int backup_block2; + unsigned char unused2[8]; + unsigned int ifc_list[32]; + int bad_block_list[32]; + unsigned char cardtype; + unsigned char cardflags; + unsigned short unused3; + unsigned int cluster_size; + unsigned int FATentries_per_cluster; + unsigned int clusters_per_block; + int cardform; + unsigned int rootdir_cluster2; + unsigned int unknown1; + unsigned int unknown2; + unsigned int max_allocatable_clusters; + unsigned int unknown3; + unsigned int unknown4; + int unknown5; +} MCDevInfo; + +static MCDevInfo devinfo; + +typedef struct _sceMcStDateTime +{ + unsigned char Resv2; + unsigned char Sec; + unsigned char Min; + unsigned char Hour; + unsigned char Day; + unsigned char Month; + unsigned short Year; +} sceMcStDateTime; + +static void long_multiply(unsigned int v1, unsigned int v2, unsigned int* HI, unsigned int* LO) +{ + register long a, b, c, d; + register long x, y; + + a = (v1 >> 16) & 0xffff; + b = v1 & 0xffff; + c = (v2 >> 16) & 0xffff; + d = v2 & 0xffff; + + *LO = b * d; + x = a * d + c * b; + y = ((*LO >> 16) & 0xffff) + x; + + *LO = (*LO & 0xffff) | ((y & 0xffff) << 16); + *HI = (y >> 16) & 0xffff; + + *HI += a * c; +} + +static int mc_getmcrtime(sceMcStDateTime* mctime) +{ + time_t rawtime; + struct tm* ptm; + + time(&rawtime); + ptm = gmtime(&rawtime); + + mctime->Resv2 = 0; + mctime->Sec = ((((ptm->tm_sec >> 4) << 2) + (ptm->tm_sec >> 4)) << 1) + (ptm->tm_sec & 0xf); + mctime->Min = ((((ptm->tm_min >> 4) << 2) + (ptm->tm_min >> 4)) << 1) + (ptm->tm_min & 0xf); + mctime->Hour = ((((ptm->tm_hour >> 4) << 2) + (ptm->tm_hour >> 4)) << 1) + (ptm->tm_hour & 0xf); + mctime->Day = ((((ptm->tm_mday >> 4) << 2) + (ptm->tm_mday >> 4)) << 1) + (ptm->tm_mday & 0xf); + + mctime->Month = (ptm->tm_mon + 1) & 0xf; + + mctime->Year = (((((ptm->tm_year - 100) >> 4) << 2) + ((ptm->tm_year - 100) >> 4)) << 1) + (((ptm->tm_year - 100) & 0xf) | 0x7d0); + + return 0; +} + +static int mc_writecluster(FILE* fd, int cluster, void* buf, int dup) +{ + register int r, size; + MCDevInfo* mcdi = (MCDevInfo*)&devinfo; + + fseek(fd, cluster * mcdi->cluster_size, SEEK_SET); + size = mcdi->cluster_size * dup; + r = fwrite(buf, 1, size, fd); + if (r != size) + return -1; + + return 0; +} + +int OPL::createVmc(const char* filename, int size_kb, int blocksize) { + typedef struct + { + unsigned short mode; + unsigned short unused; + unsigned int length; + sceMcStDateTime created; + unsigned int cluster; + unsigned int dir_entry; + sceMcStDateTime modified; + unsigned int attr; + unsigned int unused2[7]; + char name[32]; + unsigned char unused3[416]; + } McFsEntry; + + static char SUPERBLOCK_MAGIC[] = "Sony PS2 Memory Card Format "; + static char SUPERBLOCK_VERSION[] = "1.2.0.0"; + static unsigned char cluster_buf[(16 * 1024) + 16]; + static FILE* genvmc_fh = NULL; + + register int i, r, b, ifc_index, fat_index; + register int ifc_length, fat_length, alloc_offset; + register int ret, j = 0, z = 0; + MCDevInfo* mcdi = (MCDevInfo*)&devinfo; + + genvmc_fh = fopen(filename, "wb"); + if (genvmc_fh == NULL) + return -101; + + memset((void*)&mcdi->magic, 0, sizeof(mcdi->magic) + sizeof(mcdi->version)); + strcpy((char*)&mcdi->magic, SUPERBLOCK_MAGIC); + strcat((char*)&mcdi->magic, SUPERBLOCK_VERSION); + + mcdi->cluster_size = 1024; + mcdi->blocksize = blocksize; + mcdi->pages_per_cluster = 2; + mcdi->pagesize = mcdi->cluster_size / mcdi->pages_per_cluster; + mcdi->clusters_per_block = mcdi->blocksize / mcdi->pages_per_cluster; + mcdi->clusters_per_card = (size_kb * 1024) / mcdi->cluster_size; + mcdi->cardtype = 0x02; + mcdi->cardflags = 0x2b; + mcdi->cardform = -1; + mcdi->FATentries_per_cluster = mcdi->cluster_size / sizeof(unsigned int); + + for (i = 0; i < 32; i++) + mcdi->bad_block_list[i] = -1; + + memset(cluster_buf, 0xff, sizeof(cluster_buf)); + for (i = 0; i < mcdi->clusters_per_card; i += 16) { + r = mc_writecluster(genvmc_fh, i, cluster_buf, 16); + if (r < 0) { + r = -102; + ret = fclose(genvmc_fh); + if (!(ret < 0)) + genvmc_fh = NULL; + + return r; + } + } + + fat_length = (((mcdi->clusters_per_card << 2) - 1) / mcdi->cluster_size) + 1; + ifc_length = (((fat_length << 2) - 1) / mcdi->cluster_size) + 1; + + if (!(ifc_length <= 32)) { + ifc_length = 32; + fat_length = mcdi->FATentries_per_cluster << 5; + } + + for (i = 0; i < 32; i++) + mcdi->ifc_list[i] = -1; + ifc_index = mcdi->blocksize / 2; + i = ifc_index; + for (j = 0; j < ifc_length; j++, i++) + mcdi->ifc_list[j] = i; + + fat_index = i; + + unsigned char* ifc_mem = (unsigned char*)malloc((ifc_length * mcdi->cluster_size) + 0XFF); + if (ifc_mem == NULL) { + r = -103; + ret = fclose(genvmc_fh); + if (!(ret < 0)) + genvmc_fh = NULL; + + return r; + } + memset(ifc_mem, 0, ifc_length * mcdi->cluster_size); + + unsigned int* ifc = (unsigned int*)ifc_mem; + for (j = 0; j < fat_length; j++, i++) { + + if (i >= mcdi->clusters_per_card) { + free(ifc_mem); + r = -104; + ret = fclose(genvmc_fh); + if (!(ret < 0)) + genvmc_fh = NULL; + + return r; + } + ifc[j] = i; + } + + for (z = 0; z < ifc_length; z++) { + r = mc_writecluster(genvmc_fh, mcdi->ifc_list[z], &ifc_mem[z * mcdi->cluster_size], 1); + if (r < 0) { + + free(ifc_mem); + r = -105; + ret = fclose(genvmc_fh); + if (!(ret < 0)) + genvmc_fh = NULL; + + return r; + } + } + + free(ifc_mem); + + alloc_offset = i; + + mcdi->backup_block1 = (mcdi->clusters_per_card / mcdi->clusters_per_block) - 1; + mcdi->backup_block2 = (mcdi->clusters_per_card / mcdi->clusters_per_block) - 2; + + unsigned int hi, lo, temp; + long_multiply(mcdi->clusters_per_card, 0x10624dd3, &hi, &lo); + temp = (hi >> 6) - (mcdi->clusters_per_card >> 31); + mcdi->max_allocatable_clusters = (((((temp << 5) - temp) << 2) + temp) << 3) + 1; + j = alloc_offset; + + i = (mcdi->clusters_per_card / mcdi->clusters_per_block) - 2; + for (z = 0; j < (i * mcdi->clusters_per_block); j += mcdi->FATentries_per_cluster) { + + memset(cluster_buf, 0, mcdi->cluster_size); + unsigned int* fc = (unsigned int*)cluster_buf; + int sz_u32 = (i * mcdi->clusters_per_block) - j; + if (sz_u32 > mcdi->FATentries_per_cluster) + sz_u32 = mcdi->FATentries_per_cluster; + for (b = 0; b < sz_u32; b++) + fc[b] = 0x7fffffff; + + if (z == 0) { + mcdi->alloc_offset = j; + mcdi->rootdir_cluster = 0; + fc[0] = 0xffffffff; + } + z += sz_u32; + + r = mc_writecluster(genvmc_fh, fat_index++, cluster_buf, 1); + if (r < 0) { + r = -107; + ret = fclose(genvmc_fh); + if (!(ret < 0)) + genvmc_fh = NULL; + + return r; + } + } + + mcdi->alloc_end = (i * mcdi->clusters_per_block) - mcdi->alloc_offset; + + if (z < mcdi->clusters_per_block) { + r = -108; + ret = fclose(genvmc_fh); + if (!(ret < 0)) + genvmc_fh = NULL; + + return r; + } + + mcdi->unknown1 = 0; + mcdi->unknown2 = 0; + mcdi->unknown5 = -1; + mcdi->rootdir_cluster2 = mcdi->rootdir_cluster; + + McFsEntry* rootdir_entry[2]; + sceMcStDateTime time; + + mc_getmcrtime(&time); + rootdir_entry[0] = (McFsEntry*)&cluster_buf[0]; + rootdir_entry[1] = (McFsEntry*)&cluster_buf[sizeof(McFsEntry)]; + memset((void*)rootdir_entry[0], 0, sizeof(McFsEntry)); + memset((void*)rootdir_entry[1], 0, sizeof(McFsEntry)); + rootdir_entry[0]->mode = 0x8000 | 0x0400 | 0x20 | 0x01 | 0x02 | 0x04; + rootdir_entry[0]->length = 2; + memcpy((void*)&rootdir_entry[0]->created, (void*)&time, sizeof(sceMcStDateTime)); + memcpy((void*)&rootdir_entry[0]->modified, (void*)&time, sizeof(sceMcStDateTime)); + rootdir_entry[0]->cluster = 0; + rootdir_entry[0]->dir_entry = 0; + strcpy(rootdir_entry[0]->name, "."); + rootdir_entry[1]->mode = 0x8000 | 0x2000 | 0x0400 | 0x20 | 0x02 | 0x04; + rootdir_entry[1]->length = 2; + memcpy((void*)&rootdir_entry[1]->created, (void*)&time, sizeof(sceMcStDateTime)); + memcpy((void*)&rootdir_entry[1]->modified, (void*)&time, sizeof(sceMcStDateTime)); + rootdir_entry[1]->cluster = 0; + rootdir_entry[1]->dir_entry = 0; + strcpy(rootdir_entry[1]->name, ".."); + + r = mc_writecluster(genvmc_fh, mcdi->alloc_offset + mcdi->rootdir_cluster, cluster_buf, 1); + if (r < 0) { + r = -109; + ret = fclose(genvmc_fh); + if (!(ret < 0)) + genvmc_fh = NULL; + + return r; + } + + mcdi->cardform = 1; + + memset(cluster_buf, 0xff, mcdi->cluster_size); + memcpy(cluster_buf, (void*)mcdi, sizeof(MCDevInfo)); + r = mc_writecluster(genvmc_fh, 0, cluster_buf, 1); + if (r < 0) { + r = -110; + ret = fclose(genvmc_fh); + if (!(ret < 0)) + genvmc_fh = NULL; + + return r; + } + + r = fclose(genvmc_fh); + if (r < 0) + return -111; + genvmc_fh = NULL; + + return 0; +} \ No newline at end of file diff --git a/OPL Game Installer/opl.h b/OPL Game Installer/opl.h new file mode 100644 index 0000000..86b22af --- /dev/null +++ b/OPL Game Installer/opl.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include +#include +#include + +namespace OPL { + bool writeCfg(const char* path, const char* key, const char* value); + int downloadArts(const char* dir, const char* gameId); + bool fetchImage(const char* url, std::vector& buffer); + + int writeUl(const char* drive, const char* game_name, const char* game_id, const char* media, int parts); + int crc32Hex(const char* string); + int createVmc(const char* filename, int size_kb, int blocksize); +}; \ No newline at end of file diff --git a/OPL Game Installer/resource.h b/OPL Game Installer/resource.h new file mode 100644 index 0000000..9432164 --- /dev/null +++ b/OPL Game Installer/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by OPL Game Installer.rc +// +#define IDI_ICON1 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/OPL Game Installer/utilities.h b/OPL Game Installer/utilities.h new file mode 100644 index 0000000..8ad322f --- /dev/null +++ b/OPL Game Installer/utilities.h @@ -0,0 +1,152 @@ +#pragma once + +#include +#include +#include +#include +#include + +static std::string padStart(std::string input, size_t length, char padChar) { + return input.length() >= length ? input : std::string(length - input.length(), padChar) + input; +} + +static std::string toUpperCase(std::string input) { + for (char& c : input) { + c = std::toupper(c); + } + + return input; +} + +static std::string replaceStr(std::string str, const std::string& from, const std::string& to) { + size_t start_pos = 0; + start_pos = str.find(from, start_pos); + if (start_pos != std::string::npos) { + str.replace(start_pos, from.length(), to); + start_pos += to.length(); + } + return str; +} + +static std::string replaceMultiStr(std::string str, std::initializer_list from, std::initializer_list to) { + + for (int i = 0; i < from.size(); i++) { + size_t start_pos = 0; + start_pos = str.find(from.begin()[i], start_pos); + + if (start_pos != std::string::npos) { + str.replace(start_pos, from.begin()[i].length(), to.begin()[i]); + start_pos += to.begin()[i].length(); + } + } + + return str; +} + +static std::string eraseUntilChar(std::string str, const std::string& from, const std::string& to) { + size_t firstPos = str.find(from); + size_t secondPos = str.find(to, firstPos); + + if (firstPos != std::string::npos && secondPos != std::string::npos && secondPos > firstPos) { + str.erase(firstPos, secondPos - firstPos); + } + + return str; +} + +static char* locationFix(std::string path) { + char* fixed = new char[path.length() + 1]; + std::strcpy(fixed, path.c_str()); + + std::replace(fixed, fixed + strlen(fixed), '\\', '/'); + + auto last = std::unique(fixed, fixed + strlen(fixed), [](char a, char b) { + return a == '/' && b == '/'; + }); + *last = '\0'; + +#ifdef _WIN32 + if (fixed[1] == ':') { + fixed[0] = std::toupper(fixed[0]); + } +#endif + + return fixed; +} + + +static char* generateHex(int length) { + std::srand(static_cast(std::time(nullptr))); + + static const char hex_characters[] = "0123456789ABCDEF"; + + char* result = new char[length + 1]; + for (int i = 0; i < length; ++i) { + int randomIndex = std::rand() % 16; + result[i] = hex_characters[randomIndex]; + } + + result[length] = '\0'; + return result; +} + +static bool isDirectory(const char* path) { + struct stat fileInfo; + if (stat(path, &fileInfo) != 0) { + return false; + } + + return (fileInfo.st_mode & S_IFDIR); +} + +static char* stringToCharp(std::string str) { + char* data = new char[str.size()]; + std::copy(str.begin(), str.end(), data); + data[str.size()] = '\0'; + return data; +} + +static char* getFilenameByPath(const char* path, bool ext = false) { + + std::string pathStr(path); + size_t found = pathStr.find_last_of("/\\"); + if (found == std::string::npos) { + return nullptr; + } + + std::string gameName = pathStr.substr(found + 1); + if (!ext) { + size_t last = gameName.find_last_of("."); + if (last != std::string::npos) { + gameName = gameName.substr(0, last); + } + } + + return stringToCharp(gameName); +} + +static char* decimalToHex(int number, int lenght) { + std::ostringstream stream; + stream << std::setw(lenght) << std::setfill('0') << std::uppercase << std::hex << number; + return stringToCharp(stream.str()); +} + +static int __strcmpi(const char* str1, const char* str2) { + int result; +#ifdef _WIN32 + result = _strcmpi(str1, str2); +#else + result = strcasecmp(str1, str2); +#endif + + return result; +} +static size_t findFromCharArray(char* arr[], const char* searchText, size_t arrSize) { + for (size_t i = 0; i < arrSize; ++i) { + if (strstr(arr[i], searchText) != nullptr) { + return i; + } + } + + return -1; +} \ No newline at end of file diff --git a/README.md b/README.md index 00b0395..d4c8935 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,54 @@ # OPL-Game-Installer -This repository has an Open PS2 Loader game installer for FAT32 disc, you can also create a custom sized virtual memory card and download arts of the game. +You cannot write more than 4GB of data to a FAT32 disc, so you can't easily install a game on a disc. This repository allows you to do this easily, it also creates a [Virtual Memory](https://en.wikipedia.org/wiki/Virtual_memory) Card of the desired size for the game and downloads the images of the game from the server. + +#### What is Virtual Memory Card (VMC)? +Virtual Memory Card allows you to save the game on the disc you use in games without using an memory card. + +![](https://img.shields.io/badge/language-c++-e76089?style=plastic) ![](https://img.shields.io/badge/license-GNU-green?style=plastic) ![](https://img.shields.io/badge/arch-x64%20%7C%20x86-d9654f?style=plastic) ![](https://img.shields.io/badge/config-Debug%20%7C%20Release-c0c0c0?style=plastic) + +![Image of RequestX International Developer Group on Discord](https://raw.githubusercontent.com/kruz1337/OPL-Game-Installer/main/thumbnail.png?token=GHSAT0AAAAAACLHJHX2ZRO2DKK4VHJSVUEAZLQ6XQQ) + +## How to build OPL-Game-Installer with Curl? +* If you don't know how do you do this go download released version. + +* First of all you should download project files on project page or clone this repository from GitBash or GitHub Desktop on your PC. [OPL-Game-Installer.zip](https://github.com/kruz1337/OPL-Game-Installer/releases) + +* If you download project files with manual method you need extract zip file. + +* Open the "include" folder into the "OPL Game Installer/libcurl" directory, then copy Curl includes files into it. + +* Then, compile the Curl library according to your build configuration and create a "release64" folder for "Release | x64" build. The folder layout should be like this: +``` +./OPL Game Installer/libcurl +├─include +├─release64 +├─release86 +├─debug64 +└─debug86 +``` + +* After then, copy Curl compiled library file into the folder you opened. + +* If the name is different, change it to "libcurl_a" or you should change it from ```"Linker > Additional Dependencies"``` in project settings. + +* Run .sln file on Visual Studio (2019+). + +* Press Build button or press CTRL+B on your keyboard. + +* Check out bin folder include that. + +## Usage with Arguments +```"./OPL Game Installer.exe" [OPTIONS]``` + +### Options: +``` +--copy If the size of the Iso file does not exceed the limit, its allows copy directly to the disc. +--add-cover Download arts of the game. +--create-vmc Creates virtual memory of the entered size once defined. (Size=MB) +``` + +## TO-DO List +* Add ability of fetching the Size, Title, Type, Star Count and Description of the game from the server. +* Add fetching random images from the server. +* Add property of converting ul to iso. +* Build for Linux. diff --git a/thumbnail.png b/thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..f14ce2097803809aacaf9e1cf28055c7cda1c681 GIT binary patch literal 54189 zcmeFZXIN8P*ESkttB)wKw~8nTSU?1%H;GcVD7~mik!t8YN|zSYEhq>mMHGa9bV3L% zbO?%o^cs*F1!;j0T7VFeoRy&N{eJKBmUDibKj-AS)U{UTnq|yU?s1P9pWiglWM|`H z1A##7+FI9dgFyRDL7;u8e;ojxcz&RJ1OFVluXPvrm*W%T4@;sL2QLV8@QuqIGaoa3 zJ+K|rLjqzCwRMmP@VF1O27#`s2Hc0(xjFcpv2}2A@l-xXti_x=<6^IT&QxAsO8>sP zgR_fPpqGPDpuruxKsP%j`*W%)XRZc-fet(zd?056Jls9K!2!zWnEir*YsO>Ab7z=M zeB6}J{mB^cjG6w;GwM(;hcikN(&BbfQqpG>l_VgS?d25ZWiOqPk&=;>l#-Q{krkJ^ z43<{_%POAv@i_+!=Vk8*zI|Qe$6&yn@;PT8pZj1*Nq>KT34d7$sF#zZw33pNq?C-L zjEp$YLfreIrw=4R+|&E~&mOKjc-wio-1l*TdY)nQ2(g9w`Y4|RrnH-`C5XIk>%@q=UPIhl8h&H_%r4SKIr}P#>tbGxWdc z`q$V0VE{n3`ue}d__uTM@c1=^x6chfU>QFI@^4#v-+6f7LGrePH`Le5&f$h1FwOIf z)!YZGdpSUSpk8;NQ1_oZb@S(x�>d8zZfB=Ax;Kr#;l)TjU?_I9!MLI4GY3Hcnbx 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 k|I-MB$^Ux+GEb!9TVf)+G#L