mirror of
https://github.com/hedge-dev/UnleashedRecomp.git
synced 2025-10-30 07:11:05 +00:00
1580 lines
56 KiB
C++
1580 lines
56 KiB
C++
#include "installer_wizard.h"
|
|
|
|
#include <nfd.h>
|
|
|
|
#include <apu/embedded_player.h>
|
|
#include <install/installer.h>
|
|
#include <gpu/video.h>
|
|
#include <gpu/imgui/imgui_snapshot.h>
|
|
#include <hid/hid.h>
|
|
#include <hid/hid_detail.h>
|
|
#include <locale/locale.h>
|
|
#include <patches/aspect_ratio_patches.h>
|
|
#include <ui/imgui_utils.h>
|
|
#include <ui/button_guide.h>
|
|
#include <ui/message_window.h>
|
|
#include <ui/sdl_listener.h>
|
|
#include <ui/game_window.h>
|
|
#include <decompressor.h>
|
|
|
|
#include <res/images/common/hedge-dev.dds.h>
|
|
#include <res/images/installer/install_001.dds.h>
|
|
#include <res/images/installer/install_002.dds.h>
|
|
#include <res/images/installer/install_003.dds.h>
|
|
#include <res/images/installer/install_004.dds.h>
|
|
#include <res/images/installer/install_005.dds.h>
|
|
#include <res/images/installer/install_006.dds.h>
|
|
#include <res/images/installer/install_007.dds.h>
|
|
#include <res/images/installer/install_008.dds.h>
|
|
#include <res/images/installer/miles_electric_icon.dds.h>
|
|
#include <res/images/installer/arrow_circle.dds.h>
|
|
#include <res/images/installer/pulse_install.dds.h>
|
|
|
|
// 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<std::filesystem::path, int(DLC::Count)> g_dlcSourcePaths;
|
|
static std::array<bool, int(DLC::Count)> g_dlcInstalled = {};
|
|
static std::array<std::unique_ptr<GuestTexture>, 8> g_installTextures;
|
|
static std::unique_ptr<GuestTexture> g_milesElectricIcon;
|
|
static std::unique_ptr<GuestTexture> g_arrowCircle;
|
|
static std::unique_ptr<GuestTexture> g_pulseInstall;
|
|
static std::unique_ptr<GuestTexture> g_upHedgeDev;
|
|
static Journal g_installerJournal;
|
|
static Installer::Sources g_installerSources;
|
|
static uint64_t g_installerAvailableSize = 0;
|
|
static std::unique_ptr<std::thread> g_installerThread;
|
|
static double g_installerStartTime = 0.0;
|
|
static double g_installerEndTime = DBL_MAX;
|
|
static float g_installerProgressRatioCurrent = 0.0f;
|
|
static std::atomic<float> g_installerProgressRatioTarget = 0.0f;
|
|
static std::atomic<bool> 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<std::filesystem::path> g_currentPickerResults;
|
|
static std::atomic<bool> g_currentPickerResultsReady = false;
|
|
static std::string g_currentPickerErrorMessage;
|
|
static std::unique_ptr<std::thread> 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<std::pair<ImVec2, ImVec2>> 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(g_aspectRatioOffsetX + IMAGE_X), Scale(g_aspectRatioOffsetY + IMAGE_Y) };
|
|
ImVec2 max = { min.x + Scale(IMAGE_WIDTH), min.y + Scale(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 = g_aspectRatioOffsetX + 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(g_aspectRatioOffsetX + 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 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(g_aspectRatioOffsetX + CONTAINER_X), Scale(g_aspectRatioOffsetY + CONTAINER_Y) };
|
|
ImVec2 descriptionMax = { Scale(g_aspectRatioOffsetX + CONTAINER_X + CONTAINER_WIDTH), Scale(g_aspectRatioOffsetY + CONTAINER_Y + CONTAINER_HEIGHT) };
|
|
SetProceduralOrigin(descriptionMin);
|
|
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);
|
|
|
|
auto colWhite = IM_COL32(255, 255, 255, 255 * textAlpha);
|
|
|
|
ImVec2 imageMin =
|
|
{
|
|
/* X */ Scale(g_aspectRatioOffsetX + CONTAINER_X) + (Scale(CONTAINER_WIDTH) / 2) - (imageScale / 2) - (hedgeDevTextSize.x / 2) - hedgeDevTextMarginX,
|
|
/* Y */ Scale(g_aspectRatioOffsetY + CONTAINER_Y) + (Scale(CONTAINER_HEIGHT) / 2) - (imageScale / 2) + imageMarginY
|
|
};
|
|
|
|
ImVec2 imageMax = { imageMin.x + imageScale, imageMin.y + imageScale };
|
|
|
|
drawList->AddImage(g_upHedgeDev.get(), imageMin, imageMax, { 0, 0 }, { 1, 1 }, colWhite);
|
|
|
|
drawList->AddText
|
|
(
|
|
g_seuratFont,
|
|
fontSize,
|
|
{ /* X */ imageMax.x + hedgeDevTextMarginX, /* Y */ imageMin.y + (imageScale / 2) - (hedgeDevTextSize.y / 2) },
|
|
colWhite,
|
|
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(g_aspectRatioOffsetY + CONTAINER_Y) + Scale(CONTAINER_HEIGHT) - marqueeTextSize.y - marqueeTextMarginY };
|
|
ImVec2 textMin = { Scale(g_aspectRatioOffsetX + CONTAINER_X), textPos.y };
|
|
ImVec2 textMax = { Scale(g_aspectRatioOffsetX + CONTAINER_X) + Scale(CONTAINER_WIDTH), Scale(g_aspectRatioOffsetY + CONTAINER_Y) + Scale(CONTAINER_HEIGHT) };
|
|
|
|
SetMarqueeFade(textMin, textMax, Scale(32));
|
|
DrawTextWithMarquee(g_seuratFont, fontSize, textPos, textMin, textMax, colWhite, CREDITS_TEXT, g_appearTime, 0.9, Scale(250));
|
|
ResetMarqueeFade();
|
|
}
|
|
|
|
ImVec2 sideMin = { descriptionMax.x, descriptionMin.y };
|
|
ImVec2 sideMax = { res.x, 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();
|
|
}
|
|
|
|
ResetProceduralOrigin();
|
|
}
|
|
|
|
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(g_aspectRatioOffsetX + CONTAINER_X + CONTAINER_BUTTON_GAP);
|
|
maxX = Scale(g_aspectRatioOffsetX + CONTAINER_X + CONTAINER_BUTTON_GAP + CONTAINER_BUTTON_WIDTH);
|
|
break;
|
|
case ButtonColumnMiddle:
|
|
minX = Scale(g_aspectRatioOffsetX + CONTAINER_X + CONTAINER_WIDTH / 2.0f - CONTAINER_BUTTON_WIDTH / 2.0f);
|
|
maxX = Scale(g_aspectRatioOffsetX + CONTAINER_X + CONTAINER_WIDTH / 2.0f + CONTAINER_BUTTON_WIDTH / 2.0f);
|
|
break;
|
|
case ButtonColumnRight:
|
|
minX = Scale(g_aspectRatioOffsetX + CONTAINER_X + CONTAINER_WIDTH - CONTAINER_BUTTON_GAP - CONTAINER_BUTTON_WIDTH);
|
|
maxX = Scale(g_aspectRatioOffsetX + 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(g_aspectRatioOffsetY + CONTAINER_Y + CONTAINER_HEIGHT - CONTAINER_BUTTON_GAP - BUTTON_HEIGHT - minusY) };
|
|
ImVec2 max = { maxX, Scale(g_aspectRatioOffsetY + 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(g_aspectRatioOffsetX + CONTAINER_X) + BOTTOM_X_GAP, Scale(g_aspectRatioOffsetY + CONTAINER_Y + CONTAINER_HEIGHT + BOTTOM_Y_GAP) };
|
|
ImVec2 max = { Scale(g_aspectRatioOffsetX + CONTAINER_X + CONTAINER_WIDTH - BOTTOM_X_GAP), Scale(g_aspectRatioOffsetY + 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<std::filesystem::path> &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<std::thread>(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<std::filesystem::path> &paths)
|
|
{
|
|
assert((g_currentPage == WizardPage::SelectGameAndUpdate) || (g_currentPage == WizardPage::SelectDLC));
|
|
|
|
constexpr size_t failedPathLimit = 5;
|
|
bool isFailedPathsOverLimit = false;
|
|
std::list<std::filesystem::path> 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(g_aspectRatioOffsetY + CONTAINER_Y + CONTAINER_HEIGHT - CONTAINER_BUTTON_GAP - BUTTON_HEIGHT - minusY) };
|
|
ImVec2 max = { maxX, Scale(g_aspectRatioOffsetY + 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<std::filesystem::path> 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(g_aspectRatioOffsetX + CONTAINER_X + BOTTOM_X_GAP), Scale(g_aspectRatioOffsetY + CONTAINER_Y + CONTAINER_HEIGHT + BOTTOM_Y_GAP) };
|
|
ImVec2 max = { Scale(g_aspectRatioOffsetX + CONTAINER_X + BOTTOM_X_GAP + textSize.x * squashRatio), Scale(g_aspectRatioOffsetY + 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<std::thread>(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(g_aspectRatioOffsetX + CONTAINER_X + CONTAINER_WIDTH - textSize.x * squashRatio - BOTTOM_X_GAP), Scale(g_aspectRatioOffsetY + CONTAINER_Y + CONTAINER_HEIGHT + BOTTOM_Y_GAP) };
|
|
ImVec2 max = { Scale(g_aspectRatioOffsetX + CONTAINER_X + CONTAINER_WIDTH - BOTTOM_X_GAP), Scale(g_aspectRatioOffsetY + 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(g_aspectRatioOffsetX + CONTAINER_X + CONTAINER_WIDTH / 5);
|
|
float minX = std::lerp(Scale(g_aspectRatioOffsetX + CONTAINER_X - BORDER_SIZE - BORDER_OVERSHOOT), midX, borderScale);
|
|
float maxX = std::lerp(Scale(g_aspectRatioOffsetX + CONTAINER_X + CONTAINER_WIDTH + SIDE_CONTAINER_WIDTH + BORDER_OVERSHOOT), midX, borderScale);
|
|
float minY = bottomBorder ? Scale(g_aspectRatioOffsetY + CONTAINER_Y + CONTAINER_HEIGHT) : Scale(g_aspectRatioOffsetY + 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(g_aspectRatioOffsetX + CONTAINER_X + CONTAINER_WIDTH) : Scale(g_aspectRatioOffsetX + CONTAINER_X - BORDER_SIZE);
|
|
float maxX = minX + Scale(BORDER_SIZE);
|
|
float midY = Scale(g_aspectRatioOffsetY + CONTAINER_Y + CONTAINER_HEIGHT / 2);
|
|
float minY = std::lerp(Scale(g_aspectRatioOffsetY + CONTAINER_Y - BORDER_OVERSHOOT), midY, borderScale);
|
|
float maxY = std::lerp(Scale(g_aspectRatioOffsetY + 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<std::string, 2> 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;
|
|
}
|