From aeadcfcf90e987188722f3e163e861038f9b5401 Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 30 Nov 2024 16:49:20 -0300 Subject: [PATCH] Add installer wizard. --- UnleashedRecomp/CMakeLists.txt | 13 + UnleashedRecomp/gpu/video.cpp | 112 +-- UnleashedRecomp/gpu/video.h | 9 +- UnleashedRecomp/install/installer.cpp | 124 +++- UnleashedRecomp/install/installer.h | 29 +- UnleashedRecomp/main.cpp | 35 +- UnleashedRecomp/ui/installer_wizard.cpp | 902 ++++++++++++++++++++++++ UnleashedRecomp/ui/installer_wizard.h | 13 + UnleashedRecomp/ui/options_menu.cpp | 3 +- UnleashedRecomp/ui/window.cpp | 2 +- UnleashedRecomp/ui/window.h | 11 +- UnleashedRecompResources | 2 +- vcpkg.json | 3 +- 13 files changed, 1158 insertions(+), 100 deletions(-) create mode 100644 UnleashedRecomp/ui/installer_wizard.cpp create mode 100644 UnleashedRecomp/ui/installer_wizard.h diff --git a/UnleashedRecomp/CMakeLists.txt b/UnleashedRecomp/CMakeLists.txt index 5d8972f1..303210cc 100644 --- a/UnleashedRecomp/CMakeLists.txt +++ b/UnleashedRecomp/CMakeLists.txt @@ -85,6 +85,7 @@ set(SWA_PATCHES_CXX_SOURCES set(SWA_UI_CXX_SOURCES "ui/achievement_menu.cpp" "ui/achievement_overlay.cpp" + "ui/installer_wizard.cpp" "ui/options_menu.cpp" "ui/sdl_listener.cpp" "ui/window.cpp" @@ -106,6 +107,16 @@ set(SWA_INSTALL_CXX_SOURCES "install/hashes/update.cpp" ) +set(INSTALLER_IMAGES_DIR "../UnleashedRecompResources/images/installer") +BIN2H(SOURCE_FILE "${INSTALLER_IMAGES_DIR}/install_001.dds" HEADER_FILE "res/install_001_dds.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_install001DDS") +BIN2H(SOURCE_FILE "${INSTALLER_IMAGES_DIR}/install_002.dds" HEADER_FILE "res/install_002_dds.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_install002DDS") +BIN2H(SOURCE_FILE "${INSTALLER_IMAGES_DIR}/install_003.dds" HEADER_FILE "res/install_003_dds.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_install003DDS") +BIN2H(SOURCE_FILE "${INSTALLER_IMAGES_DIR}/install_004.dds" HEADER_FILE "res/install_004_dds.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_install004DDS") +BIN2H(SOURCE_FILE "${INSTALLER_IMAGES_DIR}/install_005.dds" HEADER_FILE "res/install_005_dds.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_install005DDS") +BIN2H(SOURCE_FILE "${INSTALLER_IMAGES_DIR}/install_006.dds" HEADER_FILE "res/install_006_dds.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_install006DDS") +BIN2H(SOURCE_FILE "${INSTALLER_IMAGES_DIR}/install_007.dds" HEADER_FILE "res/install_007_dds.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_install007DDS") +BIN2H(SOURCE_FILE "${INSTALLER_IMAGES_DIR}/install_008.dds" HEADER_FILE "res/install_008_dds.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_install008DDS") + set(LIBMSPACK_PATH ${SWA_THIRDPARTY_ROOT}/libmspack/libmspack/mspack) set(LIBMSPACK_C_SOURCES @@ -172,6 +183,7 @@ find_package(imgui CONFIG REQUIRED) find_package(magic_enum CONFIG REQUIRED) find_package(unofficial-tiny-aes-c CONFIG REQUIRED) find_path(READERWRITERQUEUE_INCLUDE_DIRS "readerwriterqueue/atomicops.h") +find_package(nfd CONFIG REQUIRED) file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/D3D12) add_custom_command(TARGET UnleashedRecomp POST_BUILD @@ -209,6 +221,7 @@ target_link_libraries(UnleashedRecomp PRIVATE imgui::imgui magic_enum::magic_enum unofficial::tiny-aes-c::tiny-aes-c + nfd::nfd ) target_include_directories(UnleashedRecomp PRIVATE diff --git a/UnleashedRecomp/gpu/video.cpp b/UnleashedRecomp/gpu/video.cpp index abed18dd..84944944 100644 --- a/UnleashedRecomp/gpu/video.cpp +++ b/UnleashedRecomp/gpu/video.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include "imgui_snapshot.h" #include "imgui_common.h" @@ -1066,6 +1067,7 @@ static void CreateImGuiBackend() AchievementMenu::Init(); AchievementOverlay::Init(); OptionsMenu::Init(); + InstallerWizard::Init(); ImGui_ImplSDL2_InitForOther(Window::s_pWindow); @@ -1174,7 +1176,9 @@ static void CreateImGuiBackend() g_imPipeline = g_device->createGraphicsPipeline(pipelineDesc); } -static void CreateHostDevice() +static void BeginCommandList(); + +void Video::CreateHostDevice() { for (uint32_t i = 0; i < 16; i++) g_inputSlots[i].index = i; @@ -1364,17 +1368,19 @@ static void CreateHostDevice() desc.renderTargetCount = 1; g_gammaCorrectionPipeline = g_device->createGraphicsPipeline(desc); - g_xdbfTextureCache = std::unordered_map(); + g_backBuffer = g_userHeap.AllocPhysical(ResourceType::RenderTarget); + g_backBuffer->width = 1280; + g_backBuffer->height = 720; + g_backBuffer->format = BACKBUFFER_FORMAT; + g_backBuffer->textureHolder = g_device->createTexture(RenderTextureDesc::Texture2D(1, 1, 1, BACKBUFFER_FORMAT, RenderTextureFlag::RENDER_TARGET)); - for (auto& achievement : g_xdbfWrapper.GetAchievements(XDBF_LANGUAGE_ENGLISH)) - { - // huh? - if (!achievement.pImageBuffer || !achievement.ImageBufferSize) - continue; + BeginCommandList(); - g_xdbfTextureCache[achievement.ID] = - LoadTexture((uint8_t*)achievement.pImageBuffer, achievement.ImageBufferSize).release(); - } + RenderTextureBarrier blankTextureBarriers[TEXTURE_DESCRIPTOR_NULL_COUNT]; + for (size_t i = 0; i < TEXTURE_DESCRIPTOR_NULL_COUNT; i++) + blankTextureBarriers[i] = RenderTextureBarrier(g_blankTextures[i].get(), RenderTextureLayout::SHADER_READ); + + g_commandLists[g_frame]->barriers(RenderBarrierStage::NONE, blankTextureBarriers, std::size(blankTextureBarriers)); } static void WaitForGPU() @@ -1398,12 +1404,11 @@ static void WaitForGPU() } } -static bool g_pendingRenderThread; +static std::atomic g_pendingRenderThread; static void WaitForRenderThread() { - while (g_pendingRenderThread) - Sleep(0); + g_pendingRenderThread.wait(true); } static void BeginCommandList() @@ -1491,21 +1496,17 @@ static void BeginCommandList() static uint32_t CreateDevice(uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5, be* a6) { - CreateHostDevice(); + g_xdbfTextureCache = std::unordered_map(); - g_backBuffer = g_userHeap.AllocPhysical(ResourceType::RenderTarget); - g_backBuffer->width = 1280; - g_backBuffer->height = 720; - g_backBuffer->format = BACKBUFFER_FORMAT; - g_backBuffer->textureHolder = g_device->createTexture(RenderTextureDesc::Texture2D(1, 1, 1, BACKBUFFER_FORMAT, RenderTextureFlag::RENDER_TARGET)); + for (auto &achievement : g_xdbfWrapper.GetAchievements(XDBF_LANGUAGE_ENGLISH)) + { + // huh? + if (!achievement.pImageBuffer || !achievement.ImageBufferSize) + continue; - BeginCommandList(); - - RenderTextureBarrier blankTextureBarriers[TEXTURE_DESCRIPTOR_NULL_COUNT]; - for (size_t i = 0; i < TEXTURE_DESCRIPTOR_NULL_COUNT; i++) - blankTextureBarriers[i] = RenderTextureBarrier(g_blankTextures[i].get(), RenderTextureLayout::SHADER_READ); - - g_commandLists[g_frame]->barriers(RenderBarrierStage::NONE, blankTextureBarriers, std::size(blankTextureBarriers)); + g_xdbfTextureCache[achievement.ID] = + LoadTexture((uint8_t *)achievement.pImageBuffer, achievement.ImageBufferSize).release(); + } auto device = g_userHeap.AllocPhysical(); memset(device, 0, sizeof(*device)); @@ -1739,6 +1740,7 @@ static void DrawImGui() AchievementMenu::Draw(); OptionsMenu::Draw(); AchievementOverlay::Draw(); + InstallerWizard::Draw(); ImGui::Render(); @@ -1753,8 +1755,17 @@ static void DrawImGui() } } +static void SetFramebuffer(GuestSurface *renderTarget, GuestSurface *depthStencil, bool settingForClear); +static void FlushViewport(); + static void ProcDrawImGui(const RenderCommand& cmd) { + // Make sure the backbuffer is the current target. + AddBarrier(g_backBuffer, RenderTextureLayout::COLOR_WRITE); + FlushBarriers(); + SetFramebuffer(g_backBuffer, nullptr, false); + FlushViewport(); + auto& commandList = g_commandLists[g_frame]; commandList->setGraphicsPipelineLayout(g_imPipelineLayout.get()); @@ -1828,21 +1839,21 @@ static void ProcDrawImGui(const RenderCommand& cmd) } } -static bool g_precompiledPipelineStateCache = false; +static bool g_shouldPrecompilePipelines = false; -static void Present() +void Video::HostPresent() { - DrawImGui(); WaitForRenderThread(); + DrawImGui(); - g_pendingRenderThread = true; + g_pendingRenderThread.store(true); RenderCommand cmd; cmd.type = RenderCommandType::Present; g_renderQueue.enqueue(cmd); // All the shaders are available at this point. We can precompile embedded PSOs then. - if (!g_precompiledPipelineStateCache) + if (g_shouldPrecompilePipelines) { // This is all the model consumer thread needs to see. ++g_compilingDataCount; @@ -1850,10 +1861,20 @@ static void Present() if ((++g_pendingDataCount) == 1) g_pendingDataCount.notify_all(); - g_precompiledPipelineStateCache = true; + g_shouldPrecompilePipelines = false; } } +void Video::StartPipelinePrecompilation() +{ + g_shouldPrecompilePipelines = true; +} + +static void GuestPresent() +{ + Video::HostPresent(); +} + static void SetRootDescriptor(const UploadAllocation& allocation, size_t index) { auto& commandList = g_commandLists[g_frame]; @@ -1899,11 +1920,11 @@ static void ProcPresent(const RenderCommand& cmd) constants.gammaB = 1.0f / std::clamp(constants.gammaB + offset, 0.1f, 4.0f); constants.textureDescriptorIndex = g_intermediaryBackBufferTextureDescriptorIndex; - auto& framebuffer = g_backBuffer->framebuffers[swapChainTexture]; + auto &framebuffer = g_backBuffer->framebuffers[swapChainTexture]; if (!framebuffer) { RenderFramebufferDesc desc; - desc.colorAttachments = const_cast(&swapChainTexture); + desc.colorAttachments = const_cast(&swapChainTexture); desc.colorAttachmentsCount = 1; framebuffer = g_device->createFramebuffer(desc); } @@ -1914,7 +1935,7 @@ static void ProcPresent(const RenderCommand& cmd) RenderTextureBarrier(swapChainTexture, RenderTextureLayout::COLOR_WRITE) }; - auto& commandList = g_commandLists[g_frame]; + auto &commandList = g_commandLists[g_frame]; commandList->barriers(RenderBarrierStage::GRAPHICS, srcBarriers, std::size(srcBarriers)); commandList->setGraphicsPipelineLayout(g_pipelineLayout.get()); commandList->setPipeline(g_gammaCorrectionPipeline.get()); @@ -1933,14 +1954,14 @@ static void ProcPresent(const RenderCommand& cmd) } } - auto& commandList = g_commandLists[g_frame]; + auto &commandList = g_commandLists[g_frame]; commandList->end(); if (g_swapChainValid) { - const RenderCommandList* commandLists[] = { commandList.get() }; - RenderCommandSemaphore* waitSemaphores[] = { g_acquireSemaphores[g_frame].get() }; - RenderCommandSemaphore* signalSemaphores[] = { g_renderSemaphores[g_frame].get() }; + const RenderCommandList *commandLists[] = { commandList.get() }; + RenderCommandSemaphore *waitSemaphores[] = { g_acquireSemaphores[g_frame].get() }; + RenderCommandSemaphore *signalSemaphores[] = { g_renderSemaphores[g_frame].get() }; g_queue->executeCommandLists( commandLists, std::size(commandLists), @@ -1974,7 +1995,8 @@ static void ProcPresent(const RenderCommand& cmd) BeginCommandList(); - g_pendingRenderThread = false; + g_pendingRenderThread.store(false); + g_pendingRenderThread.notify_all(); } static GuestSurface* GetBackBuffer() @@ -4120,10 +4142,10 @@ static RenderFormat ConvertDXGIFormat(ddspp::DXGIFormat format) } } -static bool LoadTexture(GuestTexture& texture, uint8_t* data, size_t dataSize) +static bool LoadTexture(GuestTexture& texture, const uint8_t* data, size_t dataSize) { ddspp::Descriptor ddsDesc; - if (ddspp::decode_header(data, ddsDesc) != ddspp::Error) + if (ddspp::decode_header((unsigned char *)(data), ddsDesc) != ddspp::Error) { RenderTextureDesc desc; desc.dimension = ConvertTextureDimension(ddsDesc.type); @@ -4191,7 +4213,7 @@ static bool LoadTexture(GuestTexture& texture, uint8_t* data, size_t dataSize) for (auto& slice : slices) { - uint8_t* srcData = data + ddsDesc.headerSize + slice.srcOffset; + const uint8_t* srcData = data + ddsDesc.headerSize + slice.srcOffset; uint8_t* dstData = mappedMemory + slice.dstOffset; if (slice.srcRowPitch == slice.dstRowPitch) @@ -4284,7 +4306,7 @@ static bool LoadTexture(GuestTexture& texture, uint8_t* data, size_t dataSize) return false; } -std::unique_ptr LoadTexture(uint8_t* data, size_t dataSize) +std::unique_ptr LoadTexture(const uint8_t* data, size_t dataSize) { GuestTexture texture(ResourceType::Texture); @@ -5531,7 +5553,7 @@ GUEST_FUNCTION_HOOK(sub_82BE96F0, GetSurfaceDesc); GUEST_FUNCTION_HOOK(sub_82BE04B0, GetVertexDeclaration); GUEST_FUNCTION_HOOK(sub_82BE0530, HashVertexDeclaration); -GUEST_FUNCTION_HOOK(sub_82BDA8C0, Present); +GUEST_FUNCTION_HOOK(sub_82BDA8C0, GuestPresent); GUEST_FUNCTION_HOOK(sub_82BDD330, GetBackBuffer); GUEST_FUNCTION_HOOK(sub_82BE9498, CreateTexture); diff --git a/UnleashedRecomp/gpu/video.h b/UnleashedRecomp/gpu/video.h index 2c237e6c..0c27f8a4 100644 --- a/UnleashedRecomp/gpu/video.h +++ b/UnleashedRecomp/gpu/video.h @@ -10,6 +10,13 @@ using namespace plume; +struct Video +{ + static void CreateHostDevice(); + static void HostPresent(); + static void StartPipelinePrecompilation(); +}; + struct GuestSamplerState { be data[6]; @@ -379,6 +386,6 @@ enum GuestTextureAddress D3DTADDRESS_BORDER = 6 }; -extern std::unique_ptr LoadTexture(uint8_t* data, size_t dataSize); +extern std::unique_ptr LoadTexture(const uint8_t* data, size_t dataSize); extern void VideoConfigValueChangedCallback(class IConfigDef* config); diff --git a/UnleashedRecomp/install/installer.cpp b/UnleashedRecomp/install/installer.cpp index 0058b2e0..7029d92d 100644 --- a/UnleashedRecomp/install/installer.cpp +++ b/UnleashedRecomp/install/installer.cpp @@ -66,7 +66,7 @@ static std::unique_ptr createFileSystemFromPath(const std::fi } } -static bool copyFile(const FilePair &pair, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, bool skipHashChecks, std::vector &fileData, Journal &journal, const std::function &progressCallback) { +static bool copyFile(const FilePair &pair, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, bool skipHashChecks, std::vector &fileData, Journal &journal, const std::function &progressCallback) { const std::string filename(pair.first); const uint32_t hashCount = pair.second; if (!sourceVfs.exists(filename)) @@ -136,7 +136,8 @@ static bool copyFile(const FilePair &pair, const uint64_t *fileHashes, VirtualFi return false; } - progressCallback(++journal.progressCounter); + journal.progressCounter += fileData.size(); + progressCallback(); return true; } @@ -200,7 +201,25 @@ bool Installer::checkGameInstall(const std::filesystem::path &baseDirectory) return std::filesystem::exists(baseDirectory / GameDirectory / GameExecutableFile); } -bool Installer::copyFiles(std::span filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, const std::string &validationFile, bool skipHashChecks, Journal &journal, const std::function &progressCallback) +bool Installer::computeTotalSize(std::span filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, Journal &journal, uint64_t &totalSize) +{ + for (FilePair pair : filePairs) + { + const std::string filename(pair.first); + if (!sourceVfs.exists(filename)) + { + journal.lastResult = Journal::Result::FileMissing; + journal.lastErrorMessage = std::format("File {} does not exist in the file system.", filename); + return false; + } + + totalSize += sourceVfs.getSize(filename); + } + + return true; +} + +bool Installer::copyFiles(std::span filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, const std::string &validationFile, bool skipHashChecks, Journal &journal, const std::function &progressCallback) { if (!std::filesystem::exists(targetDirectory) && !std::filesystem::create_directories(targetDirectory)) { @@ -265,45 +284,48 @@ bool Installer::parseContent(const std::filesystem::path &sourcePath, std::uniqu } } -bool Installer::install(const Input &input, const std::filesystem::path &targetDirectory, Journal &journal, const std::function &progressCallback) +constexpr uint32_t PatcherContribution = 512 * 1024 * 1024; + +bool Installer::parseSources(const Input &input, Journal &journal, Sources &sources) { + sources = Sources(); + // Parse the contents of the base game. - std::unique_ptr gameSource; if (!input.gameSource.empty()) { - if (!parseContent(input.gameSource, gameSource, journal)) + if (!parseContent(input.gameSource, sources.game, journal)) { return false; } - journal.progressTotal += GameFilesSize; + if (!computeTotalSize({ GameFiles, GameFilesSize }, GameHashes, *sources.game, journal, sources.totalSize)) + { + return false; + } } // Parse the contents of Update. - std::unique_ptr updateSource; if (!input.updateSource.empty()) { - if (!parseContent(input.updateSource, updateSource, journal)) + // Add an arbitrary progress size for the patching process. + journal.progressTotal += PatcherContribution; + + if (!parseContent(input.updateSource, sources.update, journal)) { return false; } - journal.progressTotal += UpdateFilesSize; + if (!computeTotalSize({ UpdateFiles, UpdateFilesSize }, UpdateHashes, *sources.update, journal, sources.totalSize)) + { + return false; + } } // Parse the contents of the DLC Packs. - struct DLCSource { - std::unique_ptr sourceVfs; - std::span filePairs; - const uint64_t *fileHashes = nullptr; - std::string targetSubDirectory; - }; - - std::vector dlcSources; for (const auto &path : input.dlcSources) { - dlcSources.emplace_back(); - DLCSource &dlcSource = dlcSources.back(); + sources.dlc.emplace_back(); + DLCSource &dlcSource = sources.dlc.back(); if (!parseContent(path, dlcSource.sourceVfs, journal)) { return false; @@ -346,17 +368,51 @@ bool Installer::install(const Input &input, const std::filesystem::path &targetD return false; } - journal.progressTotal += dlcSource.filePairs.size(); + if (!computeTotalSize(dlcSource.filePairs, dlcSource.fileHashes, *dlcSource.sourceVfs, journal, sources.totalSize)) + { + return false; + } } - // Install the base game. - if (!copyFiles({ GameFiles, GameFilesSize }, GameHashes, *gameSource, targetDirectory / GameDirectory, GameExecutableFile, input.skipHashChecks, journal, progressCallback)) + // Add the total size in bytes as the journal progress. + journal.progressTotal += sources.totalSize; + + return true; +} + +bool Installer::install(const Sources &sources, const std::filesystem::path &targetDirectory, bool skipHashChecks, Journal &journal, const std::function &progressCallback) +{ + // Install files in reverse order of importance. In case of a process crash or power outage, this will increase the likelihood of the installation + // missing critical files required for the game to run. These files are used as the way to detect if the game is installed. + + // Install the DLC. + if (!sources.dlc.empty()) + { + journal.createdDirectories.insert(targetDirectory / DLCDirectory); + } + + for (const DLCSource &dlcSource : sources.dlc) + { + if (!copyFiles(dlcSource.filePairs, dlcSource.fileHashes, *dlcSource.sourceVfs, targetDirectory / dlcSource.targetSubDirectory, DLCValidationFile, skipHashChecks, journal, progressCallback)) + { + return false; + } + } + + // If no game or update was specified, we're finished. This means the user was only installing the DLC. + if ((sources.game == nullptr) && (sources.update == nullptr)) + { + return true; + } + + // Install the update. + if (!copyFiles({ UpdateFiles, UpdateFilesSize }, UpdateHashes, *sources.update, targetDirectory / UpdateDirectory, UpdateExecutablePatchFile, skipHashChecks, journal, progressCallback)) { return false; } - // Install the update. - if (!copyFiles({ UpdateFiles, UpdateFilesSize }, UpdateHashes, *updateSource, targetDirectory / UpdateDirectory, UpdateExecutablePatchFile, input.skipHashChecks, journal, progressCallback)) + // Install the base game. + if (!copyFiles({ GameFiles, GameFilesSize }, GameHashes, *sources.game, targetDirectory / GameDirectory, GameExecutableFile, skipHashChecks, journal, progressCallback)) { return false; } @@ -374,6 +430,10 @@ bool Installer::install(const Input &input, const std::filesystem::path &targetD return false; } + // Update the progress with the artificial amount attributed to the patching. + journal.progressCounter += PatcherContribution; + progressCallback(); + // Replace the executable by renaming and deleting in a safe way. std::error_code ec; std::filesystem::path oldXexPath = targetDirectory / GameDirectory / (GameExecutableFile + OldExtension); @@ -396,20 +456,6 @@ bool Installer::install(const Input &input, const std::filesystem::path &targetD std::filesystem::remove(oldXexPath); - // Install the DLC. - if (!dlcSources.empty()) - { - journal.createdDirectories.insert(targetDirectory / DLCDirectory); - } - - for (const DLCSource &dlcSource : dlcSources) - { - if (!copyFiles(dlcSource.filePairs, dlcSource.fileHashes, *dlcSource.sourceVfs, targetDirectory / dlcSource.targetSubDirectory, DLCValidationFile, input.skipHashChecks, journal, progressCallback)) - { - return false; - } - } - return true; } diff --git a/UnleashedRecomp/install/installer.h b/UnleashedRecomp/install/installer.h index 43006146..e02e50c9 100644 --- a/UnleashedRecomp/install/installer.h +++ b/UnleashedRecomp/install/installer.h @@ -13,7 +13,8 @@ enum class DLC { Mazuri, Holoska, ApotosShamar, - EmpireCityAdabat + EmpireCityAdabat, + Count = EmpireCityAdabat }; struct Journal @@ -35,8 +36,8 @@ struct Journal UnknownDLCType }; - uint32_t progressCounter = 0; - uint32_t progressTotal = 0; + uint64_t progressCounter = 0; + uint64_t progressTotal = 0; std::list createdFiles; std::set createdDirectories; Result lastResult = Result::Success; @@ -53,13 +54,29 @@ struct Installer std::filesystem::path gameSource; std::filesystem::path updateSource; std::list dlcSources; - bool skipHashChecks = false; + }; + + struct DLCSource { + std::unique_ptr sourceVfs; + std::span filePairs; + const uint64_t *fileHashes = nullptr; + std::string targetSubDirectory; + }; + + struct Sources + { + std::unique_ptr game; + std::unique_ptr update; + std::vector dlc; + uint64_t totalSize = 0; }; static bool checkGameInstall(const std::filesystem::path &baseDirectory); - static bool copyFiles(std::span filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, const std::string &validationFile, bool skipHashChecks, Journal &journal, const std::function &progressCallback); + static bool computeTotalSize(std::span filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, Journal &journal, uint64_t &totalSize); + static bool copyFiles(std::span filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, const std::string &validationFile, bool skipHashChecks, Journal &journal, const std::function &progressCallback); static bool parseContent(const std::filesystem::path &sourcePath, std::unique_ptr &targetVfs, Journal &journal); - static bool install(const Input &input, const std::filesystem::path &targetDirectory, Journal &journal, const std::function &progressCallback); + static bool parseSources(const Input &input, Journal &journal, Sources &sources); + static bool install(const Sources &sources, const std::filesystem::path &targetDirectory, bool skipHashChecks, Journal &journal, const std::function &progressCallback); static void rollback(Journal &journal); // Convenience method for checking if the specified file contains the game. This should be used when the user selects the file. diff --git a/UnleashedRecomp/main.cpp b/UnleashedRecomp/main.cpp index af7c5e38..5f853261 100644 --- a/UnleashedRecomp/main.cpp +++ b/UnleashedRecomp/main.cpp @@ -15,6 +15,8 @@ #include #include #include +#include +#include #define GAME_XEX_PATH "game:\\default.xex" @@ -27,8 +29,7 @@ CodeCache g_codeCache; XDBFWrapper g_xdbfWrapper; std::unordered_map g_xdbfTextureCache; -// Name inspired from nt's entry point -void KiSystemStartup() +void HostStartup() { #ifdef _WIN32 CoInitializeEx(nullptr, COINIT_MULTITHREADED); @@ -39,7 +40,11 @@ void KiSystemStartup() g_codeCache.Init(); g_memory.Alloc(XMAIOBegin, 0xFFFF, MEM_COMMIT); +} +// Name inspired from nt's entry point +void KiSystemStartup() +{ const auto gameContent = XamMakeContent(XCONTENTTYPE_RESERVED, "Game"); const auto updateContent = XamMakeContent(XCONTENTTYPE_RESERVED, "Update"); XamRegisterContent(gameContent, DirectoryExists(".\\game") ? ".\\game" : "."); @@ -136,15 +141,39 @@ uint32_t LdrLoadModule(const char* path) return entry; } -int main() +int main(int argc, char *argv[]) { + bool forceInstaller = false; + bool forceDLCInstaller = false; + for (uint32_t i = 1; i < argc; i++) + { + forceInstaller = forceInstaller || (strcmp(argv[i], "--install") == 0); + forceDLCInstaller = forceDLCInstaller || (strcmp(argv[i], "--install-dlc") == 0); + } + Config::Load(); + + HostStartup(); + + Video::CreateHostDevice(); + + bool isGameInstalled = Installer::checkGameInstall("."); + if (forceInstaller || forceDLCInstaller || !isGameInstalled) + { + if (!InstallerWizard::Run(isGameInstalled && forceDLCInstaller)) + { + return 1; + } + } + AchievementData::Load(); KiSystemStartup(); uint32_t entry = LdrLoadModule(FileSystem::TransformPath(GAME_XEX_PATH)); + Video::StartPipelinePrecompilation(); + GuestThread::Start(entry); return 0; diff --git a/UnleashedRecomp/ui/installer_wizard.cpp b/UnleashedRecomp/ui/installer_wizard.cpp new file mode 100644 index 00000000..0f7a6679 --- /dev/null +++ b/UnleashedRecomp/ui/installer_wizard.cpp @@ -0,0 +1,902 @@ +#include "installer_wizard.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#define SKIP_SOURCE_CHECKS 0 + +static constexpr double SCANLINES_ANIMATION_DURATION = 8.0; + +static constexpr double TITLE_ANIMATION_TIME = SCANLINES_ANIMATION_DURATION + 8.0; // 8 frame delay +static constexpr double TITLE_ANIMATION_DURATION = 8.0; + +static constexpr double CONTAINER_LINE_ANIMATION_DURATION = 8.0; + +static constexpr double CONTAINER_OUTER_TIME = CONTAINER_LINE_ANIMATION_DURATION + 8.0; // 8 frame delay +static constexpr double CONTAINER_OUTER_DURATION = 8.0; + +static constexpr double CONTAINER_INNER_TIME = CONTAINER_OUTER_TIME + CONTAINER_OUTER_DURATION + 8.0; // 8 frame delay +static constexpr double CONTAINER_INNER_DURATION = 8.0; + +static constexpr double CONTAINER_BACKGROUND_TIME = CONTAINER_INNER_TIME + CONTAINER_INNER_DURATION + 8.0; // 8 frame delay +static constexpr double CONTAINER_BACKGROUND_DURATION = 16.0; + +static constexpr double ALL_ANIMATIONS_FULL_DURATION = CONTAINER_BACKGROUND_TIME + CONTAINER_BACKGROUND_DURATION; + +constexpr float IMAGE_X = 140.0f; +constexpr float IMAGE_Y = 106.0f; +constexpr float IMAGE_WIDTH = 512.0f; +constexpr float IMAGE_HEIGHT = 512.0f; + +constexpr float CONTAINER_X = 510.0f; +constexpr float CONTAINER_Y = 225.0f; +constexpr float CONTAINER_WIDTH = 528.0f; +constexpr float CONTAINER_HEIGHT = 245.0f; +constexpr float SIDE_CONTAINER_WIDTH = CONTAINER_WIDTH / 2.0f; + +constexpr float BOTTOM_X_GAP = 4.0f; +constexpr float BOTTOM_Y_GAP = 4.0f; +constexpr float SOURCE_BUTTON_WIDTH = 250.0f; +constexpr float SOURCE_BUTTON_GAP = 9.0f; +constexpr float BUTTON_HEIGHT = 22.0f; +constexpr float BUTTON_TEXT_GAP = 28.0f; + +constexpr float BORDER_SIZE = 1.0f; +constexpr float BORDER_OVERSHOOT = 36.0f; +constexpr float FAKE_PROGRESS_RATIO = 0.25f; + +static constexpr size_t GRID_SIZE = 9; + +static ImFont *g_seuratFont; +static ImFont *g_dfsogeistdFont; +static ImFont *g_newRodinFont; + +static double g_appearTime = 0.0; +static double g_disappearTime = DBL_MAX; +static bool g_isDisappearing = false; +static std::filesystem::path g_gameSourcePath; +static std::filesystem::path g_updateSourcePath; +static std::array g_dlcSourcePaths; +static std::array, 8> g_installTextures; +static Journal g_installerJournal; +static Installer::Sources g_installerSources; +static uint64_t g_installerAvailableSize = 0; +static std::unique_ptr g_installerThread; +static double g_installerStartTime = 0.0; +static float g_installerProgressRatioCurrent = 0.0f; +static std::atomic g_installerProgressRatioTarget = 0.0f; +static std::atomic g_installerFinished = false; +static bool g_installerFailed = false; +static std::string g_installerErrorMessage; + +enum class WizardPage +{ + Introduction, + SelectGameAndUpdate, + SelectDLC, + CheckSpace, + Installing, + InstallSucceeded, + InstallFailed, +}; + +static WizardPage g_currentPage = WizardPage::Introduction; + +const char INSTALLER_TEXT[] = "INSTALLER"; +const char INSTALLING_TEXT[] = "INSTALLING"; +const char CREDITS_TEXT[] = "Sajid (RIP)"; + +const char *WIZARD_TEXT[] = +{ + "Welcome to Unleashed Recompiled!\n\nMake sure you have a copy of Sonic Unleashed's files for Xbox 360 before proceeding with the installation.", + "Select the files for the Game and the Update. You can use digital dumps (PIRS), a folder with the game's contents or a disc image (ISO).", + "Select the files for the DLC Packs. These can be digital dumps (PIRS) or a folder with their contents.", + "The content will be installed to the program's folder. Please confirm you have enough space available.\n\n", + "Please wait while the content is being installed...", + "Installation is complete.\n\nThis project's been brought to you by:\n\n", + "Installation has failed.\n\nError:\n\n" +}; + +static const int WIZARD_INSTALL_TEXTURE_INDEX[] = +{ + 0, + 1, + 2, + 3, + 4, + 7, // Force Werehog on InstallSucceeded. + 5 // Force Eggman on InstallFailed. +}; + +const char GAME_SOURCE_TEXT[] = "FULL GAME"; +const char UPDATE_SOURCE_TEXT[] = "UPDATE"; +const char *DLC_SOURCE_TEXT[] = +{ + "SPAGONIA", + "CHUN-NAN", + "MAZURI", + "HOLOSKA", + "APOTOS & SHAMAR", + "EMPIRE CITY & ADABAT", +}; + +const char FILES_BUTTON_TEXT[] = "ADD FILES"; +const char FOLDER_BUTTON_TEXT[] = "ADD FOLDER"; +const char NEXT_BUTTON_TEXT[] = "NEXT"; +const char SKIP_BUTTON_TEXT[] = "SKIP"; +const char REQUIRED_SPACE_TEXT[] = "Required space"; +const char AVAILABLE_SPACE_TEXT[] = "Available space"; + +static int DLCIndex(DLC dlc) +{ + assert(dlc != DLC::Unknown); + return (int)(dlc) - 1; +} + +static double ComputeMotionInstaller(double timeAppear, double timeDisappear, double offset, double total) +{ + return ComputeMotion(timeAppear, offset, total) * (1.0 - ComputeMotion(timeDisappear, ALL_ANIMATIONS_FULL_DURATION - offset - total, total)); +} + +static void DrawBackground() +{ + auto &res = ImGui::GetIO().DisplaySize; + auto drawList = ImGui::GetForegroundDrawList(); + drawList->AddRectFilled({ 0.0, 0.0 }, res, IM_COL32_BLACK); +} + +static void DrawLeftImage() +{ + int installTextureIndex = WIZARD_INSTALL_TEXTURE_INDEX[int(g_currentPage)]; + if (g_currentPage == WizardPage::Installing) + { + // Cycle through the available images while time passes during installation. + constexpr double InstallationSpeed = 1.0 / 15.0; + double installationTime = (ImGui::GetTime() - g_installerStartTime) * InstallationSpeed; + installTextureIndex += int(installationTime); + } + + double imageAlpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_BACKGROUND_TIME, CONTAINER_BACKGROUND_DURATION); + int a = std::lround(255.0 * imageAlpha); + GuestTexture *guestTexture = g_installTextures[installTextureIndex % g_installTextures.size()].get(); + auto &res = ImGui::GetIO().DisplaySize; + auto drawList = ImGui::GetForegroundDrawList(); + ImVec2 min = { Scale(IMAGE_X), Scale(IMAGE_Y) }; + ImVec2 max = { Scale(IMAGE_X + IMAGE_WIDTH), Scale(IMAGE_Y + IMAGE_HEIGHT) }; + drawList->AddImage(guestTexture, min, max, ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, a)); + + min.y = (min.y + max.y) / 2.0f; + drawList->AddRectFilledMultiColor(min, max, IM_COL32_BLACK_TRANS, IM_COL32_BLACK_TRANS, IM_COL32(0, 0, 0, 255), IM_COL32(0, 0, 0, 255)); +} + +static void DrawScanlineBars() +{ + constexpr uint32_t COLOR0 = IM_COL32(203, 255, 0, 0); + constexpr uint32_t COLOR1 = IM_COL32(203, 255, 0, 55); + constexpr uint32_t FADE_COLOR0 = IM_COL32(0, 0, 0, 255); + constexpr uint32_t FADE_COLOR1 = IM_COL32(0, 0, 0, 0); + constexpr uint32_t OUTLINE_COLOR = IM_COL32(115, 178, 104, 255); + + float height = Scale(105.0f) * ComputeMotionInstaller(g_appearTime, g_disappearTime, 0.0, SCANLINES_ANIMATION_DURATION); + + auto &res = ImGui::GetIO().DisplaySize; + auto drawList = ImGui::GetForegroundDrawList(); + + SetShaderModifier(IMGUI_SHADER_MODIFIER_SCANLINE); + + // Top bar + drawList->AddRectFilledMultiColor + ( + { 0.0f, 0.0f }, + { res.x, height }, + COLOR0, + COLOR0, + COLOR1, + COLOR1 + ); + + // Bottom bar + drawList->AddRectFilledMultiColor + ( + { res.x, res.y }, + { 0.0f, res.y - height }, + COLOR0, + COLOR0, + COLOR1, + COLOR1 + ); + + SetShaderModifier(IMGUI_SHADER_MODIFIER_NONE); + + // Installer text + // TODO: localise this. + const char *headerText = g_currentPage == WizardPage::Installing ? INSTALLING_TEXT : INSTALLER_TEXT; + int textAlpha = std::lround(255.0f * ComputeMotionInstaller(g_appearTime, g_disappearTime, TITLE_ANIMATION_TIME, TITLE_ANIMATION_DURATION)); + DrawTextWithOutline(g_dfsogeistdFont, Scale(48.0f), { Scale(122.0f), Scale(56.0f) }, IM_COL32(255, 195, 0, textAlpha), headerText, 4, IM_COL32(0, 0, 0, textAlpha)); + + // Top bar line + drawList->AddLine + ( + { 0.0f, height }, + { res.x, height }, + OUTLINE_COLOR, + Scale(1) + ); + + // Bottom bar line + drawList->AddLine + ( + { 0.0f, res.y - height }, + { res.x, res.y - height }, + OUTLINE_COLOR, + Scale(1) + ); +} + +static float AlignToNextGrid(float value) +{ + return floor(value / GRID_SIZE) * GRID_SIZE; +} + +static void DrawContainer(ImVec2 min, ImVec2 max, bool useInnerColor) +{ + double containerHeight = ComputeMotionInstaller(g_appearTime, g_disappearTime, 0.0, CONTAINER_LINE_ANIMATION_DURATION); + + float center = (min.y + max.y) / 2.0f; + min.y = Lerp(center, min.y, containerHeight); + max.y = Lerp(center, max.y, containerHeight); + + auto &res = ImGui::GetIO().DisplaySize; + auto drawList = ImGui::GetForegroundDrawList(); + + double outerAlpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_OUTER_TIME, CONTAINER_OUTER_DURATION); + double innerAlpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_INNER_TIME, CONTAINER_INNER_DURATION); + double backgroundAlpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_BACKGROUND_TIME, CONTAINER_BACKGROUND_DURATION); + + const uint32_t outerColor = IM_COL32(0, 60, 0, 128 * outerAlpha); + const uint32_t innerColor = IM_COL32(0, 40, 0, 96 * innerAlpha); + const uint32_t backgroundColor = IM_COL32(0, 60, 0, 96 * backgroundAlpha); + + float gridSize = Scale(GRID_SIZE); + drawList->AddRectFilled(min, max, backgroundColor); + SetShaderModifier(IMGUI_SHADER_MODIFIER_CHECKERBOARD); + drawList->AddRectFilled(min, max, useInnerColor ? innerColor: outerColor); + SetShaderModifier(IMGUI_SHADER_MODIFIER_NONE); + + // The draw area + drawList->PushClipRect({ min.x + gridSize * 2.0f, min.y + gridSize * 2.0f }, { max.x - gridSize * 2.0f + 1.0f, max.y - gridSize * 2.0f + 1.0f }); +} + +static void DrawDescriptionContainer() +{ + auto &res = ImGui::GetIO().DisplaySize; + auto drawList = ImGui::GetForegroundDrawList(); + + ImVec2 descriptionMin = { Scale(AlignToNextGrid(CONTAINER_X)), Scale(AlignToNextGrid(CONTAINER_Y)) }; + ImVec2 descriptionMax = { Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH)), Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT)) }; + DrawContainer(descriptionMin, descriptionMax, false); + + char descriptionText[512]; + strncpy(descriptionText, WIZARD_TEXT[int(g_currentPage)], sizeof(descriptionText) - 1); + + if (g_currentPage == WizardPage::CheckSpace) + { + constexpr double DivisorGiB = (1024.0 * 1024.0 * 1024.0); + double requiredGiB = double(g_installerSources.totalSize) / DivisorGiB; + double availableGiB = double(g_installerAvailableSize) / DivisorGiB; + snprintf(descriptionText, sizeof(descriptionText), "%s%s: %2.2f GiB\n\n%s: %2.2f GiB", WIZARD_TEXT[int(g_currentPage)], REQUIRED_SPACE_TEXT, requiredGiB, AVAILABLE_SPACE_TEXT, availableGiB); + } + else if (g_currentPage == WizardPage::InstallSucceeded) + { + strncat(descriptionText, CREDITS_TEXT, sizeof(descriptionText) - 1); + } + else if (g_currentPage == WizardPage::InstallFailed) + { + strncat(descriptionText, g_installerErrorMessage.c_str(), sizeof(descriptionText) - 1); + } + + double textAlpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_BACKGROUND_TIME, CONTAINER_BACKGROUND_DURATION); + auto clipRectMin = drawList->GetClipRectMin(); + auto clipRectMax = drawList->GetClipRectMax(); + auto size = Scale(26.0f); + drawList->AddText + ( + g_seuratFont, + size, + { clipRectMin.x, clipRectMin.y }, + IM_COL32(255, 255, 255, 255 * textAlpha), + descriptionText, + 0, + clipRectMax.x - clipRectMin.x + ); + + drawList->PopClipRect(); + + ImVec2 sideMin = { descriptionMax.x, descriptionMin.y }; + ImVec2 sideMax = { Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH + SIDE_CONTAINER_WIDTH)), descriptionMax.y }; + DrawContainer(sideMin, sideMax, true); + drawList->PopClipRect(); +} + +static void DrawButtonContainer(ImVec2 min, ImVec2 max, int baser, int baseg, float alpha) +{ + auto &res = ImGui::GetIO().DisplaySize; + auto drawList = ImGui::GetForegroundDrawList(); + SetShaderModifier(IMGUI_SHADER_MODIFIER_SCANLINE_BUTTON); + drawList->AddRectFilledMultiColor(min, max, IM_COL32(baser, baseg + 130, 0, 223 * alpha), IM_COL32(baser, baseg + 130, 0, 178 * alpha), IM_COL32(baser, baseg + 130, 0, 223 * alpha), IM_COL32(baser, baseg + 130, 0, 178 * alpha)); + drawList->AddRectFilledMultiColor(min, max, IM_COL32(baser, baseg, 0, 13 * alpha), IM_COL32(baser, baseg, 0, 0), IM_COL32(baser, baseg, 0, 55 * alpha), IM_COL32(baser, baseg, 0, 6 * alpha)); + drawList->AddRectFilledMultiColor(min, max, IM_COL32(baser, baseg + 130, 0, 13 * alpha), IM_COL32(baser, baseg + 130, 0, 111 * alpha), IM_COL32(baser, baseg + 130, 0, 0), IM_COL32(baser, baseg + 130, 0, 55 * alpha)); + SetShaderModifier(IMGUI_SHADER_MODIFIER_NONE); +} + +static void DrawButton(ImVec2 min, ImVec2 max, const char *buttonText, bool sourceButton, bool buttonEnabled, bool &buttonPressed) +{ + buttonPressed = false; + + auto &res = ImGui::GetIO().DisplaySize; + auto drawList = ImGui::GetForegroundDrawList(); + float alpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_BACKGROUND_TIME, CONTAINER_BACKGROUND_DURATION); + if (!buttonEnabled) + { + alpha *= 0.5f; + } + + int baser = 0; + int baseg = 0; + if (!sourceButton && buttonEnabled && (alpha >= 1.0f) && ImGui::IsMouseHoveringRect(min, max, false)) + { + baser = 48; + baseg = 32; + + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + { + buttonPressed = true; + } + } + + DrawButtonContainer(min, max, baser, baseg, alpha); + + float size = Scale(sourceButton ? 15.0f : 20.0f); + ImFont *font = sourceButton ? g_newRodinFont : g_dfsogeistdFont; + ImVec2 textSize = font->CalcTextSizeA(size, FLT_MAX, 0.0f, buttonText); + + min.x += ((max.x - min.x) - textSize.x) / 2.0f; + min.y += ((max.y - min.y) - textSize.y) / 2.0f; + + if (!sourceButton) + { + // Fixes slight misalignment caused by this particular font. + min.y -= Scale(1.0f); + } + + SetGradient + ( + min, + { min.x + textSize.x, min.y + textSize.y }, + IM_COL32(baser + 192, 255, 0, 255), + IM_COL32(baser + 128, baseg + 170, 0, 255) + ); + + DrawTextWithOutline + ( + font, + size, + min, + IM_COL32(255, 255, 255, 255 * alpha), + buttonText, + sourceButton ? 1.0f : 1.5f, + IM_COL32(baser, baseg, 0, 255 * alpha) + ); + + ResetGradient(); +} + +enum SourceColumn +{ + SourceColumnLeft, + SourceColumnMiddle, + SourceColumnRight +}; + +static void DrawSourceButton(SourceColumn sourceColumn, float yRatio, const char *sourceText, bool sourceSet) +{ + bool buttonPressed; + float minX, maxX; + switch (sourceColumn) + { + case SourceColumnLeft: + minX = Scale(AlignToNextGrid(CONTAINER_X) + SOURCE_BUTTON_GAP); + maxX = Scale(AlignToNextGrid(CONTAINER_X) + SOURCE_BUTTON_GAP + SOURCE_BUTTON_WIDTH); + break; + case SourceColumnMiddle: + minX = Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH / 2.0f) - SOURCE_BUTTON_WIDTH / 2.0f); + maxX = Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH / 2.0f) + SOURCE_BUTTON_WIDTH / 2.0f); + break; + case SourceColumnRight: + minX = Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH) - SOURCE_BUTTON_GAP - SOURCE_BUTTON_WIDTH); + maxX = Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH) - SOURCE_BUTTON_GAP); + break; + } + + float minusY = (SOURCE_BUTTON_GAP + BUTTON_HEIGHT) * yRatio; + ImVec2 min = { minX, Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) - SOURCE_BUTTON_GAP - BUTTON_HEIGHT - minusY) }; + ImVec2 max = { maxX, Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) - SOURCE_BUTTON_GAP - minusY) }; + DrawButton(min, max, sourceText, true, sourceSet, buttonPressed); +} + +static void DrawProgressBar(float progressRatio) +{ + auto &res = ImGui::GetIO().DisplaySize; + auto drawList = ImGui::GetForegroundDrawList(); + float alpha = 1.0; + const uint32_t innerColor0 = IM_COL32(0, 65, 0, 255 * alpha); + const uint32_t innerColor1 = IM_COL32(0, 32, 0, 255 * alpha); + float xPadding = Scale(6.0f); + float yPadding = Scale(3.0f); + ImVec2 min = { Scale(AlignToNextGrid(CONTAINER_X) + BOTTOM_X_GAP), Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) + BOTTOM_Y_GAP) }; + ImVec2 max = { Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH) - BOTTOM_X_GAP), Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) + BOTTOM_Y_GAP + BUTTON_HEIGHT) }; + + DrawButtonContainer(min, max, 0, 0, alpha); + + drawList->AddRectFilledMultiColor + ( + { min.x + xPadding, min.y + yPadding }, + { max.x - xPadding, max.y - yPadding }, + innerColor0, + innerColor0, + innerColor1, + innerColor1 + ); + + const uint32_t sliderColor0 = IM_COL32(57, 241, 0, 255 * alpha); + const uint32_t sliderColor1 = IM_COL32(2, 106, 0, 255 * alpha); + xPadding += Scale(1.0f); + yPadding += Scale(1.0f); + + ImVec2 sliderMin = { min.x + xPadding, min.y + yPadding }; + ImVec2 sliderMax = { max.x - xPadding, max.y - yPadding }; + sliderMax.x = sliderMin.x + (sliderMax.x - sliderMin.x) * progressRatio; + drawList->AddRectFilledMultiColor(sliderMin, sliderMax, sliderColor0, sliderColor0, sliderColor1, sliderColor1); +} + +static bool ConvertPathSet(const nfdpathset_t *pathSet, std::list &filePaths) +{ + nfdpathsetsize_t pathSetCount = 0; + if (NFD_PathSet_GetCount(pathSet, &pathSetCount) != NFD_OKAY) + { + return false; + } + + for (nfdpathsetsize_t i = 0; i < pathSetCount; i++) + { + char *pathSetPath = nullptr; + if (NFD_PathSet_GetPathU8(pathSet, i, &pathSetPath) != NFD_OKAY) + { + filePaths.clear(); + return false; + } + + filePaths.emplace_back(std::filesystem::path(std::u8string_view((const char8_t *)(pathSetPath)))); + NFD_PathSet_FreePathU8(pathSetPath); + } + + return true; +} + +static bool ShowFilesPicker(std::list &filePaths) +{ + filePaths.clear(); + + const nfdpathset_t *pathSet; + nfdresult_t result = NFD_OpenDialogMultipleU8(&pathSet, nullptr, 0, nullptr); + if (result == NFD_OKAY) + { + bool pathsConverted = ConvertPathSet(pathSet, filePaths); + NFD_PathSet_Free(pathSet); + return pathsConverted; + } + else + { + return false; + } +} + +static bool ShowFoldersPicker(std::list &folderPaths) +{ + folderPaths.clear(); + + const nfdpathset_t *pathSet; + nfdresult_t result = NFD_PickFolderMultipleU8(&pathSet, nullptr); + if (result == NFD_OKAY) + { + bool pathsConverted = ConvertPathSet(pathSet, folderPaths); + NFD_PathSet_Free(pathSet); + return pathsConverted; + } + else + { + return false; + } +} + +static void ParseSourcePaths(std::list &paths) +{ + assert((g_currentPage == WizardPage::SelectGameAndUpdate) || (g_currentPage == WizardPage::SelectDLC)); + + std::list failedPaths; + if (g_currentPage == WizardPage::SelectGameAndUpdate) + { + for (const std::filesystem::path &path : paths) + { + if (Installer::parseGame(path)) + { + g_gameSourcePath = path; + } + else if (Installer::parseUpdate(path)) + { + g_updateSourcePath = path; + } + else + { + failedPaths.push_back(path); + } + } + } + else if(g_currentPage == WizardPage::SelectDLC) + { + for (const std::filesystem::path &path : paths) + { + DLC dlc = Installer::parseDLC(path); + if (dlc != DLC::Unknown) + { + g_dlcSourcePaths[DLCIndex(dlc)] = path; + } + else + { + failedPaths.push_back(path); + } + } + } + + if (!failedPaths.empty()) + { + // TODO: Show list of content that failed to parse with a prompt. + } +} + +static void DrawSourcePickers() +{ + bool buttonPressed = false; + std::list paths; + if (g_currentPage == WizardPage::SelectGameAndUpdate || g_currentPage == WizardPage::SelectDLC) + { + ImVec2 textSize = g_dfsogeistdFont->CalcTextSizeA(20.0f, FLT_MAX, 0.0f, FILES_BUTTON_TEXT); + textSize.x += BUTTON_TEXT_GAP; + + ImVec2 min = { Scale(AlignToNextGrid(CONTAINER_X) + BOTTOM_X_GAP), Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) + BOTTOM_Y_GAP) }; + ImVec2 max = { Scale(AlignToNextGrid(CONTAINER_X) + BOTTOM_X_GAP + textSize.x), Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) + BOTTOM_Y_GAP + BUTTON_HEIGHT) }; + DrawButton(min, max, FILES_BUTTON_TEXT, false, true, buttonPressed); + if (buttonPressed && ShowFilesPicker(paths)) + { + ParseSourcePaths(paths); + } + + min.x += Scale(BOTTOM_X_GAP + textSize.x); + textSize = g_dfsogeistdFont->CalcTextSizeA(20.0f, FLT_MAX, 0.0f, FOLDER_BUTTON_TEXT); + textSize.x += BUTTON_TEXT_GAP; + + max.x = min.x + Scale(textSize.x); + DrawButton(min, max, FOLDER_BUTTON_TEXT, false, true, buttonPressed); + if (buttonPressed && ShowFoldersPicker(paths)) + { + ParseSourcePaths(paths); + } + } +} + +static void DrawSources() +{ + if (g_currentPage == WizardPage::SelectGameAndUpdate) + { + DrawSourceButton(SourceColumnMiddle, 1.5f, GAME_SOURCE_TEXT, !g_gameSourcePath.empty()); + DrawSourceButton(SourceColumnMiddle, 0.5f, UPDATE_SOURCE_TEXT, !g_updateSourcePath.empty()); + } + + if (g_currentPage == WizardPage::SelectDLC) + { + for (int i = 0; i < 6; i++) + { + DrawSourceButton((i < 3) ? SourceColumnLeft : SourceColumnRight, float(i % 3), DLC_SOURCE_TEXT[i], !g_dlcSourcePaths[i].empty()); + } + } +} + +static void DrawInstallingProgress() +{ + if (g_currentPage == WizardPage::Installing) + { + float ratioTarget = g_installerProgressRatioTarget.load(); + g_installerProgressRatioCurrent += (4.0f * ImGui::GetIO().DeltaTime * (ratioTarget - g_installerProgressRatioCurrent)); + DrawProgressBar(g_installerProgressRatioCurrent); + + if (g_installerFinished) + { + g_installerThread->join(); + g_installerThread.reset(); + g_currentPage = g_installerFailed ? WizardPage::InstallFailed : WizardPage::InstallSucceeded; + } + } +} + +static void InstallerThread() +{ + if (!Installer::install(g_installerSources, ".", false, g_installerJournal, [&]() { + g_installerProgressRatioTarget = float(double(g_installerJournal.progressCounter) / double(g_installerJournal.progressTotal)); + })) + { + g_installerFailed = true; + g_installerErrorMessage = g_installerJournal.lastErrorMessage; + + // Delete all files that were copied. + Installer::rollback(g_installerJournal); + } + + g_installerFinished = true; +} + +static void InstallerStart() +{ + g_installerStartTime = ImGui::GetTime(); + g_installerProgressRatioCurrent = 0.0f; + g_installerProgressRatioTarget = 0.0f; + g_installerFailed = false; + g_installerFinished = false; + g_installerThread = std::make_unique(InstallerThread); +} + +static bool InstallerParseSources() +{ + std::filesystem::space_info spaceInfo = std::filesystem::space("."); + g_installerAvailableSize = spaceInfo.available; + + Installer::Input installerInput; + installerInput.gameSource = g_gameSourcePath; + installerInput.updateSource = g_updateSourcePath; + + for (std::filesystem::path &path : g_dlcSourcePaths) { + if (!path.empty()) + { + installerInput.dlcSources.push_back(path); + } + } + + return Installer::parseSources(installerInput, g_installerJournal, g_installerSources); +} + +static void DrawNextButton() +{ + if (g_currentPage != WizardPage::Installing) { + bool nextButtonEnabled = !g_isDisappearing; +#if !SKIP_SOURCE_CHECKS + if (nextButtonEnabled && g_currentPage == WizardPage::SelectGameAndUpdate) + { + nextButtonEnabled = !g_gameSourcePath.empty() && !g_updateSourcePath.empty(); + } +#endif + bool skipButton = false; + if (g_currentPage == WizardPage::SelectDLC) + { + skipButton = std::all_of(g_dlcSourcePaths.begin(), g_dlcSourcePaths.end(), [](const std::filesystem::path &path) { return path.empty(); }); + } + + const char *buttonText = skipButton ? SKIP_BUTTON_TEXT : NEXT_BUTTON_TEXT; + ImVec2 textSize = g_newRodinFont->CalcTextSizeA(20.0f, FLT_MAX, 0.0f, buttonText); + textSize.x += BUTTON_TEXT_GAP; + + ImVec2 min = { Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH) - textSize.x - BOTTOM_X_GAP), Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) + BOTTOM_Y_GAP) }; + ImVec2 max = { Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH) - BOTTOM_X_GAP), Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) + BOTTOM_Y_GAP + BUTTON_HEIGHT) }; + + bool buttonPressed = false; + DrawButton(min, max, buttonText, false, nextButtonEnabled, buttonPressed); + + if (buttonPressed) + { + XexPatcher::Result patcherResult; + if (g_currentPage == WizardPage::SelectGameAndUpdate && (patcherResult = Installer::checkGameUpdateCompatibility(g_gameSourcePath, g_updateSourcePath), patcherResult != XexPatcher::Result::Success)) + { + // TODO: Show error prompt for compatibility failure check. Tell user to try with different files. + } + else if (g_currentPage == WizardPage::SelectDLC && !InstallerParseSources()) + { + // TODO: Show an error that the sources were unable to be parsed. Ask to try again. + } + else if (g_currentPage == WizardPage::InstallSucceeded) + { + g_isDisappearing = true; + g_disappearTime = ImGui::GetTime(); + } + else if (g_currentPage == WizardPage::InstallFailed) + { + g_currentPage = WizardPage::Introduction; + } + else + { + g_currentPage = WizardPage(int(g_currentPage) + 1); + + if (g_currentPage == WizardPage::Installing) + { + InstallerStart(); + } + } + } + } +} + +static void DrawHorizontalBorder(bool bottomBorder) +{ + const uint32_t FADE_COLOR_LEFT = IM_COL32(155, 155, 155, 0); + const uint32_t SOLID_COLOR = IM_COL32(155, 200, 155, 255); + const uint32_t FADE_COLOR_RIGHT = IM_COL32(155, 225, 155, 0); + auto drawList = ImGui::GetForegroundDrawList(); + double borderScale = 1.0 - ComputeMotionInstaller(g_appearTime, g_disappearTime, 0.0, CONTAINER_LINE_ANIMATION_DURATION); + float midX = Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH / 5)); + float minX = std::lerp(Scale(AlignToNextGrid(CONTAINER_X) - BORDER_SIZE - BORDER_OVERSHOOT), midX, borderScale); + float maxX = std::lerp(Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH + SIDE_CONTAINER_WIDTH) + BORDER_OVERSHOOT), midX, borderScale); + float minY = bottomBorder ? Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT)) : Scale(AlignToNextGrid(CONTAINER_Y) - BORDER_SIZE); + float maxY = minY + Scale(BORDER_SIZE); + drawList->AddRectFilledMultiColor + ( + { minX, minY }, + { midX, maxY }, + FADE_COLOR_LEFT, + SOLID_COLOR, + SOLID_COLOR, + FADE_COLOR_LEFT + ); + + drawList->AddRectFilledMultiColor + ( + { midX, minY }, + { maxX, maxY }, + SOLID_COLOR, + FADE_COLOR_RIGHT, + FADE_COLOR_RIGHT, + SOLID_COLOR + ); +} + +static void DrawVerticalBorder(bool rightBorder) +{ + const uint32_t SOLID_COLOR = IM_COL32(155, rightBorder ? 225 : 155, 155, 255); + const uint32_t FADE_COLOR = IM_COL32(155, rightBorder ? 225 : 155, 155, 0); + auto drawList = ImGui::GetForegroundDrawList(); + double borderScale = 1.0 - ComputeMotionInstaller(g_appearTime, g_disappearTime, 0.0, CONTAINER_LINE_ANIMATION_DURATION); + float minX = rightBorder ? Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH)) : Scale(AlignToNextGrid(CONTAINER_X) - BORDER_SIZE); + float maxX = minX + Scale(BORDER_SIZE); + float midY = Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT / 2)); + float minY = std::lerp(Scale(AlignToNextGrid(CONTAINER_Y) - BORDER_OVERSHOOT), midY, borderScale); + float maxY = std::lerp(Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) + BORDER_OVERSHOOT), midY, borderScale); + drawList->AddRectFilledMultiColor + ( + { minX, minY }, + { maxX, midY }, + FADE_COLOR, + FADE_COLOR, + SOLID_COLOR, + SOLID_COLOR + ); + + drawList->AddRectFilledMultiColor + ( + { minX, midY }, + { maxX, maxY }, + SOLID_COLOR, + SOLID_COLOR, + FADE_COLOR, + FADE_COLOR + ); +} + +static void DrawBorders() +{ + DrawHorizontalBorder(false); + DrawHorizontalBorder(true); + DrawVerticalBorder(false); + DrawVerticalBorder(true); +} + +void InstallerWizard::Init() +{ + auto &io = ImGui::GetIO(); + constexpr float FONT_SCALE = 2.0f; + g_seuratFont = io.Fonts->AddFontFromFileTTF("FOT-SeuratPro-M.otf", 26.0f * FONT_SCALE); + g_dfsogeistdFont = io.Fonts->AddFontFromFileTTF("DFSoGeiStd-W7.otf", 48.0f * FONT_SCALE); + g_newRodinFont = io.Fonts->AddFontFromFileTTF("FOT-NewRodinPro-DB.otf", 20.0f * FONT_SCALE); + g_installTextures[0] = LoadTexture(g_install001DDS, g_install001DDS_size); + g_installTextures[1] = LoadTexture(g_install002DDS, g_install002DDS_size); + g_installTextures[2] = LoadTexture(g_install003DDS, g_install003DDS_size); + g_installTextures[3] = LoadTexture(g_install004DDS, g_install004DDS_size); + g_installTextures[4] = LoadTexture(g_install005DDS, g_install005DDS_size); + g_installTextures[5] = LoadTexture(g_install006DDS, g_install006DDS_size); + g_installTextures[6] = LoadTexture(g_install007DDS, g_install007DDS_size); + g_installTextures[7] = LoadTexture(g_install008DDS, g_install008DDS_size); +} + +void InstallerWizard::Draw() +{ + if (!s_isVisible) + { + return; + } + + DrawBackground(); + DrawLeftImage(); + DrawScanlineBars(); + DrawDescriptionContainer(); + DrawSourcePickers(); + DrawSources(); + DrawInstallingProgress(); + DrawNextButton(); + DrawBorders(); + + if (g_isDisappearing) + { + const double disappearDuration = ALL_ANIMATIONS_FULL_DURATION / 60.0; + if (ImGui::GetTime() > (g_disappearTime + disappearDuration)) + { + s_isVisible = false; + } + } +} + +void InstallerWizard::Shutdown() +{ + if (g_installerThread != nullptr) + { + g_installerThread->join(); + g_installerThread.reset(); + } + + g_installerSources.game.reset(); + g_installerSources.update.reset(); + g_installerSources.dlc.clear(); +} + +bool InstallerWizard::Run(bool skipGame) +{ + NFD_Init(); + + if (skipGame) + { + g_currentPage = WizardPage::SelectDLC; + } + + Window::SetCursorAllowed(true); + s_isVisible = true; + + while (s_isVisible) + { + SDL_PumpEvents(); + SDL_FlushEvents(SDL_FIRSTEVENT, SDL_LASTEVENT); + Window::Update(); + Video::HostPresent(); + } + + Window::SetCursorAllowed(false); + NFD_Quit(); + + return true; +} diff --git a/UnleashedRecomp/ui/installer_wizard.h b/UnleashedRecomp/ui/installer_wizard.h new file mode 100644 index 00000000..0b8e6cff --- /dev/null +++ b/UnleashedRecomp/ui/installer_wizard.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +struct InstallerWizard +{ + inline static bool s_isVisible = false; + + static void Init(); + static void Draw(); + static void Shutdown(); + static bool Run(bool skipGame); +}; diff --git a/UnleashedRecomp/ui/options_menu.cpp b/UnleashedRecomp/ui/options_menu.cpp index 88f5ae1f..0cf17835 100644 --- a/UnleashedRecomp/ui/options_menu.cpp +++ b/UnleashedRecomp/ui/options_menu.cpp @@ -982,12 +982,11 @@ void OptionsMenu::Init() void OptionsMenu::Draw() { - auto pInputState = SWA::CInputState::GetInstance(); - if (!s_isVisible) return; // We've entered the menu now, no need to check this. + auto pInputState = SWA::CInputState::GetInstance(); if (pInputState->GetPadState().IsReleased(SWA::eKeyState_A)) g_isEnterKeyBuffered = false; diff --git a/UnleashedRecomp/ui/window.cpp b/UnleashedRecomp/ui/window.cpp index 0a0cf7be..939e9922 100644 --- a/UnleashedRecomp/ui/window.cpp +++ b/UnleashedRecomp/ui/window.cpp @@ -85,7 +85,7 @@ int Window_OnSDLEvent(void*, SDL_Event* event) case SDL_WINDOWEVENT_FOCUS_GAINED: Window::s_isFocused = true; - SDL_ShowCursor(Window::IsFullscreen() ? SDL_DISABLE : SDL_ENABLE); + SDL_ShowCursor(Window::IsFullscreen() && !Window::s_cursorAllowed ? SDL_DISABLE : SDL_ENABLE); break; case SDL_WINDOWEVENT_RESTORED: diff --git a/UnleashedRecomp/ui/window.h b/UnleashedRecomp/ui/window.h index bceb219d..96464122 100644 --- a/UnleashedRecomp/ui/window.h +++ b/UnleashedRecomp/ui/window.h @@ -21,6 +21,7 @@ public: inline static bool s_isFocused; inline static bool s_isIconNight; + inline static bool s_cursorAllowed = false; static SDL_Surface* GetIconSurface(void* pIconBmp, size_t iconSize) { @@ -76,7 +77,7 @@ public: if (isEnabled) { SDL_SetWindowFullscreen(s_pWindow, SDL_WINDOW_FULLSCREEN_DESKTOP); - SDL_ShowCursor(SDL_DISABLE); + SDL_ShowCursor(s_cursorAllowed ? SDL_ENABLE : SDL_DISABLE); } else { @@ -87,6 +88,14 @@ public: return isEnabled; } + + static void SetCursorAllowed(bool isCursorAllowed) + { + s_cursorAllowed = isCursorAllowed; + + // Refresh fullscreen state to enable the right cursor behavior. + SetFullscreen(IsFullscreen()); + } static bool IsMaximised() { diff --git a/UnleashedRecompResources b/UnleashedRecompResources index 7183c6e0..d9063dd2 160000 --- a/UnleashedRecompResources +++ b/UnleashedRecompResources @@ -1 +1 @@ -Subproject commit 7183c6e0e90480fe0f2c270c87cea5cd5df7d5c3 +Subproject commit d9063dd234b92fd7ab35e72d1839d0fcfdc83456 diff --git a/vcpkg.json b/vcpkg.json index 5e135232..1ca62733 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -22,6 +22,7 @@ "features": [ "sdl2-binding" ] }, "magic-enum", - "readerwriterqueue" + "readerwriterqueue", + "nativefiledialog-extended" ] }