diff --git a/UnleashedRecomp/CMakeLists.txt b/UnleashedRecomp/CMakeLists.txt index ec385879..eb19d846 100644 --- a/UnleashedRecomp/CMakeLists.txt +++ b/UnleashedRecomp/CMakeLists.txt @@ -111,6 +111,7 @@ set(SWA_PATCHES_CXX_SOURCES set(SWA_UI_CXX_SOURCES "ui/achievement_menu.cpp" "ui/achievement_overlay.cpp" + "ui/installer_wizard.cpp" "ui/button_guide.cpp" "ui/message_window.cpp" "ui/options_menu.cpp" @@ -199,6 +200,7 @@ find_package(unofficial-concurrentqueue REQUIRED) find_package(imgui CONFIG REQUIRED) find_package(magic_enum CONFIG REQUIRED) find_package(unofficial-tiny-aes-c CONFIG REQUIRED) +find_package(nfd CONFIG REQUIRED) find_path(MINIAUDIO_INCLUDE_DIRS "miniaudio.h") file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/D3D12) @@ -237,6 +239,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 @@ -318,6 +321,8 @@ 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}/font/im_font_atlas.bin" DEST_FILE "${RESOURCES_OUTPUT_PATH}/font/im_font_atlas.bin" ARRAY_NAME "g_im_font_atlas" COMPRESSION_TYPE "zstd") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/font/im_font_atlas.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/font/im_font_atlas.dds" ARRAY_NAME "g_im_font_atlas_texture" COMPRESSION_TYPE "zstd") 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_NAME "g_trophy" COMPRESSION_TYPE "zstd") 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_NAME "g_general_window" COMPRESSION_TYPE "zstd") BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/common/left_mouse_button.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/common/left_mouse_button.dds" ARRAY_NAME "g_left_mouse_button" COMPRESSION_TYPE "zstd") @@ -327,5 +332,14 @@ BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/co BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/common/start_back.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/common/start_back.dds" ARRAY_NAME "g_start_back" COMPRESSION_TYPE "zstd") BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/game_icon.bmp" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/game_icon.bmp" 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_NAME "g_game_icon_night") -BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/font/im_font_atlas.bin" DEST_FILE "${RESOURCES_OUTPUT_PATH}/font/im_font_atlas.bin" ARRAY_NAME "g_im_font_atlas" COMPRESSION_TYPE "zstd") -BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/font/im_font_atlas.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/font/im_font_atlas.dds" ARRAY_NAME "g_im_font_atlas_texture" COMPRESSION_TYPE "zstd") +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_NAME "g_arrow_circle" COMPRESSION_TYPE "zstd") +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_NAME "g_install_001" COMPRESSION_TYPE "zstd") +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_NAME "g_install_002" COMPRESSION_TYPE "zstd") +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_NAME "g_install_003" COMPRESSION_TYPE "zstd") +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_NAME "g_install_004" COMPRESSION_TYPE "zstd") +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_NAME "g_install_005" COMPRESSION_TYPE "zstd") +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_NAME "g_install_006" COMPRESSION_TYPE "zstd") +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_NAME "g_install_007" COMPRESSION_TYPE "zstd") +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_NAME "g_install_008" COMPRESSION_TYPE "zstd") +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_NAME "g_miles_electric_icon" COMPRESSION_TYPE "zstd") +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_NAME "g_pulse_install" COMPRESSION_TYPE "zstd") 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/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/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 059d8c5f..a0a688d1 100644 --- a/UnleashedRecomp/gpu/video.cpp +++ b/UnleashedRecomp/gpu/video.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include "imgui_snapshot.h" #include "imgui_common.h" @@ -1060,6 +1061,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() @@ -1082,6 +1085,7 @@ static void CreateImGuiBackend() ButtonGuide::Init(); MessageWindow::Init(); OptionsMenu::Init(); + InstallerWizard::Init(); ImGui_ImplSDL2_InitForOther(Window::s_pWindow); @@ -1223,7 +1227,9 @@ static void CreateImGuiBackend() #endif } -static void CreateHostDevice() +static void BeginCommandList(); + +void Video::CreateHostDevice() { for (uint32_t i = 0; i < 16; i++) g_inputSlots[i].index = i; @@ -1413,20 +1419,22 @@ 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() +void Video::WaitForGPU() { if (g_vulkan) { @@ -1447,12 +1455,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() @@ -1469,7 +1476,7 @@ static void BeginCommandList() if (!g_swapChainValid) { - WaitForGPU(); + Video::WaitForGPU(); g_backBuffer->framebuffers.clear(); g_swapChainValid = g_swapChain->resize(); g_needsResize = g_swapChainValid; @@ -1493,7 +1500,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); @@ -1540,21 +1547,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)); @@ -1788,6 +1791,7 @@ static void DrawImGui() AchievementMenu::Draw(); OptionsMenu::Draw(); AchievementOverlay::Draw(); + InstallerWizard::Draw(); MessageWindow::Draw(); ButtonGuide::Draw(); @@ -1804,8 +1808,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()); @@ -1849,6 +1862,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 @@ -1879,21 +1898,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; @@ -1901,10 +1920,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]; @@ -1950,11 +1979,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); } @@ -1965,7 +1994,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()); @@ -1984,14 +2013,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), @@ -2025,7 +2054,8 @@ static void ProcPresent(const RenderCommand& cmd) BeginCommandList(); - g_pendingRenderThread = false; + g_pendingRenderThread.store(false); + g_pendingRenderThread.notify_all(); } static GuestSurface* GetBackBuffer() @@ -4171,10 +4201,10 @@ static RenderFormat ConvertDXGIFormat(ddspp::DXGIFormat format) } } -static bool LoadTexture(GuestTexture& texture, uint8_t* data, size_t dataSize, RenderComponentMapping componentMapping) +static bool LoadTexture(GuestTexture& texture, const uint8_t* data, size_t dataSize, RenderComponentMapping componentMapping) { 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); @@ -4243,7 +4273,7 @@ static bool LoadTexture(GuestTexture& texture, uint8_t* data, size_t dataSize, R 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) @@ -4336,7 +4366,7 @@ static bool LoadTexture(GuestTexture& texture, uint8_t* data, size_t dataSize, R return false; } -std::unique_ptr LoadTexture(uint8_t* data, size_t dataSize, RenderComponentMapping componentMapping) +std::unique_ptr LoadTexture(const uint8_t* data, size_t dataSize, RenderComponentMapping componentMapping) { GuestTexture texture(ResourceType::Texture); @@ -5583,7 +5613,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 102f88b9..bb636b21 100644 --- a/UnleashedRecomp/gpu/video.h +++ b/UnleashedRecomp/gpu/video.h @@ -10,6 +10,14 @@ using namespace plume; +struct Video +{ + static void CreateHostDevice(); + static void HostPresent(); + static void StartPipelinePrecompilation(); + static void WaitForGPU(); +}; + struct GuestSamplerState { be data[6]; @@ -379,6 +387,6 @@ enum GuestTextureAddress D3DTADDRESS_BORDER = 6 }; -extern std::unique_ptr LoadTexture(uint8_t* data, size_t dataSize, RenderComponentMapping componentMapping = RenderComponentMapping()); +extern std::unique_ptr LoadTexture(const uint8_t* data, size_t dataSize, RenderComponentMapping componentMapping = RenderComponentMapping()); extern void VideoConfigValueChangedCallback(class IConfigDef* config); diff --git a/UnleashedRecomp/install/installer.cpp b/UnleashedRecomp/install/installer.cpp index 0058b2e0..c28783b5 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,46 @@ 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::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) + { + 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 +305,49 @@ 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) { + journal = Journal(); + 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 +390,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 +452,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 +478,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..fae798ec 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,30 @@ 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 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); - 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/locale/locale.h b/UnleashedRecomp/locale/locale.h index 541271e8..90b3d36c 100644 --- a/UnleashedRecomp/locale/locale.h +++ b/UnleashedRecomp/locale/locale.h @@ -84,6 +84,148 @@ inline static std::unordered_map #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/imgui_utils.h b/UnleashedRecomp/ui/imgui_utils.h index 7fe33be5..7fed7d57 100644 --- a/UnleashedRecomp/ui/imgui_utils.h +++ b/UnleashedRecomp/ui/imgui_utils.h @@ -52,6 +52,20 @@ static void SetShaderModifier(uint32_t shaderModifier) callbackData->setShaderModifier.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) { @@ -221,6 +235,32 @@ static float CalcWidestTextSize(const ImFont* font, float fontSize, std::span 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; @@ -274,11 +314,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 new file mode 100644 index 00000000..45807b27 --- /dev/null +++ b/UnleashedRecomp/ui/installer_wizard.cpp @@ -0,0 +1,1230 @@ +#include "installer_wizard.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// One Shot Animations Constants +static constexpr double SCANLINES_ANIMATION_TIME = 0.0; +static constexpr double SCANLINES_ANIMATION_DURATION = 15.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 IMAGE_ANIMATION_TIME = MILES_ICON_ANIMATION_TIME + MILES_ICON_ANIMATION_DURATION; +static constexpr double IMAGE_ANIMATION_DURATION = 15.0; + +static constexpr double TITLE_ANIMATION_TIME = SCANLINES_ANIMATION_DURATION; +static constexpr double TITLE_ANIMATION_DURATION = 30.0; + +static constexpr double CONTAINER_LINE_ANIMATION_TIME = SCANLINES_ANIMATION_DURATION; +static constexpr double CONTAINER_LINE_ANIMATION_DURATION = 23.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 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; + +// 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; +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 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; + +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_arrowCircleCurrentRotation = 0.0; + +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; +static std::unique_ptr g_pulseInstall; +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 +{ + SelectLanguage, + Introduction, + SelectGameAndUpdate, + SelectDLC, + CheckSpace, + Installing, + InstallSucceeded, + InstallFailed, +}; + +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; +static bool g_currentMessageUpdateRemaining = false; + +const char CREDITS_TEXT[] = "- Sajid (RIP)\n- imgui sega balls!"; + +static std::string& GetWizardText(WizardPage page) +{ + switch (page) + { + case WizardPage::SelectLanguage: return Localise("Installer_Page_SelectLanguage"); + case WizardPage::Introduction: return Localise("Installer_Page_Introduction"); + case WizardPage::SelectGameAndUpdate: return Localise("Installer_Page_SelectGameAndUpdate"); + case WizardPage::SelectDLC: return Localise("Installer_Page_SelectDLC"); + case WizardPage::CheckSpace: return Localise("Installer_Page_CheckSpace"); + case WizardPage::Installing: return Localise("Installer_Page_Installing"); + case WizardPage::InstallSucceeded: return Localise("Installer_Page_InstallSucceeded"); + case WizardPage::InstallFailed: return Localise("Installer_Page_InstallFailed"); + } + + return g_localeMissing; +} + +static const int WIZARD_INSTALL_TEXTURE_INDEX[] = +{ + 0, + 0, + 1, + 2, + 3, + 4, + 7, // Force Werehog on InstallSucceeded. + 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 *DLC_SOURCE_TEXT[] = +{ + "SPAGONIA", + "CHUN-NAN", + "MAZURI", + "HOLOSKA", + "APOTOS & SHAMAR", + "EMPIRE CITY & ADABAT", +}; + +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 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; + 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, 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; + 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 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 = 253.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); + + ImVec2 milesElectricMin = { Scale(iconsPosX - milesIconScale / 2), Scale(iconsPosY - milesIconScale / 2) }; + 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)); + + if (g_currentPage == WizardPage::Installing) + { + DrawHeaderIconsForInstallPhase(iconsPosX, iconsPosY, iconsScale); + } +} + +static void DrawScanlineBars() +{ + 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) + { + return; + } + + 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 + 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.c_str(), 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) + ); + + DrawHeaderIcons(); +} + +static float AlignToNextGrid(float value) +{ + return floor(value / GRID_SIZE) * GRID_SIZE; +} + +static void DrawContainer(ImVec2 min, ImVec2 max, bool isTextArea) +{ + auto &res = ImGui::GetIO().DisplaySize; + auto drawList = ImGui::GetForegroundDrawList(); + + 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_INNER_TIME, CONTAINER_INNER_DURATION); + + 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); + + SetShaderModifier(IMGUI_SHADER_MODIFIER_CHECKERBOARD); + 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 }); +} + +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, true); + + char descriptionText[512]; + strncpy(descriptionText, GetWizardText(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; + + // 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(g_currentPage).c_str(), + Localise("Installer_Step_RequiredSpace").c_str(), requiredGiB, + Localise("Installer_Step_AvailableSpace").c_str(), 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_INNER_TIME, CONTAINER_INNER_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, false); + 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 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) + { + squashRatio = maxTextWidth / textSize.x; + } + else + { + squashRatio = 1.0f; + } + + return textSize; +} + +static void DrawButton(ImVec2 min, ImVec2 max, const char *buttonText, bool sourceButton, bool buttonEnabled, bool &buttonPressed, float maxTextWidth = FLT_MAX) +{ + buttonPressed = false; + + auto &res = ImGui::GetIO().DisplaySize; + auto drawList = ImGui::GetForegroundDrawList(); + float alpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_INNER_TIME, CONTAINER_INNER_DURATION); + if (!buttonEnabled) + { + alpha *= 0.5f; + } + + int baser = 0; + int baseg = 0; + if (g_currentMessagePrompt.empty() && !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); + + ImFont *font = sourceButton ? g_newRodinFont : g_dfsogeistdFont; + float size = Scale(sourceButton ? 15.0f : 20.0f); + 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; + + if (!sourceButton) + { + // 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 }, + 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, + 2, + IM_COL32(baser, baseg, 0, 255 * alpha) + ); + + ResetGradient(); + SetScale({ 1.0f, 1.0f }); + SetOrigin({ 0.0f, 0.0f }); +} + +enum ButtonColumn +{ + ButtonColumnLeft, + ButtonColumnMiddle, + ButtonColumnRight +}; + +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; + ComputeButtonColumnCoordinates(buttonColumn, minX, maxX); + + 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); +} + +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); + g_currentMessageUpdateRemaining = true; + + 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); + g_currentMessageUpdateRemaining = true; + + 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)); + + constexpr size_t failedPathLimit = 5; + bool isFailedPathsOverLimit = false; + 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 if (failedPaths.size() < failedPathLimit) + { + failedPaths.push_back(path); + } + else + { + isFailedPathsOverLimit = true; + } + } + } + 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 if (failedPaths.size() < failedPathLimit) + { + failedPaths.push_back(path); + } + } + } + + if (!failedPaths.empty()) + { + std::stringstream stringStream; + stringStream << Localise("Installer_Message_InvalidFilesList") << std::endl; + for (const std::filesystem::path &path : failedPaths) + { + stringStream << std::endl << "- " << Truncate(path.filename().string(), 32, true, true); + } + + if (isFailedPathsOverLimit) + stringStream << std::endl << "- [...]"; + + g_currentMessagePrompt = stringStream.str(); + g_currentMessagePromptConfirmation = false; + } +} + +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; + std::list paths; + if (g_currentPage == WizardPage::SelectGameAndUpdate || g_currentPage == WizardPage::SelectDLC) + { + constexpr float ADD_BUTTON_MAX_TEXT_WIDTH = 160.0f; + const std::string &addFilesText = Localise("Installer_Button_AddFiles"); + 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 * 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 * squashRatio); + + const std::string &addFolderText = Localise("Installer_Button_AddFolder"); + 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 * squashRatio); + DrawButton(min, max, addFolderText.c_str(), false, true, buttonPressed, ADD_BUTTON_MAX_TEXT_WIDTH); + if (buttonPressed && ShowFoldersPicker(paths)) + { + ParseSourcePaths(paths); + } + } +} + +static void DrawSources() +{ + if (g_currentPage == WizardPage::SelectGameAndUpdate) + { + 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) + { + for (int i = 0; i < 6; i++) + { + DrawSourceButton((i < 3) ? ButtonColumnLeft : ButtonColumnRight, float(i % 3), DLC_SOURCE_TEXT[i], !g_dlcSourcePaths[i].empty() || g_dlcInstalled[i]); + } + } +} + +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, g_installPath, 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_currentPage = WizardPage::Installing; + 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_installPath); + 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 (nextButtonEnabled && g_currentPage == WizardPage::SelectGameAndUpdate) + { + nextButtonEnabled = !g_gameSourcePath.empty() && !g_updateSourcePath.empty(); + } + + 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(); }); + } + + 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 = 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 * 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, NEXT_BUTTON_MAX_TEXT_WIDTH); + + if (buttonPressed) + { + XexPatcher::Result patcherResult; + if (g_currentPage == WizardPage::SelectGameAndUpdate && (patcherResult = Installer::checkGameUpdateCompatibility(g_gameSourcePath, g_updateSourcePath), patcherResult != XexPatcher::Result::Success)) + { + g_currentMessagePrompt = Localise("Installer_Message_IncompatibleGameData"); + g_currentMessagePromptConfirmation = false; + } + else if (g_currentPage == WizardPage::SelectDLC) + { + // 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 = Localise("Installer_Message_InvalidFiles"); + 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 = Localise("Installer_Message_DLCWarning"); + 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 + { + g_currentPage = WizardPage::CheckSpace; + } + } + else if (g_currentPage == WizardPage::CheckSpace) + { + InstallerStart(); + } + else if (g_currentPage == WizardPage::InstallSucceeded) + { + g_isDisappearing = true; + g_disappearTime = ImGui::GetTime(); + } + else if (g_currentPage == WizardPage::InstallFailed) + { + g_currentPage = g_firstPage; + } + else + { + g_currentPage = WizardPage(int(g_currentPage) + 1); + } + } + } +} + +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, 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); + 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, 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)); + 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); +} + +static void DrawMessagePrompt() +{ + if (g_currentMessagePrompt.empty()) + { + 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) + { + 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) && (g_currentPage == WizardPage::SelectDLC)) + { + // 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(); + g_currentMessageResult = -1; + } +} + +void InstallerWizard::Init() +{ + auto &io = ImGui::GetIO(); + constexpr float FONT_SCALE = 2.0f; + g_seuratFont = ImFontAtlasSnapshot::GetFont("FOT-SeuratPro-M.otf", 24.0f * FONT_SCALE); + g_dfsogeistdFont = ImFontAtlasSnapshot::GetFont("DFSoGeiStd-W7.otf", 48.0f * FONT_SCALE); + g_newRodinFont = ImFontAtlasSnapshot::GetFont("FOT-NewRodinPro-DB.otf", 20.0f * FONT_SCALE); + g_installTextures[0] = LoadTexture(decompressZstd(g_install_001, g_install_001_uncompressed_size).get(), g_install_001_uncompressed_size); + g_installTextures[1] = LoadTexture(decompressZstd(g_install_002, g_install_002_uncompressed_size).get(), g_install_002_uncompressed_size); + g_installTextures[2] = LoadTexture(decompressZstd(g_install_003, g_install_003_uncompressed_size).get(), g_install_003_uncompressed_size); + g_installTextures[3] = LoadTexture(decompressZstd(g_install_004, g_install_004_uncompressed_size).get(), g_install_004_uncompressed_size); + g_installTextures[4] = LoadTexture(decompressZstd(g_install_005, g_install_005_uncompressed_size).get(), g_install_005_uncompressed_size); + g_installTextures[5] = LoadTexture(decompressZstd(g_install_006, g_install_006_uncompressed_size).get(), g_install_006_uncompressed_size); + g_installTextures[6] = LoadTexture(decompressZstd(g_install_007, g_install_007_uncompressed_size).get(), g_install_007_uncompressed_size); + g_installTextures[7] = LoadTexture(decompressZstd(g_install_008, g_install_008_uncompressed_size).get(), g_install_008_uncompressed_size); + g_milesElectricIcon = LoadTexture(decompressZstd(g_miles_electric_icon, g_miles_electric_icon_uncompressed_size).get(), g_miles_electric_icon_uncompressed_size); + g_arrowCircle = LoadTexture(decompressZstd(g_arrow_circle, g_arrow_circle_uncompressed_size).get(), g_arrow_circle_uncompressed_size); + g_pulseInstall = LoadTexture(decompressZstd(g_pulse_install, g_pulse_install_uncompressed_size).get(), g_pulse_install_uncompressed_size); +} + +void InstallerWizard::Draw() +{ + if (!s_isVisible) + { + return; + } + + DrawBackground(); + DrawLeftImage(); + DrawScanlineBars(); + DrawDescriptionContainer(); + DrawLanguagePicker(); + DrawSourcePickers(); + DrawSources(); + DrawInstallingProgress(); + DrawNextButton(); + DrawBorders(); + DrawMessagePrompt(); + + if (g_isDisappearing) + { + const double disappearDuration = ALL_ANIMATIONS_FULL_DURATION / 60.0; + if (ImGui::GetTime() > (g_disappearTime + disappearDuration)) + { + s_isVisible = false; + } + } +} + +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(); + g_pulseInstall.reset(); + + for (auto &texture : g_installTextures) + { + texture.reset(); + } +} + +bool InstallerWizard::Run(bool skipGame) +{ + NFD_Init(); + + if (skipGame) + { + 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); + s_isVisible = true; + + while (s_isVisible) + { + SDL_PumpEvents(); + SDL_FlushEvents(SDL_FIRSTEVENT, SDL_LASTEVENT); + Window::Update(); + Video::HostPresent(); + } + + Window::SetCursorAllowed(false); + NFD_Quit(); + + InstallerWizard::Shutdown(); + + 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/message_window.cpp b/UnleashedRecomp/ui/message_window.cpp index 6011df4a..b29fa638 100644 --- a/UnleashedRecomp/ui/message_window.cpp +++ b/UnleashedRecomp/ui/message_window.cpp @@ -2,7 +2,9 @@ #include "imgui_utils.h" #include #include +#include #include +#include #include #include #include @@ -30,6 +32,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; @@ -61,8 +64,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) @@ -78,44 +79,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); @@ -179,6 +143,7 @@ void MessageWindow::Init() g_fntSeurat = ImFontAtlasSnapshot::GetFont("FOT-SeuratPro-M.otf", 24.0f * FONT_SCALE); g_upSelectionCursor = LoadTexture(decompressZstd(g_select_fade, g_select_fade_uncompressed_size).get(), g_select_fade_uncompressed_size); + g_upWindow = LoadTexture(decompressZstd(g_general_window, g_general_window_uncompressed_size).get(), g_general_window_uncompressed_size); } void MessageWindow::Draw() @@ -186,7 +151,7 @@ void MessageWindow::Draw() if (!s_isVisible) return; - auto pInputState = SWA::CInputState::GetInstance(); + auto pInputState = g_isGameLoaded ? SWA::CInputState::GetInstance() : nullptr; auto drawList = ImGui::GetForegroundDrawList(); auto& res = ImGui::GetIO().DisplaySize; @@ -194,8 +159,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)) { @@ -215,12 +180,16 @@ 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))); 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 }; @@ -231,54 +200,74 @@ 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(); + + g_selectedRowIndex = -1; + + 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 (g_selectedRowIndex != -1 && 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; @@ -289,10 +278,18 @@ void MessageWindow::Draw() } else { - if (pInputState->GetPadState().IsTapped(SWA::eKeyState_A)) + if (isAccepted) + { + g_result = 0; + MessageWindow::Close(); + } } } + else if (g_isClosing) + { + s_isVisible = false; + } } bool MessageWindow::Open(std::string text, int* result, std::span buttons, int defaultButtonIndex) @@ -308,7 +305,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(); @@ -330,6 +327,7 @@ void MessageWindow::Close() g_controlsAppearTime = ImGui::GetTime(); g_isClosing = true; g_isControlsVisible = false; + g_foregroundCount = 0; g_isAwaitingResult = false; } diff --git a/UnleashedRecomp/ui/options_menu.cpp b/UnleashedRecomp/ui/options_menu.cpp index f705be5e..5e787bc3 100644 --- a/UnleashedRecomp/ui/options_menu.cpp +++ b/UnleashedRecomp/ui/options_menu.cpp @@ -989,12 +989,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 7b0772fd..f52f730f 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 8ad8279e..f7d01d52 100644 --- a/UnleashedRecomp/ui/window.h +++ b/UnleashedRecomp/ui/window.h @@ -27,6 +27,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) { @@ -99,7 +100,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 { @@ -110,6 +111,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/vcpkg.json b/vcpkg.json index 1f07072b..d778cb96 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -22,6 +22,7 @@ "features": [ "sdl2-binding" ] }, "magic-enum", + "nativefiledialog-extended", "miniaudio" ] }