Files
UnleashedRecomp-hedge-dev/UnleashedRecomp/ui/installer_wizard.cpp
Hyper df03a64305 Redirected Install button to custom implementation
This currently fades out and closes the game upon accepting the message, it does not yet reboot into the installer menu using --install-dlc.
2024-12-07 01:02:59 +00:00

1454 lines
52 KiB
C++

#include "installer_wizard.h"
#include <nfd.h>
#include <install/installer.h>
#include <gpu/video.h>
#include <gpu/imgui_snapshot.h>
#include <hid/hid.h>
#include <hid/hid_detail.h>
#include <locale/locale.h>
#include <ui/imgui_utils.h>
#include <ui/button_guide.h>
#include <ui/message_window.h>
#include <ui/sdl_listener.h>
#include <ui/window.h>
#include <decompressor.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 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 int g_currentMessageResult = -1;
static bool g_currentMessageUpdateRemaining = false;
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())
{
return;
}
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 = true;
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 = true;
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:
{
g_currentCursorIndex = -1;
for (size_t i = 0; i < g_currentCursorRects.size(); i++)
{
auto &currentRect = g_currentCursorRects[i];
if (ImGui::IsMouseHoveringRect(currentRect.first, currentRect.second, false))
{
g_currentCursorIndex = int(i);
if (event->type == SDL_MOUSEBUTTONDOWN && event->button.button == SDL_BUTTON_LEFT)
{
g_currentCursorAccepted = true;
}
break;
}
}
break;
}
}
if (tapDirection.x != 0.0f || tapDirection.y != 0.0f)
{
int newCursorIndex = -1;
if (g_currentCursorIndex >= g_currentCursorRects.size() || g_currentCursorIndex < 0)
{
newCursorIndex = g_currentCursorDefault;
}
else
{
auto &currentRect = 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)
{
// TODO: Play sound.
g_currentCursorIndex = newCursorIndex;
}
}
}
};
static SDLEventListenerForInstaller g_eventListener;
const char CREDITS_TEXT[] = "- Sajid (RIP)\n- imgui sega balls!";
static std::string& GetWizardText(WizardPage page)
{
switch (page)
{
case WizardPage::SelectLanguage: return Localise("Installer_Page_SelectLanguage");
case WizardPage::Introduction: return Localise("Installer_Page_Introduction");
case WizardPage::SelectGameAndUpdate: return Localise("Installer_Page_SelectGameAndUpdate");
case WizardPage::SelectDLC: return Localise("Installer_Page_SelectDLC");
case WizardPage::CheckSpace: return Localise("Installer_Page_CheckSpace");
case WizardPage::Installing: return Localise("Installer_Page_Installing");
case WizardPage::InstallSucceeded: return Localise("Installer_Page_InstallSucceeded");
case WizardPage::InstallFailed: return Localise("Installer_Page_InstallFailed");
}
return g_localeMissing;
}
static const int WIZARD_INSTALL_TEXTURE_INDEX[] =
{
0,
0,
1,
2,
3,
4,
7, // Force Werehog on InstallSucceeded.
5 // Force Eggman on InstallFailed.
};
// These are ordered from bottom to top in a 3x2 grid.
const char *LANGUAGE_TEXT[] =
{
"FRANÇAIS", // French
"DEUTSCH", // German
"ENGLISH", // English
"ESPAÑOL", // Spanish
"ITALIANO", // Italian
"日本語", // Japanese
};
const ELanguage LANGUAGE_ENUM[] =
{
ELanguage::French,
ELanguage::German,
ELanguage::English,
ELanguage::Spanish,
ELanguage::Italian,
ELanguage::Japanese,
};
const char *DLC_SOURCE_TEXT[] =
{
"SPAGONIA",
"CHUN-NAN",
"MAZURI",
"HOLOSKA",
"APOTOS & SHAMAR",
"EMPIRE CITY & ADABAT",
};
static int DLCIndex(DLC dlc)
{
assert(dlc != DLC::Unknown);
return (int)(dlc) - 1;
}
static double ComputeMotionInstaller(double timeAppear, double timeDisappear, double offset, double total)
{
return ComputeMotion(timeAppear, offset, total) * (1.0 - ComputeMotion(timeDisappear, ALL_ANIMATIONS_FULL_DURATION - offset - total, total));
}
static double ComputeMotionInstallerLoop(double timeAppear, double speed, double offset)
{
return std::clamp(fmodf((ImGui::GetTime() - timeAppear) * speed, 1.0f + offset) - offset, 0.0, 1.0) / 1.0;
}
static double ComputeHermiteMotionInstallerLoop(double timeAppear, double speed, double offset)
{
return (cosf(M_PI * ComputeMotionInstallerLoop(timeAppear, speed, offset) + M_PI) + 1) / 2;
}
static 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)
{
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));
min.y = (min.y + max.y) / 2.0f;
drawList->AddRectFilledMultiColor(min, max, IM_COL32_BLACK_TRANS, IM_COL32_BLACK_TRANS, IM_COL32(0, 0, 0, 255), IM_COL32(0, 0, 0, 255));
}
static void DrawHeaderIconsForInstallPhase(double iconsPosX, double iconsPosY, double iconsScale)
{
auto drawList = ImGui::GetForegroundDrawList();
// Arrow Circle Icon
ImVec2 arrowCircleMin = { Scale(iconsPosX - iconsScale / 2), Scale(iconsPosY - iconsScale / 2) };
ImVec2 arrowCircleMax = { Scale(iconsPosX + iconsScale / 2), Scale(iconsPosY + iconsScale / 2) };
ImVec2 center = { Scale(iconsPosX) + 0.5f, Scale(iconsPosY) - 0.5f };
float 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");
int textAlpha = std::lround(255.0f * ComputeMotionInstaller(g_appearTime, g_disappearTime, TITLE_ANIMATION_TIME, TITLE_ANIMATION_DURATION));
DrawTextWithOutline<int>(g_dfsogeistdFont, Scale(42.0f), { Scale(285.0f), Scale(57.0f) }, IM_COL32(255, 195, 0, textAlpha), headerText.c_str(), 4, IM_COL32(0, 0, 0, textAlpha));
// Top bar line
drawList->AddLine
(
{ 0.0f, height },
{ res.x, height },
OUTLINE_COLOR,
Scale(1)
);
// Bottom bar line
drawList->AddLine
(
{ 0.0f, res.y - height },
{ res.x, res.y - height },
OUTLINE_COLOR,
Scale(1)
);
DrawHeaderIcons();
}
static float AlignToNextGrid(float value)
{
return floor(value / GRID_SIZE) * GRID_SIZE;
}
static void DrawContainer(ImVec2 min, ImVec2 max, bool isTextArea)
{
auto &res = ImGui::GetIO().DisplaySize;
auto drawList = ImGui::GetForegroundDrawList();
double gridAlpha = ComputeMotionInstaller(g_appearTime, g_disappearTime,
isTextArea ? CONTAINER_INNER_TIME : CONTAINER_OUTER_TIME,
isTextArea ? CONTAINER_INNER_DURATION : CONTAINER_OUTER_DURATION
);
double gridOverlayAlpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_INNER_TIME, CONTAINER_INNER_DURATION);
const uint32_t gridColor = IM_COL32(0, 33, 0, (isTextArea ? 128 : 255) * gridAlpha);
const uint32_t gridOverlayColor = IM_COL32(0, 32, 0, 128 * gridOverlayAlpha);
float gridSize = Scale(GRID_SIZE);
SetShaderModifier(IMGUI_SHADER_MODIFIER_CHECKERBOARD);
drawList->AddRectFilled(min, max, gridColor);
SetShaderModifier(IMGUI_SHADER_MODIFIER_NONE);
if (isTextArea)
{
drawList->AddRectFilled(min, max, gridOverlayColor);
}
// The draw area
drawList->PushClipRect({ min.x + gridSize * 2.0f, min.y + gridSize * 2.0f }, { max.x - gridSize * 2.0f + 1.0f, max.y - gridSize * 2.0f + 1.0f });
}
static void DrawDescriptionContainer()
{
auto &res = ImGui::GetIO().DisplaySize;
auto drawList = ImGui::GetForegroundDrawList();
ImVec2 descriptionMin = { Scale(AlignToNextGrid(CONTAINER_X)), Scale(AlignToNextGrid(CONTAINER_Y)) };
ImVec2 descriptionMax = { Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH)), Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT)) };
DrawContainer(descriptionMin, descriptionMax, true);
char descriptionText[512];
strncpy(descriptionText, GetWizardText(g_currentPage).c_str(), sizeof(descriptionText) - 1);
if (g_currentPage == WizardPage::CheckSpace)
{
constexpr double DivisorGiB = (1024.0 * 1024.0 * 1024.0);
double requiredGiB = double(g_installerSources.totalSize) / DivisorGiB;
double availableGiB = double(g_installerAvailableSize) / DivisorGiB;
// TODO: the format for where the numbers are (%2.2f GiB) should be moved to the localised string.
snprintf
(
descriptionText,
sizeof(descriptionText),
"%s%s %2.2f GiB\n%s %2.2f GiB",
GetWizardText(g_currentPage).c_str(),
Localise("Installer_Step_RequiredSpace").c_str(), requiredGiB,
Localise("Installer_Step_AvailableSpace").c_str(), availableGiB
);
}
else if (g_currentPage == WizardPage::InstallSucceeded)
{
strncat(descriptionText, CREDITS_TEXT, sizeof(descriptionText) - 1);
}
else if (g_currentPage == WizardPage::InstallFailed)
{
strncat(descriptionText, g_installerErrorMessage.c_str(), sizeof(descriptionText) - 1);
}
double textAlpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_INNER_TIME, CONTAINER_INNER_DURATION);
auto clipRectMin = drawList->GetClipRectMin();
auto clipRectMax = drawList->GetClipRectMax();
auto size = Scale(26.0f);
drawList->AddText
(
g_seuratFont,
size,
{ clipRectMin.x, clipRectMin.y },
IM_COL32(255, 255, 255, 255 * textAlpha),
descriptionText,
0,
clipRectMax.x - clipRectMin.x
);
drawList->PopClipRect();
ImVec2 sideMin = { descriptionMax.x, descriptionMin.y };
ImVec2 sideMax = { Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH + SIDE_CONTAINER_WIDTH)), descriptionMax.y };
DrawContainer(sideMin, sideMax, false);
drawList->PopClipRect();
if (g_currentPage != WizardPage::Installing && textAlpha >= 1.0)
{
auto icon = hid::detail::g_inputDevice == hid::detail::EInputDevice::Controller
? 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() && !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<int>
(
font,
size,
min,
IM_COL32(255, 255, 255, 255 * alpha),
buttonText,
2,
IM_COL32(baser, baseg, 0, 255 * alpha)
);
ResetGradient();
SetScale({ 1.0f, 1.0f });
SetOrigin({ 0.0f, 0.0f });
}
enum ButtonColumn
{
ButtonColumnLeft,
ButtonColumnMiddle,
ButtonColumnRight
};
static void ComputeButtonColumnCoordinates(ButtonColumn buttonColumn, float &minX, float &maxX)
{
switch (buttonColumn)
{
case ButtonColumnLeft:
minX = Scale(AlignToNextGrid(CONTAINER_X) + CONTAINER_BUTTON_GAP);
maxX = Scale(AlignToNextGrid(CONTAINER_X) + CONTAINER_BUTTON_GAP + CONTAINER_BUTTON_WIDTH);
break;
case ButtonColumnMiddle:
minX = Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH / 2.0f) - CONTAINER_BUTTON_WIDTH / 2.0f);
maxX = Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH / 2.0f) + CONTAINER_BUTTON_WIDTH / 2.0f);
break;
case ButtonColumnRight:
minX = Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH) - CONTAINER_BUTTON_GAP - CONTAINER_BUTTON_WIDTH);
maxX = Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH) - CONTAINER_BUTTON_GAP);
break;
}
}
static void DrawSourceButton(ButtonColumn buttonColumn, float yRatio, const char *sourceText, bool sourceSet)
{
bool buttonPressed;
float minX, maxX;
ComputeButtonColumnCoordinates(buttonColumn, minX, maxX);
float minusY = (CONTAINER_BUTTON_GAP + BUTTON_HEIGHT) * yRatio;
ImVec2 min = { minX, Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) - CONTAINER_BUTTON_GAP - BUTTON_HEIGHT - minusY) };
ImVec2 max = { maxX, Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) - CONTAINER_BUTTON_GAP - minusY) };
DrawButton(min, max, sourceText, true, sourceSet, buttonPressed);
}
static void DrawProgressBar(float progressRatio)
{
auto &res = ImGui::GetIO().DisplaySize;
auto drawList = ImGui::GetForegroundDrawList();
float alpha = 1.0;
const uint32_t innerColor0 = IM_COL32(0, 65, 0, 255 * alpha);
const uint32_t innerColor1 = IM_COL32(0, 32, 0, 255 * alpha);
float xPadding = Scale(6.0f);
float yPadding = Scale(3.0f);
ImVec2 min = { Scale(AlignToNextGrid(CONTAINER_X) + BOTTOM_X_GAP), Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) + BOTTOM_Y_GAP) };
ImVec2 max = { Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH) - BOTTOM_X_GAP), Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) + BOTTOM_Y_GAP + BUTTON_HEIGHT) };
DrawButtonContainer(min, max, 0, 0, alpha);
drawList->AddRectFilledMultiColor
(
{ min.x + xPadding, min.y + yPadding },
{ max.x - xPadding, max.y - yPadding },
innerColor0,
innerColor0,
innerColor1,
innerColor1
);
const uint32_t sliderColor0 = IM_COL32(57, 241, 0, 255 * alpha);
const uint32_t sliderColor1 = IM_COL32(2, 106, 0, 255 * alpha);
xPadding += Scale(1.0f);
yPadding += Scale(1.0f);
ImVec2 sliderMin = { min.x + xPadding, min.y + yPadding };
ImVec2 sliderMax = { max.x - xPadding, max.y - yPadding };
sliderMax.x = sliderMin.x + (sliderMax.x - sliderMin.x) * progressRatio;
drawList->AddRectFilledMultiColor(sliderMin, sliderMax, sliderColor0, sliderColor0, sliderColor1, sliderColor1);
}
static bool ConvertPathSet(const nfdpathset_t *pathSet, std::list<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++)
{
char *pathSetPath = nullptr;
if (NFD_PathSet_GetPathU8(pathSet, i, &pathSetPath) != NFD_OKAY)
{
filePaths.clear();
return false;
}
filePaths.emplace_back(std::filesystem::path(std::u8string_view((const char8_t *)(pathSetPath))));
NFD_PathSet_FreePathU8(pathSetPath);
}
return true;
}
static bool ShowFilesPicker(std::list<std::filesystem::path> &filePaths)
{
filePaths.clear();
const nfdpathset_t *pathSet;
nfdresult_t result = NFD_OpenDialogMultipleU8(&pathSet, nullptr, 0, nullptr);
g_currentMessageUpdateRemaining = true;
if (result == NFD_OKAY)
{
bool pathsConverted = ConvertPathSet(pathSet, filePaths);
NFD_PathSet_Free(pathSet);
return pathsConverted;
}
else
{
return false;
}
}
static bool ShowFoldersPicker(std::list<std::filesystem::path> &folderPaths)
{
folderPaths.clear();
const nfdpathset_t *pathSet;
nfdresult_t result = NFD_PickFolderMultipleU8(&pathSet, nullptr);
g_currentMessageUpdateRemaining = true;
if (result == NFD_OKAY)
{
bool pathsConverted = ConvertPathSet(pathSet, folderPaths);
NFD_PathSet_Free(pathSet);
return pathsConverted;
}
else
{
return false;
}
}
static void ParseSourcePaths(std::list<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)
{
stringStream << std::endl << "- " << Truncate(path.filename().string(), 32, true, true);
}
if (isFailedPathsOverLimit)
stringStream << std::endl << "- [...]";
g_currentMessagePrompt = stringStream.str();
g_currentMessagePromptConfirmation = false;
}
}
static void DrawLanguagePicker()
{
bool buttonPressed = false;
if (g_currentPage == WizardPage::SelectLanguage)
{
bool buttonPressed;
float minX, maxX;
for (int i = 0; i < 6; i++)
{
ComputeButtonColumnCoordinates((i < 3) ? ButtonColumnLeft : ButtonColumnRight, minX, maxX);
float minusY = (CONTAINER_BUTTON_GAP + BUTTON_HEIGHT) * (float(i % 3));
ImVec2 min = { minX, Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) - CONTAINER_BUTTON_GAP - BUTTON_HEIGHT - minusY) };
ImVec2 max = { maxX, Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) - CONTAINER_BUTTON_GAP - minusY) };
// TODO: The active button should change its style to show an enabled toggle if it matches the current language.
DrawButton(min, max, LANGUAGE_TEXT[i], false, true, buttonPressed, 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(AlignToNextGrid(CONTAINER_X) + BOTTOM_X_GAP), Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) + BOTTOM_Y_GAP) };
ImVec2 max = { Scale(AlignToNextGrid(CONTAINER_X) + BOTTOM_X_GAP + textSize.x * squashRatio), Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) + BOTTOM_Y_GAP + BUTTON_HEIGHT) };
DrawButton(min, max, addFilesText.c_str(), false, true, buttonPressed, ADD_BUTTON_MAX_TEXT_WIDTH);
if (buttonPressed && ShowFilesPicker(paths))
{
ParseSourcePaths(paths);
}
min.x += Scale(BOTTOM_X_GAP + textSize.x * squashRatio);
const std::string &addFolderText = Localise("Installer_Button_AddFolder");
textSize = ComputeTextSize(g_dfsogeistdFont, addFolderText.c_str(), 20.0f, squashRatio, ADD_BUTTON_MAX_TEXT_WIDTH);
textSize.x += BUTTON_TEXT_GAP;
max.x = min.x + Scale(textSize.x * squashRatio);
DrawButton(min, max, addFolderText.c_str(), false, true, buttonPressed, ADD_BUTTON_MAX_TEXT_WIDTH);
if (buttonPressed && ShowFoldersPicker(paths))
{
ParseSourcePaths(paths);
}
}
}
static void DrawSources()
{
if (g_currentPage == WizardPage::SelectGameAndUpdate)
{
DrawSourceButton(ButtonColumnMiddle, 1.5f, Localise("Installer_Step_Game").c_str(), !g_gameSourcePath.empty());
DrawSourceButton(ButtonColumnMiddle, 0.5f, Localise("Installer_Step_Update").c_str(), !g_updateSourcePath.empty());
}
if (g_currentPage == WizardPage::SelectDLC)
{
for (int i = 0; i < 6; i++)
{
DrawSourceButton((i < 3) ? ButtonColumnLeft : ButtonColumnRight, float(i % 3), DLC_SOURCE_TEXT[i], !g_dlcSourcePaths[i].empty() || g_dlcInstalled[i]);
}
}
}
static void DrawInstallingProgress()
{
if (g_currentPage == WizardPage::Installing)
{
float ratioTarget = g_installerProgressRatioTarget.load();
g_installerProgressRatioCurrent += (4.0f * ImGui::GetIO().DeltaTime * (ratioTarget - g_installerProgressRatioCurrent));
DrawProgressBar(g_installerProgressRatioCurrent);
if (g_installerFinished)
{
g_installerThread->join();
g_installerThread.reset();
g_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_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::filesystem::space_info spaceInfo = std::filesystem::space(g_installPath);
g_installerAvailableSize = spaceInfo.available;
Installer::Input installerInput;
installerInput.gameSource = g_gameSourcePath;
installerInput.updateSource = g_updateSourcePath;
for (std::filesystem::path &path : g_dlcSourcePaths) {
if (!path.empty())
{
installerInput.dlcSources.push_back(path);
}
}
return Installer::parseSources(installerInput, g_installerJournal, g_installerSources);
}
static void DrawNextButton()
{
if (g_currentPage != WizardPage::Installing)
{
bool nextButtonEnabled = !g_isDisappearing;
if (nextButtonEnabled && g_currentPage == WizardPage::SelectGameAndUpdate)
{
nextButtonEnabled = !g_gameSourcePath.empty() && !g_updateSourcePath.empty();
}
bool skipButton = false;
if (g_currentPage == WizardPage::SelectDLC)
{
skipButton = std::all_of(g_dlcSourcePaths.begin(), g_dlcSourcePaths.end(), [](const std::filesystem::path &path) { return path.empty(); });
}
float squashRatio;
constexpr float NEXT_BUTTON_MAX_TEXT_WIDTH = 100.0f;
const std::string &buttonText = Localise(skipButton ? "Installer_Button_Skip" : "Installer_Button_Next");
ImVec2 textSize = ComputeTextSize(g_newRodinFont, buttonText.c_str(), 20.0f, squashRatio, NEXT_BUTTON_MAX_TEXT_WIDTH);
textSize.x += BUTTON_TEXT_GAP;
ImVec2 min = { Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH) - textSize.x * squashRatio - BOTTOM_X_GAP), Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) + BOTTOM_Y_GAP) };
ImVec2 max = { Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH) - BOTTOM_X_GAP), Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) + BOTTOM_Y_GAP + BUTTON_HEIGHT) };
bool buttonPressed = false;
DrawButton(min, max, buttonText.c_str(), false, nextButtonEnabled, buttonPressed, NEXT_BUTTON_MAX_TEXT_WIDTH);
if (buttonPressed)
{
XexPatcher::Result patcherResult;
if (g_currentPage == WizardPage::SelectGameAndUpdate && (patcherResult = Installer::checkGameUpdateCompatibility(g_gameSourcePath, g_updateSourcePath), patcherResult != XexPatcher::Result::Success))
{
g_currentMessagePrompt = Localise("Installer_Message_IncompatibleGameData");
g_currentMessagePromptConfirmation = false;
}
else if (g_currentPage == WizardPage::SelectDLC)
{
// Check if any of the DLC was not specified.
bool dlcIncomplete = false;
for (int i = 0; (i < int(DLC::Count)) && !dlcIncomplete; i++)
{
if (g_dlcSourcePaths[i].empty() && !g_dlcInstalled[i])
{
dlcIncomplete = true;
}
}
bool dlcInstallerMode = g_gameSourcePath.empty();
if (!InstallerParseSources())
{
// Some of the sources that were provided to the installer are not valid. Restart the file selection process.
g_currentMessagePrompt = Localise("Installer_Message_InvalidFiles");
g_currentMessagePromptConfirmation = false;
g_currentPage = dlcInstallerMode ? WizardPage::SelectDLC : WizardPage::SelectGameAndUpdate;
}
else if (dlcIncomplete && !dlcInstallerMode)
{
// Not all the DLC was specified, we show a prompt and await a confirmation before starting the installer.
g_currentMessagePrompt = Localise("Installer_Message_DLCWarning");
g_currentMessagePromptConfirmation = true;
}
else if (skipButton && dlcInstallerMode)
{
// Nothing was selected and the installer was in DLC mode, just close it.
g_isDisappearing = true;
g_disappearTime = ImGui::GetTime();
}
else
{
g_currentPage = WizardPage::CheckSpace;
}
}
else if (g_currentPage == WizardPage::CheckSpace)
{
InstallerStart();
}
else if (g_currentPage == WizardPage::InstallSucceeded)
{
g_isDisappearing = true;
g_disappearTime = ImGui::GetTime();
}
else if (g_currentPage == WizardPage::InstallFailed)
{
g_currentPage = g_firstPage;
}
else
{
g_currentPage = WizardPage(int(g_currentPage) + 1);
}
}
}
}
static void DrawHorizontalBorder(bool bottomBorder)
{
const uint32_t FADE_COLOR_LEFT = IM_COL32(155, 155, 155, 0);
const uint32_t SOLID_COLOR = IM_COL32(155, 200, 155, 255);
const uint32_t FADE_COLOR_RIGHT = IM_COL32(155, 225, 155, 0);
auto drawList = ImGui::GetForegroundDrawList();
double borderScale = 1.0 - ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_LINE_ANIMATION_TIME, CONTAINER_LINE_ANIMATION_DURATION);
float midX = Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH / 5));
float minX = std::lerp(Scale(AlignToNextGrid(CONTAINER_X) - BORDER_SIZE - BORDER_OVERSHOOT), midX, borderScale);
float maxX = std::lerp(Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH + SIDE_CONTAINER_WIDTH) + BORDER_OVERSHOOT), midX, borderScale);
float minY = bottomBorder ? Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT)) : Scale(AlignToNextGrid(CONTAINER_Y) - BORDER_SIZE);
float maxY = minY + Scale(BORDER_SIZE);
drawList->AddRectFilledMultiColor
(
{ minX, minY },
{ midX, maxY },
FADE_COLOR_LEFT,
SOLID_COLOR,
SOLID_COLOR,
FADE_COLOR_LEFT
);
drawList->AddRectFilledMultiColor
(
{ midX, minY },
{ maxX, maxY },
SOLID_COLOR,
FADE_COLOR_RIGHT,
FADE_COLOR_RIGHT,
SOLID_COLOR
);
}
static void DrawVerticalBorder(bool rightBorder)
{
const uint32_t SOLID_COLOR = IM_COL32(155, rightBorder ? 225 : 155, 155, 255);
const uint32_t FADE_COLOR = IM_COL32(155, rightBorder ? 225 : 155, 155, 0);
auto drawList = ImGui::GetForegroundDrawList();
double borderScale = 1.0 - ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_LINE_ANIMATION_TIME, CONTAINER_LINE_ANIMATION_DURATION);
float minX = rightBorder ? Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH)) : Scale(AlignToNextGrid(CONTAINER_X) - BORDER_SIZE);
float maxX = minX + Scale(BORDER_SIZE);
float midY = Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT / 2));
float minY = std::lerp(Scale(AlignToNextGrid(CONTAINER_Y) - BORDER_OVERSHOOT), midY, borderScale);
float maxY = std::lerp(Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) + BORDER_OVERSHOOT), midY, borderScale);
drawList->AddRectFilledMultiColor
(
{ minX, minY },
{ maxX, midY },
FADE_COLOR,
FADE_COLOR,
SOLID_COLOR,
SOLID_COLOR
);
drawList->AddRectFilledMultiColor
(
{ minX, midY },
{ maxX, maxY },
SOLID_COLOR,
SOLID_COLOR,
FADE_COLOR,
FADE_COLOR
);
}
static void DrawBorders()
{
DrawHorizontalBorder(false);
DrawHorizontalBorder(true);
DrawVerticalBorder(false);
DrawVerticalBorder(true);
}
static void DrawMessagePrompt()
{
if (g_currentMessagePrompt.empty())
{
return;
}
if (g_currentMessageUpdateRemaining)
{
// If a blocking function like the files picker is called, we must wait one update before actually showing
// the message box, as a lot of time has passed since the last real update. Otherwise, animations will play
// too quickly and input glitches might happen.
g_currentMessageUpdateRemaining = false;
return;
}
bool messageWindowReturned = false;
if (g_currentMessagePromptConfirmation)
{
std::array<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;
}
}
void InstallerWizard::Init()
{
auto &io = ImGui::GetIO();
constexpr float FONT_SCALE = 2.0f;
g_seuratFont = ImFontAtlasSnapshot::GetFont("FOT-SeuratPro-M.otf", 24.0f * FONT_SCALE);
g_dfsogeistdFont = ImFontAtlasSnapshot::GetFont("DFSoGeiStd-W7.otf", 48.0f * FONT_SCALE);
g_newRodinFont = ImFontAtlasSnapshot::GetFont("FOT-NewRodinPro-DB.otf", 20.0f * FONT_SCALE);
g_installTextures[0] = LoadTexture(decompressZstd(g_install_001, g_install_001_uncompressed_size).get(), g_install_001_uncompressed_size);
g_installTextures[1] = LoadTexture(decompressZstd(g_install_002, g_install_002_uncompressed_size).get(), g_install_002_uncompressed_size);
g_installTextures[2] = LoadTexture(decompressZstd(g_install_003, g_install_003_uncompressed_size).get(), g_install_003_uncompressed_size);
g_installTextures[3] = LoadTexture(decompressZstd(g_install_004, g_install_004_uncompressed_size).get(), g_install_004_uncompressed_size);
g_installTextures[4] = LoadTexture(decompressZstd(g_install_005, g_install_005_uncompressed_size).get(), g_install_005_uncompressed_size);
g_installTextures[5] = LoadTexture(decompressZstd(g_install_006, g_install_006_uncompressed_size).get(), g_install_006_uncompressed_size);
g_installTextures[6] = LoadTexture(decompressZstd(g_install_007, g_install_007_uncompressed_size).get(), g_install_007_uncompressed_size);
g_installTextures[7] = LoadTexture(decompressZstd(g_install_008, g_install_008_uncompressed_size).get(), g_install_008_uncompressed_size);
g_milesElectricIcon = LoadTexture(decompressZstd(g_miles_electric_icon, g_miles_electric_icon_uncompressed_size).get(), g_miles_electric_icon_uncompressed_size);
g_arrowCircle = LoadTexture(decompressZstd(g_arrow_circle, g_arrow_circle_uncompressed_size).get(), g_arrow_circle_uncompressed_size);
g_pulseInstall = LoadTexture(decompressZstd(g_pulse_install, g_pulse_install_uncompressed_size).get(), g_pulse_install_uncompressed_size);
}
void InstallerWizard::Draw()
{
if (!s_isVisible)
{
return;
}
ResetCursorRects();
DrawBackground();
DrawLeftImage();
DrawScanlineBars();
DrawDescriptionContainer();
DrawLanguagePicker();
DrawSourcePickers();
DrawSources();
DrawInstallingProgress();
DrawNextButton();
DrawBorders();
DrawMessagePrompt();
if (g_isDisappearing)
{
const double disappearDuration = ALL_ANIMATIONS_FULL_DURATION / 60.0;
if (ImGui::GetTime() > (g_disappearTime + disappearDuration))
{
s_isVisible = false;
}
}
}
void InstallerWizard::Shutdown()
{
// Wait for and erase the thread.
if (g_installerThread != nullptr)
{
g_installerThread->join();
g_installerThread.reset();
}
// Erase the sources.
g_installerSources.game.reset();
g_installerSources.update.reset();
g_installerSources.dlc.clear();
// Make sure the GPU is not currently active before deleting these textures.
Video::WaitForGPU();
// Erase the textures.
g_milesElectricIcon.reset();
g_arrowCircle.reset();
g_pulseInstall.reset();
for (auto &texture : g_installTextures)
{
texture.reset();
}
}
bool InstallerWizard::Run(bool skipGame)
{
NFD_Init();
// 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;
}
Window::SetFullscreenCursorVisibility(true);
s_isVisible = true;
while (s_isVisible)
{
SDL_PumpEvents();
SDL_FlushEvents(SDL_FIRSTEVENT, SDL_LASTEVENT);
Window::Update();
Video::HostPresent();
}
Window::SetFullscreenCursorVisibility(false);
NFD_Quit();
InstallerWizard::Shutdown();
return true;
}