From aeadcfcf90e987188722f3e163e861038f9b5401 Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 30 Nov 2024 16:49:20 -0300 Subject: [PATCH 01/28] 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" ] } From 32ff3ab3524559e0b90a9f76fba3d209ba9a0668 Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 2 Dec 2024 00:28:03 -0300 Subject: [PATCH 02/28] Skip drawing scanlines when height is 0. --- UnleashedRecomp/ui/installer_wizard.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/UnleashedRecomp/ui/installer_wizard.cpp b/UnleashedRecomp/ui/installer_wizard.cpp index 0f7a6679..054b22e9 100644 --- a/UnleashedRecomp/ui/installer_wizard.cpp +++ b/UnleashedRecomp/ui/installer_wizard.cpp @@ -191,6 +191,10 @@ static void DrawScanlineBars() 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); + if (height < 1e-6f) + { + return; + } auto &res = ImGui::GetIO().DisplaySize; auto drawList = ImGui::GetForegroundDrawList(); From 630028887a32e2fe6a1579c971ee653ea4aafdf0 Mon Sep 17 00:00:00 2001 From: PTKay Date: Mon, 2 Dec 2024 11:40:51 +0000 Subject: [PATCH 03/28] Tweak install screen to better match the original --- UnleashedRecomp/CMakeLists.txt | 1 + UnleashedRecomp/ui/installer_wizard.cpp | 42 +++++++++++++++++-------- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/UnleashedRecomp/CMakeLists.txt b/UnleashedRecomp/CMakeLists.txt index 303210cc..3e5a400a 100644 --- a/UnleashedRecomp/CMakeLists.txt +++ b/UnleashedRecomp/CMakeLists.txt @@ -116,6 +116,7 @@ BIN2H(SOURCE_FILE "${INSTALLER_IMAGES_DIR}/install_005.dds" HEADER_FILE "res/ins 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") +BIN2H(SOURCE_FILE "${INSTALLER_IMAGES_DIR}/miles_electric_icon.dds" HEADER_FILE "res/miles_electric_icon_dds.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_milesElectricIconDDS") set(LIBMSPACK_PATH ${SWA_THIRDPARTY_ROOT}/libmspack/libmspack/mspack) diff --git a/UnleashedRecomp/ui/installer_wizard.cpp b/UnleashedRecomp/ui/installer_wizard.cpp index 054b22e9..4ceb048a 100644 --- a/UnleashedRecomp/ui/installer_wizard.cpp +++ b/UnleashedRecomp/ui/installer_wizard.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #define SKIP_SOURCE_CHECKS 0 @@ -36,7 +37,7 @@ 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_X = 165.0f; constexpr float IMAGE_Y = 106.0f; constexpr float IMAGE_WIDTH = 512.0f; constexpr float IMAGE_HEIGHT = 512.0f; @@ -71,6 +72,7 @@ static std::filesystem::path g_gameSourcePath; static std::filesystem::path g_updateSourcePath; static std::array g_dlcSourcePaths; static std::array, 8> g_installTextures; +static std::unique_ptr g_installHeaderIcon; static Journal g_installerJournal; static Installer::Sources g_installerSources; static uint64_t g_installerAvailableSize = 0; @@ -229,7 +231,15 @@ static void DrawScanlineBars() // 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)); + DrawTextWithOutline(g_dfsogeistdFont, Scale(42.0f), { Scale(285.0f), Scale(57.0f) }, IM_COL32(255, 195, 0, textAlpha), headerText, 4, IM_COL32(0, 0, 0, textAlpha)); + + // Icon + float iconPosX = 225.0f; + float iconPosY = 50.0f; + GuestTexture* guestTexture = g_installHeaderIcon.get(); + ImVec2 min = { Scale(iconPosX), Scale(iconPosY) }; + ImVec2 max = { Scale(iconPosX + 58), Scale(iconPosY + 58) }; + drawList->AddImage(guestTexture, min, max, ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 255)); // Top bar line drawList->AddLine @@ -255,7 +265,7 @@ static float AlignToNextGrid(float value) return floor(value / GRID_SIZE) * GRID_SIZE; } -static void DrawContainer(ImVec2 min, ImVec2 max, bool useInnerColor) +static void DrawContainer(ImVec2 min, ImVec2 max, bool isTextArea) { double containerHeight = ComputeMotionInstaller(g_appearTime, g_disappearTime, 0.0, CONTAINER_LINE_ANIMATION_DURATION); @@ -266,20 +276,25 @@ static void DrawContainer(ImVec2 min, ImVec2 max, bool useInnerColor) 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); + double gridAlpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, + isTextArea ? CONTAINER_INNER_TIME : CONTAINER_OUTER_TIME, + isTextArea ? CONTAINER_INNER_DURATION : CONTAINER_OUTER_DURATION + ); + double gridOverlayAlpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_OUTER_TIME, CONTAINER_OUTER_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); + const uint32_t gridColor = IM_COL32(0, 33, 0, isTextArea ? 128 : 255 * gridAlpha); + const uint32_t gridOverlayColor = IM_COL32(0, 32, 0, 128 * gridOverlayAlpha); float gridSize = Scale(GRID_SIZE); - drawList->AddRectFilled(min, max, backgroundColor); + SetShaderModifier(IMGUI_SHADER_MODIFIER_CHECKERBOARD); - drawList->AddRectFilled(min, max, useInnerColor ? innerColor: outerColor); + drawList->AddRectFilled(min, max, gridColor); SetShaderModifier(IMGUI_SHADER_MODIFIER_NONE); + if (isTextArea) { + drawList->AddRectFilled(min, max, gridOverlayColor); + } + // 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 }); } @@ -291,7 +306,7 @@ static void DrawDescriptionContainer() 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); + DrawContainer(descriptionMin, descriptionMax, true); char descriptionText[512]; strncpy(descriptionText, WIZARD_TEXT[int(g_currentPage)], sizeof(descriptionText) - 1); @@ -331,7 +346,7 @@ static void DrawDescriptionContainer() ImVec2 sideMin = { descriptionMax.x, descriptionMin.y }; ImVec2 sideMax = { Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH + SIDE_CONTAINER_WIDTH)), descriptionMax.y }; - DrawContainer(sideMin, sideMax, true); + DrawContainer(sideMin, sideMax, false); drawList->PopClipRect(); } @@ -837,6 +852,7 @@ void InstallerWizard::Init() 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); + g_installHeaderIcon = LoadTexture(g_milesElectricIconDDS, g_milesElectricIconDDS_size); } void InstallerWizard::Draw() From 7a6990ae0aa17b6f15ab5dc033c2f40dd3f85b31 Mon Sep 17 00:00:00 2001 From: PTKay Date: Mon, 2 Dec 2024 13:48:25 +0000 Subject: [PATCH 04/28] Added arrow circle to installer's header --- UnleashedRecomp/CMakeLists.txt | 1 + UnleashedRecomp/ui/installer_wizard.cpp | 33 +++++++++++++++++-------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/UnleashedRecomp/CMakeLists.txt b/UnleashedRecomp/CMakeLists.txt index 3e5a400a..9af198c9 100644 --- a/UnleashedRecomp/CMakeLists.txt +++ b/UnleashedRecomp/CMakeLists.txt @@ -117,6 +117,7 @@ BIN2H(SOURCE_FILE "${INSTALLER_IMAGES_DIR}/install_006.dds" HEADER_FILE "res/ins 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") BIN2H(SOURCE_FILE "${INSTALLER_IMAGES_DIR}/miles_electric_icon.dds" HEADER_FILE "res/miles_electric_icon_dds.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_milesElectricIconDDS") +BIN2H(SOURCE_FILE "${INSTALLER_IMAGES_DIR}/arrow_circle.dds" HEADER_FILE "res/arrow_circle_dds.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_arrowCircleDDS") set(LIBMSPACK_PATH ${SWA_THIRDPARTY_ROOT}/libmspack/libmspack/mspack) diff --git a/UnleashedRecomp/ui/installer_wizard.cpp b/UnleashedRecomp/ui/installer_wizard.cpp index 4ceb048a..c16d2165 100644 --- a/UnleashedRecomp/ui/installer_wizard.cpp +++ b/UnleashedRecomp/ui/installer_wizard.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #define SKIP_SOURCE_CHECKS 0 @@ -72,7 +73,8 @@ static std::filesystem::path g_gameSourcePath; static std::filesystem::path g_updateSourcePath; static std::array g_dlcSourcePaths; static std::array, 8> g_installTextures; -static std::unique_ptr g_installHeaderIcon; +static std::unique_ptr g_milesElectricIcon; +static std::unique_ptr g_arrowCircle; static Journal g_installerJournal; static Installer::Sources g_installerSources; static uint64_t g_installerAvailableSize = 0; @@ -233,14 +235,6 @@ static void DrawScanlineBars() int textAlpha = std::lround(255.0f * ComputeMotionInstaller(g_appearTime, g_disappearTime, TITLE_ANIMATION_TIME, TITLE_ANIMATION_DURATION)); DrawTextWithOutline(g_dfsogeistdFont, Scale(42.0f), { Scale(285.0f), Scale(57.0f) }, IM_COL32(255, 195, 0, textAlpha), headerText, 4, IM_COL32(0, 0, 0, textAlpha)); - // Icon - float iconPosX = 225.0f; - float iconPosY = 50.0f; - GuestTexture* guestTexture = g_installHeaderIcon.get(); - ImVec2 min = { Scale(iconPosX), Scale(iconPosY) }; - ImVec2 max = { Scale(iconPosX + 58), Scale(iconPosY + 58) }; - drawList->AddImage(guestTexture, min, max, ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 255)); - // Top bar line drawList->AddLine ( @@ -258,6 +252,24 @@ static void DrawScanlineBars() OUTLINE_COLOR, Scale(1) ); + + // Icons + float iconsPosX = 225.0f; + float iconsPosY = 50.0f; + + // Miles Electric Icon + float milesIconScale = 59; + GuestTexture* milesElectricIconTexture = g_milesElectricIcon.get(); + ImVec2 milesElectricMin = { Scale(iconsPosX), Scale(iconsPosY) }; + ImVec2 milesElectricMax = { Scale(iconsPosX + milesIconScale), Scale(iconsPosY + milesIconScale) }; + drawList->AddImage(milesElectricIconTexture, milesElectricMin, milesElectricMax, ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 255)); + + // Arrow Circle Icon + float arrowCircleScale = 58; + GuestTexture* arrowCircleTexture = g_arrowCircle.get(); + ImVec2 arrowCircleMin = { Scale(iconsPosX), Scale(iconsPosY) }; + ImVec2 arrowCircleMax = { Scale(iconsPosX + arrowCircleScale), Scale(iconsPosY + arrowCircleScale) }; + drawList->AddImage(arrowCircleTexture, arrowCircleMin, arrowCircleMax, ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 96)); } static float AlignToNextGrid(float value) @@ -852,7 +864,8 @@ void InstallerWizard::Init() 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); - g_installHeaderIcon = LoadTexture(g_milesElectricIconDDS, g_milesElectricIconDDS_size); + g_milesElectricIcon = LoadTexture(g_milesElectricIconDDS, g_milesElectricIconDDS_size); + g_arrowCircle = LoadTexture(g_arrowCircleDDS, g_arrowCircleDDS_size); } void InstallerWizard::Draw() From 3c449b338691f254873fddeaa61a540fc0983b88 Mon Sep 17 00:00:00 2001 From: PTKay Date: Mon, 2 Dec 2024 22:14:55 +0000 Subject: [PATCH 05/28] Added missing animations and tweaked other ones for installer --- UnleashedRecomp/ui/installer_wizard.cpp | 109 +++++++++++++----------- 1 file changed, 61 insertions(+), 48 deletions(-) diff --git a/UnleashedRecomp/ui/installer_wizard.cpp b/UnleashedRecomp/ui/installer_wizard.cpp index c16d2165..015333be 100644 --- a/UnleashedRecomp/ui/installer_wizard.cpp +++ b/UnleashedRecomp/ui/installer_wizard.cpp @@ -20,23 +20,28 @@ #define SKIP_SOURCE_CHECKS 0 -static constexpr double SCANLINES_ANIMATION_DURATION = 8.0; +static constexpr double SCANLINES_ANIMATION_TIME = 0.0; +static constexpr double SCANLINES_ANIMATION_DURATION = 15.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 MILES_ICON_ANIMATION_TIME = SCANLINES_ANIMATION_TIME + 10.0; +static constexpr double MILES_ICON_ANIMATION_DURATION = 15.0; -static constexpr double CONTAINER_LINE_ANIMATION_DURATION = 8.0; +static constexpr double IMAGE_ANIMATION_TIME = MILES_ICON_ANIMATION_TIME + MILES_ICON_ANIMATION_DURATION; +static constexpr double IMAGE_ANIMATION_DURATION = 15.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 TITLE_ANIMATION_TIME = SCANLINES_ANIMATION_DURATION; +static constexpr double TITLE_ANIMATION_DURATION = 30.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_LINE_ANIMATION_TIME = SCANLINES_ANIMATION_DURATION; +static constexpr double CONTAINER_LINE_ANIMATION_DURATION = 23.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 CONTAINER_OUTER_TIME = SCANLINES_ANIMATION_DURATION + CONTAINER_LINE_ANIMATION_DURATION; +static constexpr double CONTAINER_OUTER_DURATION = 23.0; -static constexpr double ALL_ANIMATIONS_FULL_DURATION = CONTAINER_BACKGROUND_TIME + CONTAINER_BACKGROUND_DURATION; +static constexpr double CONTAINER_INNER_TIME = SCANLINES_ANIMATION_DURATION + CONTAINER_LINE_ANIMATION_DURATION + 8.0; +static constexpr double CONTAINER_INNER_DURATION = 15.0; + +static constexpr double ALL_ANIMATIONS_FULL_DURATION = CONTAINER_INNER_TIME + CONTAINER_INNER_DURATION; constexpr float IMAGE_X = 165.0f; constexpr float IMAGE_Y = 106.0f; @@ -173,7 +178,7 @@ static void DrawLeftImage() installTextureIndex += int(installationTime); } - double imageAlpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_BACKGROUND_TIME, CONTAINER_BACKGROUND_DURATION); + double imageAlpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, IMAGE_ANIMATION_TIME, IMAGE_ANIMATION_DURATION); int a = std::lround(255.0 * imageAlpha); GuestTexture *guestTexture = g_installTextures[installTextureIndex % g_installTextures.size()].get(); auto &res = ImGui::GetIO().DisplaySize; @@ -186,13 +191,42 @@ static void DrawLeftImage() 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 DrawHeaderIcons() +{ + auto drawList = ImGui::GetForegroundDrawList(); + + float iconsPosX = 255.0f; + float iconsPosY = 79.0f; + float iconsScale = 58; + + // Miles Electric Icon + float milesIconMotion = ComputeMotionInstaller(g_appearTime, g_disappearTime, MILES_ICON_ANIMATION_TIME, MILES_ICON_ANIMATION_DURATION); + float milesIconScale = iconsScale * (2 - milesIconMotion); + + GuestTexture* milesElectricIconTexture = g_milesElectricIcon.get(); + ImVec2 milesElectricMin = { Scale(iconsPosX - milesIconScale / 2), Scale(iconsPosY - milesIconScale / 2) }; + ImVec2 milesElectricMax = { Scale(iconsPosX + milesIconScale / 2), Scale(iconsPosY + milesIconScale / 2) }; + drawList->AddImage(milesElectricIconTexture, milesElectricMin, milesElectricMax, ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 255 * milesIconMotion)); + + // Arrow Circle Icon + if (g_currentPage == WizardPage::Installing) + { + GuestTexture* arrowCircleTexture = g_arrowCircle.get(); + ImVec2 arrowCircleMin = { Scale(iconsPosX - iconsScale / 2), Scale(iconsPosY - iconsScale / 2) }; + ImVec2 arrowCircleMax = { Scale(iconsPosX + iconsScale / 2), Scale(iconsPosY + iconsScale / 2) }; + drawList->AddImage(arrowCircleTexture, arrowCircleMin, arrowCircleMax, ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 96)); + } +} + 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); + double scanlinesAlpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, 0.0, SCANLINES_ANIMATION_DURATION); + + const uint32_t COLOR0 = IM_COL32(203, 255, 0, 0); + const uint32_t COLOR1 = IM_COL32(203, 255, 0, 55 * scanlinesAlpha); + const uint32_t FADE_COLOR0 = IM_COL32(0, 0, 0, 255 * scanlinesAlpha); + const uint32_t FADE_COLOR1 = IM_COL32(0, 0, 0, 0); + const uint32_t OUTLINE_COLOR = IM_COL32(115, 178, 104, 255 * scanlinesAlpha); float height = Scale(105.0f) * ComputeMotionInstaller(g_appearTime, g_disappearTime, 0.0, SCANLINES_ANIMATION_DURATION); if (height < 1e-6f) @@ -253,23 +287,7 @@ static void DrawScanlineBars() Scale(1) ); - // Icons - float iconsPosX = 225.0f; - float iconsPosY = 50.0f; - - // Miles Electric Icon - float milesIconScale = 59; - GuestTexture* milesElectricIconTexture = g_milesElectricIcon.get(); - ImVec2 milesElectricMin = { Scale(iconsPosX), Scale(iconsPosY) }; - ImVec2 milesElectricMax = { Scale(iconsPosX + milesIconScale), Scale(iconsPosY + milesIconScale) }; - drawList->AddImage(milesElectricIconTexture, milesElectricMin, milesElectricMax, ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 255)); - - // Arrow Circle Icon - float arrowCircleScale = 58; - GuestTexture* arrowCircleTexture = g_arrowCircle.get(); - ImVec2 arrowCircleMin = { Scale(iconsPosX), Scale(iconsPosY) }; - ImVec2 arrowCircleMax = { Scale(iconsPosX + arrowCircleScale), Scale(iconsPosY + arrowCircleScale) }; - drawList->AddImage(arrowCircleTexture, arrowCircleMin, arrowCircleMax, ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 96)); + DrawHeaderIcons(); } static float AlignToNextGrid(float value) @@ -278,13 +296,7 @@ static float AlignToNextGrid(float value) } static void DrawContainer(ImVec2 min, ImVec2 max, bool isTextArea) -{ - 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(); @@ -292,9 +304,9 @@ static void DrawContainer(ImVec2 min, ImVec2 max, bool isTextArea) isTextArea ? CONTAINER_INNER_TIME : CONTAINER_OUTER_TIME, isTextArea ? CONTAINER_INNER_DURATION : CONTAINER_OUTER_DURATION ); - double gridOverlayAlpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_OUTER_TIME, CONTAINER_OUTER_DURATION); + double gridOverlayAlpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_INNER_TIME, CONTAINER_INNER_DURATION); - const uint32_t gridColor = IM_COL32(0, 33, 0, isTextArea ? 128 : 255 * gridAlpha); + const uint32_t gridColor = IM_COL32(0, 33, 0, (isTextArea ? 128 : 255) * gridAlpha); const uint32_t gridOverlayColor = IM_COL32(0, 32, 0, 128 * gridOverlayAlpha); float gridSize = Scale(GRID_SIZE); @@ -303,7 +315,8 @@ static void DrawContainer(ImVec2 min, ImVec2 max, bool isTextArea) drawList->AddRectFilled(min, max, gridColor); SetShaderModifier(IMGUI_SHADER_MODIFIER_NONE); - if (isTextArea) { + if (isTextArea) + { drawList->AddRectFilled(min, max, gridOverlayColor); } @@ -339,7 +352,7 @@ static void DrawDescriptionContainer() strncat(descriptionText, g_installerErrorMessage.c_str(), sizeof(descriptionText) - 1); } - double textAlpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_BACKGROUND_TIME, CONTAINER_BACKGROUND_DURATION); + double textAlpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_INNER_TIME, CONTAINER_INNER_DURATION); auto clipRectMin = drawList->GetClipRectMin(); auto clipRectMax = drawList->GetClipRectMax(); auto size = Scale(26.0f); @@ -379,7 +392,7 @@ static void DrawButton(ImVec2 min, ImVec2 max, const char *buttonText, bool sour auto &res = ImGui::GetIO().DisplaySize; auto drawList = ImGui::GetForegroundDrawList(); - float alpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_BACKGROUND_TIME, CONTAINER_BACKGROUND_DURATION); + float alpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_INNER_TIME, CONTAINER_INNER_DURATION); if (!buttonEnabled) { alpha *= 0.5f; @@ -782,7 +795,7 @@ static void DrawHorizontalBorder(bool bottomBorder) 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); + double borderScale = 1.0 - ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_LINE_ANIMATION_TIME, 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); @@ -814,7 +827,7 @@ 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); + double borderScale = 1.0 - ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_LINE_ANIMATION_TIME, 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)); From 0ef5a57bb1a9c0063b5f153eb532b3af432673e3 Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 2 Dec 2024 19:19:37 -0300 Subject: [PATCH 06/28] Improve detection for DLC only mode. Add template for message prompts. --- UnleashedRecomp/install/installer.cpp | 22 +++++ UnleashedRecomp/install/installer.h | 1 + UnleashedRecomp/ui/installer_wizard.cpp | 110 +++++++++++++++++++----- UnleashedRecompResources | 2 +- 4 files changed, 112 insertions(+), 23 deletions(-) diff --git a/UnleashedRecomp/install/installer.cpp b/UnleashedRecomp/install/installer.cpp index 7029d92d..c28783b5 100644 --- a/UnleashedRecomp/install/installer.cpp +++ b/UnleashedRecomp/install/installer.cpp @@ -201,6 +201,27 @@ bool Installer::checkGameInstall(const std::filesystem::path &baseDirectory) return std::filesystem::exists(baseDirectory / GameDirectory / GameExecutableFile); } +bool Installer::checkDLCInstall(const std::filesystem::path &baseDirectory, DLC dlc) +{ + switch (dlc) + { + case DLC::Spagonia: + return std::filesystem::exists(baseDirectory / SpagoniaDirectory / DLCValidationFile); + case DLC::Chunnan: + return std::filesystem::exists(baseDirectory / ChunnanDirectory / DLCValidationFile); + case DLC::Mazuri: + return std::filesystem::exists(baseDirectory / MazuriDirectory / DLCValidationFile); + case DLC::Holoska: + return std::filesystem::exists(baseDirectory / HoloskaDirectory / DLCValidationFile); + case DLC::ApotosShamar: + return std::filesystem::exists(baseDirectory / ApotosShamarDirectory / DLCValidationFile); + case DLC::EmpireCityAdabat: + return std::filesystem::exists(baseDirectory / EmpireCityAdabatDirectory / DLCValidationFile); + default: + return false; + } +} + bool Installer::computeTotalSize(std::span filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, Journal &journal, uint64_t &totalSize) { for (FilePair pair : filePairs) @@ -288,6 +309,7 @@ constexpr uint32_t PatcherContribution = 512 * 1024 * 1024; bool Installer::parseSources(const Input &input, Journal &journal, Sources &sources) { + journal = Journal(); sources = Sources(); // Parse the contents of the base game. diff --git a/UnleashedRecomp/install/installer.h b/UnleashedRecomp/install/installer.h index e02e50c9..fae798ec 100644 --- a/UnleashedRecomp/install/installer.h +++ b/UnleashedRecomp/install/installer.h @@ -72,6 +72,7 @@ struct Installer }; static bool checkGameInstall(const std::filesystem::path &baseDirectory); + static bool checkDLCInstall(const std::filesystem::path &baseDirectory, DLC dlc); 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); diff --git a/UnleashedRecomp/ui/installer_wizard.cpp b/UnleashedRecomp/ui/installer_wizard.cpp index 015333be..5828afec 100644 --- a/UnleashedRecomp/ui/installer_wizard.cpp +++ b/UnleashedRecomp/ui/installer_wizard.cpp @@ -18,8 +18,6 @@ #include #include -#define SKIP_SOURCE_CHECKS 0 - static constexpr double SCANLINES_ANIMATION_TIME = 0.0; static constexpr double SCANLINES_ANIMATION_DURATION = 15.0; @@ -74,9 +72,12 @@ 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_installPath = "."; static std::filesystem::path g_gameSourcePath; static std::filesystem::path g_updateSourcePath; static std::array g_dlcSourcePaths; +static std::array g_dlcInstalled = {}; static std::array, 8> g_installTextures; static std::unique_ptr g_milesElectricIcon; static std::unique_ptr g_arrowCircle; @@ -102,7 +103,10 @@ enum class WizardPage InstallFailed, }; -static WizardPage g_currentPage = WizardPage::Introduction; +static WizardPage g_firstPage = WizardPage::Introduction; +static WizardPage g_currentPage = g_firstPage; +static std::string g_currentMessagePrompt = ""; +static bool g_currentMessagePromptConfirmation = false; const char INSTALLER_TEXT[] = "INSTALLER"; const char INSTALLING_TEXT[] = "INSTALLING"; @@ -400,7 +404,7 @@ static void DrawButton(ImVec2 min, ImVec2 max, const char *buttonText, bool sour int baser = 0; int baseg = 0; - if (!sourceButton && buttonEnabled && (alpha >= 1.0f) && ImGui::IsMouseHoveringRect(min, max, false)) + if (g_currentMessagePrompt.empty() && !sourceButton && buttonEnabled && (alpha >= 1.0f) && ImGui::IsMouseHoveringRect(min, max, false)) { baser = 48; baseg = 32; @@ -580,6 +584,7 @@ static void ParseSourcePaths(std::list &paths) { assert((g_currentPage == WizardPage::SelectGameAndUpdate) || (g_currentPage == WizardPage::SelectDLC)); + constexpr size_t failedPathLimit = 5; std::list failedPaths; if (g_currentPage == WizardPage::SelectGameAndUpdate) { @@ -593,7 +598,7 @@ static void ParseSourcePaths(std::list &paths) { g_updateSourcePath = path; } - else + else if (failedPaths.size() < failedPathLimit) { failedPaths.push_back(path); } @@ -608,7 +613,7 @@ static void ParseSourcePaths(std::list &paths) { g_dlcSourcePaths[DLCIndex(dlc)] = path; } - else + else if (failedPaths.size() < failedPathLimit) { failedPaths.push_back(path); } @@ -617,7 +622,15 @@ static void ParseSourcePaths(std::list &paths) if (!failedPaths.empty()) { - // TODO: Show list of content that failed to parse with a prompt. + std::stringstream stringStream; + stringStream << "Some of the files that were selected are not valid." << std::endl; + for (const std::filesystem::path &path : failedPaths) + { + stringStream << std::endl << path.filename().string(); + } + + g_currentMessagePrompt = stringStream.str(); + g_currentMessagePromptConfirmation = false; } } @@ -663,7 +676,7 @@ static void DrawSources() { for (int i = 0; i < 6; i++) { - DrawSourceButton((i < 3) ? SourceColumnLeft : SourceColumnRight, float(i % 3), DLC_SOURCE_TEXT[i], !g_dlcSourcePaths[i].empty()); + DrawSourceButton((i < 3) ? SourceColumnLeft : SourceColumnRight, float(i % 3), DLC_SOURCE_TEXT[i], !g_dlcSourcePaths[i].empty() || g_dlcInstalled[i]); } } } @@ -687,7 +700,7 @@ static void DrawInstallingProgress() static void InstallerThread() { - if (!Installer::install(g_installerSources, ".", false, g_installerJournal, [&]() { + if (!Installer::install(g_installerSources, g_installPath, false, g_installerJournal, [&]() { g_installerProgressRatioTarget = float(double(g_installerJournal.progressCounter) / double(g_installerJournal.progressTotal)); })) { @@ -703,6 +716,7 @@ static void InstallerThread() static void InstallerStart() { + g_currentPage = WizardPage::Installing; g_installerStartTime = ImGui::GetTime(); g_installerProgressRatioCurrent = 0.0f; g_installerProgressRatioTarget = 0.0f; @@ -713,7 +727,7 @@ static void InstallerStart() static bool InstallerParseSources() { - std::filesystem::space_info spaceInfo = std::filesystem::space("."); + std::filesystem::space_info spaceInfo = std::filesystem::space(g_installPath); g_installerAvailableSize = spaceInfo.available; Installer::Input installerInput; @@ -734,12 +748,11 @@ 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) { @@ -761,11 +774,46 @@ static void DrawNextButton() 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. + g_currentMessagePrompt = "The specified game and update file are incompatible.\n\nPlease ensure the files are for the same version and region and try again."; + g_currentMessagePromptConfirmation = false; } - else if (g_currentPage == WizardPage::SelectDLC && !InstallerParseSources()) + else if (g_currentPage == WizardPage::SelectDLC) { - // TODO: Show an error that the sources were unable to be parsed. Ask to try again. + // Check if any of the DLC was not specified. + bool dlcIncomplete = false; + for (int i = 0; (i < int(DLC::Count)) && !dlcIncomplete; i++) + { + if (g_dlcSourcePaths[i].empty() && !g_dlcInstalled[i]) + { + dlcIncomplete = true; + } + } + + bool dlcInstallerMode = g_gameSourcePath.empty(); + if (!InstallerParseSources()) + { + // Some of the sources that were provided to the installer are not valid. Restart the file selection process. + g_currentMessagePrompt = "Some of the files that have been provided are not valid.\n\nPlease make sure all the specified files are correct and try again."; + g_currentMessagePromptConfirmation = false; + g_currentPage = dlcInstallerMode ? WizardPage::SelectDLC : WizardPage::SelectGameAndUpdate; + } + else if (dlcIncomplete && !dlcInstallerMode) + { + // Not all the DLC was specified, we show a prompt and await a confirmation before starting the installer. + g_currentMessagePrompt = "It is highly recommended that you install all of the DLC, as it includes high quality lighting textures for the stages in each pack.\n\nAre you sure you want to skip this step?"; + g_currentMessagePromptConfirmation = true; + } + else if (skipButton && dlcInstallerMode) + { + // Nothing was selected and the installer was in DLC mode, just close it. + g_isDisappearing = true; + g_disappearTime = ImGui::GetTime(); + } + else + { + // Start the installer outright, this switches to the right page on its own. + InstallerStart(); + } } else if (g_currentPage == WizardPage::InstallSucceeded) { @@ -774,16 +822,11 @@ static void DrawNextButton() } else if (g_currentPage == WizardPage::InstallFailed) { - g_currentPage = WizardPage::Introduction; + g_currentPage = g_firstPage; } else { g_currentPage = WizardPage(int(g_currentPage) + 1); - - if (g_currentPage == WizardPage::Installing) - { - InstallerStart(); - } } } } @@ -862,6 +905,22 @@ static void DrawBorders() DrawVerticalBorder(true); } +static void DrawMessagePrompt() +{ + if (g_currentMessagePrompt.empty()) + { + return; + } + + // TODO: Put message prompt here. + + if (false && g_currentPage == WizardPage::SelectDLC) + { + // If user confirms the message prompt that they wish to skip installing the DLC, start the installer. + InstallerStart(); + } +} + void InstallerWizard::Init() { auto &io = ImGui::GetIO(); @@ -897,6 +956,7 @@ void InstallerWizard::Draw() DrawInstallingProgress(); DrawNextButton(); DrawBorders(); + DrawMessagePrompt(); if (g_isDisappearing) { @@ -927,7 +987,13 @@ bool InstallerWizard::Run(bool skipGame) if (skipGame) { - g_currentPage = WizardPage::SelectDLC; + for (int i = 0; i < int(DLC::Count); i++) + { + g_dlcInstalled[i] = Installer::checkDLCInstall(g_installPath, DLC(i + 1)); + } + + g_firstPage = WizardPage::SelectDLC; + g_currentPage = g_firstPage; } Window::SetCursorAllowed(true); diff --git a/UnleashedRecompResources b/UnleashedRecompResources index d9063dd2..79a2a18c 160000 --- a/UnleashedRecompResources +++ b/UnleashedRecompResources @@ -1 +1 @@ -Subproject commit d9063dd234b92fd7ab35e72d1839d0fcfdc83456 +Subproject commit 79a2a18cc8013dd6d76d8300f703f19f6e0ec484 From 03381065c74c7b5fada3c9cb58b6eb062e6bd08c Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 2 Dec 2024 20:22:54 -0300 Subject: [PATCH 07/28] Add language picker. --- UnleashedRecomp/gpu/video.cpp | 6 +- UnleashedRecomp/gpu/video.h | 1 + UnleashedRecomp/locale/locale.h | 30 ++++- UnleashedRecomp/ui/installer_wizard.cpp | 146 +++++++++++++++++------- 4 files changed, 140 insertions(+), 43 deletions(-) diff --git a/UnleashedRecomp/gpu/video.cpp b/UnleashedRecomp/gpu/video.cpp index 84944944..fd1ffc4c 100644 --- a/UnleashedRecomp/gpu/video.cpp +++ b/UnleashedRecomp/gpu/video.cpp @@ -1383,7 +1383,7 @@ void Video::CreateHostDevice() g_commandLists[g_frame]->barriers(RenderBarrierStage::NONE, blankTextureBarriers, std::size(blankTextureBarriers)); } -static void WaitForGPU() +void Video::WaitForGPU() { if (g_vulkan) { @@ -1425,7 +1425,7 @@ static void BeginCommandList() if (!g_swapChainValid) { - WaitForGPU(); + Video::WaitForGPU(); g_backBuffer->framebuffers.clear(); g_swapChainValid = g_swapChain->resize(); g_needsResize = g_swapChainValid; @@ -1449,7 +1449,7 @@ static void BeginCommandList() if (g_intermediaryBackBufferTextureDescriptorIndex == NULL) g_intermediaryBackBufferTextureDescriptorIndex = g_textureDescriptorAllocator.allocate(); - WaitForGPU(); // Fine to wait for GPU, this'll only happen during resize. + Video::WaitForGPU(); // Fine to wait for GPU, this'll only happen during resize. g_intermediaryBackBufferTexture = g_device->createTexture(RenderTextureDesc::Texture2D(width, height, 1, BACKBUFFER_FORMAT, RenderTextureFlag::RENDER_TARGET)); g_textureDescriptorSet->setTexture(g_intermediaryBackBufferTextureDescriptorIndex, g_intermediaryBackBufferTexture.get(), RenderTextureLayout::SHADER_READ); diff --git a/UnleashedRecomp/gpu/video.h b/UnleashedRecomp/gpu/video.h index 0c27f8a4..c0711df3 100644 --- a/UnleashedRecomp/gpu/video.h +++ b/UnleashedRecomp/gpu/video.h @@ -15,6 +15,7 @@ struct Video static void CreateHostDevice(); static void HostPresent(); static void StartPipelinePrecompilation(); + static void WaitForGPU(); }; struct GuestSamplerState diff --git a/UnleashedRecomp/locale/locale.h b/UnleashedRecomp/locale/locale.h index 12757bb4..a73047b7 100644 --- a/UnleashedRecomp/locale/locale.h +++ b/UnleashedRecomp/locale/locale.h @@ -83,7 +83,35 @@ inline static std::unordered_map #include +#include #include #include @@ -54,8 +55,8 @@ 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 CONTAINER_BUTTON_WIDTH = 250.0f; +constexpr float CONTAINER_BUTTON_GAP = 9.0f; constexpr float BUTTON_HEIGHT = 22.0f; constexpr float BUTTON_TEXT_GAP = 28.0f; @@ -94,6 +95,7 @@ static std::string g_installerErrorMessage; enum class WizardPage { + SelectLanguage, Introduction, SelectGameAndUpdate, SelectDLC, @@ -103,17 +105,16 @@ enum class WizardPage InstallFailed, }; -static WizardPage g_firstPage = WizardPage::Introduction; +static WizardPage g_firstPage = WizardPage::SelectLanguage; static WizardPage g_currentPage = g_firstPage; static std::string g_currentMessagePrompt = ""; static bool g_currentMessagePromptConfirmation = false; -const char INSTALLER_TEXT[] = "INSTALLER"; -const char INSTALLING_TEXT[] = "INSTALLING"; const char CREDITS_TEXT[] = "Sajid (RIP)"; const char *WIZARD_TEXT[] = { + "Please select a language.\n", "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.", @@ -125,6 +126,7 @@ const char *WIZARD_TEXT[] = static const int WIZARD_INSTALL_TEXTURE_INDEX[] = { + 0, 0, 1, 2, @@ -134,6 +136,27 @@ static const int WIZARD_INSTALL_TEXTURE_INDEX[] = 5 // Force Eggman on InstallFailed. }; +// These are ordered from bottom to top in a 3x2 grid. +const char *LANGUAGE_TEXT[] = +{ + "FRANÇAIS", // French + "DEUTSCH", // German + "ENGLISH", // English + "ESPAÑOL", // Spanish + "ITALIANO", // Italian + "日本語", // Japanese +}; + +const ELanguage LANGUAGE_ENUM[] = +{ + ELanguage::French, + ELanguage::German, + ELanguage::English, + ELanguage::Spanish, + ELanguage::Italian, + ELanguage::Japanese, +}; + const char GAME_SOURCE_TEXT[] = "FULL GAME"; const char UPDATE_SOURCE_TEXT[] = "UPDATE"; const char *DLC_SOURCE_TEXT[] = @@ -148,8 +171,6 @@ const char *DLC_SOURCE_TEXT[] = 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"; @@ -268,10 +289,9 @@ static void DrawScanlineBars() SetShaderModifier(IMGUI_SHADER_MODIFIER_NONE); // Installer text - // TODO: localise this. - const char *headerText = g_currentPage == WizardPage::Installing ? INSTALLING_TEXT : INSTALLER_TEXT; + const std::string &headerText = Localise(g_currentPage == WizardPage::Installing ? "Installer_Header_Installing" : "Installer_Header_Installer"); int textAlpha = std::lround(255.0f * ComputeMotionInstaller(g_appearTime, g_disappearTime, TITLE_ANIMATION_TIME, TITLE_ANIMATION_DURATION)); - DrawTextWithOutline(g_dfsogeistdFont, Scale(42.0f), { Scale(285.0f), Scale(57.0f) }, IM_COL32(255, 195, 0, textAlpha), headerText, 4, IM_COL32(0, 0, 0, textAlpha)); + DrawTextWithOutline(g_dfsogeistdFont, Scale(42.0f), { Scale(285.0f), Scale(57.0f) }, IM_COL32(255, 195, 0, textAlpha), headerText.c_str(), 4, IM_COL32(0, 0, 0, textAlpha)); // Top bar line drawList->AddLine @@ -452,36 +472,41 @@ static void DrawButton(ImVec2 min, ImVec2 max, const char *buttonText, bool sour ResetGradient(); } -enum SourceColumn +enum ButtonColumn { - SourceColumnLeft, - SourceColumnMiddle, - SourceColumnRight + ButtonColumnLeft, + ButtonColumnMiddle, + ButtonColumnRight }; -static void DrawSourceButton(SourceColumn sourceColumn, float yRatio, const char *sourceText, bool sourceSet) +static void ComputeButtonColumnCoordinates(ButtonColumn buttonColumn, float &minX, float &maxX) +{ + switch (buttonColumn) + { + case ButtonColumnLeft: + minX = Scale(AlignToNextGrid(CONTAINER_X) + CONTAINER_BUTTON_GAP); + maxX = Scale(AlignToNextGrid(CONTAINER_X) + CONTAINER_BUTTON_GAP + CONTAINER_BUTTON_WIDTH); + break; + case ButtonColumnMiddle: + minX = Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH / 2.0f) - CONTAINER_BUTTON_WIDTH / 2.0f); + maxX = Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH / 2.0f) + CONTAINER_BUTTON_WIDTH / 2.0f); + break; + case ButtonColumnRight: + minX = Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH) - CONTAINER_BUTTON_GAP - CONTAINER_BUTTON_WIDTH); + maxX = Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH) - CONTAINER_BUTTON_GAP); + break; + } +} + +static void DrawSourceButton(ButtonColumn buttonColumn, 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; - } + ComputeButtonColumnCoordinates(buttonColumn, minX, maxX); - 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) }; + float minusY = (CONTAINER_BUTTON_GAP + BUTTON_HEIGHT) * yRatio; + ImVec2 min = { minX, Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) - CONTAINER_BUTTON_GAP - BUTTON_HEIGHT - minusY) }; + ImVec2 max = { maxX, Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) - CONTAINER_BUTTON_GAP - minusY) }; DrawButton(min, max, sourceText, true, sourceSet, buttonPressed); } @@ -634,6 +659,32 @@ static void ParseSourcePaths(std::list &paths) } } +static void DrawLanguagePicker() +{ + bool buttonPressed = false; + if (g_currentPage == WizardPage::SelectLanguage) + { + bool buttonPressed; + float minX, maxX; + for (int i = 0; i < 6; i++) + { + ComputeButtonColumnCoordinates((i < 3) ? ButtonColumnLeft : ButtonColumnRight, minX, maxX); + + float minusY = (CONTAINER_BUTTON_GAP + BUTTON_HEIGHT) * (float(i % 3)); + ImVec2 min = { minX, Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) - CONTAINER_BUTTON_GAP - BUTTON_HEIGHT - minusY) }; + ImVec2 max = { maxX, Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) - CONTAINER_BUTTON_GAP - minusY) }; + + // TODO: The active button should change its style to show an enabled toggle if it matches the current language. + + DrawButton(min, max, LANGUAGE_TEXT[i], false, true, buttonPressed); + if (buttonPressed) + { + Config::Language = LANGUAGE_ENUM[i]; + } + } + } +} + static void DrawSourcePickers() { bool buttonPressed = false; @@ -668,15 +719,15 @@ 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()); + DrawSourceButton(ButtonColumnMiddle, 1.5f, GAME_SOURCE_TEXT, !g_gameSourcePath.empty()); + DrawSourceButton(ButtonColumnMiddle, 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() || g_dlcInstalled[i]); + DrawSourceButton((i < 3) ? ButtonColumnLeft : ButtonColumnRight, float(i % 3), DLC_SOURCE_TEXT[i], !g_dlcSourcePaths[i].empty() || g_dlcInstalled[i]); } } } @@ -759,15 +810,15 @@ static void DrawNextButton() 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); + const std::string &buttonText = Localise(skipButton ? "Installer_Button_Skip" : "Installer_Button_Next"); + ImVec2 textSize = g_newRodinFont->CalcTextSizeA(20.0f, FLT_MAX, 0.0f, buttonText.c_str()); 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); + DrawButton(min, max, buttonText.c_str(), false, nextButtonEnabled, buttonPressed); if (buttonPressed) { @@ -951,6 +1002,7 @@ void InstallerWizard::Draw() DrawLeftImage(); DrawScanlineBars(); DrawDescriptionContainer(); + DrawLanguagePicker(); DrawSourcePickers(); DrawSources(); DrawInstallingProgress(); @@ -970,15 +1022,29 @@ void InstallerWizard::Draw() void InstallerWizard::Shutdown() { + // Wait for and erase the thread. if (g_installerThread != nullptr) { g_installerThread->join(); g_installerThread.reset(); } + // Erase the sources. g_installerSources.game.reset(); g_installerSources.update.reset(); g_installerSources.dlc.clear(); + + // Make sure the GPU is not currently active before deleting these textures. + Video::WaitForGPU(); + + // Erase the textures. + g_milesElectricIcon.reset(); + g_arrowCircle.reset(); + + for (auto &texture : g_installTextures) + { + texture.reset(); + } } bool InstallerWizard::Run(bool skipGame) @@ -1010,5 +1076,7 @@ bool InstallerWizard::Run(bool skipGame) Window::SetCursorAllowed(false); NFD_Quit(); + InstallerWizard::Shutdown(); + return true; } From 55d40032b068d9d9a189c80141a3330fd0c8b90b Mon Sep 17 00:00:00 2001 From: Hyper <34012267+hyperbx@users.noreply.github.com> Date: Tue, 3 Dec 2024 02:24:12 +0000 Subject: [PATCH 08/28] Implemented message window --- UnleashedRecomp/CMakeLists.txt | 1 + UnleashedRecomp/gpu/video.cpp | 3 + UnleashedRecomp/stdafx.h | 1 + UnleashedRecomp/ui/imgui_utils.h | 81 ++++++- UnleashedRecomp/ui/message_window.cpp | 335 ++++++++++++++++++++++++++ UnleashedRecomp/ui/message_window.h | 12 + UnleashedRecomp/ui/options_menu.h | 2 +- 7 files changed, 433 insertions(+), 2 deletions(-) create mode 100644 UnleashedRecomp/ui/message_window.cpp create mode 100644 UnleashedRecomp/ui/message_window.h diff --git a/UnleashedRecomp/CMakeLists.txt b/UnleashedRecomp/CMakeLists.txt index 9af198c9..ab279650 100644 --- a/UnleashedRecomp/CMakeLists.txt +++ b/UnleashedRecomp/CMakeLists.txt @@ -86,6 +86,7 @@ set(SWA_UI_CXX_SOURCES "ui/achievement_menu.cpp" "ui/achievement_overlay.cpp" "ui/installer_wizard.cpp" + "ui/message_window.cpp" "ui/options_menu.cpp" "ui/sdl_listener.cpp" "ui/window.cpp" diff --git a/UnleashedRecomp/gpu/video.cpp b/UnleashedRecomp/gpu/video.cpp index fd1ffc4c..489be1fe 100644 --- a/UnleashedRecomp/gpu/video.cpp +++ b/UnleashedRecomp/gpu/video.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -1066,6 +1067,7 @@ static void CreateImGuiBackend() AchievementMenu::Init(); AchievementOverlay::Init(); + MessageWindow::Init(); OptionsMenu::Init(); InstallerWizard::Init(); @@ -1741,6 +1743,7 @@ static void DrawImGui() OptionsMenu::Draw(); AchievementOverlay::Draw(); InstallerWizard::Draw(); + MessageWindow::Draw(); ImGui::Render(); diff --git a/UnleashedRecomp/stdafx.h b/UnleashedRecomp/stdafx.h index bf524468..964e928a 100644 --- a/UnleashedRecomp/stdafx.h +++ b/UnleashedRecomp/stdafx.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include diff --git a/UnleashedRecomp/ui/imgui_utils.h b/UnleashedRecomp/ui/imgui_utils.h index b3a1bd92..728e5c57 100644 --- a/UnleashedRecomp/ui/imgui_utils.h +++ b/UnleashedRecomp/ui/imgui_utils.h @@ -3,6 +3,12 @@ #include #include +#define PIXELS_TO_UV_COORDS(textureWidth, textureHeight, x, y, width, height) \ + std::make_tuple(ImVec2((float)x / (float)textureWidth, (float)y / (float)textureHeight), \ + ImVec2(((float)x + (float)width) / (float)textureWidth, ((float)y + (float)height) / (float)textureHeight)) + +#define GET_UV_COORDS(tuple) std::get<0>(tuple), std::get<1>(tuple) + static std::vector> g_callbackData; static uint32_t g_callbackDataIndex = 0; @@ -150,7 +156,80 @@ static void DrawTextWithShadow(const ImFont* font, float fontSize, const ImVec2& DrawTextWithOutline(font, fontSize, { pos.x + offset, pos.y + offset }, shadowColour, text, radius, shadowColour); - drawList->AddText(font, fontSize, pos, colour, text); + drawList->AddText(font, fontSize, pos, colour, text, nullptr); +} + +static float CalcWidestTextSize(const ImFont* font, float fontSize, std::span strs) +{ + auto result = 0.0f; + + for (auto& str : strs) + result = std::max(result, font->CalcTextSizeA(fontSize, FLT_MAX, 0, str.c_str()).x); + + return result; +} + +static std::vector Split(const char* str, char delimiter) +{ + std::vector result; + + if (!str) + return result; + + const char* start = str; + const char* current = str; + + while (*current) + { + if (*current == delimiter) + { + result.emplace_back(start, current - start); + start = current + 1; + } + + current++; + } + + result.emplace_back(start); + + return result; +} + +static ImVec2 MeasureCentredParagraph(const ImFont* font, float fontSize, float lineMargin, std::vector lines) +{ + auto x = 0.0f; + auto y = 0.0f; + + for (auto& str : lines) + { + auto textSize = font->CalcTextSizeA(fontSize, FLT_MAX, 0, str.c_str()); + + x = std::max(x, textSize.x); + y += textSize.y + Scale(lineMargin); + } + + return { x, y }; +} + +static ImVec2 MeasureCentredParagraph(const ImFont* font, float fontSize, float lineMargin, const char* text) +{ + return MeasureCentredParagraph(font, fontSize, lineMargin, Split(text, '\n')); +} + +static void DrawCentredParagraph(const ImFont* font, float fontSize, const ImVec2& centre, float lineMargin, const char* text, std::function drawMethod) +{ + auto lines = Split(text, '\n'); + auto paragraphSize = MeasureCentredParagraph(font, fontSize, lineMargin, lines); + auto offsetY = 0.0f; + + for (auto& str : lines) + { + auto textSize = font->CalcTextSizeA(fontSize, FLT_MAX, 0, str.c_str()); + + drawMethod(str.c_str(), ImVec2(/* X */ centre.x - textSize.x / 2, /* Y */ centre.y - paragraphSize.y / 2 + offsetY)); + + offsetY += textSize.y + Scale(lineMargin); + } } static void DrawTextWithMarqueeShadow(const ImFont* font, float fontSize, const ImVec2& pos, const ImVec2& min, const ImVec2& max, ImU32 colour, const char* text, double time, double delay, double speed, float offset = 2.0f, float radius = 0.4f, ImU32 shadowColour = IM_COL32(0, 0, 0, 255)) diff --git a/UnleashedRecomp/ui/message_window.cpp b/UnleashedRecomp/ui/message_window.cpp new file mode 100644 index 00000000..34c7bd49 --- /dev/null +++ b/UnleashedRecomp/ui/message_window.cpp @@ -0,0 +1,335 @@ +#include "message_window.h" +#include "imgui_utils.h" +#include +#include +#include +#include "../UnleashedRecompResources/images/pause.h" + +constexpr double OVERLAY_CONTAINER_COMMON_MOTION_START = 0; +constexpr double OVERLAY_CONTAINER_COMMON_MOTION_END = 11; +constexpr double OVERLAY_CONTAINER_INTRO_FADE_START = 5; +constexpr double OVERLAY_CONTAINER_INTRO_FADE_END = 9; +constexpr double OVERLAY_CONTAINER_OUTRO_FADE_START = 0; +constexpr double OVERLAY_CONTAINER_OUTRO_FADE_END = 4; + +static bool g_isAwaitingResult = false; +static bool g_isClosing = false; +static bool g_isControlsVisible = false; + +static int g_selectedRowIndex; +static int g_foregroundCount; + +static bool g_upWasHeld; +static bool g_downWasHeld; + +static double g_appearTime; +static double g_controlsAppearTime; + +static ImFont* g_fntSeurat; + +static std::unique_ptr g_upSelectionCursor; + +std::string g_text; +int g_result; +std::vector g_buttons; +int g_defaultButtonIndex; + +bool DrawContainer(float appearTime, ImVec2 centre, ImVec2 max, bool isForeground = true) +{ + auto drawList = ImGui::GetForegroundDrawList(); + + ImVec2 _min = { centre.x - max.x, centre.y - max.y }; + ImVec2 _max = { centre.x + max.x, centre.y + max.y }; + + // Expand/retract animation. + auto containerMotion = ComputeMotion(appearTime, OVERLAY_CONTAINER_COMMON_MOTION_START, OVERLAY_CONTAINER_COMMON_MOTION_END); + + if (g_isClosing) + { + _min.x = Hermite(_min.x, centre.x, containerMotion); + _max.x = Hermite(_max.x, centre.x, containerMotion); + _min.y = Hermite(_min.y, centre.y, containerMotion); + _max.y = Hermite(_max.y, centre.y, containerMotion); + } + else + { + _min.x = Hermite(centre.x, _min.x, containerMotion); + _max.x = Hermite(centre.x, _max.x, containerMotion); + _min.y = Hermite(centre.y, _min.y, containerMotion); + _max.y = Hermite(centre.y, _max.y, containerMotion); + } + + auto vertices = GetPauseContainerVertices(_min, _max); + + // Transparency fade animation. + auto colourMotion = g_isClosing + ? ComputeMotion(appearTime, OVERLAY_CONTAINER_OUTRO_FADE_START, OVERLAY_CONTAINER_OUTRO_FADE_END) + : ComputeMotion(appearTime, OVERLAY_CONTAINER_INTRO_FADE_START, OVERLAY_CONTAINER_INTRO_FADE_END); + + auto alpha = g_isClosing + ? Lerp(1, 0, colourMotion) + : Lerp(0, 1, colourMotion); + + if (!isForeground) + g_foregroundCount++; + + if (isForeground) + drawList->AddRectFilled({ 0.0f, 0.0f }, ImGui::GetIO().DisplaySize, IM_COL32(0, 0, 0, 223 * (g_foregroundCount ? 1 : alpha))); + + auto colShadow = IM_COL32(0, 0, 0, 156 * alpha); + auto colGradientTop = IM_COL32(197, 194, 197, 200 * alpha); + auto colGradientBottom = IM_COL32(115, 113, 115, 236 * alpha); + + // Draw vertices with gradient. + SetGradient(_min, _max, colGradientTop, colGradientBottom); + drawList->AddConvexPolyFilled(vertices.data(), vertices.size(), IM_COL32(255, 255, 255, 255 * alpha)); + ResetGradient(); + + // Draw outline. + drawList->AddPolyline + ( + vertices.data(), + vertices.size(), + IM_COL32(247, 247, 247, 255 * alpha), + true, + Scale(2.5f) + ); + + // Offset vertices to draw 3D effect lines. + for (int i = 0; i < vertices.size(); i++) + { + vertices[i].x -= Scale(0.4f); + vertices[i].y -= Scale(0.2f); + } + + auto colLineTop = IM_COL32(165, 170, 165, 230 * alpha); + auto colLineBottom = IM_COL32(190, 190, 190, 230 * alpha); + auto lineThickness = Scale(1.0f); + + // Top left corner bottom to top left corner top. + drawList->AddLine(vertices[0], vertices[1], colLineTop, lineThickness * 0.5f); + + // Top left corner bottom to bottom left. + drawList->AddRectFilledMultiColor({ vertices[0].x - 0.2f, vertices[0].y }, { vertices[6].x + lineThickness - 0.2f, vertices[6].y }, colLineTop, colLineTop, colLineBottom, colLineBottom); + + // Top left corner top to top right. + drawList->AddLine(vertices[1], vertices[2], colLineTop, lineThickness); + + drawList->PushClipRect(_min, _max); + + return containerMotion >= 1.0f && !g_isClosing; +} + +void DrawButton(int rowIndex, float yOffset, float width, float height, std::string& text) +{ + auto drawList = ImGui::GetForegroundDrawList(); + + auto clipRectMin = drawList->GetClipRectMin(); + auto clipRectMax = drawList->GetClipRectMax(); + + ImVec2 min = { clipRectMin.x + ((clipRectMax.x - clipRectMin.x) - width) / 2, clipRectMin.y + height * rowIndex + yOffset }; + ImVec2 max = { min.x + width, min.y + height }; + + bool isSelected = rowIndex == g_selectedRowIndex; + + if (isSelected) + { + static auto breatheStart = ImGui::GetTime(); + auto alpha = Lerp(1.0f, 0.75f, (sin((ImGui::GetTime() - breatheStart) * (2.0f * M_PI / (55.0f / 60.0f))) + 1.0f) / 2.0f); + auto colour = IM_COL32(255, 255, 255, 255 * alpha); + + auto width = Scale(11); + auto left = PIXELS_TO_UV_COORDS(128, 128, 0, 0, 11, 50); + auto centre = PIXELS_TO_UV_COORDS(128, 128, 11, 0, 8, 50); + auto right = PIXELS_TO_UV_COORDS(128, 128, 19, 0, 11, 50); + + drawList->AddImage(g_upSelectionCursor.get(), min, { min.x + width, max.y }, GET_UV_COORDS(left), colour); + drawList->AddImage(g_upSelectionCursor.get(), { min.x + width, min.y }, { max.x - width, max.y }, GET_UV_COORDS(centre), colour); + drawList->AddImage(g_upSelectionCursor.get(), { max.x - width, min.y }, max, GET_UV_COORDS(right), colour); + } + + auto fontSize = Scale(28); + auto textSize = g_fntSeurat->CalcTextSizeA(fontSize, FLT_MAX, 0, text.c_str()); + + DrawTextWithShadow + ( + g_fntSeurat, + fontSize, + { /* X */ min.x + ((max.x - min.x) - textSize.x) / 2, /* Y */ min.y + ((max.y - min.y) - textSize.y) / 2 }, + isSelected ? IM_COL32(255, 128, 0, 255) : IM_COL32(255, 255, 255, 255), + text.c_str() + ); +} + +static void ResetSelection() +{ + g_selectedRowIndex = g_defaultButtonIndex; + g_upWasHeld = false; + g_downWasHeld = false; +} + +void MessageWindow::Init() +{ + auto& io = ImGui::GetIO(); + + constexpr float FONT_SCALE = 2.0f; + + g_fntSeurat = io.Fonts->AddFontFromFileTTF("FOT-SeuratPro-M.otf", 28.0f * FONT_SCALE); + + g_upSelectionCursor = LoadTexture((uint8_t*)g_res_pause, g_res_pause_size); +} + +void MessageWindow::Draw() +{ + if (!s_isVisible) + return; + + auto pInputState = SWA::CInputState::GetInstance(); + auto drawList = ImGui::GetForegroundDrawList(); + auto& res = ImGui::GetIO().DisplaySize; + + ImVec2 centre = { res.x / 2, res.y / 2 }; + + auto fontSize = Scale(28); + auto textSize = MeasureCentredParagraph(g_fntSeurat, fontSize, 5, g_text.c_str()); + auto textMarginX = Scale(32); + auto textMarginY = Scale(40); + + if (DrawContainer(g_appearTime, centre, { textSize.x / 2 + textMarginX, textSize.y / 2 + textMarginY }, !g_isControlsVisible)) + { + DrawCentredParagraph + ( + g_fntSeurat, + fontSize, + { centre.x, centre.y + Scale(3) }, + 5, + g_text.c_str(), + + [=](const char* str, ImVec2 pos) + { + DrawTextWithShadow(g_fntSeurat, fontSize, pos, IM_COL32(255, 255, 255, 255), str); + } + ); + + drawList->PopClipRect(); + + if (g_buttons.size()) + { + auto itemWidth = std::max(Scale(162), Scale(CalcWidestTextSize(g_fntSeurat, fontSize, g_buttons))); + auto itemHeight = Scale(57); + auto windowMarginX = Scale(18); + auto windowMarginY = Scale(25); + + ImVec2 controlsMax = { /* X */ itemWidth / 2 + windowMarginX, /* Y */ itemHeight / 2 * g_buttons.size() + windowMarginY }; + + if (g_isControlsVisible && DrawContainer(g_controlsAppearTime, centre, controlsMax)) + { + auto rowCount = 0; + + for (auto& button : g_buttons) + DrawButton(rowCount++, windowMarginY, itemWidth, itemHeight, button); + + drawList->PopClipRect(); + + bool upIsHeld = pInputState->GetPadState().IsDown(SWA::eKeyState_DpadUp) || + pInputState->GetPadState().LeftStickVertical > 0.5f; + + bool downIsHeld = pInputState->GetPadState().IsDown(SWA::eKeyState_DpadDown) || + pInputState->GetPadState().LeftStickVertical < -0.5f; + + bool scrollUp = !g_upWasHeld && upIsHeld; + bool scrollDown = !g_downWasHeld && downIsHeld; + + if (scrollUp) + { + --g_selectedRowIndex; + if (g_selectedRowIndex < 0) + g_selectedRowIndex = rowCount - 1; + } + else if (scrollDown) + { + ++g_selectedRowIndex; + if (g_selectedRowIndex >= rowCount) + g_selectedRowIndex = 0; + } + + if (scrollUp || scrollDown) + Game_PlaySound("sys_actstg_pausecursor"); + + g_upWasHeld = upIsHeld; + g_downWasHeld = downIsHeld; + + if (pInputState->GetPadState().IsTapped(SWA::eKeyState_A)) + { + g_result = g_selectedRowIndex; + + Game_PlaySound("sys_actstg_pausedecide"); + MessageWindow::Close(); + } + else if (pInputState->GetPadState().IsTapped(SWA::eKeyState_B)) + { + g_result = -1; + + Game_PlaySound("sys_actstg_pausecansel"); + MessageWindow::Close(); + } + } + else + { + if (!g_isControlsVisible && pInputState->GetPadState().IsTapped(SWA::eKeyState_A)) + { + g_controlsAppearTime = ImGui::GetTime(); + g_isControlsVisible = true; + + Game_PlaySound("sys_actstg_pausewinopen"); + } + } + } + else + { + if (pInputState->GetPadState().IsTapped(SWA::eKeyState_A)) + MessageWindow::Close(); + } + } +} + +bool MessageWindow::Open(std::string text, int* result, std::span buttons, int defaultButtonIndex) +{ + if (!g_isAwaitingResult && *result == -1) + { + s_isVisible = true; + g_isClosing = false; + g_isControlsVisible = false; + g_foregroundCount = 0; + g_appearTime = ImGui::GetTime(); + g_controlsAppearTime = ImGui::GetTime(); + + g_text = text; + g_buttons = std::vector(buttons.begin(), buttons.end()); + g_defaultButtonIndex = defaultButtonIndex; + + ResetSelection(); + + Game_PlaySound("sys_actstg_pausewinopen"); + + g_isAwaitingResult = true; + } + + *result = g_result; + + return !g_isAwaitingResult; +} + +void MessageWindow::Close() +{ + if (!g_isClosing) + { + g_appearTime = ImGui::GetTime(); + g_controlsAppearTime = ImGui::GetTime(); + g_isClosing = true; + g_isControlsVisible = false; + g_isAwaitingResult = false; + } + + Game_PlaySound("sys_actstg_pausewinclose"); +} diff --git a/UnleashedRecomp/ui/message_window.h b/UnleashedRecomp/ui/message_window.h new file mode 100644 index 00000000..b1baca00 --- /dev/null +++ b/UnleashedRecomp/ui/message_window.h @@ -0,0 +1,12 @@ +#pragma once + +class MessageWindow +{ +public: + inline static bool s_isVisible = false; + + static void Init(); + static void Draw(); + static bool Open(std::string text, int* result, std::span buttons = {}, int defaultButtonIndex = 0); + static void Close(); +}; diff --git a/UnleashedRecomp/ui/options_menu.h b/UnleashedRecomp/ui/options_menu.h index 6ea404b5..85268806 100644 --- a/UnleashedRecomp/ui/options_menu.h +++ b/UnleashedRecomp/ui/options_menu.h @@ -2,7 +2,7 @@ #include -struct OptionsMenu +class OptionsMenu { public: inline static bool s_isVisible = false; From cb008c80eb92e40f1d41992602477fb8be6f9350 Mon Sep 17 00:00:00 2001 From: Hyper <34012267+hyperbx@users.noreply.github.com> Date: Tue, 3 Dec 2024 02:28:19 +0000 Subject: [PATCH 09/28] Merge branch 'bin2c' into options-menu --- CMakeLists.txt | 5 +- UnleashedRecomp/CMakeLists.txt | 33 +++++++++++ UnleashedRecomp/res/.gitignore | 1 + UnleashedRecomp/ui/window.h | 8 +-- tools/CMakeLists.txt | 1 + tools/file_to_c/CMakeLists.txt | 8 +++ tools/file_to_c/file_to_c.cpp | 105 +++++++++++++++++++++++++++++++++ 7 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 tools/CMakeLists.txt create mode 100644 tools/file_to_c/CMakeLists.txt create mode 100644 tools/file_to_c/file_to_c.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0c605965..853ef904 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required (VERSION 3.20) include($ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake) set(SWA_THIRDPARTY_ROOT ${CMAKE_SOURCE_DIR}/thirdparty) +set(SWA_TOOLS_ROOT ${CMAKE_SOURCE_DIR}/tools) set(CMAKE_CXX_STANDARD 23) set(BUILD_SHARED_LIBS OFF) @@ -14,9 +15,9 @@ endif() set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") add_subdirectory(${SWA_THIRDPARTY_ROOT}) -project("UnleashedRecomp-ALL") +add_subdirectory(${SWA_TOOLS_ROOT}) -include("thirdparty/PowerRecomp/cmake/bin2h.cmake") +project("UnleashedRecomp-ALL") # Include sub-projects. add_subdirectory("UnleashedRecompLib") diff --git a/UnleashedRecomp/CMakeLists.txt b/UnleashedRecomp/CMakeLists.txt index ab279650..4edbfbed 100644 --- a/UnleashedRecomp/CMakeLists.txt +++ b/UnleashedRecomp/CMakeLists.txt @@ -3,6 +3,32 @@ set(TARGET_NAME "SWA") option(SWA_XAUDIO2 "Use XAudio2 for audio playback" OFF) +function(BIN2C) + cmake_parse_arguments(BIN2C_ARGS "" "TARGET_OBJ;SOURCE_FILE;DEST_FILE;ARRAY_TYPE;ARRAY_NAME" "" ${ARGN}) + + if(NOT BIN2C_ARGS_TARGET_OBJ) + message(FATAL_ERROR "TARGET_OBJ not specified.") + endif() + + if(NOT BIN2C_ARGS_SOURCE_FILE) + message(FATAL_ERROR "SOURCE_FILE not specified.") + endif() + + if(NOT BIN2C_ARGS_DEST_FILE) + set(BIN2C_ARGS_DEST_FILE "${BIN2C_ARGS_SOURCE_FILE}") + endif() + + add_custom_command(OUTPUT "${BIN2C_ARGS_DEST_FILE}.c" + COMMAND file_to_c "${BIN2C_ARGS_SOURCE_FILE}" "${BIN2C_ARGS_ARRAY_NAME}" "${BIN2C_ARGS_ARRAY_TYPE}" "${BIN2C_ARGS_DEST_FILE}.c" "${BIN2C_ARGS_DEST_FILE}.h" + DEPENDS "${BIN2C_ARGS_SOURCE_FILE}" file_to_c + BYPRODUCTS "${BIN2C_ARGS_DEST_FILE}.h" + COMMENT "Generating binary header for ${BIN2C_ARGS_SOURCE_FILE}..." + ) + + set_source_files_properties(${BIN2C_ARGS_DEST_FILE}.c PROPERTIES SKIP_PRECOMPILE_HEADERS ON) + target_sources(${BIN2C_ARGS_TARGET_OBJ} PRIVATE ${BIN2C_ARGS_DEST_FILE}.c) +endfunction() + add_compile_options( /fp:strict -march=sandybridge @@ -302,3 +328,10 @@ generate_aggregate_header( "${CMAKE_CURRENT_SOURCE_DIR}/api" "${CMAKE_CURRENT_SOURCE_DIR}/api/SWA.h" ) + +set(RESOURCES_SOURCE_PATH "${PROJECT_SOURCE_DIR}/../UnleashedRecompResources") +set(RESOURCES_OUTPUT_PATH "${PROJECT_SOURCE_DIR}/res") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/achievements_menu/trophy.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/achievements_menu/trophy.dds" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_trophy") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/game_icon.bmp" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/game_icon.bmp" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_game_icon") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/game_icon_night.bmp" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/game_icon_night.bmp" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_game_icon_night") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/pause.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/pause.dds" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_pause") diff --git a/UnleashedRecomp/res/.gitignore b/UnleashedRecomp/res/.gitignore index e3285599..435cdeaa 100644 --- a/UnleashedRecomp/res/.gitignore +++ b/UnleashedRecomp/res/.gitignore @@ -1,2 +1,3 @@ ![Ww][Ii][Nn]32/ +*.c *.h \ No newline at end of file diff --git a/UnleashedRecomp/ui/window.h b/UnleashedRecomp/ui/window.h index 96464122..9e48300b 100644 --- a/UnleashedRecomp/ui/window.h +++ b/UnleashedRecomp/ui/window.h @@ -1,7 +1,7 @@ #pragma once -#include -#include +#include +#include #include #include @@ -47,11 +47,11 @@ public: { if (isNight) { - SetIcon((void*)g_iconNight, g_iconNight_size); + SetIcon(g_game_icon_night, sizeof(g_game_icon_night)); } else { - SetIcon((void*)g_icon, g_icon_size); + SetIcon(g_game_icon, sizeof(g_game_icon)); } } diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt new file mode 100644 index 00000000..d732343c --- /dev/null +++ b/tools/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(${SWA_TOOLS_ROOT}/file_to_c) diff --git a/tools/file_to_c/CMakeLists.txt b/tools/file_to_c/CMakeLists.txt new file mode 100644 index 00000000..08975574 --- /dev/null +++ b/tools/file_to_c/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.20) + +include(CMakeParseArguments) + +project("file_to_c") +set(CMAKE_CXX_STANDARD 17) + +add_executable(file_to_c "file_to_c.cpp") diff --git a/tools/file_to_c/file_to_c.cpp b/tools/file_to_c/file_to_c.cpp new file mode 100644 index 00000000..4d69e8c9 --- /dev/null +++ b/tools/file_to_c/file_to_c.cpp @@ -0,0 +1,105 @@ +/* + MIT License + + Copyright (c) 2024 RT64 Contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include +#include +#include +#include + +std::vector read_file(const char* path) { + std::ifstream input_file{path, std::ios::binary}; + std::vector ret{}; + + if (!input_file.good()) { + return ret; + } + + // Get the length of the file + input_file.seekg(0, std::ios::end); + ret.resize(input_file.tellg()); + + // Read the file contents into the vector + input_file.seekg(0, std::ios::beg); + input_file.read(ret.data(), ret.size()); + + return ret; +} + +void create_parent_if_needed(const char* path) { + std::filesystem::path parent_path = std::filesystem::path{path}.parent_path(); + if (!parent_path.empty()) { + std::filesystem::create_directories(parent_path); + } +} + +int main(int argc, const char** argv) { + if (argc != 6) { + printf("Usage: %s [input file] [array name] [array type] [output C file] [output C header]\n", argv[0]); + return EXIT_SUCCESS; + } + + const char* input_path = argv[1]; + const char* array_name = argv[2]; + const char* array_type = argv[3]; + const char* output_c_path = argv[4]; + const char* output_h_path = argv[5]; + + // Read the input file's contents + std::vector contents = read_file(input_path); + + if (contents.empty()) { + fprintf(stderr, "Failed to open file %s! (Or it's empty)\n", input_path); + return EXIT_FAILURE; + } + + // Create the output directories if they don't exist + create_parent_if_needed(output_c_path); + create_parent_if_needed(output_h_path); + + // Write the C file with the array + { + std::ofstream output_c_file{output_c_path}; + output_c_file << "extern " << array_type << " " << array_name << "[" << contents.size() << "];\n"; + output_c_file << array_type << " " << array_name << "[" << contents.size() << "] = {"; + + for (char x : contents) { + output_c_file << (int)x << ", "; + } + + output_c_file << "};\n"; + } + + // Write the header file with the extern array + { + std::ofstream output_h_file{output_h_path}; + output_h_file << + "#ifdef __cplusplus\n" + " extern \"C\" {\n" + "#endif\n" + "extern " << array_type << " " << array_name << "[" << contents.size() << "];\n" + "#ifdef __cplusplus\n" + " }\n" + "#endif\n"; + } +} From f939c70438a7bd7c26d64cd5c924fc5f8cf772b9 Mon Sep 17 00:00:00 2001 From: Hyper <34012267+hyperbx@users.noreply.github.com> Date: Tue, 3 Dec 2024 03:12:23 +0000 Subject: [PATCH 10/28] Update embedded resources --- UnleashedRecomp/CMakeLists.txt | 26 +++++++--------- UnleashedRecomp/ui/installer_wizard.cpp | 40 ++++++++++++------------- UnleashedRecomp/ui/message_window.cpp | 4 +-- UnleashedRecompResources | 2 +- thirdparty/PowerRecomp | 2 +- 5 files changed, 34 insertions(+), 40 deletions(-) diff --git a/UnleashedRecomp/CMakeLists.txt b/UnleashedRecomp/CMakeLists.txt index 4edbfbed..00aa2ad3 100644 --- a/UnleashedRecomp/CMakeLists.txt +++ b/UnleashedRecomp/CMakeLists.txt @@ -49,10 +49,6 @@ add_compile_definitions( _DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR # Microsoft wtf? _CRT_SECURE_NO_WARNINGS) -# Generate icon bitmap header for SDL surface. -BIN2H(SOURCE_FILE "../UnleashedRecompResources/images/game_icon.bmp" HEADER_FILE "res/icon.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_icon") -BIN2H(SOURCE_FILE "../UnleashedRecompResources/images/game_icon_night.bmp" HEADER_FILE "res/icon_night.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_iconNight") - set(SWA_PRECOMPILED_HEADERS "stdafx.h" ) @@ -134,18 +130,6 @@ 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") -BIN2H(SOURCE_FILE "${INSTALLER_IMAGES_DIR}/miles_electric_icon.dds" HEADER_FILE "res/miles_electric_icon_dds.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_milesElectricIconDDS") -BIN2H(SOURCE_FILE "${INSTALLER_IMAGES_DIR}/arrow_circle.dds" HEADER_FILE "res/arrow_circle_dds.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_arrowCircleDDS") - set(LIBMSPACK_PATH ${SWA_THIRDPARTY_ROOT}/libmspack/libmspack/mspack) set(LIBMSPACK_C_SOURCES @@ -332,6 +316,16 @@ generate_aggregate_header( set(RESOURCES_SOURCE_PATH "${PROJECT_SOURCE_DIR}/../UnleashedRecompResources") set(RESOURCES_OUTPUT_PATH "${PROJECT_SOURCE_DIR}/res") BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/achievements_menu/trophy.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/achievements_menu/trophy.dds" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_trophy") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/arrow_circle.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/arrow_circle.dds" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_arrow_circle") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_001.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_001.dds" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_install_001") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_002.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_002.dds" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_install_002") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_003.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_003.dds" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_install_003") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_004.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_004.dds" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_install_004") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_005.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_005.dds" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_install_005") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_006.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_006.dds" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_install_006") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_007.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_007.dds" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_install_007") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_008.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_008.dds" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_install_008") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/miles_electric_icon.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/miles_electric_icon.dds" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_miles_electric_icon") BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/game_icon.bmp" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/game_icon.bmp" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_game_icon") BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/game_icon_night.bmp" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/game_icon_night.bmp" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_game_icon_night") BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/pause.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/pause.dds" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_pause") diff --git a/UnleashedRecomp/ui/installer_wizard.cpp b/UnleashedRecomp/ui/installer_wizard.cpp index 55029ddf..1183496d 100644 --- a/UnleashedRecomp/ui/installer_wizard.cpp +++ b/UnleashedRecomp/ui/installer_wizard.cpp @@ -8,16 +8,16 @@ #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include static constexpr double SCANLINES_ANIMATION_TIME = 0.0; static constexpr double SCANLINES_ANIMATION_DURATION = 15.0; @@ -979,16 +979,16 @@ void InstallerWizard::Init() 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); - g_milesElectricIcon = LoadTexture(g_milesElectricIconDDS, g_milesElectricIconDDS_size); - g_arrowCircle = LoadTexture(g_arrowCircleDDS, g_arrowCircleDDS_size); + g_installTextures[0] = LoadTexture(g_install_001, sizeof(g_install_001)); + g_installTextures[1] = LoadTexture(g_install_002, sizeof(g_install_002)); + g_installTextures[2] = LoadTexture(g_install_003, sizeof(g_install_003)); + g_installTextures[3] = LoadTexture(g_install_004, sizeof(g_install_004)); + g_installTextures[4] = LoadTexture(g_install_005, sizeof(g_install_005)); + g_installTextures[5] = LoadTexture(g_install_006, sizeof(g_install_006)); + g_installTextures[6] = LoadTexture(g_install_007, sizeof(g_install_007)); + g_installTextures[7] = LoadTexture(g_install_008, sizeof(g_install_008)); + g_milesElectricIcon = LoadTexture(g_miles_electric_icon, sizeof(g_miles_electric_icon)); + g_arrowCircle = LoadTexture(g_arrow_circle, sizeof(g_arrow_circle)); } void InstallerWizard::Draw() diff --git a/UnleashedRecomp/ui/message_window.cpp b/UnleashedRecomp/ui/message_window.cpp index 34c7bd49..c82a044b 100644 --- a/UnleashedRecomp/ui/message_window.cpp +++ b/UnleashedRecomp/ui/message_window.cpp @@ -3,7 +3,7 @@ #include #include #include -#include "../UnleashedRecompResources/images/pause.h" +#include constexpr double OVERLAY_CONTAINER_COMMON_MOTION_START = 0; constexpr double OVERLAY_CONTAINER_COMMON_MOTION_END = 11; @@ -176,7 +176,7 @@ void MessageWindow::Init() g_fntSeurat = io.Fonts->AddFontFromFileTTF("FOT-SeuratPro-M.otf", 28.0f * FONT_SCALE); - g_upSelectionCursor = LoadTexture((uint8_t*)g_res_pause, g_res_pause_size); + g_upSelectionCursor = LoadTexture(g_pause, sizeof(g_pause)); } void MessageWindow::Draw() diff --git a/UnleashedRecompResources b/UnleashedRecompResources index 79a2a18c..b04e3bdc 160000 --- a/UnleashedRecompResources +++ b/UnleashedRecompResources @@ -1 +1 @@ -Subproject commit 79a2a18cc8013dd6d76d8300f703f19f6e0ec484 +Subproject commit b04e3bdcd9e76a0717cec69822f4eb01cd97d5c6 diff --git a/thirdparty/PowerRecomp b/thirdparty/PowerRecomp index 675b482e..ea3e60cb 160000 --- a/thirdparty/PowerRecomp +++ b/thirdparty/PowerRecomp @@ -1 +1 @@ -Subproject commit 675b482ec4852b873590fb999d24b426bade2b3a +Subproject commit ea3e60cb0d618ec8d24cc745d72b1ac39cc722cc From 819692e67d89b8d31262892e7decbc87022c9472 Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 3 Dec 2024 09:22:54 -0300 Subject: [PATCH 11/28] Framework for max width for buttons. --- UnleashedRecomp/locale/locale.h | 16 ++++++++++ UnleashedRecomp/ui/installer_wizard.cpp | 40 ++++++++++++++++++------- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/UnleashedRecomp/locale/locale.h b/UnleashedRecomp/locale/locale.h index a73047b7..0c81ff0d 100644 --- a/UnleashedRecomp/locale/locale.h +++ b/UnleashedRecomp/locale/locale.h @@ -112,6 +112,22 @@ inline static std::unordered_mapCalcTextSizeA(size, FLT_MAX, 0.0f, text); + if (textSize.x > maxTextWidth) + { + float shrinkRatio = maxTextWidth / textSize.x; + size *= shrinkRatio; + textSize.x *= shrinkRatio; + textSize.y *= shrinkRatio; + } + + return textSize; +} + +static ImVec2 ComputeTextSize(ImFont *font, const char *text, float size, float maxTextWidth = FLT_MAX) +{ + return ComputeTextSizeAndRatio(font, text, size, maxTextWidth); +} + +static void DrawButton(ImVec2 min, ImVec2 max, const char *buttonText, bool sourceButton, bool buttonEnabled, bool &buttonPressed, float maxTextWidth = FLT_MAX) { buttonPressed = false; @@ -437,10 +454,9 @@ static void DrawButton(ImVec2 min, ImVec2 max, const char *buttonText, bool sour 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); - + float size = Scale(sourceButton ? 15.0f : 20.0f); + ImVec2 textSize = ComputeTextSizeAndRatio(font, buttonText, size, Scale(maxTextWidth)); min.x += ((max.x - min.x) - textSize.x) / 2.0f; min.y += ((max.y - min.y) - textSize.y) / 2.0f; @@ -691,23 +707,27 @@ static void DrawSourcePickers() 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); + constexpr float ADD_BUTTON_MAX_TEXT_WIDTH = 160.0f; + const std::string &addFilesText = Localise("Installer_Button_AddFiles"); + ImVec2 textSize = ComputeTextSize(g_dfsogeistdFont, addFilesText.c_str(), 20.0f, ADD_BUTTON_MAX_TEXT_WIDTH); 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); + DrawButton(min, max, addFilesText.c_str(), false, true, buttonPressed, ADD_BUTTON_MAX_TEXT_WIDTH); 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); + + const std::string &addFolderText = Localise("Installer_Button_AddFolder"); + textSize = ComputeTextSize(g_dfsogeistdFont, addFolderText.c_str(), 20.0f, ADD_BUTTON_MAX_TEXT_WIDTH); textSize.x += BUTTON_TEXT_GAP; max.x = min.x + Scale(textSize.x); - DrawButton(min, max, FOLDER_BUTTON_TEXT, false, true, buttonPressed); + DrawButton(min, max, addFolderText.c_str(), false, true, buttonPressed, ADD_BUTTON_MAX_TEXT_WIDTH); if (buttonPressed && ShowFoldersPicker(paths)) { ParseSourcePaths(paths); From 26e8e228a7edea37ee2a3e873ccef0f1b9448529 Mon Sep 17 00:00:00 2001 From: Hyper <34012267+hyperbx@users.noreply.github.com> Date: Tue, 3 Dec 2024 20:23:16 +0000 Subject: [PATCH 12/28] installer_wizard: use integer outline for button text --- UnleashedRecomp/ui/installer_wizard.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/UnleashedRecomp/ui/installer_wizard.cpp b/UnleashedRecomp/ui/installer_wizard.cpp index 42b1b6b5..7de4bc68 100644 --- a/UnleashedRecomp/ui/installer_wizard.cpp +++ b/UnleashedRecomp/ui/installer_wizard.cpp @@ -474,16 +474,16 @@ static void DrawButton(ImVec2 min, ImVec2 max, const char *buttonText, bool sour 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) - ); + DrawTextWithOutline + ( + font, + size, + min, + IM_COL32(255, 255, 255, 255 * alpha), + buttonText, + 2, + IM_COL32(baser, baseg, 0, 255 * alpha) + ); ResetGradient(); } From 82e4f5d42a5af40492077fbdde297933a16414fa Mon Sep 17 00:00:00 2001 From: PTKay Date: Tue, 3 Dec 2024 21:15:00 +0000 Subject: [PATCH 13/28] Added arrow circle spinning animation during installation screen --- UnleashedRecomp/ui/installer_wizard.cpp | 28 ++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/UnleashedRecomp/ui/installer_wizard.cpp b/UnleashedRecomp/ui/installer_wizard.cpp index 7de4bc68..b77f954e 100644 --- a/UnleashedRecomp/ui/installer_wizard.cpp +++ b/UnleashedRecomp/ui/installer_wizard.cpp @@ -40,6 +40,8 @@ static constexpr double CONTAINER_OUTER_DURATION = 23.0; static constexpr double CONTAINER_INNER_TIME = SCANLINES_ANIMATION_DURATION + CONTAINER_LINE_ANIMATION_DURATION + 8.0; static constexpr double CONTAINER_INNER_DURATION = 15.0; +static constexpr double ARROW_CIRCLE_SPIN_FACTOR = 2; + static constexpr double ALL_ANIMATIONS_FULL_DURATION = CONTAINER_INNER_TIME + CONTAINER_INNER_DURATION; constexpr float IMAGE_X = 165.0f; @@ -70,6 +72,8 @@ static ImFont *g_seuratFont; static ImFont *g_dfsogeistdFont; static ImFont *g_newRodinFont; +static double g_arrowCircleCurrentRotation = 0.0; + static double g_appearTime = 0.0; static double g_disappearTime = DBL_MAX; static bool g_isDisappearing = false; @@ -237,7 +241,29 @@ static void DrawHeaderIcons() GuestTexture* arrowCircleTexture = g_arrowCircle.get(); ImVec2 arrowCircleMin = { Scale(iconsPosX - iconsScale / 2), Scale(iconsPosY - iconsScale / 2) }; ImVec2 arrowCircleMax = { Scale(iconsPosX + iconsScale / 2), Scale(iconsPosY + iconsScale / 2) }; - drawList->AddImage(arrowCircleTexture, arrowCircleMin, arrowCircleMax, ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 96)); + + ImVec2 center = { Scale(iconsPosX), Scale(iconsPosY) }; + float currentAngle = g_arrowCircleCurrentRotation * (3.14159f / 180.0f); // Rotation angle in radians + float cos_a = cosf(currentAngle); + float sin_a = sinf(currentAngle); + + // Calculate rotated corners + ImVec2 corners[4] = { + ImRotate(ImVec2(arrowCircleMin.x - center.x, arrowCircleMin.y - center.y), cos_a, sin_a), + ImRotate(ImVec2(arrowCircleMax.x - center.x, arrowCircleMin.y - center.y), cos_a, sin_a), + ImRotate(ImVec2(arrowCircleMax.x - center.x, arrowCircleMax.y - center.y), cos_a, sin_a), + ImRotate(ImVec2(arrowCircleMin.x - center.x, arrowCircleMax.y - center.y), cos_a, sin_a), + }; + + for (int i = 0; i < 4; ++i) { + corners[i].x += center.x; // Add center.x to corner.x + corners[i].y += center.y; // Add center.y to corner.y + } + + drawList->AddImageQuad(arrowCircleTexture, corners[0], corners[1], corners[2], corners[3], ImVec2(0, 0), ImVec2(1, 0), ImVec2(1, 1), ImVec2(0, 1), IM_COL32(255, 255, 255, 96)); + + // Update rotation for next frame + g_arrowCircleCurrentRotation = fmodf(g_arrowCircleCurrentRotation - ARROW_CIRCLE_SPIN_FACTOR, 360.0f); } } From 7f85fb6ecb80a61e3067ed28317eaa30f3e8c69a Mon Sep 17 00:00:00 2001 From: Hyper <34012267+hyperbx@users.noreply.github.com> Date: Tue, 3 Dec 2024 22:44:03 +0000 Subject: [PATCH 14/28] installer_wizard: fix arrow circle spinning animation misaligning --- UnleashedRecomp/ui/installer_wizard.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnleashedRecomp/ui/installer_wizard.cpp b/UnleashedRecomp/ui/installer_wizard.cpp index b77f954e..01849cfa 100644 --- a/UnleashedRecomp/ui/installer_wizard.cpp +++ b/UnleashedRecomp/ui/installer_wizard.cpp @@ -242,7 +242,7 @@ static void DrawHeaderIcons() ImVec2 arrowCircleMin = { Scale(iconsPosX - iconsScale / 2), Scale(iconsPosY - iconsScale / 2) }; ImVec2 arrowCircleMax = { Scale(iconsPosX + iconsScale / 2), Scale(iconsPosY + iconsScale / 2) }; - ImVec2 center = { Scale(iconsPosX), Scale(iconsPosY) }; + ImVec2 center = { Scale(iconsPosX) + 0.5f, Scale(iconsPosY) }; float currentAngle = g_arrowCircleCurrentRotation * (3.14159f / 180.0f); // Rotation angle in radians float cos_a = cosf(currentAngle); float sin_a = sinf(currentAngle); From efd2c40cfd15b8e9e1142f368e4b2fdc5e1aec14 Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 3 Dec 2024 20:44:55 -0300 Subject: [PATCH 15/28] Add Scale and Origin to ImGui shaders. Change text to be squashed. --- UnleashedRecomp/gpu/imgui_common.h | 14 +++++- UnleashedRecomp/gpu/shader/imgui_common.hlsli | 2 + UnleashedRecomp/gpu/shader/imgui_vs.hlsl | 3 +- UnleashedRecomp/gpu/video.cpp | 8 ++++ UnleashedRecomp/locale/locale.h | 2 + UnleashedRecomp/ui/imgui_utils.h | 14 ++++++ UnleashedRecomp/ui/installer_wizard.cpp | 46 ++++++++++--------- 7 files changed, 66 insertions(+), 23 deletions(-) diff --git a/UnleashedRecomp/gpu/imgui_common.h b/UnleashedRecomp/gpu/imgui_common.h index d550bf38..2a357f21 100644 --- a/UnleashedRecomp/gpu/imgui_common.h +++ b/UnleashedRecomp/gpu/imgui_common.h @@ -10,7 +10,9 @@ enum class ImGuiCallback : int32_t { SetGradient = -1, - SetShaderModifier = -2 + SetShaderModifier = -2, + SetOrigin = -3, + SetScale = -4, }; union ImGuiCallbackData @@ -27,6 +29,16 @@ union ImGuiCallbackData { uint32_t shaderModifier; } setShaderModifier; + + struct + { + float origin[2]; + } setOrigin; + + struct + { + float scale[2]; + } setScale; }; #endif diff --git a/UnleashedRecomp/gpu/shader/imgui_common.hlsli b/UnleashedRecomp/gpu/shader/imgui_common.hlsli index fb838d7f..85a9ecc1 100644 --- a/UnleashedRecomp/gpu/shader/imgui_common.hlsli +++ b/UnleashedRecomp/gpu/shader/imgui_common.hlsli @@ -9,6 +9,8 @@ struct PushConstants uint ShaderModifier; uint Texture2DDescriptorIndex; float2 InverseDisplaySize; + float2 Origin; + float2 Scale; }; Texture2D g_Texture2DDescriptorHeap[] : register(t0, space0); diff --git a/UnleashedRecomp/gpu/shader/imgui_vs.hlsl b/UnleashedRecomp/gpu/shader/imgui_vs.hlsl index 90481cd7..5b7820d4 100644 --- a/UnleashedRecomp/gpu/shader/imgui_vs.hlsl +++ b/UnleashedRecomp/gpu/shader/imgui_vs.hlsl @@ -2,7 +2,8 @@ void main(in float2 position : POSITION, in float2 uv : TEXCOORD, in float4 color : COLOR, out Interpolators interpolators) { - interpolators.Position = float4(position * g_PushConstants.InverseDisplaySize * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); + float2 correctedPosition = g_PushConstants.Origin + (position - g_PushConstants.Origin) * g_PushConstants.Scale; + interpolators.Position = float4(correctedPosition * g_PushConstants.InverseDisplaySize * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); interpolators.UV = uv; interpolators.Color = color; } diff --git a/UnleashedRecomp/gpu/video.cpp b/UnleashedRecomp/gpu/video.cpp index b3fc8e3f..a1240954 100644 --- a/UnleashedRecomp/gpu/video.cpp +++ b/UnleashedRecomp/gpu/video.cpp @@ -1057,6 +1057,8 @@ struct ImGuiPushConstants uint32_t shaderModifier{}; uint32_t texture2DDescriptorIndex{}; ImVec2 inverseDisplaySize{}; + ImVec2 origin{ 0.0f, 0.0f }; + ImVec2 scale{ 1.0f, 1.0f }; }; static void CreateImGuiBackend() @@ -1812,6 +1814,12 @@ static void ProcDrawImGui(const RenderCommand& cmd) case ImGuiCallback::SetShaderModifier: commandList->setGraphicsPushConstants(0, &callbackData->setShaderModifier, offsetof(ImGuiPushConstants, shaderModifier), sizeof(callbackData->setShaderModifier)); break; + case ImGuiCallback::SetOrigin: + commandList->setGraphicsPushConstants(0, &callbackData->setOrigin, offsetof(ImGuiPushConstants, origin), sizeof(callbackData->setOrigin)); + break; + case ImGuiCallback::SetScale: + commandList->setGraphicsPushConstants(0, &callbackData->setScale, offsetof(ImGuiPushConstants, scale), sizeof(callbackData->setScale)); + break; } } else diff --git a/UnleashedRecomp/locale/locale.h b/UnleashedRecomp/locale/locale.h index 0c81ff0d..ac1eb716 100644 --- a/UnleashedRecomp/locale/locale.h +++ b/UnleashedRecomp/locale/locale.h @@ -103,6 +103,7 @@ inline static std::unordered_mapsetShaderModifier.shaderModifier = shaderModifier; } +static void SetOrigin(ImVec2 origin) +{ + auto callbackData = AddCallback(ImGuiCallback::SetOrigin); + callbackData->setOrigin.origin[0] = origin.x; + callbackData->setOrigin.origin[1] = origin.y; +} + +static void SetScale(ImVec2 scale) +{ + auto callbackData = AddCallback(ImGuiCallback::SetScale); + callbackData->setScale.scale[0] = scale.x; + callbackData->setScale.scale[1] = scale.y; +} + // Aspect ratio aware. static float Scale(float size) { diff --git a/UnleashedRecomp/ui/installer_wizard.cpp b/UnleashedRecomp/ui/installer_wizard.cpp index 01849cfa..d1172879 100644 --- a/UnleashedRecomp/ui/installer_wizard.cpp +++ b/UnleashedRecomp/ui/installer_wizard.cpp @@ -434,25 +434,21 @@ static void DrawButtonContainer(ImVec2 min, ImVec2 max, int baser, int baseg, fl SetShaderModifier(IMGUI_SHADER_MODIFIER_NONE); } -static ImVec2 ComputeTextSizeAndRatio(ImFont *font, const char *text, float &size, float maxTextWidth = FLT_MAX) +static ImVec2 ComputeTextSize(ImFont *font, const char *text, float size, float &squashRatio, float maxTextWidth = FLT_MAX) { ImVec2 textSize = font->CalcTextSizeA(size, FLT_MAX, 0.0f, text); if (textSize.x > maxTextWidth) { - float shrinkRatio = maxTextWidth / textSize.x; - size *= shrinkRatio; - textSize.x *= shrinkRatio; - textSize.y *= shrinkRatio; + squashRatio = maxTextWidth / textSize.x; + } + else + { + squashRatio = 1.0f; } return textSize; } -static ImVec2 ComputeTextSize(ImFont *font, const char *text, float size, float maxTextWidth = FLT_MAX) -{ - return ComputeTextSizeAndRatio(font, text, size, maxTextWidth); -} - static void DrawButton(ImVec2 min, ImVec2 max, const char *buttonText, bool sourceButton, bool buttonEnabled, bool &buttonPressed, float maxTextWidth = FLT_MAX) { buttonPressed = false; @@ -482,7 +478,8 @@ static void DrawButton(ImVec2 min, ImVec2 max, const char *buttonText, bool sour ImFont *font = sourceButton ? g_newRodinFont : g_dfsogeistdFont; float size = Scale(sourceButton ? 15.0f : 20.0f); - ImVec2 textSize = ComputeTextSizeAndRatio(font, buttonText, size, Scale(maxTextWidth)); + float squashRatio; + ImVec2 textSize = ComputeTextSize(font, buttonText, size, squashRatio, Scale(maxTextWidth)); min.x += ((max.x - min.x) - textSize.x) / 2.0f; min.y += ((max.y - min.y) - textSize.y) / 2.0f; @@ -491,11 +488,13 @@ static void DrawButton(ImVec2 min, ImVec2 max, const char *buttonText, bool sour // Fixes slight misalignment caused by this particular font. min.y -= Scale(1.0f); } - + + SetOrigin({ min.x + textSize.x / 2.0f, min.y }); + SetScale({ squashRatio, 1.0f }); SetGradient ( min, - { min.x + textSize.x, min.y + textSize.y }, + { min.x + textSize.x, textSize.y }, IM_COL32(baser + 192, 255, 0, 255), IM_COL32(baser + 128, baseg + 170, 0, 255) ); @@ -512,6 +511,8 @@ static void DrawButton(ImVec2 min, ImVec2 max, const char *buttonText, bool sour ); ResetGradient(); + SetScale({ 1.0f, 1.0f }); + SetOrigin({ 0.0f, 0.0f }); } enum ButtonColumn @@ -735,24 +736,25 @@ static void DrawSourcePickers() { constexpr float ADD_BUTTON_MAX_TEXT_WIDTH = 160.0f; const std::string &addFilesText = Localise("Installer_Button_AddFiles"); - ImVec2 textSize = ComputeTextSize(g_dfsogeistdFont, addFilesText.c_str(), 20.0f, ADD_BUTTON_MAX_TEXT_WIDTH); + float squashRatio; + ImVec2 textSize = ComputeTextSize(g_dfsogeistdFont, addFilesText.c_str(), 20.0f, squashRatio, ADD_BUTTON_MAX_TEXT_WIDTH); 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) }; + ImVec2 max = { Scale(AlignToNextGrid(CONTAINER_X) + BOTTOM_X_GAP + textSize.x * squashRatio), Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) + BOTTOM_Y_GAP + BUTTON_HEIGHT) }; DrawButton(min, max, addFilesText.c_str(), false, true, buttonPressed, ADD_BUTTON_MAX_TEXT_WIDTH); if (buttonPressed && ShowFilesPicker(paths)) { ParseSourcePaths(paths); } - min.x += Scale(BOTTOM_X_GAP + textSize.x); + min.x += Scale(BOTTOM_X_GAP + textSize.x * squashRatio); const std::string &addFolderText = Localise("Installer_Button_AddFolder"); - textSize = ComputeTextSize(g_dfsogeistdFont, addFolderText.c_str(), 20.0f, ADD_BUTTON_MAX_TEXT_WIDTH); + textSize = ComputeTextSize(g_dfsogeistdFont, addFolderText.c_str(), 20.0f, squashRatio, ADD_BUTTON_MAX_TEXT_WIDTH); textSize.x += BUTTON_TEXT_GAP; - max.x = min.x + Scale(textSize.x); + max.x = min.x + Scale(textSize.x * squashRatio); DrawButton(min, max, addFolderText.c_str(), false, true, buttonPressed, ADD_BUTTON_MAX_TEXT_WIDTH); if (buttonPressed && ShowFoldersPicker(paths)) { @@ -856,15 +858,17 @@ static void DrawNextButton() skipButton = std::all_of(g_dlcSourcePaths.begin(), g_dlcSourcePaths.end(), [](const std::filesystem::path &path) { return path.empty(); }); } + float squashRatio; + constexpr float NEXT_BUTTON_MAX_TEXT_WIDTH = 100.0f; const std::string &buttonText = Localise(skipButton ? "Installer_Button_Skip" : "Installer_Button_Next"); - ImVec2 textSize = g_newRodinFont->CalcTextSizeA(20.0f, FLT_MAX, 0.0f, buttonText.c_str()); + ImVec2 textSize = ComputeTextSize(g_newRodinFont, buttonText.c_str(), 20.0f, squashRatio, NEXT_BUTTON_MAX_TEXT_WIDTH); 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 min = { Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH) - textSize.x * squashRatio - 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.c_str(), false, nextButtonEnabled, buttonPressed); + DrawButton(min, max, buttonText.c_str(), false, nextButtonEnabled, buttonPressed, NEXT_BUTTON_MAX_TEXT_WIDTH); if (buttonPressed) { From f01d422b310c985a90e3aa66a7ca41cffa44859e Mon Sep 17 00:00:00 2001 From: Hyper <34012267+hyperbx@users.noreply.github.com> Date: Wed, 4 Dec 2024 00:09:34 +0000 Subject: [PATCH 16/28] message_window: implemented mouse input --- UnleashedRecomp/ui/message_window.cpp | 98 +++++++++++++++++---------- 1 file changed, 63 insertions(+), 35 deletions(-) diff --git a/UnleashedRecomp/ui/message_window.cpp b/UnleashedRecomp/ui/message_window.cpp index 9e3639b5..3c1a1163 100644 --- a/UnleashedRecomp/ui/message_window.cpp +++ b/UnleashedRecomp/ui/message_window.cpp @@ -2,6 +2,7 @@ #include "imgui_utils.h" #include #include +#include #include #include @@ -184,7 +185,7 @@ void MessageWindow::Draw() if (!s_isVisible) return; - auto pInputState = SWA::CInputState::GetInstance(); + auto pInputState = InstallerWizard::s_isVisible ? nullptr : SWA::CInputState::GetInstance(); auto drawList = ImGui::GetForegroundDrawList(); auto& res = ImGui::GetIO().DisplaySize; @@ -213,6 +214,10 @@ void MessageWindow::Draw() drawList->PopClipRect(); + bool isAccepted = pInputState + ? pInputState->GetPadState().IsTapped(SWA::eKeyState_A) + : ImGui::IsMouseClicked(ImGuiMouseButton_Left); + if (g_buttons.size()) { auto itemWidth = std::max(Scale(162), Scale(CalcWidestTextSize(g_fntSeurat, fontSize, g_buttons))); @@ -229,54 +234,72 @@ void MessageWindow::Draw() for (auto& button : g_buttons) DrawButton(rowCount++, windowMarginY, itemWidth, itemHeight, button); - drawList->PopClipRect(); - - bool upIsHeld = pInputState->GetPadState().IsDown(SWA::eKeyState_DpadUp) || - pInputState->GetPadState().LeftStickVertical > 0.5f; - - bool downIsHeld = pInputState->GetPadState().IsDown(SWA::eKeyState_DpadDown) || - pInputState->GetPadState().LeftStickVertical < -0.5f; - - bool scrollUp = !g_upWasHeld && upIsHeld; - bool scrollDown = !g_downWasHeld && downIsHeld; - - if (scrollUp) + if (pInputState) { - --g_selectedRowIndex; - if (g_selectedRowIndex < 0) - g_selectedRowIndex = rowCount - 1; + bool upIsHeld = pInputState->GetPadState().IsDown(SWA::eKeyState_DpadUp) || + pInputState->GetPadState().LeftStickVertical > 0.5f; + + bool downIsHeld = pInputState->GetPadState().IsDown(SWA::eKeyState_DpadDown) || + pInputState->GetPadState().LeftStickVertical < -0.5f; + + bool scrollUp = !g_upWasHeld && upIsHeld; + bool scrollDown = !g_downWasHeld && downIsHeld; + + if (scrollUp) + { + --g_selectedRowIndex; + if (g_selectedRowIndex < 0) + g_selectedRowIndex = rowCount - 1; + } + else if (scrollDown) + { + ++g_selectedRowIndex; + if (g_selectedRowIndex >= rowCount) + g_selectedRowIndex = 0; + } + + if (scrollUp || scrollDown) + Game_PlaySound("sys_actstg_pausecursor"); + + g_upWasHeld = upIsHeld; + g_downWasHeld = downIsHeld; + + if (pInputState->GetPadState().IsTapped(SWA::eKeyState_B)) + { + g_result = -1; + + Game_PlaySound("sys_actstg_pausecansel"); + MessageWindow::Close(); + } } - else if (scrollDown) + else { - ++g_selectedRowIndex; - if (g_selectedRowIndex >= rowCount) - g_selectedRowIndex = 0; + auto clipRectMin = drawList->GetClipRectMin(); + auto clipRectMax = drawList->GetClipRectMax(); + + for (int i = 0; i < rowCount; i++) + { + ImVec2 itemMin = { clipRectMin.x + windowMarginX, clipRectMin.y + windowMarginY + itemHeight * i }; + ImVec2 itemMax = { clipRectMax.x - windowMarginX, clipRectMin.y + windowMarginY + itemHeight * i + itemHeight }; + + if (ImGui::IsMouseHoveringRect(itemMin, itemMax, false)) + g_selectedRowIndex = i; + } } - if (scrollUp || scrollDown) - Game_PlaySound("sys_actstg_pausecursor"); - - g_upWasHeld = upIsHeld; - g_downWasHeld = downIsHeld; - - if (pInputState->GetPadState().IsTapped(SWA::eKeyState_A)) + if (isAccepted) { g_result = g_selectedRowIndex; Game_PlaySound("sys_actstg_pausedecide"); MessageWindow::Close(); } - else if (pInputState->GetPadState().IsTapped(SWA::eKeyState_B)) - { - g_result = -1; - Game_PlaySound("sys_actstg_pausecansel"); - MessageWindow::Close(); - } + drawList->PopClipRect(); } else { - if (!g_isControlsVisible && pInputState->GetPadState().IsTapped(SWA::eKeyState_A)) + if (!g_isControlsVisible && isAccepted) { g_controlsAppearTime = ImGui::GetTime(); g_isControlsVisible = true; @@ -287,8 +310,12 @@ void MessageWindow::Draw() } else { - if (pInputState->GetPadState().IsTapped(SWA::eKeyState_A)) + if (isAccepted) + { + g_result = 0; + MessageWindow::Close(); + } } } } @@ -328,6 +355,7 @@ void MessageWindow::Close() g_controlsAppearTime = ImGui::GetTime(); g_isClosing = true; g_isControlsVisible = false; + g_foregroundCount = 0; g_isAwaitingResult = false; } From 55773a65aab791df385ee411c20f5e0ee2a42b89 Mon Sep 17 00:00:00 2001 From: Hyper <34012267+hyperbx@users.noreply.github.com> Date: Wed, 4 Dec 2024 00:09:43 +0000 Subject: [PATCH 17/28] installer_wizard: implemented message windows --- UnleashedRecomp/exports.cpp | 5 +++ UnleashedRecomp/ui/installer_wizard.cpp | 54 ++++++++++++++++++++----- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/UnleashedRecomp/exports.cpp b/UnleashedRecomp/exports.cpp index fbf01c7e..e47eaefb 100644 --- a/UnleashedRecomp/exports.cpp +++ b/UnleashedRecomp/exports.cpp @@ -2,11 +2,16 @@ #include #include #include +#include #include #include SWA_API void Game_PlaySound(const char* pName) { + // TODO: use own sound player. + if (InstallerWizard::s_isVisible) + return; + guest_stack_var soundPlayer; GuestToHostFunction(sub_82B4DF50, soundPlayer.get(), ((be*)g_memory.Translate(0x83367900))->get(), 7, 0, 0); diff --git a/UnleashedRecomp/ui/installer_wizard.cpp b/UnleashedRecomp/ui/installer_wizard.cpp index d1172879..12e04232 100644 --- a/UnleashedRecomp/ui/installer_wizard.cpp +++ b/UnleashedRecomp/ui/installer_wizard.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -230,15 +231,13 @@ static void DrawHeaderIcons() float milesIconMotion = ComputeMotionInstaller(g_appearTime, g_disappearTime, MILES_ICON_ANIMATION_TIME, MILES_ICON_ANIMATION_DURATION); float milesIconScale = iconsScale * (2 - milesIconMotion); - GuestTexture* milesElectricIconTexture = g_milesElectricIcon.get(); ImVec2 milesElectricMin = { Scale(iconsPosX - milesIconScale / 2), Scale(iconsPosY - milesIconScale / 2) }; ImVec2 milesElectricMax = { Scale(iconsPosX + milesIconScale / 2), Scale(iconsPosY + milesIconScale / 2) }; - drawList->AddImage(milesElectricIconTexture, milesElectricMin, milesElectricMax, ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 255 * milesIconMotion)); + drawList->AddImage(g_milesElectricIcon.get(), milesElectricMin, milesElectricMax, ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 255 * milesIconMotion)); // Arrow Circle Icon if (g_currentPage == WizardPage::Installing) { - GuestTexture* arrowCircleTexture = g_arrowCircle.get(); ImVec2 arrowCircleMin = { Scale(iconsPosX - iconsScale / 2), Scale(iconsPosY - iconsScale / 2) }; ImVec2 arrowCircleMax = { Scale(iconsPosX + iconsScale / 2), Scale(iconsPosY + iconsScale / 2) }; @@ -248,19 +247,21 @@ static void DrawHeaderIcons() float sin_a = sinf(currentAngle); // Calculate rotated corners - ImVec2 corners[4] = { + ImVec2 corners[4] = + { ImRotate(ImVec2(arrowCircleMin.x - center.x, arrowCircleMin.y - center.y), cos_a, sin_a), ImRotate(ImVec2(arrowCircleMax.x - center.x, arrowCircleMin.y - center.y), cos_a, sin_a), ImRotate(ImVec2(arrowCircleMax.x - center.x, arrowCircleMax.y - center.y), cos_a, sin_a), ImRotate(ImVec2(arrowCircleMin.x - center.x, arrowCircleMax.y - center.y), cos_a, sin_a), }; - for (int i = 0; i < 4; ++i) { + for (int i = 0; i < IM_ARRAYSIZE(corners); ++i) + { corners[i].x += center.x; // Add center.x to corner.x corners[i].y += center.y; // Add center.y to corner.y } - drawList->AddImageQuad(arrowCircleTexture, corners[0], corners[1], corners[2], corners[3], ImVec2(0, 0), ImVec2(1, 0), ImVec2(1, 1), ImVec2(0, 1), IM_COL32(255, 255, 255, 96)); + drawList->AddImageQuad(g_arrowCircle.get(), corners[0], corners[1], corners[2], corners[3], ImVec2(0, 0), ImVec2(1, 0), ImVec2(1, 1), ImVec2(0, 1), IM_COL32(255, 255, 255, 96)); // Update rotation for next frame g_arrowCircleCurrentRotation = fmodf(g_arrowCircleCurrentRotation - ARROW_CIRCLE_SPIN_FACTOR, 360.0f); @@ -494,7 +495,7 @@ static void DrawButton(ImVec2 min, ImVec2 max, const char *buttonText, bool sour SetGradient ( min, - { min.x + textSize.x, textSize.y }, + { min.x + textSize.x, min.y + textSize.y }, IM_COL32(baser + 192, 255, 0, 255), IM_COL32(baser + 128, baseg + 170, 0, 255) ); @@ -845,7 +846,8 @@ static bool InstallerParseSources() static void DrawNextButton() { - if (g_currentPage != WizardPage::Installing) { + if (g_currentPage != WizardPage::Installing) + { bool nextButtonEnabled = !g_isDisappearing; if (nextButtonEnabled && g_currentPage == WizardPage::SelectGameAndUpdate) { @@ -875,8 +877,15 @@ static void DrawNextButton() XexPatcher::Result patcherResult; if (g_currentPage == WizardPage::SelectGameAndUpdate && (patcherResult = Installer::checkGameUpdateCompatibility(g_gameSourcePath, g_updateSourcePath), patcherResult != XexPatcher::Result::Success)) { - g_currentMessagePrompt = "The specified game and update file are incompatible.\n\nPlease ensure the files are for the same version and region and try again."; + // TODO: localise this. + g_currentMessagePrompt = "The specified game and\nupdate file are incompatible.\n\nPlease ensure the files are\nfor the same version and\nregion and try again."; g_currentMessagePromptConfirmation = false; + + static int result = -1; + MessageWindow::Open(g_currentMessagePrompt, &result); + + if (result == 0) + g_currentMessagePrompt.clear(); } else if (g_currentPage == WizardPage::SelectDLC) { @@ -893,16 +902,39 @@ static void DrawNextButton() bool dlcInstallerMode = g_gameSourcePath.empty(); if (!InstallerParseSources()) { + // TODO: localise this. // Some of the sources that were provided to the installer are not valid. Restart the file selection process. - g_currentMessagePrompt = "Some of the files that have been provided are not valid.\n\nPlease make sure all the specified files are correct and try again."; + g_currentMessagePrompt = "Some of the files that have\nbeen provided are not valid.\n\nPlease make sure all the\nspecified files are correct\nand try again."; g_currentMessagePromptConfirmation = false; + + static int result = -1; + MessageWindow::Open(g_currentMessagePrompt, &result); + + if (result == 0) + g_currentMessagePrompt.clear(); + g_currentPage = dlcInstallerMode ? WizardPage::SelectDLC : WizardPage::SelectGameAndUpdate; } else if (dlcIncomplete && !dlcInstallerMode) { + // TODO: localise this. // Not all the DLC was specified, we show a prompt and await a confirmation before starting the installer. - g_currentMessagePrompt = "It is highly recommended that you install all of the DLC, as it includes high quality lighting textures for the stages in each pack.\n\nAre you sure you want to skip this step?"; + g_currentMessagePrompt = "It is highly recommended\nthat you install all of the DLC,\nas it includes high quality\nlighting textures for the\nstages in each pack.\n\nAre you sure you want to\nskip this step?"; g_currentMessagePromptConfirmation = true; + + static int result = -1; + std::array buttons = { "Yes", "No" }; + MessageWindow::Open(g_currentMessagePrompt, &result, buttons, 1); + + if (result == 0) + { + g_currentMessagePrompt.clear(); + } + else + { + // Reset message window. + result = -1; + } } else if (skipButton && dlcInstallerMode) { From 81e76972accb5c761b22f607fd54b79c2de049b3 Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 3 Dec 2024 22:05:59 -0300 Subject: [PATCH 18/28] Fix message box flow. --- UnleashedRecomp/ui/installer_wizard.cpp | 53 ++++++++++--------------- 1 file changed, 21 insertions(+), 32 deletions(-) diff --git a/UnleashedRecomp/ui/installer_wizard.cpp b/UnleashedRecomp/ui/installer_wizard.cpp index 12e04232..b72d8181 100644 --- a/UnleashedRecomp/ui/installer_wizard.cpp +++ b/UnleashedRecomp/ui/installer_wizard.cpp @@ -114,6 +114,7 @@ static WizardPage g_firstPage = WizardPage::SelectLanguage; static WizardPage g_currentPage = g_firstPage; static std::string g_currentMessagePrompt = ""; static bool g_currentMessagePromptConfirmation = false; +static int g_currentMessageResult = -1; const char CREDITS_TEXT[] = "Sajid (RIP)"; @@ -880,12 +881,6 @@ static void DrawNextButton() // TODO: localise this. g_currentMessagePrompt = "The specified game and\nupdate file are incompatible.\n\nPlease ensure the files are\nfor the same version and\nregion and try again."; g_currentMessagePromptConfirmation = false; - - static int result = -1; - MessageWindow::Open(g_currentMessagePrompt, &result); - - if (result == 0) - g_currentMessagePrompt.clear(); } else if (g_currentPage == WizardPage::SelectDLC) { @@ -906,13 +901,6 @@ static void DrawNextButton() // Some of the sources that were provided to the installer are not valid. Restart the file selection process. g_currentMessagePrompt = "Some of the files that have\nbeen provided are not valid.\n\nPlease make sure all the\nspecified files are correct\nand try again."; g_currentMessagePromptConfirmation = false; - - static int result = -1; - MessageWindow::Open(g_currentMessagePrompt, &result); - - if (result == 0) - g_currentMessagePrompt.clear(); - g_currentPage = dlcInstallerMode ? WizardPage::SelectDLC : WizardPage::SelectGameAndUpdate; } else if (dlcIncomplete && !dlcInstallerMode) @@ -921,20 +909,6 @@ static void DrawNextButton() // Not all the DLC was specified, we show a prompt and await a confirmation before starting the installer. g_currentMessagePrompt = "It is highly recommended\nthat you install all of the DLC,\nas it includes high quality\nlighting textures for the\nstages in each pack.\n\nAre you sure you want to\nskip this step?"; g_currentMessagePromptConfirmation = true; - - static int result = -1; - std::array buttons = { "Yes", "No" }; - MessageWindow::Open(g_currentMessagePrompt, &result, buttons, 1); - - if (result == 0) - { - g_currentMessagePrompt.clear(); - } - else - { - // Reset message window. - result = -1; - } } else if (skipButton && dlcInstallerMode) { @@ -1045,12 +1019,27 @@ static void DrawMessagePrompt() return; } - // TODO: Put message prompt here. - - if (false && g_currentPage == WizardPage::SelectDLC) + bool messageWindowReturned = false; + if (g_currentMessagePromptConfirmation) { - // If user confirms the message prompt that they wish to skip installing the DLC, start the installer. - InstallerStart(); + std::array YesNoButtons = { "Yes", "No" }; + messageWindowReturned = MessageWindow::Open(g_currentMessagePrompt, &g_currentMessageResult, YesNoButtons, 1); + } + else + { + messageWindowReturned = MessageWindow::Open(g_currentMessagePrompt, &g_currentMessageResult); + } + + if (messageWindowReturned) + { + if (g_currentMessagePromptConfirmation && (g_currentMessageResult == 0)) + { + // If user confirms the message prompt that they wish to skip installing the DLC, start the installer. + InstallerStart(); + } + + g_currentMessagePrompt.clear(); + g_currentMessageResult = -1; } } From 5866f4ea68ba13fe53081c7f7f967aa1668bfea3 Mon Sep 17 00:00:00 2001 From: Hyper <34012267+hyperbx@users.noreply.github.com> Date: Wed, 4 Dec 2024 01:09:41 +0000 Subject: [PATCH 19/28] message_window: use pause container texture --- UnleashedRecomp/ui/message_window.cpp | 52 +++++---------------------- 1 file changed, 8 insertions(+), 44 deletions(-) diff --git a/UnleashedRecomp/ui/message_window.cpp b/UnleashedRecomp/ui/message_window.cpp index 3c1a1163..f8d08163 100644 --- a/UnleashedRecomp/ui/message_window.cpp +++ b/UnleashedRecomp/ui/message_window.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include constexpr double OVERLAY_CONTAINER_COMMON_MOTION_START = 0; @@ -29,6 +30,7 @@ static double g_controlsAppearTime; static ImFont* g_fntSeurat; static std::unique_ptr g_upSelectionCursor; +static std::unique_ptr g_upWindow; std::string g_text; int g_result; @@ -60,8 +62,6 @@ bool DrawContainer(float appearTime, ImVec2 centre, ImVec2 max, bool isForegroun _max.y = Hermite(centre.y, _max.y, containerMotion); } - auto vertices = GetPauseContainerVertices(_min, _max); - // Transparency fade animation. auto colourMotion = g_isClosing ? ComputeMotion(appearTime, OVERLAY_CONTAINER_OUTRO_FADE_START, OVERLAY_CONTAINER_OUTRO_FADE_END) @@ -77,44 +77,7 @@ bool DrawContainer(float appearTime, ImVec2 centre, ImVec2 max, bool isForegroun if (isForeground) drawList->AddRectFilled({ 0.0f, 0.0f }, ImGui::GetIO().DisplaySize, IM_COL32(0, 0, 0, 223 * (g_foregroundCount ? 1 : alpha))); - auto colShadow = IM_COL32(0, 0, 0, 156 * alpha); - auto colGradientTop = IM_COL32(197, 194, 197, 200 * alpha); - auto colGradientBottom = IM_COL32(115, 113, 115, 236 * alpha); - - // Draw vertices with gradient. - SetGradient(_min, _max, colGradientTop, colGradientBottom); - drawList->AddConvexPolyFilled(vertices.data(), vertices.size(), IM_COL32(255, 255, 255, 255 * alpha)); - ResetGradient(); - - // Draw outline. - drawList->AddPolyline - ( - vertices.data(), - vertices.size(), - IM_COL32(247, 247, 247, 255 * alpha), - true, - Scale(2.5f) - ); - - // Offset vertices to draw 3D effect lines. - for (int i = 0; i < vertices.size(); i++) - { - vertices[i].x -= Scale(0.4f); - vertices[i].y -= Scale(0.2f); - } - - auto colLineTop = IM_COL32(165, 170, 165, 230 * alpha); - auto colLineBottom = IM_COL32(190, 190, 190, 230 * alpha); - auto lineThickness = Scale(1.0f); - - // Top left corner bottom to top left corner top. - drawList->AddLine(vertices[0], vertices[1], colLineTop, lineThickness * 0.5f); - - // Top left corner bottom to bottom left. - drawList->AddRectFilledMultiColor({ vertices[0].x - 0.2f, vertices[0].y }, { vertices[6].x + lineThickness - 0.2f, vertices[6].y }, colLineTop, colLineTop, colLineBottom, colLineBottom); - - // Top left corner top to top right. - drawList->AddLine(vertices[1], vertices[2], colLineTop, lineThickness); + DrawPauseContainer(g_upWindow, _min, _max, alpha); drawList->PushClipRect(_min, _max); @@ -178,6 +141,7 @@ void MessageWindow::Init() g_fntSeurat = io.Fonts->AddFontFromFileTTF("FOT-SeuratPro-M.otf", 28.0f * FONT_SCALE); g_upSelectionCursor = LoadTexture(g_select_fade, sizeof(g_select_fade)); + g_upWindow = LoadTexture(g_general_window, sizeof(g_general_window)); } void MessageWindow::Draw() @@ -193,8 +157,8 @@ void MessageWindow::Draw() auto fontSize = Scale(28); auto textSize = MeasureCentredParagraph(g_fntSeurat, fontSize, 5, g_text.c_str()); - auto textMarginX = Scale(32); - auto textMarginY = Scale(40); + auto textMarginX = Scale(37); + auto textMarginY = Scale(45); if (DrawContainer(g_appearTime, centre, { textSize.x / 2 + textMarginX, textSize.y / 2 + textMarginY }, !g_isControlsVisible)) { @@ -222,8 +186,8 @@ void MessageWindow::Draw() { auto itemWidth = std::max(Scale(162), Scale(CalcWidestTextSize(g_fntSeurat, fontSize, g_buttons))); auto itemHeight = Scale(57); - auto windowMarginX = Scale(18); - auto windowMarginY = Scale(25); + auto windowMarginX = Scale(23); + auto windowMarginY = Scale(30); ImVec2 controlsMax = { /* X */ itemWidth / 2 + windowMarginX, /* Y */ itemHeight / 2 * g_buttons.size() + windowMarginY }; From 0d696b53eb943c38e355313f08ed85a555343719 Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 3 Dec 2024 22:11:33 -0300 Subject: [PATCH 20/28] Add extra condition for starting the installer. --- UnleashedRecomp/ui/installer_wizard.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnleashedRecomp/ui/installer_wizard.cpp b/UnleashedRecomp/ui/installer_wizard.cpp index b72d8181..fb0d45fc 100644 --- a/UnleashedRecomp/ui/installer_wizard.cpp +++ b/UnleashedRecomp/ui/installer_wizard.cpp @@ -1032,7 +1032,7 @@ static void DrawMessagePrompt() if (messageWindowReturned) { - if (g_currentMessagePromptConfirmation && (g_currentMessageResult == 0)) + if (g_currentMessagePromptConfirmation && (g_currentMessageResult == 0) && (g_currentPage == WizardPage::SelectDLC)) { // If user confirms the message prompt that they wish to skip installing the DLC, start the installer. InstallerStart(); From 56153cb60a5172feb8995051d4d2455a6791de62 Mon Sep 17 00:00:00 2001 From: Hyper <34012267+hyperbx@users.noreply.github.com> Date: Wed, 4 Dec 2024 01:18:35 +0000 Subject: [PATCH 21/28] message_window: only accept mouse click if option is selected --- UnleashedRecomp/ui/message_window.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/UnleashedRecomp/ui/message_window.cpp b/UnleashedRecomp/ui/message_window.cpp index f8d08163..adcff1e5 100644 --- a/UnleashedRecomp/ui/message_window.cpp +++ b/UnleashedRecomp/ui/message_window.cpp @@ -241,6 +241,8 @@ void MessageWindow::Draw() auto clipRectMin = drawList->GetClipRectMin(); auto clipRectMax = drawList->GetClipRectMax(); + g_selectedRowIndex = -1; + for (int i = 0; i < rowCount; i++) { ImVec2 itemMin = { clipRectMin.x + windowMarginX, clipRectMin.y + windowMarginY + itemHeight * i }; @@ -251,7 +253,7 @@ void MessageWindow::Draw() } } - if (isAccepted) + if (g_selectedRowIndex != -1 && isAccepted) { g_result = g_selectedRowIndex; From 8fe163e7c2abbded480a70561fc422ba981ef4fa Mon Sep 17 00:00:00 2001 From: Hyper <34012267+hyperbx@users.noreply.github.com> Date: Wed, 4 Dec 2024 01:19:03 +0000 Subject: [PATCH 22/28] Implemented safer way to check if the game is loaded --- UnleashedRecomp/app.cpp | 2 ++ UnleashedRecomp/app.h | 1 + UnleashedRecomp/ui/message_window.cpp | 6 +++--- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/UnleashedRecomp/app.cpp b/UnleashedRecomp/app.cpp index 18187e51..8d5e8b37 100644 --- a/UnleashedRecomp/app.cpp +++ b/UnleashedRecomp/app.cpp @@ -3,12 +3,14 @@ #include #include +bool g_isGameLoaded = false; double g_deltaTime; // CApplication::Update PPC_FUNC_IMPL(__imp__sub_822C1130); PPC_FUNC(sub_822C1130) { + g_isGameLoaded = true; g_deltaTime = ctx.f1.f64; SDL_PumpEvents(); diff --git a/UnleashedRecomp/app.h b/UnleashedRecomp/app.h index 4e1d379e..b9b1bf5c 100644 --- a/UnleashedRecomp/app.h +++ b/UnleashedRecomp/app.h @@ -1,3 +1,4 @@ #pragma once +extern bool g_isGameLoaded; extern double g_deltaTime; diff --git a/UnleashedRecomp/ui/message_window.cpp b/UnleashedRecomp/ui/message_window.cpp index adcff1e5..eb21a8cd 100644 --- a/UnleashedRecomp/ui/message_window.cpp +++ b/UnleashedRecomp/ui/message_window.cpp @@ -2,7 +2,7 @@ #include "imgui_utils.h" #include #include -#include +#include #include #include #include @@ -149,7 +149,7 @@ void MessageWindow::Draw() if (!s_isVisible) return; - auto pInputState = InstallerWizard::s_isVisible ? nullptr : SWA::CInputState::GetInstance(); + auto pInputState = g_isGameLoaded ? SWA::CInputState::GetInstance() : nullptr; auto drawList = ImGui::GetForegroundDrawList(); auto& res = ImGui::GetIO().DisplaySize; @@ -299,7 +299,7 @@ bool MessageWindow::Open(std::string text, int* result, std::span b g_text = text; g_buttons = std::vector(buttons.begin(), buttons.end()); - g_defaultButtonIndex = defaultButtonIndex; + g_defaultButtonIndex = g_isGameLoaded ? defaultButtonIndex : -1; ResetSelection(); From ba541c0fcad029461436c6ca6505a05ac055fe8c Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 3 Dec 2024 23:15:59 -0300 Subject: [PATCH 23/28] Add queued update when using files pickers. --- UnleashedRecomp/ui/installer_wizard.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/UnleashedRecomp/ui/installer_wizard.cpp b/UnleashedRecomp/ui/installer_wizard.cpp index fb0d45fc..b4b74322 100644 --- a/UnleashedRecomp/ui/installer_wizard.cpp +++ b/UnleashedRecomp/ui/installer_wizard.cpp @@ -115,6 +115,7 @@ static WizardPage g_currentPage = g_firstPage; static std::string g_currentMessagePrompt = ""; static bool g_currentMessagePromptConfirmation = false; static int g_currentMessageResult = -1; +static bool g_currentMessageUpdateRemaining = false; const char CREDITS_TEXT[] = "Sajid (RIP)"; @@ -620,6 +621,8 @@ static bool ShowFilesPicker(std::list &filePaths) const nfdpathset_t *pathSet; nfdresult_t result = NFD_OpenDialogMultipleU8(&pathSet, nullptr, 0, nullptr); + g_currentMessageUpdateRemaining = true; + if (result == NFD_OKAY) { bool pathsConverted = ConvertPathSet(pathSet, filePaths); @@ -638,6 +641,8 @@ static bool ShowFoldersPicker(std::list &folderPaths) const nfdpathset_t *pathSet; nfdresult_t result = NFD_PickFolderMultipleU8(&pathSet, nullptr); + g_currentMessageUpdateRemaining = true; + if (result == NFD_OKAY) { bool pathsConverted = ConvertPathSet(pathSet, folderPaths); @@ -1019,6 +1024,15 @@ static void DrawMessagePrompt() return; } + if (g_currentMessageUpdateRemaining) + { + // If a blocking function like the files picker is called, we must wait one update before actually showing + // the message box, as a lot of time has passed since the last real update. Otherwise, animations will play + // too quickly and input glitches might happen. + g_currentMessageUpdateRemaining = false; + return; + } + bool messageWindowReturned = false; if (g_currentMessagePromptConfirmation) { From 7b0e6dc635022610f49ce76db8fdf46d9388cbeb Mon Sep 17 00:00:00 2001 From: Hyper <34012267+hyperbx@users.noreply.github.com> Date: Wed, 4 Dec 2024 03:00:33 +0000 Subject: [PATCH 24/28] installer_wizard: implement localisation --- UnleashedRecomp/locale/locale.h | 96 +++++++++++++++++++++++++ UnleashedRecomp/ui/imgui_utils.h | 48 ++++++++++++- UnleashedRecomp/ui/installer_wizard.cpp | 73 +++++++++++-------- 3 files changed, 186 insertions(+), 31 deletions(-) diff --git a/UnleashedRecomp/locale/locale.h b/UnleashedRecomp/locale/locale.h index ac1eb716..ab224ceb 100644 --- a/UnleashedRecomp/locale/locale.h +++ b/UnleashedRecomp/locale/locale.h @@ -98,6 +98,78 @@ inline static std::unordered_map maxLength) + { + if (useEllipsis && maxLength > ellipsis.length()) + { + if (usePrefixEllipsis) + { + return ellipsis + input.substr(0, maxLength - ellipsis.length()); + } + else + { + return input.substr(0, maxLength - ellipsis.length()) + ellipsis; + } + } + else + { + return input.substr(0, maxLength); + } + } + + return input; +} + static std::vector Split(const char* str, char delimiter) { std::vector result; @@ -285,11 +311,29 @@ static void DrawCentredParagraph(const ImFont* font, float fontSize, const ImVec auto paragraphSize = MeasureCentredParagraph(font, fontSize, lineMargin, lines); auto offsetY = 0.0f; - for (auto& str : lines) + auto hasList = std::strstr(text, "- "); + auto isList = false; + auto listOffsetX = 0.0f; + + for (int i = 0; i < lines.size(); i++) { + auto& str = lines[i]; auto textSize = font->CalcTextSizeA(fontSize, FLT_MAX, 0, str.c_str()); - drawMethod(str.c_str(), ImVec2(/* X */ centre.x - textSize.x / 2, /* Y */ centre.y - paragraphSize.y / 2 + offsetY)); + if (hasList) + { + if (!isList && str.starts_with("- ") && lines.size() > i + 1 && lines[i + 1].starts_with("- ")) + { + isList = true; + listOffsetX = centre.x - textSize.x / 2; + } + else if (isList && !str.starts_with("- ")) + { + isList = false; + } + } + + drawMethod(str.c_str(), ImVec2(/* X */ isList ? listOffsetX : centre.x - textSize.x / 2, /* Y */ centre.y - paragraphSize.y / 2 + offsetY)); offsetY += textSize.y + Scale(lineMargin); } diff --git a/UnleashedRecomp/ui/installer_wizard.cpp b/UnleashedRecomp/ui/installer_wizard.cpp index b4b74322..7ff0cae9 100644 --- a/UnleashedRecomp/ui/installer_wizard.cpp +++ b/UnleashedRecomp/ui/installer_wizard.cpp @@ -117,19 +117,24 @@ static bool g_currentMessagePromptConfirmation = false; static int g_currentMessageResult = -1; static bool g_currentMessageUpdateRemaining = false; -const char CREDITS_TEXT[] = "Sajid (RIP)"; +const char CREDITS_TEXT[] = "- Sajid (RIP)\n- imgui sega balls!"; -const char *WIZARD_TEXT[] = +static std::string& GetWizardText(int index) { - "Please select a language.\n", - "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" -}; + switch (index) + { + case 0: return Localise("Installer_Page_Language"); + case 1: return Localise("Installer_Page_Welcome"); + case 2: return Localise("Installer_Page_AddGame"); + case 3: return Localise("Installer_Page_AddDLC"); + case 4: return Localise("Installer_Page_FreeSpace"); + case 5: return Localise("Installer_Page_Installing"); + case 6: return Localise("Installer_Page_InstallComplete"); + case 7: return Localise("Installer_Page_InstallFailed"); + } + + return g_localeMissing; +} static const int WIZARD_INSTALL_TEXTURE_INDEX[] = { @@ -164,8 +169,6 @@ const ELanguage LANGUAGE_ENUM[] = ELanguage::Japanese, }; -const char GAME_SOURCE_TEXT[] = "FULL GAME"; -const char UPDATE_SOURCE_TEXT[] = "UPDATE"; const char *DLC_SOURCE_TEXT[] = { "SPAGONIA", @@ -176,9 +179,6 @@ const char *DLC_SOURCE_TEXT[] = "EMPIRE CITY & ADABAT", }; -const char REQUIRED_SPACE_TEXT[] = "Required space"; -const char AVAILABLE_SPACE_TEXT[] = "Available space"; - static int DLCIndex(DLC dlc) { assert(dlc != DLC::Unknown); @@ -243,7 +243,7 @@ static void DrawHeaderIcons() ImVec2 arrowCircleMin = { Scale(iconsPosX - iconsScale / 2), Scale(iconsPosY - iconsScale / 2) }; ImVec2 arrowCircleMax = { Scale(iconsPosX + iconsScale / 2), Scale(iconsPosY + iconsScale / 2) }; - ImVec2 center = { Scale(iconsPosX) + 0.5f, Scale(iconsPosY) }; + ImVec2 center = { Scale(iconsPosX) + 0.5f, Scale(iconsPosY) - 0.5f }; float currentAngle = g_arrowCircleCurrentRotation * (3.14159f / 180.0f); // Rotation angle in radians float cos_a = cosf(currentAngle); float sin_a = sinf(currentAngle); @@ -385,14 +385,24 @@ static void DrawDescriptionContainer() DrawContainer(descriptionMin, descriptionMax, true); char descriptionText[512]; - strncpy(descriptionText, WIZARD_TEXT[int(g_currentPage)], sizeof(descriptionText) - 1); + strncpy(descriptionText, GetWizardText(int(g_currentPage)).c_str(), 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); + + // TODO: the format for where the numbers are (%2.2f GiB) should be moved to the localised string. + snprintf + ( + descriptionText, + sizeof(descriptionText), + "%s%s %2.2f GiB\n%s %2.2f GiB", + GetWizardText(int(g_currentPage)).c_str(), + Localise("Installer_Step_RequiredSpace").c_str(), requiredGiB, + Localise("Installer_Step_AvailableSpace").c_str(), availableGiB + ); } else if (g_currentPage == WizardPage::InstallSucceeded) { @@ -660,6 +670,7 @@ static void ParseSourcePaths(std::list &paths) assert((g_currentPage == WizardPage::SelectGameAndUpdate) || (g_currentPage == WizardPage::SelectDLC)); constexpr size_t failedPathLimit = 5; + bool isFailedPathsOverLimit = false; std::list failedPaths; if (g_currentPage == WizardPage::SelectGameAndUpdate) { @@ -677,6 +688,10 @@ static void ParseSourcePaths(std::list &paths) { failedPaths.push_back(path); } + else + { + isFailedPathsOverLimit = true; + } } } else if(g_currentPage == WizardPage::SelectDLC) @@ -698,12 +713,15 @@ static void ParseSourcePaths(std::list &paths) if (!failedPaths.empty()) { std::stringstream stringStream; - stringStream << "Some of the files that were selected are not valid." << std::endl; + stringStream << Localise("Installer_Message_InvalidFilesList") << std::endl; for (const std::filesystem::path &path : failedPaths) { - stringStream << std::endl << path.filename().string(); + stringStream << std::endl << "- " << Truncate(path.filename().string(), 32, true, true); } + if (isFailedPathsOverLimit) + stringStream << std::endl << "- [...]"; + g_currentMessagePrompt = stringStream.str(); g_currentMessagePromptConfirmation = false; } @@ -774,8 +792,8 @@ static void DrawSources() { if (g_currentPage == WizardPage::SelectGameAndUpdate) { - DrawSourceButton(ButtonColumnMiddle, 1.5f, GAME_SOURCE_TEXT, !g_gameSourcePath.empty()); - DrawSourceButton(ButtonColumnMiddle, 0.5f, UPDATE_SOURCE_TEXT, !g_updateSourcePath.empty()); + DrawSourceButton(ButtonColumnMiddle, 1.5f, Localise("Installer_Step_Game").c_str(), !g_gameSourcePath.empty()); + DrawSourceButton(ButtonColumnMiddle, 0.5f, Localise("Installer_Step_Update").c_str(), !g_updateSourcePath.empty()); } if (g_currentPage == WizardPage::SelectDLC) @@ -883,8 +901,7 @@ static void DrawNextButton() XexPatcher::Result patcherResult; if (g_currentPage == WizardPage::SelectGameAndUpdate && (patcherResult = Installer::checkGameUpdateCompatibility(g_gameSourcePath, g_updateSourcePath), patcherResult != XexPatcher::Result::Success)) { - // TODO: localise this. - g_currentMessagePrompt = "The specified game and\nupdate file are incompatible.\n\nPlease ensure the files are\nfor the same version and\nregion and try again."; + g_currentMessagePrompt = Localise("Installer_Message_IncompatibleGameData"); g_currentMessagePromptConfirmation = false; } else if (g_currentPage == WizardPage::SelectDLC) @@ -902,17 +919,15 @@ static void DrawNextButton() bool dlcInstallerMode = g_gameSourcePath.empty(); if (!InstallerParseSources()) { - // TODO: localise this. // Some of the sources that were provided to the installer are not valid. Restart the file selection process. - g_currentMessagePrompt = "Some of the files that have\nbeen provided are not valid.\n\nPlease make sure all the\nspecified files are correct\nand try again."; + g_currentMessagePrompt = Localise("Installer_Message_InvalidFiles"); g_currentMessagePromptConfirmation = false; g_currentPage = dlcInstallerMode ? WizardPage::SelectDLC : WizardPage::SelectGameAndUpdate; } else if (dlcIncomplete && !dlcInstallerMode) { - // TODO: localise this. // Not all the DLC was specified, we show a prompt and await a confirmation before starting the installer. - g_currentMessagePrompt = "It is highly recommended\nthat you install all of the DLC,\nas it includes high quality\nlighting textures for the\nstages in each pack.\n\nAre you sure you want to\nskip this step?"; + g_currentMessagePrompt = Localise("Installer_Message_DLCWarning"); g_currentMessagePromptConfirmation = true; } else if (skipButton && dlcInstallerMode) From 130d72d805a4304a5759cad89388e2c90b8316c7 Mon Sep 17 00:00:00 2001 From: Hyper <34012267+hyperbx@users.noreply.github.com> Date: Wed, 4 Dec 2024 03:04:20 +0000 Subject: [PATCH 25/28] installer_wizard: use enum for localisation --- UnleashedRecomp/locale/locale.h | 12 ++++++------ UnleashedRecomp/ui/installer_wizard.cpp | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/UnleashedRecomp/locale/locale.h b/UnleashedRecomp/locale/locale.h index ab224ceb..62f03783 100644 --- a/UnleashedRecomp/locale/locale.h +++ b/UnleashedRecomp/locale/locale.h @@ -99,31 +99,31 @@ inline static std::unordered_map Date: Wed, 4 Dec 2024 03:13:16 +0000 Subject: [PATCH 26/28] message_window: fix visibility persisting after window closes --- UnleashedRecomp/ui/message_window.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/UnleashedRecomp/ui/message_window.cpp b/UnleashedRecomp/ui/message_window.cpp index eb21a8cd..e6bf9ada 100644 --- a/UnleashedRecomp/ui/message_window.cpp +++ b/UnleashedRecomp/ui/message_window.cpp @@ -284,6 +284,10 @@ void MessageWindow::Draw() } } } + else if (g_isClosing) + { + s_isVisible = false; + } } bool MessageWindow::Open(std::string text, int* result, std::span buttons, int defaultButtonIndex) From 0f7780165c9750880c27a7af593f7e08d2f89540 Mon Sep 17 00:00:00 2001 From: PTKay Date: Wed, 4 Dec 2024 12:10:29 +0000 Subject: [PATCH 27/28] Fix arrow circle animation and added pulse animation --- UnleashedRecomp/CMakeLists.txt | 1 + UnleashedRecomp/ui/installer_wizard.cpp | 105 +++++++++++++++++------- 2 files changed, 75 insertions(+), 31 deletions(-) diff --git a/UnleashedRecomp/CMakeLists.txt b/UnleashedRecomp/CMakeLists.txt index e7270cbf..6f106120 100644 --- a/UnleashedRecomp/CMakeLists.txt +++ b/UnleashedRecomp/CMakeLists.txt @@ -327,6 +327,7 @@ BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/in BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_007.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_007.dds" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_install_007") BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_008.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_008.dds" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_install_008") BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/miles_electric_icon.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/miles_electric_icon.dds" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_miles_electric_icon") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/pulse_install.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/pulse_install.dds" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_pulse_install") BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/common/general_window.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/common/general_window.dds" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_general_window") BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/common/select_fade.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/common/select_fade.dds" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_select_fade") BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/common/select_fill.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/common/select_fill.dds" ARRAY_TYPE "unsigned char" ARRAY_NAME "g_select_fill") diff --git a/UnleashedRecomp/ui/installer_wizard.cpp b/UnleashedRecomp/ui/installer_wizard.cpp index 1efc1682..5a078f5b 100644 --- a/UnleashedRecomp/ui/installer_wizard.cpp +++ b/UnleashedRecomp/ui/installer_wizard.cpp @@ -19,7 +19,9 @@ #include #include #include +#include +// One Shot Animations Constants static constexpr double SCANLINES_ANIMATION_TIME = 0.0; static constexpr double SCANLINES_ANIMATION_DURATION = 15.0; @@ -41,10 +43,15 @@ static constexpr double CONTAINER_OUTER_DURATION = 23.0; static constexpr double CONTAINER_INNER_TIME = SCANLINES_ANIMATION_DURATION + CONTAINER_LINE_ANIMATION_DURATION + 8.0; static constexpr double CONTAINER_INNER_DURATION = 15.0; -static constexpr double ARROW_CIRCLE_SPIN_FACTOR = 2; - static constexpr double ALL_ANIMATIONS_FULL_DURATION = CONTAINER_INNER_TIME + CONTAINER_INNER_DURATION; +// Loop Animations Constants - their time range is [0.0, 1.0 + DELAY] +static constexpr double ARROW_CIRCLE_LOOP_SPEED = 1; + +static constexpr double PULSE_ANIMATION_LOOP_SPEED = 1.5; +static constexpr double PULSE_ANIMATION_LOOP_DELAY = 0.5; +static constexpr double PULSE_ANIMATION_LOOP_FADE_HIGH_POINT = 0.5; + constexpr float IMAGE_X = 165.0f; constexpr float IMAGE_Y = 106.0f; constexpr float IMAGE_WIDTH = 512.0f; @@ -87,6 +94,7 @@ static std::array g_dlcInstalled = {}; static std::array, 8> g_installTextures; static std::unique_ptr g_milesElectricIcon; static std::unique_ptr g_arrowCircle; +static std::unique_ptr g_pulseInstall; static Journal g_installerJournal; static Installer::Sources g_installerSources; static uint64_t g_installerAvailableSize = 0; @@ -190,6 +198,14 @@ static double ComputeMotionInstaller(double timeAppear, double timeDisappear, do return ComputeMotion(timeAppear, offset, total) * (1.0 - ComputeMotion(timeDisappear, ALL_ANIMATIONS_FULL_DURATION - offset - total, total)); } +static double ComputeMotionInstallerLoop(double timeAppear, double speed, double offset) { + return std::clamp(fmodf((ImGui::GetTime() - timeAppear) * speed, 1.0f + offset) - offset, 0.0, 1.0) / 1.0; +} + +static double ComputeHermiteMotionInstallerLoop(double timeAppear, double speed, double offset) { + return (cosf(M_PI * ComputeMotionInstallerLoop(timeAppear, speed, offset) + M_PI) + 1) / 2; +} + static void DrawBackground() { auto &res = ImGui::GetIO().DisplaySize; @@ -221,11 +237,63 @@ static void DrawLeftImage() 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 DrawHeaderIconsForInstallPhase(double iconsPosX, double iconsPosY, double iconsScale) +{ + auto drawList = ImGui::GetForegroundDrawList(); + + // Arrow Circle Icon + ImVec2 arrowCircleMin = { Scale(iconsPosX - iconsScale / 2), Scale(iconsPosY - iconsScale / 2) }; + ImVec2 arrowCircleMax = { Scale(iconsPosX + iconsScale / 2), Scale(iconsPosY + iconsScale / 2) }; + ImVec2 center = { Scale(iconsPosX) + 0.5f, Scale(iconsPosY) - 0.5f }; + + float rotationMotion = ComputeMotionInstallerLoop(g_installerStartTime, ARROW_CIRCLE_LOOP_SPEED, 0); + float rotation = -2 * M_PI * rotationMotion; + + // Calculate rotated corners + float cosCurrentAngle = cosf(rotation); + float sinCurrentAngle = sinf(rotation); + ImVec2 corners[4] = + { + ImRotate(ImVec2(arrowCircleMin.x - center.x, arrowCircleMin.y - center.y), cosCurrentAngle, sinCurrentAngle), + ImRotate(ImVec2(arrowCircleMax.x - center.x, arrowCircleMin.y - center.y), cosCurrentAngle, sinCurrentAngle), + ImRotate(ImVec2(arrowCircleMax.x - center.x, arrowCircleMax.y - center.y), cosCurrentAngle, sinCurrentAngle), + ImRotate(ImVec2(arrowCircleMin.x - center.x, arrowCircleMax.y - center.y), cosCurrentAngle, sinCurrentAngle), + }; + + for (int i = 0; i < IM_ARRAYSIZE(corners); ++i) + { + corners[i].x += center.x; + corners[i].y += center.y; + } + + drawList->AddImageQuad(g_arrowCircle.get(), corners[0], corners[1], corners[2], corners[3], ImVec2(0, 0), ImVec2(1, 0), ImVec2(1, 1), ImVec2(0, 1), IM_COL32(255, 255, 255, 96)); + + + // Pulse + float pulseMotion = ComputeMotionInstallerLoop(g_installerStartTime, PULSE_ANIMATION_LOOP_SPEED, PULSE_ANIMATION_LOOP_DELAY); + float pulseHermiteMotion = ComputeHermiteMotionInstallerLoop(g_installerStartTime, PULSE_ANIMATION_LOOP_SPEED, PULSE_ANIMATION_LOOP_DELAY); + + float pulseFade = pulseMotion / PULSE_ANIMATION_LOOP_FADE_HIGH_POINT; + if (pulseMotion >= PULSE_ANIMATION_LOOP_FADE_HIGH_POINT) { + // Calculate linear fade-out from high point time - ({PULSE_ANIMATION_LOOP_FADE_HIGH_POINT}, 1) - to loop end - (1, 0) -. + float m = -1 / (1 - PULSE_ANIMATION_LOOP_FADE_HIGH_POINT); + float b = m * (-PULSE_ANIMATION_LOOP_FADE_HIGH_POINT) + 1; + + pulseFade = m * pulseMotion + b; + } + + float pulseScale = iconsScale * pulseHermiteMotion * 1.5; + + ImVec2 pulseMin = { Scale(iconsPosX - pulseScale / 2), Scale(iconsPosY - pulseScale / 2) }; + ImVec2 pulseMax = { Scale(iconsPosX + pulseScale / 2), Scale(iconsPosY + pulseScale / 2) }; + drawList->AddImage(g_pulseInstall.get(), pulseMin, pulseMax, ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 255 * pulseFade)); +} + static void DrawHeaderIcons() { auto drawList = ImGui::GetForegroundDrawList(); - float iconsPosX = 255.0f; + float iconsPosX = 253.0f; float iconsPosY = 79.0f; float iconsScale = 58; @@ -237,36 +305,9 @@ static void DrawHeaderIcons() ImVec2 milesElectricMax = { Scale(iconsPosX + milesIconScale / 2), Scale(iconsPosY + milesIconScale / 2) }; drawList->AddImage(g_milesElectricIcon.get(), milesElectricMin, milesElectricMax, ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 255 * milesIconMotion)); - // Arrow Circle Icon if (g_currentPage == WizardPage::Installing) { - ImVec2 arrowCircleMin = { Scale(iconsPosX - iconsScale / 2), Scale(iconsPosY - iconsScale / 2) }; - ImVec2 arrowCircleMax = { Scale(iconsPosX + iconsScale / 2), Scale(iconsPosY + iconsScale / 2) }; - - ImVec2 center = { Scale(iconsPosX) + 0.5f, Scale(iconsPosY) - 0.5f }; - float currentAngle = g_arrowCircleCurrentRotation * (3.14159f / 180.0f); // Rotation angle in radians - float cos_a = cosf(currentAngle); - float sin_a = sinf(currentAngle); - - // Calculate rotated corners - ImVec2 corners[4] = - { - ImRotate(ImVec2(arrowCircleMin.x - center.x, arrowCircleMin.y - center.y), cos_a, sin_a), - ImRotate(ImVec2(arrowCircleMax.x - center.x, arrowCircleMin.y - center.y), cos_a, sin_a), - ImRotate(ImVec2(arrowCircleMax.x - center.x, arrowCircleMax.y - center.y), cos_a, sin_a), - ImRotate(ImVec2(arrowCircleMin.x - center.x, arrowCircleMax.y - center.y), cos_a, sin_a), - }; - - for (int i = 0; i < IM_ARRAYSIZE(corners); ++i) - { - corners[i].x += center.x; // Add center.x to corner.x - corners[i].y += center.y; // Add center.y to corner.y - } - - drawList->AddImageQuad(g_arrowCircle.get(), corners[0], corners[1], corners[2], corners[3], ImVec2(0, 0), ImVec2(1, 0), ImVec2(1, 1), ImVec2(0, 1), IM_COL32(255, 255, 255, 96)); - - // Update rotation for next frame - g_arrowCircleCurrentRotation = fmodf(g_arrowCircleCurrentRotation - ARROW_CIRCLE_SPIN_FACTOR, 360.0f); + DrawHeaderIconsForInstallPhase(iconsPosX, iconsPosY, iconsScale); } } @@ -1089,6 +1130,7 @@ void InstallerWizard::Init() g_installTextures[7] = LoadTexture(g_install_008, sizeof(g_install_008)); g_milesElectricIcon = LoadTexture(g_miles_electric_icon, sizeof(g_miles_electric_icon)); g_arrowCircle = LoadTexture(g_arrow_circle, sizeof(g_arrow_circle)); + g_pulseInstall = LoadTexture(g_pulse_install, sizeof(g_pulse_install)); } void InstallerWizard::Draw() @@ -1140,6 +1182,7 @@ void InstallerWizard::Shutdown() // Erase the textures. g_milesElectricIcon.reset(); g_arrowCircle.reset(); + g_pulseInstall.reset(); for (auto &texture : g_installTextures) { From 5de906e948899f8048078b3b25af33a3fac0761e Mon Sep 17 00:00:00 2001 From: Dario Date: Wed, 4 Dec 2024 10:06:55 -0300 Subject: [PATCH 28/28] Come back check space. --- UnleashedRecomp/ui/installer_wizard.cpp | 11 +++++++---- UnleashedRecompResources | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/UnleashedRecomp/ui/installer_wizard.cpp b/UnleashedRecomp/ui/installer_wizard.cpp index 5a078f5b..d99f7943 100644 --- a/UnleashedRecomp/ui/installer_wizard.cpp +++ b/UnleashedRecomp/ui/installer_wizard.cpp @@ -979,10 +979,13 @@ static void DrawNextButton() } else { - // Start the installer outright, this switches to the right page on its own. - InstallerStart(); + g_currentPage = WizardPage::CheckSpace; } } + else if (g_currentPage == WizardPage::CheckSpace) + { + InstallerStart(); + } else if (g_currentPage == WizardPage::InstallSucceeded) { g_isDisappearing = true; @@ -1104,8 +1107,8 @@ static void DrawMessagePrompt() { if (g_currentMessagePromptConfirmation && (g_currentMessageResult == 0) && (g_currentPage == WizardPage::SelectDLC)) { - // If user confirms the message prompt that they wish to skip installing the DLC, start the installer. - InstallerStart(); + // If user confirms the message prompt that they wish to skip installing the DLC, proceed to the next step. + g_currentPage = WizardPage::CheckSpace; } g_currentMessagePrompt.clear(); diff --git a/UnleashedRecompResources b/UnleashedRecompResources index 5b5ad279..771f6705 160000 --- a/UnleashedRecompResources +++ b/UnleashedRecompResources @@ -1 +1 @@ -Subproject commit 5b5ad2794a2c78d50dc6a85e71954fb6b9e80ae2 +Subproject commit 771f670594a2d274bfb47a209017b2616ed423cf