diff --git a/Core/CMakeLists.txt b/Core/CMakeLists.txt index 4514429bfc..1c9a19417f 100644 --- a/Core/CMakeLists.txt +++ b/Core/CMakeLists.txt @@ -357,6 +357,8 @@ add_library(Core STATIC FileSystems/BlobFileSystem.h FileSystems/BlockDevices.cpp FileSystems/BlockDevices.h + FileSystems/CSOCompress.cpp + FileSystems/CSOCompress.h FileSystems/DirectoryFileSystem.cpp FileSystems/DirectoryFileSystem.h FileSystems/FileSystem.h @@ -713,7 +715,7 @@ endif() list(APPEND CoreExtraLibs armips) -target_link_libraries(Core GPU Common chdr kirk cityhash sfmt19937 xbrz xxhash rcheevos at3_standalone lua +target_link_libraries(Core GPU Common chdr kirk cityhash sfmt19937 xbrz xxhash libdeflate rcheevos at3_standalone lua aemu_postoffice_client ${CoreExtraLibs} ${CMAKE_DL_LIBS}) # Winsock diff --git a/Core/Config.cpp b/Core/Config.cpp index 7edd3af46f..15318c5edc 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -331,6 +331,8 @@ static const ConfigSetting generalSettings[] = { ConfigSetting("CompressSymbols", SETTING(g_Config, bCompressSymbols), true, CfgFlag::DEFAULT), ConfigSetting("CacheFullIsoInRam", SETTING(g_Config, bCacheFullIsoInRam), false, CfgFlag::PER_GAME), ConfigSetting("AutoUpgradeFirmware", SETTING(g_Config, bAutoUpgradeFirmware), true, CfgFlag::DEFAULT), + ConfigSetting("CSOPreset", SETTING(g_Config, iCSOPreset), 0, CfgFlag::DEFAULT), + ConfigSetting("PackKeepOriginal", SETTING(g_Config, bPackKeepOriginal), false, CfgFlag::DEFAULT), ConfigSetting("RemoteISOPort", SETTING(g_Config, iRemoteISOPort), 0, CfgFlag::DEFAULT), ConfigSetting("LastRemoteISOServer", SETTING(g_Config, sLastRemoteISOServer), "", CfgFlag::DEFAULT), ConfigSetting("LastRemoteISOPort", SETTING(g_Config, iLastRemoteISOPort), 0, CfgFlag::DEFAULT), diff --git a/Core/Config.h b/Core/Config.h index 7705d27b43..2dac232622 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -251,6 +251,9 @@ public: bool bCompressSymbols; bool bCacheFullIsoInRam; bool bAutoUpgradeFirmware; + // The game screen's "Compress game" (UI/GamePacker.h). + int iCSOPreset; // CSOCompress::Preset: 0 = Best, 1 = PSP Compatible + bool bPackKeepOriginal; int iRemoteISOPort; // Also used for serving a local remote debugger. std::string sLastRemoteISOServer; int iLastRemoteISOPort; diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index aaadb94767..c413dd7ed7 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -585,6 +585,7 @@ + @@ -738,6 +739,8 @@ + + MaxSpeed true @@ -919,6 +922,7 @@ + @@ -1060,6 +1064,19 @@ + + + + + + + + + + + + + diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters index 5c5604c4e8..1ec7c1115c 100644 --- a/Core/Core.vcxproj.filters +++ b/Core/Core.vcxproj.filters @@ -82,6 +82,9 @@ {6c6e0188-fabf-4203-aaa2-896c42be2c20} + + {5b0c7e1a-3d2f-4c8e-9a61-d4f0b2e7c913} + {0201ff27-4fa8-4cf3-bbb9-accfdc0c7046} @@ -141,6 +144,9 @@ FileSystems + + FileSystems + FileSystems @@ -502,6 +508,12 @@ MIPS + + Ext\libdeflate + + + Ext\libdeflate + Ext @@ -1422,6 +1434,9 @@ FileSystems + + FileSystems + FileSystems @@ -1779,6 +1794,45 @@ Ext + + Ext\libdeflate + + + Ext\libdeflate + + + Ext\libdeflate + + + Ext\libdeflate + + + Ext\libdeflate + + + Ext\libdeflate + + + Ext\libdeflate + + + Ext\libdeflate + + + Ext\libdeflate + + + Ext\libdeflate + + + Ext\libdeflate + + + Ext\libdeflate + + + Ext\libdeflate + Ext diff --git a/Core/FileSystems/CSOCompress.cpp b/Core/FileSystems/CSOCompress.cpp new file mode 100644 index 0000000000..1337efd5b2 --- /dev/null +++ b/Core/FileSystems/CSOCompress.cpp @@ -0,0 +1,354 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Common/CommonTypes.h" +#include "Common/File/FileUtil.h" +#include "Common/Log.h" +#include "Common/Swap.h" +#include "Common/Thread/ThreadUtil.h" +#include "Core/FileSystems/BlockDevices.h" +#include "Core/Loaders.h" +#include "Core/FileSystems/CSOCompress.h" + +#ifdef SHARED_ZLIB +#include +#else +#include "ext/zlib/zlib.h" +#endif + +#include "ext/libdeflate/libdeflate.h" + +namespace CSOCompress { + +namespace { + +const uint32_t SECTOR_SIZE = 2048; +const uint32_t CSO_INDEX_UNCOMPRESSED = 0x80000000; +// Input read and compressed per round, so the threads always have work queued. +const size_t BATCH_BYTES = 32 * 1024 * 1024; + +struct PresetSettings { + uint32_t blockSize; + bool zlib; // zlib level 9, default strategy. + bool libdeflate; // libdeflate level 12. +}; + +// The trials maxcso's presets ran (with lz4, 7-zip and zlib's brute force strategies left out). +PresetSettings SettingsFor(Preset preset) { + switch (preset) { + case Preset::Best: + // 64K: only ~0.25% bigger than 256K, and each read of a new block inflates a quarter as much. + return { 65536, false, true }; + case Preset::PSPCompatible: + default: + // Some custom firmware can't read libdeflate's output or larger blocks. + return { 2048, true, false }; + } +} + +// One thread's encoders, reused for every block it compresses. +class BlockCompressor { +public: + explicit BlockCompressor(const PresetSettings &settings) : settings_(settings), scratch_(settings.blockSize) { + if (settings_.zlib) { + zlibOk_ = deflateInit2(&z_, 9, Z_DEFLATED, -15, 9, Z_DEFAULT_STRATEGY) == Z_OK; + } + if (settings_.libdeflate) { + libdeflate_ = libdeflate_alloc_compressor(12); + } + } + ~BlockCompressor() { + if (zlibOk_) { + deflateEnd(&z_); + } + if (libdeflate_) { + libdeflate_free_compressor(libdeflate_); + } + } + + // Leaves the smallest raw deflate stream in *out, or nothing when none beats the block itself. + void Compress(const u8 *in, std::vector *out) { + const uint32_t blockSize = settings_.blockSize; + out->clear(); + auto submit = [&](const u8 *data, size_t size) { + // Not smaller than the block: stored as is, which also reads faster. + if (size > 0 && size < blockSize && (out->empty() || size < out->size())) { + out->assign(data, data + size); + } + }; + + if (libdeflate_) { + // libdeflate returns 0 when the result doesn't fit, i.e. isn't smaller. + const size_t size = libdeflate_deflate_compress(libdeflate_, in, blockSize, scratch_.data(), scratch_.size()); + submit(scratch_.data(), size); + } + if (zlibOk_ && deflateReset(&z_) == Z_OK) { + z_.next_in = (Bytef *)in; + z_.avail_in = blockSize; + z_.next_out = scratch_.data(); + z_.avail_out = (uInt)scratch_.size(); + int res; + while ((res = deflate(&z_, Z_FINISH)) == Z_OK) { + continue; + } + if (res == Z_STREAM_END) { + submit(scratch_.data(), z_.total_out); + } + } + } + +private: + PresetSettings settings_; + std::vector scratch_; + z_stream z_{}; + bool zlibOk_ = false; + libdeflate_compressor *libdeflate_ = nullptr; +}; + +// Opens anything PPSSPP plays as a disc, as 2048-byte sectors. +struct Source { + std::unique_ptr loader; + std::unique_ptr device; + uint64_t size = 0; + + bool Open(const Path &path, std::string *error) { + loader.reset(ConstructFileLoader(path)); + if (!loader || !loader->Exists()) { + *error = "Could not open the game"; + return false; + } + device.reset(ConstructBlockDevice(loader.get(), error)); + if (!device) { + return false; + } + size = (uint64_t)device->GetNumBlocks() * SECTOR_SIZE; + if (device->GetUncompressedSize() != size) { + *error = "The image isn't a whole number of sectors"; + return false; + } + if (size == 0) { + *error = "The image is empty"; + return false; + } + return true; + } +}; + +bool WriteAll(FILE *f, const void *data, size_t size) { + return size == 0 || fwrite(data, 1, size, f) == size; +} + +// Fails a conversion, removing what it wrote. +bool Fail(FILE *&f, const Path &output, std::string *error, const char *message) { + if (f) { + fclose(f); + f = nullptr; + } + File::Delete(output, true); + if (message) { + *error = message; + } + return false; +} + +} // namespace + +bool Compress(const Path &input, const Path &output, Preset preset, const ProgressFunc &progress, std::string *error, bool *cancelled) { + *cancelled = false; + if (File::Exists(output)) { + *error = "Output file already exists"; + return false; + } + Source src; + if (!src.Open(input, error)) { + return false; + } + + const PresetSettings settings = SettingsFor(preset); + const uint32_t blockSize = settings.blockSize; + const uint32_t sectorsPerBlock = blockSize / SECTOR_SIZE; + const uint32_t numBlocks = (uint32_t)((src.size + blockSize - 1) / blockSize); + const uint32_t numSectors = src.device->GetNumBlocks(); + + // The index holds offsets >> shift in 31 bits. Like maxcso, size it for the worst case, every + // block stored uncompressed. + const uint64_t headerSize = 24; + const uint64_t dataStart = headerSize + ((uint64_t)numBlocks + 1) * 4; + const uint64_t worst = dataStart + src.size; + uint8_t indexShift = 0; + for (int i = 62; i >= 31; --i) { + if (worst >= (1ULL << i)) { + indexShift = (uint8_t)(i + 1 - 31); + break; + } + } + const uint64_t align = 1ULL << indexShift; + auto alignUp = [align](uint64_t pos) { + return (pos + align - 1) & ~(align - 1); + }; + + FILE *f = File::OpenCFile(output, "wb"); + if (!f) { + *error = "Could not create the output file"; + return false; + } + std::vector index(numBlocks + 1); + // Header and index go in at the end, once the offsets are known. + std::vector zeros((size_t)(alignUp(dataStart)), 0); + if (!WriteAll(f, zeros.data(), zeros.size())) { + return Fail(f, output, error, "Could not write the output file"); + } + uint64_t pos = zeros.size(); + + const unsigned threads = std::max(1u, std::thread::hardware_concurrency()); + const uint32_t batchBlocks = std::max(threads * 4, (uint32_t)(BATCH_BYTES / blockSize)); + std::vector in((size_t)batchBlocks * blockSize); + std::vector> out(batchBlocks); + static const u8 padding[2048]{}; + + INFO_LOG(Log::Loader, "CSOCompress: %s -> %s, %u blocks of %u, %u threads", input.c_str(), output.c_str(), numBlocks, blockSize, threads); + for (uint32_t first = 0; first < numBlocks; first += batchBlocks) { + if (progress && !progress((float)first / (float)numBlocks)) { + *cancelled = true; + return Fail(f, output, error, "Cancelled"); + } + const uint32_t count = std::min(batchBlocks, numBlocks - first); + + // The last block past the end of the image is zero-padded, like maxcso does. + const uint32_t firstSector = first * sectorsPerBlock; + const uint32_t sectors = std::min(count * sectorsPerBlock, numSectors - firstSector); + memset(in.data() + (size_t)sectors * SECTOR_SIZE, 0, ((size_t)count * sectorsPerBlock - sectors) * SECTOR_SIZE); + if (!src.device->ReadBlocks(firstSector, (int)sectors, in.data())) { + return Fail(f, output, error, "Could not read the game"); + } + + std::atomic next{0}; + auto work = [&]() { + BlockCompressor compressor(settings); + uint32_t i; + while ((i = next++) < count) { + compressor.Compress(in.data() + (size_t)i * blockSize, &out[i]); + } + }; + std::vector pool; + const unsigned workers = std::min(threads, count); + for (unsigned t = 1; t < workers; ++t) { + pool.emplace_back([&]() { + SetCurrentThreadName("CSOCompress"); + work(); + }); + } + work(); + for (std::thread &t : pool) { + t.join(); + } + + for (uint32_t i = 0; i < count; ++i) { + // Not smaller once padded to the index alignment (images over 2 GB): store it. + const bool stored = out[i].empty() || alignUp(out[i].size()) >= blockSize; + const u8 *data = stored ? in.data() + (size_t)i * blockSize : out[i].data(); + const size_t size = stored ? blockSize : out[i].size(); + index[first + i] = (u32)(pos >> indexShift) | (stored ? CSO_INDEX_UNCOMPRESSED : 0); + if (!WriteAll(f, data, size)) { + return Fail(f, output, error, "Could not write the output file"); + } + pos += size; + const uint64_t aligned = alignUp(pos); + if (!WriteAll(f, padding, (size_t)(aligned - pos))) { + return Fail(f, output, error, "Could not write the output file"); + } + pos = aligned; + } + } + index[numBlocks] = (u32)(pos >> indexShift); + + // The header: "CISO", its size, the image size, the block size, version 1 and the shift. + u8 header[24]{}; + memcpy(header, "CISO", 4); + const u32_le headerSizeLE = (u32)headerSize; + const u64_le sizeLE = src.size; + const u32_le blockSizeLE = blockSize; + memcpy(header + 4, &headerSizeLE, 4); + memcpy(header + 8, &sizeLE, 8); + memcpy(header + 16, &blockSizeLE, 4); + header[20] = 1; + header[21] = indexShift; + if (File::Fseek(f, 0, SEEK_SET) != 0 || !WriteAll(f, header, sizeof(header)) || !WriteAll(f, index.data(), index.size() * sizeof(u32_le))) { + return Fail(f, output, error, "Could not write the output file"); + } + if (fclose(f) != 0) { + f = nullptr; + return Fail(f, output, error, "Could not write the output file"); + } + if (progress) { + progress(1.0f); + } + return true; +} + +bool Extract(const Path &input, const Path &output, const ProgressFunc &progress, std::string *error, bool *cancelled) { + *cancelled = false; + if (File::Exists(output)) { + *error = "Output file already exists"; + return false; + } + Source src; + if (!src.Open(input, error)) { + return false; + } + FILE *f = File::OpenCFile(output, "wb"); + if (!f) { + *error = "Could not create the output file"; + return false; + } + + const uint32_t numSectors = src.device->GetNumBlocks(); + const uint32_t batch = (uint32_t)(BATCH_BYTES / SECTOR_SIZE); + std::vector buffer((size_t)batch * SECTOR_SIZE); + for (uint32_t first = 0; first < numSectors; first += batch) { + if (progress && !progress((float)first / (float)numSectors)) { + *cancelled = true; + return Fail(f, output, error, "Cancelled"); + } + const uint32_t count = std::min(batch, numSectors - first); + if (!src.device->ReadBlocks(first, (int)count, buffer.data())) { + return Fail(f, output, error, "Could not read the game"); + } + if (!WriteAll(f, buffer.data(), (size_t)count * SECTOR_SIZE)) { + return Fail(f, output, error, "Could not write the output file"); + } + } + if (fclose(f) != 0) { + f = nullptr; + return Fail(f, output, error, "Could not write the output file"); + } + if (progress) { + progress(1.0f); + } + return true; +} + +} // namespace CSOCompress diff --git a/Core/FileSystems/CSOCompress.h b/Core/FileSystems/CSOCompress.h new file mode 100644 index 0000000000..16247124d4 --- /dev/null +++ b/Core/FileSystems/CSOCompress.h @@ -0,0 +1,43 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#pragma once + +#include +#include + +#include "Common/File/Path.h" + +// Writes PSP disc images as CSO v1 or plain ISO. The input is anything PPSSPP can play as a disc +// (ISO, CSO, CHD, a PSN EBOOT.PBP), read through its own block devices, so a repack is one pass +// with no temporary file. The presets match maxcso's (LunaMoo/maxcso). +namespace CSOCompress { + +enum class Preset { + Best = 0, // libdeflate level 12, 64K blocks. Small, fast to compress and quick to read. + PSPCompatible = 1, // zlib level 9, 2K blocks: what every PSP custom firmware reads. +}; + +// Return false to cancel. +typedef std::function ProgressFunc; + +// Blocking, and uses every core. Never overwrites, and deletes a partial output on failure or +// cancel (*cancelled is set then). +bool Compress(const Path &input, const Path &output, Preset preset, const ProgressFunc &progress, std::string *error, bool *cancelled); +bool Extract(const Path &input, const Path &output, const ProgressFunc &progress, std::string *error, bool *cancelled); + +} // namespace CSOCompress diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index a9f0b09ad5..9354bf4e16 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -63,6 +63,8 @@ list(APPEND UISource TabbedDialogScreen.cpp GameScreen.h GameScreen.cpp + GamePacker.h + GamePacker.cpp GameSettingsScreen.h GameSettingsScreen.cpp DeveloperToolsScreen.h diff --git a/UI/GameBrowser.cpp b/UI/GameBrowser.cpp index 579518a9c1..486edcae11 100644 --- a/UI/GameBrowser.cpp +++ b/UI/GameBrowser.cpp @@ -41,6 +41,7 @@ #include "Core/Util/VideoPlayer.h" #include "UI/GameBrowser.h" +#include "UI/GamePacker.h" #include "UI/RemoteISOScreen.h" #include "UI/SavedataScreen.h" #include "UI/Store.h" @@ -660,6 +661,20 @@ void GameButton::Draw(UIContext &dc) { dc.FillRect(Drawable(overlayColor), overlayBounds); } + // Covers the icon while the game is being compressed. Clicking opens the game screen instead of playing. + GamePacker::Status packStatus; + if (GamePacker::IsPacking(gamePath_) && GamePacker::GetStatus(&packStatus)) { + auto ga = GetI18NCategory(I18NCat::GAME); + const Bounds iconBounds(x, y, w, h); + dc.FillRect(Drawable(0xD0505050), iconBounds); + const float scale = gridStyle_ || largeList ? 0.8f * g_Config.fGameGridScale : 0.6f; + dc.SetFontStyle(dc.GetTheme().uiFont); + dc.SetFontScale(scale, scale); + const std::string text = std::string(ga->T("Compressing...")) + StringFromFormat("\n%d%%", (int)(packStatus.progress * 100.0f)); + dc.DrawTextRectSqueeze(text, iconBounds, 0xFFFFFFFF, ALIGN_CENTER); + dc.SetFontScale(1.0f, 1.0f); + } + dc.RebindTexture(); } diff --git a/UI/GamePacker.cpp b/UI/GamePacker.cpp new file mode 100644 index 0000000000..83c146ce5f --- /dev/null +++ b/UI/GamePacker.cpp @@ -0,0 +1,478 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#include +#include +#include +#include + +#include "Common/Data/Color/RGBAUtil.h" +#include "Common/Data/Text/I18n.h" +#include "Common/Data/Text/Parsers.h" +#include "Common/File/FileUtil.h" +#include "Common/Log.h" +#include "Common/StringUtils.h" +#include "Common/System/OSD.h" +#include "Common/Thread/ThreadUtil.h" +#include "Common/TimeUtil.h" +#include "Common/UI/Context.h" + +#include "Core/Config.h" +#include "Core/System.h" +#include "Core/Util/RecentFiles.h" + +#include "UI/BackgroundAudio.h" +#include "UI/GameInfoCache.h" +#include "UI/GamePacker.h" + +namespace GamePacker { +namespace { + +struct Job { + Path gamePath; // As the game list knows it, maybe a PSP/GAME/. + Path file; // The image itself. + Path finalOutput; // Where the converted game ends up. + Path writeOutput; // What gets written: finalOutput, or a temporary name when that's the original's. + bool replace = false; // Delete the original when done. + bool toISO = false; + CSOCompress::Preset preset = CSOCompress::Preset::Best; + + std::atomic progress{0.0f}; + double startTime = 0.0; + std::atomic cancel{false}; + std::atomic done{false}; + bool success = false; // Set before done. + bool cancelled = false; // Set before done. + std::string error; // Set before done. + + std::thread thread; +}; + +// Only touched on the UI thread. The worker gets the raw pointer, and the job lives until it's joined. +std::unique_ptr g_job; +int g_generation = 0; +Result g_lastResult; + +void RunJob(Job *job) { + SetCurrentThreadName("GamePacker"); + INFO_LOG(Log::Loader, "GamePacker: %s %s -> %s", job->toISO ? "Extracting" : "Compressing", job->file.c_str(), job->writeOutput.c_str()); + + auto progress = [job](float p) { + job->progress = p; + return !job->cancel; + }; + std::string error; + bool cancelled = false; + bool success; + if (job->toISO) { + success = CSOCompress::Extract(job->file, job->writeOutput, progress, &error, &cancelled); + } else { + success = CSOCompress::Compress(job->file, job->writeOutput, job->preset, progress, &error, &cancelled); + } + if (!success && !cancelled) { + ERROR_LOG(Log::Loader, "GamePacker: Failed on %s: %s", job->file.c_str(), error.c_str()); + } + job->success = success; + job->cancelled = cancelled; + job->error = error; + job->done = true; +} + +// "" for ".", or for an EBOOT.PBP the game's disc id. +std::string OutputBaseName(const Path &gamePath, const Path &file) { + std::string name = file.GetFilename(); + const std::string ext = file.GetFileExtension(); + if (ext.size() <= name.size()) { + name.resize(name.size() - ext.size()); + } + if (equalsNoCase(file.GetFilename(), "EBOOT.PBP")) { + std::shared_ptr info = g_gameInfoCache->GetInfo(nullptr, gamePath, GameInfoFlags::PARAM_SFO); + if (info->Ready(GameInfoFlags::PARAM_SFO)) { + const std::string id = SanitizeString(info->id, StringRestriction::FileName); + if (!id.empty()) { + return id; + } + } + } + return name; +} + +// Where an option puts its result, before looking at what's already there. +Path FinalOutput(const Path &gamePath, const Path &file, Container from, const Option &option) { + const Container to = option.toISO ? Container::ISO : Container::CSO; + if (to == from) { + return file; + } + return file.NavigateUp() / (OutputBaseName(gamePath, file) + (option.toISO ? ".iso" : ".cso")); +} + +// " (2).", " (3)."... for a name that's taken. +Path NumberedName(const Path &path) { + const std::string ext = path.GetFileExtension(); + std::string name = path.GetFilename(); + name.resize(name.size() - ext.size()); + for (int i = 2; i < 1000; i++) { + Path candidate = path.NavigateUp() / StringFromFormat("%s (%d)%s", name.c_str(), i, ext.c_str()); + if (!File::Exists(candidate)) { + return candidate; + } + } + return Path(); +} + +} // namespace + +Container DetectContainer(const Path &gamePath, Path *file) { + // Content URIs are fine too (Android's storage), only remote games aren't. + if (gamePath.Type() == PathType::HTTP) { + return Container::None; + } + Path image = gamePath; + if (File::IsDirectory(gamePath)) { + image = gamePath / "EBOOT.PBP"; + if (!File::Exists(image)) { + return Container::None; + } + } + const std::string ext = image.GetFileExtension(); + Container container = Container::None; + if (ext == ".iso") { + container = Container::ISO; + } else if (ext == ".cso") { + container = Container::CSO; + } else if (ext == ".chd") { + container = Container::CHD; + } else if (ext == ".pbp") { + container = Container::PBP; + } + if (container != Container::None && file) { + *file = image; + } + return container; +} + +std::vector