#include "installer_wizard.h" #include #include #include #include #include #include #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; static constexpr double INSTALL_ICONS_FADE_IN_ANIMATION_TIME = 0.0; static constexpr double INSTALL_ICONS_FADE_IN_ANIMATION_DURATION = 15.0; // 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 std::unique_ptr g_upHedgeDev; 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 double g_installerEndTime = DBL_MAX; 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 std::list g_currentPickerResults; static std::atomic g_currentPickerResultsReady = false; static std::string g_currentPickerErrorMessage; static std::unique_ptr g_currentPickerThread; static bool g_pickerTutorialCleared[2] = {}; static bool g_pickerTutorialTriggered = false; static bool g_pickerTutorialFolderMode = false; static bool g_currentPickerVisible = false; static bool g_currentPickerFolderMode = false; static int g_currentMessageResult = -1; static ImVec2 g_joypadAxis = {}; static int g_currentCursorIndex = -1; static int g_currentCursorDefault = 0; static bool g_currentCursorAccepted = false; static std::vector> g_currentCursorRects; class SDLEventListenerForInstaller : public SDLEventListener { public: void OnSDLEvent(SDL_Event *event) override { constexpr float AxisValueRange = 32767.0f; constexpr float AxisTapRange = 0.5f; if (!InstallerWizard::s_isVisible || !g_currentMessagePrompt.empty() || g_currentPickerVisible) { return; } int newCursorIndex = -1; ImVec2 tapDirection = {}; switch (event->type) { case SDL_KEYDOWN: switch (event->key.keysym.scancode) { case SDL_SCANCODE_LEFT: case SDL_SCANCODE_RIGHT: tapDirection.x = (event->key.keysym.scancode == SDL_SCANCODE_RIGHT) ? 1.0f : -1.0f; break; case SDL_SCANCODE_UP: case SDL_SCANCODE_DOWN: tapDirection.y = (event->key.keysym.scancode == SDL_SCANCODE_DOWN) ? 1.0f : -1.0f; break; case SDL_SCANCODE_RETURN: case SDL_SCANCODE_KP_ENTER: g_currentCursorAccepted = (g_currentCursorIndex >= 0); break; } break; case SDL_CONTROLLERBUTTONDOWN: switch (event->cbutton.button) { case SDL_CONTROLLER_BUTTON_DPAD_LEFT: tapDirection = { -1.0f, 0.0f }; break; case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: tapDirection = { 1.0f, 0.0f }; break; case SDL_CONTROLLER_BUTTON_DPAD_UP: tapDirection = { 0.0f, -1.0f }; break; case SDL_CONTROLLER_BUTTON_DPAD_DOWN: tapDirection = { 0.0f, 1.0f }; break; case SDL_CONTROLLER_BUTTON_A: g_currentCursorAccepted = (g_currentCursorIndex >= 0); break; } break; case SDL_CONTROLLERAXISMOTION: { if (event->caxis.axis < 2) { float newAxisValue = event->caxis.value / AxisValueRange; bool sameDirection = (newAxisValue * g_joypadAxis[event->caxis.axis]) > 0.0f; bool wasInRange = abs(g_joypadAxis[event->caxis.axis]) > AxisTapRange; bool isInRange = abs(newAxisValue) > AxisTapRange; if (sameDirection && !wasInRange && isInRange) { tapDirection[event->caxis.axis] = newAxisValue; } g_joypadAxis[event->caxis.axis] = newAxisValue; } break; } case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEMOTION: { for (size_t i = 0; i < g_currentCursorRects.size(); i++) { auto ¤tRect = g_currentCursorRects[i]; if (ImGui::IsMouseHoveringRect(currentRect.first, currentRect.second, false)) { newCursorIndex = int(i); if (event->type == SDL_MOUSEBUTTONDOWN && event->button.button == SDL_BUTTON_LEFT) { g_currentCursorAccepted = true; } break; } } if (newCursorIndex < 0) { g_currentCursorIndex = -1; } break; } } if (tapDirection.x != 0.0f || tapDirection.y != 0.0f) { if (g_currentCursorIndex >= g_currentCursorRects.size() || g_currentCursorIndex < 0) { newCursorIndex = g_currentCursorDefault; } else { auto ¤tRect = g_currentCursorRects[g_currentCursorIndex]; ImVec2 currentPoint = ImVec2 ( (currentRect.first.x + currentRect.second.x) / 2.0f + tapDirection.x * (currentRect.second.x - currentRect.first.x) / 2.0f, (currentRect.first.y + currentRect.second.y) / 2.0f + tapDirection.y * (currentRect.second.y - currentRect.first.y) / 2.0f ); float closestDistance = FLT_MAX; for (size_t i = 0; i < g_currentCursorRects.size(); i++) { if (g_currentCursorIndex == i) { continue; } auto &targetRect = g_currentCursorRects[i]; ImVec2 targetPoint = ImVec2 ( (targetRect.first.x + targetRect.second.x) / 2.0f + tapDirection.x * (targetRect.first.x - targetRect.second.x) / 2.0f, (targetRect.first.y + targetRect.second.y) / 2.0f + tapDirection.y * (targetRect.first.y - targetRect.second.y) / 2.0f ); ImVec2 delta = ImVec2(targetPoint.x - currentPoint.x, targetPoint.y - currentPoint.y); float projectedDistance = delta.x * tapDirection.x + delta.y * tapDirection.y; float manhattanDistance = abs(delta.x) + abs(delta.y); if (projectedDistance > 0.0f && manhattanDistance < closestDistance) { newCursorIndex = int(i); closestDistance = manhattanDistance; } } } } if (newCursorIndex >= 0) { if (g_currentCursorIndex != newCursorIndex) { Game_PlaySound("sys_worldmap_cursor"); } g_currentCursorIndex = newCursorIndex; } } }; static SDLEventListenerForInstaller g_eventListener; const char CREDITS_TEXT[] = "Skyth, Hyper, Darío, Sajid, RadiantDerg, PTKay, DeaThProj, NextinHKRY, M&M, LadyLunanova"; 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 bool PushCursorRect(ImVec2 min, ImVec2 max, bool &cursorPressed, bool makeDefault = false) { int currentIndex = int(g_currentCursorRects.size()); g_currentCursorRects.emplace_back(min, max); if (makeDefault) { g_currentCursorDefault = currentIndex; } if (g_currentCursorIndex == currentIndex) { if (g_currentCursorAccepted) { Game_PlaySound("sys_worldmap_finaldecide"); cursorPressed = true; g_currentCursorAccepted = false; } return true; } else { return false; } } static void ResetCursorRects() { g_currentCursorDefault = 0; g_currentCursorRects.clear(); } 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)); } 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 arrowCircleFadeMotion = ComputeMotionInstaller(g_installerStartTime, g_installerEndTime, INSTALL_ICONS_FADE_IN_ANIMATION_TIME, INSTALL_ICONS_FADE_IN_ANIMATION_DURATION); 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 * arrowCircleFadeMotion)); // Pulse float pulseFadeMotion = ComputeMotionInstaller(g_installerStartTime, g_installerEndTime, INSTALL_ICONS_FADE_IN_ANIMATION_TIME, INSTALL_ICONS_FADE_IN_ANIMATION_DURATION); 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 * pulseFadeMotion)); } 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 (int(g_currentPage) >= int(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"); auto alphaMotion = 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, 255 * alphaMotion), headerText.c_str(), 4, IM_COL32(0, 0, 0, 255 * alphaMotion), IMGUI_SHADER_MODIFIER_TITLE_BEVEL); // 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(); DrawVersionString(g_newRodinFont, IM_COL32(255, 255, 255, 70 * alphaMotion)); } 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(); auto fontSize = Scale(26.0f); 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]; char requiredSpaceText[128]; char availableSpaceText[128]; 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; snprintf(requiredSpaceText, sizeof(requiredSpaceText), Localise("Installer_Step_RequiredSpace").c_str(), requiredGiB); snprintf(availableSpaceText, sizeof(availableSpaceText), (g_installerAvailableSize > 0) ? Localise("Installer_Step_AvailableSpace").c_str() : "", availableGiB); snprintf(descriptionText, sizeof(descriptionText), "%s%s\n%s", GetWizardText(g_currentPage).c_str(), requiredSpaceText, availableSpaceText); } 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(); drawList->AddText ( g_seuratFont, fontSize, { clipRectMin.x, clipRectMin.y }, IM_COL32(255, 255, 255, 255 * textAlpha), descriptionText, 0, clipRectMax.x - clipRectMin.x ); drawList->PopClipRect(); if (g_currentPage == WizardPage::InstallSucceeded) { auto hedgeDevStr = "hedge-dev"; auto hedgeDevTextSize = g_seuratFont->CalcTextSizeA(fontSize, FLT_MAX, 0, hedgeDevStr); auto hedgeDevTextMarginX = Scale(15); auto imageScale = hedgeDevTextSize.x / 3; auto imageMarginY = Scale(15); ImVec2 imageMin = { /* X */ Scale(CONTAINER_X) + (Scale(CONTAINER_WIDTH) / 2) - (imageScale / 2) - (hedgeDevTextSize.x / 2) - hedgeDevTextMarginX, /* Y */ Scale(CONTAINER_Y) + (Scale(CONTAINER_HEIGHT) / 2) - (imageScale / 2) + imageMarginY }; ImVec2 imageMax = { imageMin.x + imageScale, imageMin.y + imageScale }; drawList->AddImage(g_upHedgeDev.get(), imageMin, imageMax); drawList->AddText ( g_seuratFont, fontSize, { /* X */ imageMax.x + hedgeDevTextMarginX, /* Y */ imageMin.y + (imageScale / 2) - (hedgeDevTextSize.y / 2) }, IM_COL32_WHITE, hedgeDevStr ); auto marqueeTextSize = g_seuratFont->CalcTextSizeA(fontSize, FLT_MAX, 0, CREDITS_TEXT); auto marqueeTextMarginX = Scale(5); auto marqueeTextMarginY = Scale(15); ImVec2 textPos = { descriptionMax.x, Scale(CONTAINER_Y) + Scale(CONTAINER_HEIGHT) - marqueeTextSize.y - marqueeTextMarginY }; ImVec2 textMin = { Scale(CONTAINER_X), textPos.y }; ImVec2 textMax = { Scale(CONTAINER_X) + Scale(CONTAINER_WIDTH), Scale(CONTAINER_Y) + Scale(CONTAINER_HEIGHT) }; SetMarqueeFade(textMin, textMax, Scale(32)); DrawTextWithMarquee(g_seuratFont, fontSize, textPos, textMin, textMax, IM_COL32_WHITE, CREDITS_TEXT, g_appearTime, 0.9, Scale(250)); ResetMarqueeFade(); } 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(); if (g_currentPage != WizardPage::Installing && textAlpha >= 1.0) { auto icon = hid::detail::IsInputDeviceController() ? EButtonIcon::A : hid::detail::g_inputDevice == hid::detail::EInputDevice::Keyboard ? EButtonIcon::Enter : EButtonIcon::LMB; ButtonGuide::Open(Button(Localise("Common_Select"), icon)); } else { ButtonGuide::Close(); } } 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, bool makeDefault = false) { 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() && !g_currentPickerVisible && !sourceButton && buttonEnabled && (alpha >= 1.0f)) { bool cursorOnButton = PushCursorRect(min, max, buttonPressed, makeDefault); if (cursorOnButton) { baser = 48; baseg = 32; } } 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, 4, 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++) { nfdnchar_t *pathSetPath = nullptr; if (NFD_PathSet_GetPathN(pathSet, i, &pathSetPath) != NFD_OKAY) { filePaths.clear(); return false; } filePaths.emplace_back(std::filesystem::path(pathSetPath)); NFD_PathSet_FreePathN(pathSetPath); } return true; } static void PickerThreadProcess() { const nfdpathset_t *pathSet; nfdresult_t result = NFD_ERROR; if (g_currentPickerFolderMode) { result = NFD_PickFolderMultipleN(&pathSet, nullptr); } else { result = NFD_OpenDialogMultipleN(&pathSet, nullptr, 0, nullptr); } if (result == NFD_OKAY) { bool pathsConverted = ConvertPathSet(pathSet, g_currentPickerResults); NFD_PathSet_Free(pathSet); } else if (result == NFD_ERROR) { g_currentPickerErrorMessage = NFD_GetError(); } g_currentPickerResultsReady = true; } static void PickerStart(bool folderMode) { if (g_currentPickerThread != nullptr) { g_currentPickerThread->join(); g_currentPickerThread.reset(); } g_currentPickerResults.clear(); g_currentPickerFolderMode = folderMode; g_currentPickerResultsReady = false; g_currentPickerVisible = true; // Optional single thread mode for testing on systems that do not interact well with the separate thread being used for NFD. constexpr bool singleThreadMode = false; if (singleThreadMode) PickerThreadProcess(); else g_currentPickerThread = std::make_unique(PickerThreadProcess); } static void PickerShow(bool folderMode) { if (g_pickerTutorialCleared[folderMode]) { PickerStart(folderMode); } else { g_currentMessagePrompt = Localise(folderMode ? "Installer_Message_FolderPickerTutorial" : "Installer_Message_FilePickerTutorial"); g_currentMessagePromptConfirmation = false; g_pickerTutorialTriggered = true; g_pickerTutorialFolderMode = folderMode; } } static bool 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) { std::u8string filenameU8 = path.filename().u8string(); stringStream << std::endl << "- " << Truncate(std::string(filenameU8.begin(), filenameU8.end()), 32, true, true); } if (isFailedPathsOverLimit) stringStream << std::endl << "- [...]"; g_currentMessagePrompt = stringStream.str(); g_currentMessagePromptConfirmation = false; } return failedPaths.empty(); } 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, FLT_MAX, LANGUAGE_ENUM[i] == ELanguage::English); 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) { PickerShow(false); } 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) { PickerShow(true); } } } 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_installerEndTime = ImGui::GetTime(); 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); } // Rest for a bit after finishing the installation, the device is tired std::this_thread::sleep_for(std::chrono::seconds(1)); g_installerFinished = true; } static void InstallerStart() { g_currentPage = WizardPage::Installing; g_installerStartTime = ImGui::GetTime(); g_installerEndTime = DBL_MAX; g_installerProgressRatioCurrent = 0.0f; g_installerProgressRatioTarget = 0.0f; g_installerFailed = false; g_installerFinished = false; g_installerThread = std::make_unique(InstallerThread); } static bool InstallerParseSources(std::string &errorMessage) { std::error_code spaceErrorCode; std::filesystem::space_info spaceInfo = std::filesystem::space(g_installPath, spaceErrorCode); if (!spaceErrorCode) { 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); } } bool sourcesParsed = Installer::parseSources(installerInput, g_installerJournal, g_installerSources); errorMessage = g_installerJournal.lastErrorMessage; return sourcesParsed; } 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(); std::string sourcesErrorMessage; if (!InstallerParseSources(sourcesErrorMessage)) { // Some of the sources that were provided to the installer are not valid. Restart the file selection process. std::stringstream stringStream; stringStream << Localise("Installer_Message_InvalidFiles"); if (!sourcesErrorMessage.empty()) { stringStream << std::endl << std::endl << sourcesErrorMessage; } g_currentMessagePrompt = stringStream.str(); 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; } bool messageWindowReturned = false; if (g_currentMessagePromptConfirmation) { std::array YesNoButtons = { Localise("Common_Yes"), Localise("Common_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; } } static void PickerCheckTutorial() { if (!g_pickerTutorialTriggered || !g_currentMessagePrompt.empty()) { return; } PickerStart(g_pickerTutorialFolderMode); g_pickerTutorialTriggered = false; } static void PickerCheckResults() { if (!g_currentPickerResultsReady) { return; } if (!g_currentPickerErrorMessage.empty()) { g_currentMessagePrompt = g_currentPickerErrorMessage; g_currentMessagePromptConfirmation = false; g_currentPickerErrorMessage.clear(); } if (!g_currentPickerResults.empty() && ParseSourcePaths(g_currentPickerResults)) { g_pickerTutorialCleared[g_pickerTutorialFolderMode] = true; } g_currentPickerResultsReady = false; g_currentPickerVisible = false; } void InstallerWizard::Init() { auto &io = ImGui::GetIO(); g_seuratFont = ImFontAtlasSnapshot::GetFont("FOT-SeuratPro-M.otf"); g_dfsogeistdFont = ImFontAtlasSnapshot::GetFont("DFSoGeiStd-W7.otf"); g_newRodinFont = ImFontAtlasSnapshot::GetFont("FOT-NewRodinPro-DB.otf"); g_installTextures[0] = LOAD_ZSTD_TEXTURE(g_install_001); g_installTextures[1] = LOAD_ZSTD_TEXTURE(g_install_002); g_installTextures[2] = LOAD_ZSTD_TEXTURE(g_install_003); g_installTextures[3] = LOAD_ZSTD_TEXTURE(g_install_004); g_installTextures[4] = LOAD_ZSTD_TEXTURE(g_install_005); g_installTextures[5] = LOAD_ZSTD_TEXTURE(g_install_006); g_installTextures[6] = LOAD_ZSTD_TEXTURE(g_install_007); g_installTextures[7] = LOAD_ZSTD_TEXTURE(g_install_008); g_milesElectricIcon = LOAD_ZSTD_TEXTURE(g_miles_electric_icon); g_arrowCircle = LOAD_ZSTD_TEXTURE(g_arrow_circle); g_pulseInstall = LOAD_ZSTD_TEXTURE(g_pulse_install); g_upHedgeDev = LOAD_ZSTD_TEXTURE(g_hedgedev); } void InstallerWizard::Draw() { if (!s_isVisible) { return; } ResetCursorRects(); DrawBackground(); DrawLeftImage(); DrawScanlineBars(); DrawDescriptionContainer(); DrawLanguagePicker(); DrawSourcePickers(); DrawSources(); DrawInstallingProgress(); DrawNextButton(); DrawBorders(); DrawMessagePrompt(); PickerCheckTutorial(); PickerCheckResults(); 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 threads. if (g_installerThread != nullptr) { g_installerThread->join(); g_installerThread.reset(); } if (g_currentPickerThread != nullptr) { g_currentPickerThread->join(); g_currentPickerThread.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(std::filesystem::path installPath, bool skipGame) { g_installPath = installPath; EmbeddedPlayer::Init(); NFD_Init(); // Guarantee one controller is initialized. We'll rely on SDL's event loop to get the controller events. XAMINPUT_STATE inputState; hid::GetState(0, &inputState); 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; } GameWindow::SetFullscreenCursorVisibility(true); s_isVisible = true; while (s_isVisible) { Video::WaitOnSwapChain(); SDL_PumpEvents(); SDL_FlushEvents(SDL_FIRSTEVENT, SDL_LASTEVENT); GameWindow::Update(); Video::Present(); } GameWindow::SetFullscreenCursorVisibility(false); NFD_Quit(); InstallerWizard::Shutdown(); EmbeddedPlayer::Shutdown(); return true; }