diff --git a/MarathonRecomp/CMakeLists.txt b/MarathonRecomp/CMakeLists.txt index 9216b8996..509443847 100644 --- a/MarathonRecomp/CMakeLists.txt +++ b/MarathonRecomp/CMakeLists.txt @@ -568,7 +568,8 @@ BIN2C(TARGET_OBJ MarathonRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/gam BIN2C(TARGET_OBJ MarathonRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/music/installer.ogg" DEST_FILE "${RESOURCES_OUTPUT_PATH}/music/installer.ogg" ARRAY_NAME "g_installer_music") BIN2C(TARGET_OBJ MarathonRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/sounds/window_open.ogg" DEST_FILE "${RESOURCES_OUTPUT_PATH}/sounds/window_open.ogg" ARRAY_NAME "g_window_open") BIN2C(TARGET_OBJ MarathonRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/sounds/window_close.ogg" DEST_FILE "${RESOURCES_OUTPUT_PATH}/sounds/window_close.ogg" ARRAY_NAME "g_window_close") -BIN2C(TARGET_OBJ MarathonRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/sounds/cursor2.ogg" DEST_FILE "${RESOURCES_OUTPUT_PATH}/sounds/cursor2.ogg" ARRAY_NAME "g_cursor2") +BIN2C(TARGET_OBJ MarathonRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/sounds/cursor.ogg" DEST_FILE "${RESOURCES_OUTPUT_PATH}/sounds/cursor.ogg" ARRAY_NAME "g_cursor") BIN2C(TARGET_OBJ MarathonRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/sounds/deside.ogg" DEST_FILE "${RESOURCES_OUTPUT_PATH}/sounds/deside.ogg" ARRAY_NAME "g_deside") BIN2C(TARGET_OBJ MarathonRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/sounds/move.ogg" DEST_FILE "${RESOURCES_OUTPUT_PATH}/sounds/move.ogg" ARRAY_NAME "g_move") BIN2C(TARGET_OBJ MarathonRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/sounds/main_deside.ogg" DEST_FILE "${RESOURCES_OUTPUT_PATH}/sounds/main_deside.ogg" ARRAY_NAME "g_main_deside") +BIN2C(TARGET_OBJ MarathonRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/sounds/cannot_deside.ogg" DEST_FILE "${RESOURCES_OUTPUT_PATH}/sounds/cannot_deside.ogg" ARRAY_NAME "g_cannot_deside") diff --git a/MarathonRecomp/apu/embedded_player.cpp b/MarathonRecomp/apu/embedded_player.cpp index 561680f7e..cd1ad0a0e 100644 --- a/MarathonRecomp/apu/embedded_player.cpp +++ b/MarathonRecomp/apu/embedded_player.cpp @@ -5,20 +5,22 @@ #include #include #include -#include +#include #include #include #include +#include enum class EmbeddedSound { WindowOpen, WindowClose, - Cursor2, + Cursor, Deside, Move, MainDeside, - Count, + CannotDeside, + Count }; struct EmbeddedSoundData @@ -31,10 +33,11 @@ static const std::unordered_map g_embeddedSound { { "window_open", EmbeddedSound::WindowOpen }, { "window_close", EmbeddedSound::WindowClose }, - { "cursor2", EmbeddedSound::Cursor2 }, + { "cursor", EmbeddedSound::Cursor }, { "deside", EmbeddedSound::Deside }, { "move", EmbeddedSound::Move }, { "main_deside", EmbeddedSound::MainDeside }, + { "cannot_deside", EmbeddedSound::CannotDeside }, }; static size_t g_channelIndex; @@ -57,9 +60,9 @@ static void PlayEmbeddedSound(EmbeddedSound s) soundData = g_window_close; soundDataSize = sizeof(g_window_close); break; - case EmbeddedSound::Cursor2: - soundData = g_cursor2; - soundDataSize = sizeof(g_cursor2); + case EmbeddedSound::Cursor: + soundData = g_cursor; + soundDataSize = sizeof(g_cursor); break; case EmbeddedSound::Deside: soundData = g_deside; @@ -73,6 +76,10 @@ static void PlayEmbeddedSound(EmbeddedSound s) soundData = g_main_deside; soundDataSize = sizeof(g_main_deside); break; + case EmbeddedSound::CannotDeside: + soundData = g_cannot_deside; + soundDataSize = sizeof(g_cannot_deside); + break; default: assert(false && "Unknown embedded sound."); return; diff --git a/MarathonRecomp/gpu/video.cpp b/MarathonRecomp/gpu/video.cpp index 64c28a741..4677c9add 100644 --- a/MarathonRecomp/gpu/video.cpp +++ b/MarathonRecomp/gpu/video.cpp @@ -2915,6 +2915,7 @@ static void DrawImGui() ImGui::End(); #endif + UpdateImGuiUtils(); AchievementMenu::Draw(); OptionsMenu::Draw(); InstallerWizard::Draw(); diff --git a/MarathonRecomp/install/installer.cpp b/MarathonRecomp/install/installer.cpp index 2279e1937..adeeb039e 100644 --- a/MarathonRecomp/install/installer.cpp +++ b/MarathonRecomp/install/installer.cpp @@ -17,13 +17,13 @@ static const std::string GameDirectory = "game"; static const std::string DLCDirectory = "dlc"; -static const std::string EpisodeSonicDirectory = DLCDirectory + "/Episode Sonic Boss Attack"; -static const std::string EpisodeShadowDirectory = DLCDirectory + "/Episode Shadow Boss Attack"; -static const std::string EpisodeSilverDirectory = DLCDirectory + "/Episode Silver Boss Attack"; -static const std::string EpisodeAmigoDirectory = DLCDirectory + "/Team Attack Amigo"; -static const std::string MissionSonicDirectory = DLCDirectory + "/Mission Pack Sonic Very Hard"; -static const std::string MissionShadowDirectory = DLCDirectory + "/Mission Pack Shadow Very Hard"; -static const std::string MissionSilverDirectory = DLCDirectory + "/Mission Pack Silver Very Hard"; +static const std::string EpisodeSonicDirectory = DLCDirectory + "/Additional Episode - Sonic Boss Attack"; +static const std::string EpisodeShadowDirectory = DLCDirectory + "/Additional Episode - Shadow Boss Attack"; +static const std::string EpisodeSilverDirectory = DLCDirectory + "/Additional Episode - Silver Boss Attack"; +static const std::string EpisodeAmigoDirectory = DLCDirectory + "/Additional Episode - Team Attack Amigo"; +static const std::string MissionSonicDirectory = DLCDirectory + "/Additional Mission Pack - Sonic Very Hard"; +static const std::string MissionShadowDirectory = DLCDirectory + "/Additional Mission Pack - Shadow Very Hard"; +static const std::string MissionSilverDirectory = DLCDirectory + "/Additional Mission Pack - Silver Very Hard"; static const std::string GameExecutableFile = "default.xex"; static const std::string DLCValidationFile = "download.arc"; static const std::string ISOExtension = ".iso"; @@ -70,7 +70,7 @@ static bool checkFile(const FilePair &pair, const uint64_t *fileHashes, const st if (!std::filesystem::exists(filePath)) { journal.lastResult = Journal::Result::FileMissing; - journal.lastErrorMessage = fmt::format("File {} does not exist.", fileName); + journal.lastErrorMessage = fmt::format("File \"{}\" does not exist.", fileName); return false; } @@ -79,7 +79,7 @@ static bool checkFile(const FilePair &pair, const uint64_t *fileHashes, const st if (ec) { journal.lastResult = Journal::Result::FileReadFailed; - journal.lastErrorMessage = fmt::format("Failed to read file size for {}.", fileName); + journal.lastErrorMessage = fmt::format("Failed to read file size for \"{}\".", fileName); return false; } @@ -99,7 +99,7 @@ static bool checkFile(const FilePair &pair, const uint64_t *fileHashes, const st if (!fileStream.is_open() || fileStream.bad()) { journal.lastResult = Journal::Result::FileReadFailed; - journal.lastErrorMessage = fmt::format("Failed to read file {}.", fileName); + journal.lastErrorMessage = fmt::format("Failed to read file \"{}\".", fileName); return false; } @@ -113,7 +113,7 @@ static bool checkFile(const FilePair &pair, const uint64_t *fileHashes, const st if (!fileHashFound) { journal.lastResult = Journal::Result::FileHashFailed; - journal.lastErrorMessage = fmt::format("File {} did not match any of the known hashes.", fileName); + journal.lastErrorMessage = fmt::format("File \"{}\" did not match any of the known hashes.", fileName); return false; } @@ -136,14 +136,14 @@ static bool copyFile(const FilePair &pair, const uint64_t *fileHashes, VirtualFi if (!sourceVfs.exists(filename)) { journal.lastResult = Journal::Result::FileMissing; - journal.lastErrorMessage = fmt::format("File {} does not exist in {}.", filename, sourceVfs.getName()); + journal.lastErrorMessage = fmt::format("File \"{}\" does not exist in \"{}\".", filename, sourceVfs.getName()); return false; } if (!sourceVfs.load(filename, fileData)) { journal.lastResult = Journal::Result::FileReadFailed; - journal.lastErrorMessage = fmt::format("Failed to read file {} from {}.", filename, sourceVfs.getName()); + journal.lastErrorMessage = fmt::format("Failed to read file \"{}\" from \"{}\".", filename, sourceVfs.getName()); return false; } @@ -159,7 +159,7 @@ static bool copyFile(const FilePair &pair, const uint64_t *fileHashes, VirtualFi if (!fileHashFound) { journal.lastResult = Journal::Result::FileHashFailed; - journal.lastErrorMessage = fmt::format("File {} from {} did not match any of the known hashes.", filename, sourceVfs.getName()); + journal.lastErrorMessage = fmt::format("File \"{}\" from \"{}\" did not match any of the known hashes.", filename, sourceVfs.getName()); return false; } } @@ -187,7 +187,7 @@ static bool copyFile(const FilePair &pair, const uint64_t *fileHashes, VirtualFi if (!outStream.is_open()) { journal.lastResult = Journal::Result::FileCreationFailed; - journal.lastErrorMessage = fmt::format("Failed to create file at {}.", fromPath(targetPath)); + journal.lastErrorMessage = fmt::format("Failed to create file at \"{}\".", fromPath(targetPath)); return false; } @@ -197,7 +197,7 @@ static bool copyFile(const FilePair &pair, const uint64_t *fileHashes, VirtualFi if (outStream.bad()) { journal.lastResult = Journal::Result::FileWriteFailed; - journal.lastErrorMessage = fmt::format("Failed to create file at {}.", fromPath(targetPath)); + journal.lastErrorMessage = fmt::format("Failed to create file at \"{}\".", fromPath(targetPath)); return false; } @@ -253,7 +253,7 @@ static DLC detectDLC(const std::filesystem::path &sourcePath, VirtualFileSystem } journal.lastResult = Journal::Result::UnknownDLCType; - journal.lastErrorMessage = fmt::format("DLC type for {} is unknown.", name); + journal.lastErrorMessage = fmt::format("DLC type for \"{}\" is unknown.", name); return DLC::Unknown; } @@ -384,7 +384,7 @@ bool Installer::computeTotalSize(std::span filePairs, const uint if (!sourceVfs.exists(filename)) { journal.lastResult = Journal::Result::FileMissing; - journal.lastErrorMessage = fmt::format("File {} does not exist in {}.", filename, sourceVfs.getName()); + journal.lastErrorMessage = fmt::format("File \"{}\" does not exist in \"{}\".", filename, sourceVfs.getName()); return false; } @@ -421,7 +421,7 @@ bool Installer::copyFiles(std::span filePairs, const uint64_t *f if (!std::filesystem::exists(targetDirectory) && !std::filesystem::create_directories(targetDirectory, ec)) { journal.lastResult = Journal::Result::DirectoryCreationFailed; - journal.lastErrorMessage = "Unable to create directory at " + fromPath(targetDirectory); + journal.lastErrorMessage = "Unable to create directory at \"" + fromPath(targetDirectory) + "\"."; return false; } @@ -452,7 +452,7 @@ bool Installer::parseContent(const std::filesystem::path &sourcePath, std::uniqu else { journal.lastResult = Journal::Result::VirtualFileSystemFailed; - journal.lastErrorMessage = "Unable to open " + fromPath(sourcePath); + journal.lastErrorMessage = "Unable to open \"" + fromPath(sourcePath) + "\"."; return false; } } diff --git a/MarathonRecomp/locale/config_locale.cpp b/MarathonRecomp/locale/config_locale.cpp index 0d0692d85..74a15435f 100644 --- a/MarathonRecomp/locale/config_locale.cpp +++ b/MarathonRecomp/locale/config_locale.cpp @@ -74,7 +74,7 @@ CONFIG_DEFINE_ENUM_LOCALE(ELanguage) CONFIG_DEFINE_LOCALE(VoiceLanguage) { - { ELanguage::English, { "Voice Language", "Change the language used for character voices." } }, + { ELanguage::English, { "Voice Language", "Change the language used for character voices." } }, { ELanguage::Japanese, { "音声言語", "ゲーム内の音声言語を変更できます" } }, { ELanguage::German, { "Stimmeinstellung", "Ändere die Sprache, die für Charakterstimmen benutzt wird." } }, { ELanguage::French, { "Langue de doublage", "Modifie la langue utilisée pour la voix des personnages." } }, diff --git a/MarathonRecomp/locale/locale.cpp b/MarathonRecomp/locale/locale.cpp index 4dbf81ecd..d2a056d50 100644 --- a/MarathonRecomp/locale/locale.cpp +++ b/MarathonRecomp/locale/locale.cpp @@ -381,6 +381,13 @@ std::unordered_map> { ELanguage::Italian, "PROGRESSI %d/%d" } } }, + { + // Locale required for font atlas generation. + "Installer_MusicCredits", + { + { ELanguage::English, "♬ Result & Chill Lofi - Hotline Sehwani & SilverIceSound" }, + }, + }, { "Installer_Header_Installer", { @@ -418,7 +425,7 @@ std::unordered_map> "Installer_Page_Introduction", { { ELanguage::English, "Welcome to Marathon Recompiled!\n\nYou'll need an Xbox 360 copy of\nSONIC THE HEDGEHOG in order to proceed with the installation." }, - { ELanguage::Japanese, "Marathon Recompiledへようこそ!\nインストールにはXbox 360版の\n「ソニック・ザ・ヘッジホッグ」\nが必要です" }, + { ELanguage::Japanese, "Marathon Recompiledへようこそ!\n\nインストールにはXbox 360版の\n「ソニック・ザ・ヘッジホッグ」\nが必要です" }, { ELanguage::German, "Willkommen zu Marathon Recompiled!\n\nEs wird eine Xbox 360 Kopie von\nSONIC THE HEDGEHOG benötigt um mit der Installation fortfahren zu können." }, { ELanguage::French, "Bienvenue sur Marathon Recompiled !\n\nVous aurez besoin d'une copie de\nSONIC THE HEDGEHOG pour\nXbox 360 pour procéder à l'installation." }, { ELanguage::Spanish, "¡Bienvenido a Marathon Recompiled!\n\nNecesitas una copia de\nSONIC THE HEDGEHOG de\nXbox 360 para continuar con la instalación." }, @@ -451,9 +458,9 @@ std::unordered_map> "Installer_Page_CheckSpace", { { ELanguage::English, "The content will be installed to the program's folder.\n\n" }, - { ELanguage::Japanese, "コンテンツはプログラムのフォルダに\nインストールされます\n" }, - { ELanguage::German, "Der Inhalt wird in dem Ordner des Programms installiert.\n" }, - { ELanguage::French, "Le contenu sera installé dans le même dossier que le programme.\n" }, + { ELanguage::Japanese, "コンテンツはプログラムのフォルダに\nインストールされます\n\n" }, + { ELanguage::German, "Der Inhalt wird in dem Ordner des Programms installiert.\n\n" }, + { ELanguage::French, "Le contenu sera installé dans le même dossier que le programme.\n\n" }, { ELanguage::Spanish, "El contenido será instalado a la carpeta del programa.\n\n" }, { ELanguage::Italian, "Il contenuto verrà installato nella cartella di questo programma.\n\n" } } @@ -472,34 +479,34 @@ std::unordered_map> { "Installer_Page_InstallSucceeded", { - { ELanguage::English, "Installation complete!\nThis project is brought to you by:" }, - { ELanguage::Japanese, "インストール完了!\nプロジェクト制作:" }, - { ELanguage::German, "Installation abgeschlossen!\nDieses Projekt wird präsentiert von:" }, - { ELanguage::French, "Installation terminée !\nCe projet vous est présenté\npar :" }, - { ELanguage::Spanish, "¡Instalación completada!\nEste proyecto ha sido posible gracias a:" }, - { ELanguage::Italian, "Installazione completata!\nQuesto progetto è stato creato da:" } + { ELanguage::English, "Installation complete!\n\nThis project is brought to you by:" }, + { ELanguage::Japanese, "インストール完了!\n\nプロジェクト制作:" }, + { ELanguage::German, "Installation abgeschlossen!\n\nDieses Projekt wird präsentiert von:" }, + { ELanguage::French, "Installation terminée !\n\nCe projet vous est présenté par :" }, + { ELanguage::Spanish, "¡Instalación completada!\n\nEste proyecto ha sido posible gracias a:" }, + { ELanguage::Italian, "Installazione completata!\n\nQuesto progetto è stato creato da:" } } }, { "Installer_Page_InstallFailed", { - { ELanguage::English, "Installation failed.\n\nError: " }, - { ELanguage::Japanese, "インストールに失敗しました\n\nエラー: " }, - { ELanguage::German, "Installation fehlgeschlagen.\n\nFehler: " }, - { ELanguage::French, "L'installation a échoué.\n\nErreur : " }, - { ELanguage::Spanish, "La instalación falló.\n\nError: " }, - { ELanguage::Italian, "Installazione fallita.\n\nErrore: " } + { ELanguage::English, "Installation failed.\n\n" }, + { ELanguage::Japanese, "インストールに失敗しました\n\n" }, + { ELanguage::German, "Installation fehlgeschlagen.\n\n" }, + { ELanguage::French, "L'installation a échoué.\n\n" }, + { ELanguage::Spanish, "La instalación falló.\n\n" }, + { ELanguage::Italian, "Installazione fallita.\n\n" } } }, { "Installer_Step_Game", { - { ELanguage::English, "GAME" }, - { ELanguage::Japanese, "ゲーム" }, - { ELanguage::German, "SPIEL" }, - { ELanguage::French, "JEU" }, - { ELanguage::Spanish, "JUEGO" }, - { ELanguage::Italian, "GIOCO" } + { ELanguage::English, "Game Data" }, + { ELanguage::Japanese, "ゲームデータ" }, + { ELanguage::German, "Spieldaten" }, + { ELanguage::French, "Fichiers du jeu" }, + { ELanguage::Spanish, "Archivos del juego" }, + { ELanguage::Italian, "Dati del gioco" } } }, { @@ -507,8 +514,8 @@ std::unordered_map> { { ELanguage::English, "Required space: %2.2f GiB" }, { ELanguage::Japanese, "必要な容量: %2.2f GiB" }, - { ELanguage::German, "Benötigter Speicherplatz:\n%2.2f GiB" }, - { ELanguage::French, "Espace nécessaire :\n%2.2f Gio" }, + { ELanguage::German, "Benötigter Speicherplatz:\n%2.2f GiB\n" }, + { ELanguage::French, "Espace nécessaire : %2.2f Gio" }, { ELanguage::Spanish, "Espacio necesario: %2.2f GiB" }, { ELanguage::Italian, "Spazio necessario: %2.2f GiB" } } @@ -518,8 +525,8 @@ std::unordered_map> { { ELanguage::English, "Available space: %2.2f GiB" }, { ELanguage::Japanese, "使用可能な容量: %2.2f GiB" }, - { ELanguage::German, "Verfügbarer Speicherplatz:\n%2.2f GiB" }, - { ELanguage::French, "Espace disponible :\n%2.2f Gio" }, + { ELanguage::German, "Verfügbarer Speicherplatz:\n%2.2f GiB\n" }, + { ELanguage::French, "Espace disponible : %2.2f Gio" }, { ELanguage::Spanish, "Espacio disponible: %2.2f GiB" }, { ELanguage::Italian, "Spazio disponibile: %2.2f GiB" } } @@ -527,56 +534,45 @@ std::unordered_map> { "Installer_Button_Next", { - { ELanguage::English, "NEXT" }, + { ELanguage::English, "Next" }, { ELanguage::Japanese, "次へ" }, - { ELanguage::German, "WEITER" }, - { ELanguage::French, "SUIVANT" }, - { ELanguage::Spanish, "SIGUIENTE" }, - { ELanguage::Italian, "CONTINUA" } + { ELanguage::German, "Weiter" }, + { ELanguage::French, "Suivant" }, + { ELanguage::Spanish, "Siguiente" }, + { ELanguage::Italian, "Continua" } } }, { "Installer_Button_Skip", { - { ELanguage::English, "SKIP" }, + { ELanguage::English, "Skip" }, { ELanguage::Japanese, "スキップ" }, - { ELanguage::German, "ÜBERSPRINGEN" }, - { ELanguage::French, "IGNORER" }, - { ELanguage::Spanish, "SALTAR" }, - { ELanguage::Italian, "SALTA" } - } - }, - { - "Installer_Button_Retry", - { - { ELanguage::English, "RETRY" }, - { ELanguage::Japanese, "リトライ" }, - { ELanguage::German, "ERNEUT VERSUCHEN" }, - { ELanguage::French, "RÉESSAYER" }, - { ELanguage::Spanish, "REINTENTAR" }, - { ELanguage::Italian, "RIPROVA" } + { ELanguage::German, "Überspringen" }, + { ELanguage::French, "Ignorer" }, + { ELanguage::Spanish, "Saltar" }, + { ELanguage::Italian, "Salta" } } }, { "Installer_Button_AddFiles", { - { ELanguage::English, "ADD FILES" }, + { ELanguage::English, "Add Files" }, { ELanguage::Japanese, "ファイルを追加" }, - { ELanguage::German, "DATEIEN HINZUFÜGEN" }, - { ELanguage::French, "AJOUTER DES FICHIERS" }, - { ELanguage::Spanish, "AÑADIR ARCHIVOS" }, - { ELanguage::Italian, "AGGIUNGI FILE" } + { ELanguage::German, "Dateien hinzufügen" }, + { ELanguage::French, "Ajouter des fichiers" }, + { ELanguage::Spanish, "Añadir archivos" }, + { ELanguage::Italian, "Aggiungi file" } } }, { "Installer_Button_AddFolder", { - { ELanguage::English, "ADD FOLDER" }, + { ELanguage::English, "Add Folder" }, { ELanguage::Japanese, "フォルダを追加" }, - { ELanguage::German, "ORDNER HINZUFÜGEN" }, - { ELanguage::French, "AJOUTER UN DOSSIER" }, - { ELanguage::Spanish, "AÑADIR CARPETA" }, - { ELanguage::Italian, "AGGIUNGI CARTELLA" } + { ELanguage::German, "Ordner hinzufügen" }, + { ELanguage::French, "Ajouter un dossier" }, + { ELanguage::Spanish, "Añadir carpeta" }, + { ELanguage::Italian, "Aggiungi cartella" } } }, { @@ -595,40 +591,41 @@ std::unordered_map> // Notes: message appears in the event there are some invalid files after adding the DLC and moving onto the next step. "Installer_Message_InvalidFiles", { - { ELanguage::English, "Some of the files that have\nbeen provided are not valid.\n\nPlease make sure all the\nspecified files are correct\nand try again." }, - { ELanguage::Japanese, "提供されたファイルの一部が有効ではありません\n指定されたファイルがすべて正しいことを確認して\nもう一度お試しください" }, - { ELanguage::German, "Einige Dateien, die bereitgestellt\nwurden sind ungültig.\n\nBitte stelle sicher, dass\ndie angegebenen Dateien korrekt\nsind und versuche es erneut." }, - { ELanguage::French, "Certains fichiers fournis ne\nsont pas valides.\n\nVeuillez vous assurer que tous\nles fichiers spécifiés sont\ncorrects et réessayez." }, - { ELanguage::Spanish, "Algunos de los archivos\nseleccionados no son válidos.\n\nPor favor, asegúrate de que\ntodos los archivos son correctos\ne inténtalo de nuevo.\n" }, - { ELanguage::Italian, "Alcuni dei file che sono stati\nselezionati non sono validi.\n\nAssicurati che tutti\ni file sono quelli corretti\ne riprova." } + { ELanguage::English, "Some of the files that have been provided are not valid. Please make sure all the specified files are correct and try again." }, + { ELanguage::Japanese, "提供されたファイルの一部が有効ではありません指定されたファイルがすべて正しいことを確認してもう一度お試しください" }, + { ELanguage::German, "Einige Dateien, die bereitgestellt wurden sind ungültig. Bitte stelle sicher, dass die angegebenen Dateien korrekt sind und versuche es erneut." }, + { ELanguage::French, "Certains fichiers fournis ne sont pas valides. Veuillez vous assurer que tous les fichiers spécifiés sont corrects et réessayez." }, + { ELanguage::Spanish, "Algunos de los archivos seleccionados no son válidos. Por favor, asegúrate de que todos los archivos son correctos e inténtalo de nuevo." }, + { ELanguage::Italian, "Alcuni dei file che sono stati selezionati non sono validi. Assicurati che tutti i file sono quelli corretti e riprova." } } }, { // Notes: message appears when clicking the "Add Files" option for the first time. "Installer_Message_FilePickerTutorial", { - { ELanguage::English, "Select a digital dump with\ncontent from the game.\n\nThese files can be obtained from\nyour Xbox 360 hard drive by\nfollowing the instructions on\nthe GitHub page.\n\nFor choosing a folder with extracted\nand unmodified game files, use\nthe \"Add Folder\" option instead." }, - { ELanguage::Japanese, "ゲームのコンテンツを含む デジタルダンプを選択してください\n\nこれらのファイルは GitHubページの指示に従って\nXbox 360ハードドライブから取得できます\n\n抽出された変更されていない\nゲームファイルを含むフォルダーを選択するには\n代わりに「フォルダの追加」オプションを使用してください" }, - { ELanguage::German, "Wähle einen digitalen Dump von dem Spiel.\n\nDie Dateien können über die Festplatte deiner\nXbox 360 erlangt werden.\nFolge hierfür den Anweisungen auf der GitHub Seite.\n\nUm einen Ordner mit unmodifizierten Spieldateien auszuwählen, benutze die \"Ordner Hinzufügen\" Option stattdessen." }, - { ELanguage::French, "Sélectionnez une copie\ndématérialisée avec le contenu du\njeu de base.\n\nCes fichiers peuvent être obtenus\nà partir du disque dur de votre\nXbox 360 en suivant les\ninstructions de la page GitHub.\n\nPour choisir un dossier contenant\nles fichiers de jeu extraits et\nnon modifiés, utilisez plutôt\nl'option \"Ajouter un dossier\"." }, - { ELanguage::Spanish, "Selecciona una copia digital\ncon contenido del juego.\n\nPuedes obtener los archivos\nde tu disco duro de Xbox 360\nsiguiendo las instrucciones de\nla página de GitHub.\n\nPara elegir una carpeta con\narchivos extraídos sin modificar,\nutiliza la opción \"Añadir Carpeta\"." }, - { ELanguage::Italian, "Seleziona una copia digitale\ncon i contenuti del gioco.\n\nQuesti file possono essere ottenuti\ndall'hard drive della tua Xbox 360\nseguendo le istruzioni\nsulla pagina GitHub.\n\nPer selezionare una cartella\ncon file estratti e non modificati\nusa l'opzione \"Aggiungi cartella\"." } + { ELanguage::English, "Select a digital dump with content from the game.\n\nThese files can be obtained from your Xbox 360 hard drive by following the instructions on the GitHub page.\n\nFor choosing a folder with extracted and unmodified game files, use the \"Add Folder\" option instead." }, + { ELanguage::Japanese, "ゲームのコンテンツを含む デジタルダンプを選択してください\n\nこれらのファイルは GitHubページの指示に従って\nXbox 360ハードドライブから取得できます\n\n抽出された変更されていないゲームファイルを含むフォルダーを選択するには代わりに「フォルダの追加」オプションを使用してください" }, + { ELanguage::German, "Wähle einen digitalen Dump von dem Spiel.\n\nDie Dateien können über die Festplatte deiner\nXbox 360 erlangt werden. Folge hierfür den Anweisungen auf der GitHub Seite.\n\nUm einen Ordner mit unmodifizierten Spieldateien auszuwählen, benutze die \"Ordner hinzufügen\" Option stattdessen." }, + { ELanguage::French, "Sélectionnez une copie dématérialisée avec le contenu du jeu de base.\n\nCes fichiers peuvent être obtenus à partir du disque dur de votre Xbox 360 en suivant les instructions de la page GitHub.\n\nPour choisir un dossier contenant les fichiers de jeu extraits et non modifiés, utilisez plutôt l'option \"Ajouter un dossier\"." }, + { ELanguage::Spanish, "Selecciona una copia digital con contenido del juego.\n\nPuedes obtener los archivos de tu disco duro de\nXbox 360 siguiendo las instrucciones de la página de GitHub.\n\nPara elegir una carpeta con archivos extraídos sin modificar, utiliza la opción \"Añadir carpeta\"." }, + { ELanguage::Italian, "Seleziona una copia digitale con i contenuti del gioco.\n\nQuesti file possono essere ottenuti dall'hard drive della tua Xbox 360 seguendo le istruzioni sulla pagina GitHub.\n\nPer selezionare una cartella con file estratti e non modificati usa l'opzione \"Aggiungi cartella\"." } } }, { // Notes: message appears when clicking the "Add Folder" option for the first time. "Installer_Message_FolderPickerTutorial", { - { ELanguage::English, "Select a folder that contains the\nunmodified files that have been\nextracted from the game.\n\nThese files can be obtained from\nyour Xbox 360 hard drive by\nfollowing the instructions on\nthe GitHub page.\n\nFor choosing a digital dump,\nuse the \"Add Files\" option instead." }, - { ELanguage::Japanese, "ゲームから抽出された変更されていない\nファイルを含むフォルダを選択してください\n\nこれらのファイルは GitHubページの指示に従って\nXbox 360ハードドライブから取得できます\n\nデジタルダンプを選択するには\n代わりに「ファイルの追加」オプションを使用してください" }, - { ELanguage::German, "Wähle einen Ordner, der unmodifizierte Dateien, die vom Spiel extrahiert wurden enthält.\n\nDie Dateien können über die Festplatte deiner\nXbox 360 erlangt werden.\nFolge hierfür den Anweisungen auf der GitHub Seite.\n\nUm einen digitalen Dump auszuwählen, benutze die \"Datei Hinzufügen\" Option stattdessen." }, - { ELanguage::French, "Sélectionnez un dossier contenant\nles fichiers extraits du jeu de\nbase.\n\nCes fichiers peuvent être obtenus\nà partir du disque dur de votre\nXbox 360 en suivant les\ninstructions de la page GitHub.\n\nPour choisir une copie\ndématérialisée, utilisez plutôt\nl'option \"Ajouter des fichiers\"." }, - { ELanguage::Spanish, "Selecciona una carpeta que\ncontenga los archivos sin\nmodificar extraídos del juego.\n\nPuedes obtener los archivos\nde tu disco duro de Xbox 360\nsiguiendo las instrucciones de\nla página de GitHub.\n\nPara elegir una copia digital,\nutiliza la opción \"Añadir Archivos\"." }, - { ELanguage::Italian, "Seleziona una cartella che contiene\ni file non modificati che sono stati\nestratti dal gioco.\n\nQuesti file possono essere ottenuti\ndall'hard drive della tua Xbox 360\nseguendo le istruzioni\nsulla pagina GitHub.\n\nPer selezionare una copia digitale\nusa l'opzione \"Aggiungi file\"." } + { ELanguage::English, "Select a folder that contains the unmodified files that have been extracted from the game.\n\nThese files can be obtained from your Xbox 360 hard drive by following the instructions on the GitHub page.\n\nFor choosing a digital dump, use the \"Add Files\" option instead." }, + { ELanguage::Japanese, "ゲームから抽出された変更されていないファイルを含むフォルダを選択してください\n\nこれらのファイルは GitHubページの指示に従って\nXbox 360ハードドライブから取得できます\n\nデジタルダンプを選択するには\n代わりに「ファイルの追加」オプションを使用してください" }, + { ELanguage::German, "Wähle einen Ordner, der unmodifizierte Dateien, die vom Spiel extrahiert wurden enthält.\n\nDie Dateien können über die Festplatte deiner\nXbox 360 erlangt werden. Folge hierfür den Anweisungen auf der GitHub Seite.\n\nUm einen digitalen Dump auszuwählen, benutze die \"Dateien hinzufügen\" Option stattdessen." }, + { ELanguage::French, "Sélectionnez un dossier contenant les fichiers extraits du jeu de base.\n\nCes fichiers peuvent être obtenus à partir du disque dur de votre Xbox 360 en suivant les instructions de la page GitHub.\n\nPour choisir une copie dématérialisée, utilisez plutôt l'option \"Ajouter des fichiers\"." }, + { ELanguage::Spanish, "Selecciona una carpeta que contenga los archivos sin modificar extraídos del juego.\n\nPuedes obtener los archivos de tu disco duro de\nXbox 360 siguiendo las instrucciones de la página de GitHub.\n\nPara elegir una copia digital, utiliza la opción \"Añadir archivos\"." }, + { ELanguage::Italian, "Seleziona una cartella che contiene i file non modificati che sono stati estratti dal gioco.\n\nQuesti file possono essere ottenuti dall'hard drive della tua Xbox 360 seguendo le istruzioni sulla pagina GitHub.\n\nPer selezionare una copia digitale usa l'opzione \"Aggiungi file\"." } } }, { // Notes: message appears when choosing the Install option at the title screen when the user is missing DLC content. + // TODO: adjust line breaks for new message window. "Installer_Message_TitleMissingDLC", { { ELanguage::English, "This will restart the game to\nallow you to install any DLC\nthat you are missing.\n\nWould you like to install missing\ncontent?" }, @@ -641,6 +638,7 @@ std::unordered_map> }, { // Notes: message appears when choosing the Install option at the title screen when the user is not missing any content. + // TODO: adjust line breaks for new message window. "Installer_Message_Title", { { ELanguage::English, "This restarts the game to\nallow you to install any DLC\nthat you may be missing.\n\nYou are not currently\nmissing any DLC.\n\nWould you like to proceed\nanyway?" }, diff --git a/MarathonRecomp/main.cpp b/MarathonRecomp/main.cpp index 600d1848e..e0d1e9fdb 100644 --- a/MarathonRecomp/main.cpp +++ b/MarathonRecomp/main.cpp @@ -297,19 +297,17 @@ int main(int argc, char *argv[]) std::filesystem::path modulePath; bool isGameInstalled = Installer::checkGameInstall(GetGamePath(), modulePath); bool runInstallerWizard = forceInstaller || forceDLCInstaller || !isGameInstalled; - if (runInstallerWizard) - { - if (!Video::CreateHostDevice(sdlVideoDriver, graphicsApiRetry)) - { - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, GameWindow::GetTitle(), Localise("Video_BackendError").c_str(), GameWindow::s_pWindow); - std::_Exit(1); - } - - if (!InstallerWizard::Run(GetGamePath(), isGameInstalled && forceDLCInstaller)) - { - std::_Exit(0); - } - } + if (runInstallerWizard) + { + if (!Video::CreateHostDevice(sdlVideoDriver, graphicsApiRetry)) + { + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, GameWindow::GetTitle(), Localise("Video_BackendError").c_str(), GameWindow::s_pWindow); + std::_Exit(1); + } + + if (!InstallerWizard::Run(GetGamePath(), isGameInstalled && forceDLCInstaller)) + std::_Exit(0); + } // ModLoader::Init(); diff --git a/MarathonRecomp/res/credits.h b/MarathonRecomp/res/credits.h index 7cbd82ee0..d261c9d79 100644 --- a/MarathonRecomp/res/credits.h +++ b/MarathonRecomp/res/credits.h @@ -1,22 +1,17 @@ #pragma once -inline std::array g_credits = +inline std::array g_credits = { - "Skyth", - "Sajid", + "ga2mer", + "IsaacMarovitz", + "squidbus", "Hyper", - "Darío", - "DeaThProj", - "RadiantDerg", - "PTKay", - "SuperSonic16", - "NextinHKRY", - "LadyLunanova", + "Rei-san", + "Desko", "LJSTAR", - "saguinee", - "Goalringmod27", - "M&M", - "DaGuAr", "brianuuuSonic", - "Kitzuku" + "Kitzuku", + "Ray Vassos", + "DaGuAr", + "NextinHKRY" }; diff --git a/MarathonRecomp/ui/common_menu.cpp b/MarathonRecomp/ui/common_menu.cpp index 7301b1763..943fe25d0 100644 --- a/MarathonRecomp/ui/common_menu.cpp +++ b/MarathonRecomp/ui/common_menu.cpp @@ -1,5 +1,6 @@ #include "common_menu.h" #include +#include #include #include @@ -353,6 +354,13 @@ void CommonMenu::Draw() DrawVersionString(IM_COL32(0, 0, 0, 70 * verAlphaMotionTime)); } + + // Draw faded letterbox at narrow aspect ratios. + if (g_aspectRatio < NARROW_ASPECT_RATIO) + { + BlackBar::Show(true); + BlackBar::SetBorderMargin(Scale(BlackBar::ms_MenuBorderMargin, true)); + } } void CommonMenu::Open() diff --git a/MarathonRecomp/ui/imgui_utils.cpp b/MarathonRecomp/ui/imgui_utils.cpp index 9d32e9686..188ce9b64 100644 --- a/MarathonRecomp/ui/imgui_utils.cpp +++ b/MarathonRecomp/ui/imgui_utils.cpp @@ -13,17 +13,23 @@ #include #include #include +#include +#include #include ImFont* g_pFntRodin; ImFont* g_pFntNewRodin; +float g_fntRodinSize{}; + std::unique_ptr g_upTexButtonWindow; std::unique_ptr g_upTexController; std::unique_ptr g_upTexKbm; std::unique_ptr g_upTexWindow; std::unique_ptr g_upTexSelectArrow; std::unique_ptr g_upTexMainMenu1; +std::unique_ptr g_upTexMainMenu7; +std::unique_ptr g_upTexMainMenu8; std::unique_ptr g_upTexArrow; void InitImGuiUtils() @@ -37,9 +43,16 @@ void InitImGuiUtils() g_upTexWindow = LOAD_ZSTD_TEXTURE(g_window); g_upTexSelectArrow = LOAD_ZSTD_TEXTURE(g_select_arrow); g_upTexMainMenu1 = LOAD_ZSTD_TEXTURE(g_main_menu1); + g_upTexMainMenu7 = LOAD_ZSTD_TEXTURE(g_main_menu7); + g_upTexMainMenu8 = LOAD_ZSTD_TEXTURE(g_main_menu8); g_upTexArrow = LOAD_ZSTD_TEXTURE(g_arrow); } +void UpdateImGuiUtils() +{ + g_fntRodinSize = Scale(Config::Language == ELanguage::Japanese ? 28 : 27, true); +} + void SetGradient(const ImVec2& min, const ImVec2& max, ImU32 top, ImU32 bottom) { SetGradient(min, max, top, top, bottom, bottom); @@ -380,25 +393,21 @@ void DrawContainerBox(ImVec2 min, ImVec2 max, float alpha) auto commonHeight = Scale(50); auto bottomHeight = Scale(5); - auto tl = PIXELS_TO_UV_COORDS(1024, 1024, 0, 400, 50, 50); - auto tc = PIXELS_TO_UV_COORDS(1024, 1024, 50, 400, 50, 50); - auto cl = PIXELS_TO_UV_COORDS(1024, 1024, 0, 450, 50, 50); - auto cc = PIXELS_TO_UV_COORDS(1024, 1024, 50, 450, 50, 50); - auto bl = PIXELS_TO_UV_COORDS(1024, 1024, 0, 500, 50, 50); - auto bc = PIXELS_TO_UV_COORDS(1024, 1024, 50, 500, 50, 50); - - auto color = IM_COL32(255, 255, 255, 100 * alpha); - - SetHorizontalGradient({ max.x - commonWidth, min.y }, max, IM_COL32_WHITE, IM_COL32(255, 255, 255, 0)); - - drawList->AddImage(g_upTexMainMenu1.get(), min, { min.x + commonWidth, min.y + commonHeight }, GET_UV_COORDS(tl), color); - drawList->AddImage(g_upTexMainMenu1.get(), { min.x + commonWidth, min.y }, { max.x, min.y + commonHeight }, GET_UV_COORDS(tc), color); - drawList->AddImage(g_upTexMainMenu1.get(), { min.x, min.y + commonHeight }, { min.x + commonWidth, max.y - commonHeight }, GET_UV_COORDS(cl), color); - drawList->AddImage(g_upTexMainMenu1.get(), { min.x + commonWidth, min.y + commonHeight }, { max.x, max.y - commonHeight }, GET_UV_COORDS(cc), color); - drawList->AddImage(g_upTexMainMenu1.get(), { min.x, max.y - commonHeight }, { min.x + commonWidth, max.y + bottomHeight }, GET_UV_COORDS(bl), color); - drawList->AddImage(g_upTexMainMenu1.get(), { min.x + commonWidth, max.y - commonHeight }, { max.x, max.y + bottomHeight }, GET_UV_COORDS(bc), color); - - ResetGradient(); + auto tl = PIXELS_TO_UV_COORDS(1024, 1024, 1, 400, 50, 50); + auto tc = PIXELS_TO_UV_COORDS(1024, 1024, 51, 400, 50, 50); + auto cl = PIXELS_TO_UV_COORDS(1024, 1024, 1, 450, 50, 50); + auto cc = PIXELS_TO_UV_COORDS(1024, 1024, 51, 450, 50, 50); + auto bl = PIXELS_TO_UV_COORDS(1024, 1024, 1, 500, 50, 50); + auto bc = PIXELS_TO_UV_COORDS(1024, 1024, 51, 500, 50, 50); + + auto colour = IM_COL32(255, 255, 255, 100 * alpha); + + drawList->AddImage(g_upTexMainMenu1.get(), min, { min.x + commonWidth, min.y + commonHeight }, GET_UV_COORDS(tl), colour); + drawList->AddImage(g_upTexMainMenu1.get(), { min.x + commonWidth, min.y }, { max.x, min.y + commonHeight }, GET_UV_COORDS(tc), colour); + drawList->AddImage(g_upTexMainMenu1.get(), { min.x, min.y + commonHeight }, { min.x + commonWidth, max.y - commonHeight }, GET_UV_COORDS(cl), colour); + drawList->AddImage(g_upTexMainMenu1.get(), { min.x + commonWidth, min.y + commonHeight }, { max.x, max.y - commonHeight }, GET_UV_COORDS(cc), colour); + drawList->AddImage(g_upTexMainMenu1.get(), { min.x, max.y - commonHeight }, { min.x + commonWidth, max.y + bottomHeight }, GET_UV_COORDS(bl), colour); + drawList->AddImage(g_upTexMainMenu1.get(), { min.x + commonWidth, max.y - commonHeight }, { max.x, max.y + bottomHeight }, GET_UV_COORDS(bc), colour); } void DrawTextBasic(const ImFont* font, float fontSize, const ImVec2& pos, ImU32 colour, const char* text) @@ -709,21 +718,22 @@ ImU32 ColourLerp(ImU32 c0, ImU32 c1, float t) void DrawVersionString(const ImU32 colour) { - auto drawList = ImGui::GetBackgroundDrawList(); auto& res = ImGui::GetIO().DisplaySize; + auto drawList = ImGui::GetBackgroundDrawList(); auto fontSize = Scale(12, true); auto textSize = g_pFntNewRodin->CalcTextSizeA(fontSize, FLT_MAX, 0, g_versionString); - auto textMargin = Scale(2, true); - auto textY = res.y - textSize.y - textMargin; + auto textMarginX = Scale(8, true); + auto textMarginY = Scale(5, true); + auto textY = res.y - textSize.y - textMarginY; if (g_aspectRatio < NARROW_ASPECT_RATIO) - textY -= BlackBar::s_letterboxHeight - BlackBar::s_margin; + textY -= g_vertCentre; // TODO: remove this line after v1 release. - drawList->AddText(g_pFntNewRodin, fontSize, { textMargin, textY }, colour, "WORK IN PROGRESS"); - drawList->AddText(g_pFntNewRodin, fontSize, { res.x - textSize.x - textMargin, textY }, colour, g_versionString); + drawList->AddText(g_pFntNewRodin, fontSize, { textMarginX, textY }, colour, "WORK IN PROGRESS"); + drawList->AddText(g_pFntNewRodin, fontSize, { res.x - textSize.x - textMarginX, textY }, colour, g_versionString); } static void DrawWindowArrow(const ImVec2 pos, float scale, float rotation, uint32_t colour) @@ -1235,7 +1245,8 @@ ImGuiTextInterpData GetHidInterpTextData() hid::g_inputDevice == hid::EInputDevice::Mouse) { buttonTexture = &g_upTexKbm; - buttonTextureWidth = uint16_t(384); + buttonTextureWidth = uint16_t(84); + buttonTextureHeight = uint16_t(28); } if (hid::g_inputDevice == hid::EInputDevice::Keyboard) @@ -1281,12 +1292,12 @@ const xxHashMap g_buttonCropsPS3 = const xxHashMap g_buttonCropsMouse = { - { HashStr("button_a"), { 128, 0, 128, 128 } }, - { HashStr("button_b"), { 256, 0, 128, 128 } } + { HashStr("button_a"), { 0, 0, 28, 28 } }, + { HashStr("button_b"), { 56, 0, 28, 28 } } }; const xxHashMap g_buttonCropsKeyboard = { - { HashStr("button_a"), { 0, 0, 128, 128 } }, - { HashStr("button_b"), { 256, 0, 128, 128 } } + { HashStr("button_a"), { 28, 0, 28, 28 } }, + { HashStr("button_b"), { 56, 0, 28, 28 } } }; diff --git a/MarathonRecomp/ui/imgui_utils.h b/MarathonRecomp/ui/imgui_utils.h index 2fffa213c..103d0d5f5 100644 --- a/MarathonRecomp/ui/imgui_utils.h +++ b/MarathonRecomp/ui/imgui_utils.h @@ -20,12 +20,16 @@ extern ImFont* g_pFntRodin; extern ImFont* g_pFntNewRodin; +extern float g_fntRodinSize; + extern std::unique_ptr g_upTexButtonWindow; extern std::unique_ptr g_upTexController; extern std::unique_ptr g_upTexKbm; extern std::unique_ptr g_upTexWindow; extern std::unique_ptr g_upTexSelectArrow; extern std::unique_ptr g_upTexMainMenu1; +extern std::unique_ptr g_upTexMainMenu7; +extern std::unique_ptr g_upTexMainMenu8; struct ImGuiTextPicture { @@ -54,6 +58,7 @@ struct ImGuiTextInterpData }; void InitImGuiUtils(); +void UpdateImGuiUtils(); void SetGradient(const ImVec2& min, const ImVec2& max, ImU32 top, ImU32 bottom); void SetHorizontalGradient(const ImVec2& min, const ImVec2& max, ImU32 left, ImU32 right); diff --git a/MarathonRecomp/ui/installer_wizard.cpp b/MarathonRecomp/ui/installer_wizard.cpp index 309239f0f..bcb25a944 100644 --- a/MarathonRecomp/ui/installer_wizard.cpp +++ b/MarathonRecomp/ui/installer_wizard.cpp @@ -1,24 +1,23 @@ #include "installer_wizard.h" -#include - #include -#include #include #include #include +#include #include #include -#include #include #include -#include +#include #include +#include +#include #include #include +#include #include -#include #include #include #include @@ -27,85 +26,104 @@ #include #include #include +#include #include -// One Shot Animations Constants -static constexpr double SCANLINES_ANIMATION_TIME = 0.0; -static constexpr double SCANLINES_ANIMATION_DURATION = 15.0; - -static constexpr double IMAGE_ANIMATION_TIME = SCANLINES_ANIMATION_TIME + SCANLINES_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_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 QUITTING_EXTRA_DURATION = 60.0; - -constexpr float IMAGE_WIDTH = 1000.0f; -constexpr float IMAGE_HEIGHT = 1000.0f; - -constexpr float CONTAINER_X = 650.0f; -constexpr float CONTAINER_Y = 226.0f; -constexpr float CONTAINER_WIDTH = 526.5f; -constexpr float CONTAINER_HEIGHT = 246.0f; -constexpr float CONTAINER_PADDING = 25.0f; -constexpr float SIDE_CONTAINER_WIDTH = CONTAINER_WIDTH / 2.0f; - -constexpr float BOTTOM_X_GAP = 4.0f; -constexpr float BOTTOM_Y_GAP = 5.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; - -static constexpr size_t GRID_SIZE = 9; - -static CommonMenu g_commonMenu; - -static double g_chevronTime = 0.0; -static double g_appearTime = 0.0; -static double g_disappearTime = DBL_MAX; -static bool g_isDisappearing = false; -static bool g_isQuitting = false; - -static std::filesystem::path g_installPath; -static std::filesystem::path g_gameSourcePath; -static std::array g_dlcSourcePaths; -static std::array g_dlcInstalled = {}; -static std::array, 8> g_installTextures; -static std::unique_ptr g_upSonicNextDev; -static Journal g_installerJournal; -static Installer::Sources g_installerSources; -static uint64_t g_installerAvailableSize = 0; -static std::unique_ptr g_installerThread; -static double g_installerStartTime = 0.0; -static double g_installerEndTime = DBL_MAX; -static float g_installerProgressRatioCurrent = 0.0f; -static std::atomic g_installerProgressRatioTarget = 0.0f; -static std::atomic g_installerFinished = false; -static std::atomic g_installerHalted = false; -static std::atomic g_installerCancelled = false; -static bool g_installerFailed = false; -static std::string g_installerErrorMessage; - -static std::array g_installTexturePositions = { ImVec2(120.0f, 90.0f), // Sonic - ImVec2(100.0f, 80.0f), // Tails - ImVec2(120.0f, 90.0f), // Amy - ImVec2(10.0f, 90.0f), // Shadow - ImVec2(120.0f, 90.0f), // Rouge - ImVec2(120.0f, 10.0f), // Silver - ImVec2(10.0f, 140.0f), // Eggman - ImVec2(200.0f, 140.0f) }; // Elise +static constexpr float COMMON_MENU_INTRO_TIME = 10.0f; +static constexpr float COMMON_FADE_TIME = 25.0f; + +static constexpr float NAV_BUTTON_OFFSET_Y = 39.0f; +static constexpr float NAV_BUTTON_MARGIN = 48.0f; + +static CommonMenu g_commonMenu{}; + +static std::array, 8> g_installTextures{}; +static std::unique_ptr g_upTexSonicNextDev{}; + +static double g_chevronTime{}; +static double g_cursorArrowsTime{}; +static double g_appearTime{}; + +static double g_alphaTime{}; +static double g_alphaMotion{}; + +static bool g_isIntroAnim{ true }; +static bool g_isDisappearing{}; +static bool g_isQuitting{}; + +static std::filesystem::path g_installPath{}; +static std::filesystem::path g_gameSourcePath{}; +static std::array g_dlcSourcePaths{}; +static std::array g_dlcInstalled{}; + +static Journal g_installerJournal{}; +static Installer::Sources g_installerSources{}; +static uint64_t g_installerAvailableSize{}; +static std::unique_ptr g_installerThread{}; +static double g_installerStartTime{}; +static double g_installerEndTime{ DBL_MAX }; +static float g_installerProgressRatioCurrent{}; +static std::atomic g_installerProgressRatioTarget{}; +static std::atomic g_installerFinished{}; +static std::atomic g_installerHalted{}; +static std::atomic g_installerCancelled{}; +static bool g_installerFailed{}; +static std::string g_installerErrorMessage{}; + +static std::array g_installTexturePositions = +{ + ImVec2(110.0f, 90.0f), // Sonic + ImVec2(-20.0f, 90.0f), // Shadow + ImVec2(110.0f, 10.0f), // Silver + ImVec2(25.0f, 80.0f), // Tails + ImVec2(100.0f, 90.0f), // Rouge + ImVec2(110.0f, 90.0f), // Amy + ImVec2(180.0f, 140.0f), // Elise + ImVec2(0.0f, 140.0f) // Eggman +}; + +static const int g_installTextureIndices[] = +{ + 0, // SelectLanguage + 1, // Introduction + 0, // SelectGame -- this page doesn't display a character. + 0, // SelectDLC --- this page doesn't display a character. + 2, // CheckSpace + 3, // Installing + 6, // InstallSucceeded -- force Elise. + 7 // InstallFailed ----- force Eggman. +}; + +const char* g_languageText[] = +{ + "English", // English + "日本語", // Japanese + "Deutsch", // German + "Français", // French + "Español", // Spanish + "Italiano" // Italian +}; + +const ELanguage g_languageEnum[] = +{ + ELanguage::English, + ELanguage::Japanese, + ELanguage::German, + ELanguage::French, + ELanguage::Spanish, + ELanguage::Italian +}; + +const char* g_dlcText[] = +{ + "Sonic Boss Attack", + "Shadow Boss Attack", + "Silver Boss Attack", + "Team Attack Amigo", + "Sonic/Very Hard", + "Shadow/Very Hard", + "Silver/Very Hard", +}; enum class WizardPage { @@ -126,38 +144,45 @@ enum class MessagePromptSource Back }; -static WizardPage g_firstPage = WizardPage::SelectLanguage; +static WizardPage g_firstPage{}; static WizardPage g_currentPage = g_firstPage; -static std::string g_currentMessagePrompt = ""; -static MessagePromptSource g_currentMessagePromptSource = MessagePromptSource::Unknown; -static bool g_currentMessagePromptConfirmation = false; -static std::list g_currentPickerResults; -static std::atomic g_currentPickerResultsReady = false; -static std::string g_currentPickerErrorMessage; -static std::unique_ptr g_currentPickerThread; -static bool g_pickerTutorialCleared[2] = {}; -static bool g_pickerTutorialTriggered = false; -static bool g_pickerTutorialFolderMode = false; -static bool g_currentPickerVisible = false; -static bool g_currentPickerFolderMode = false; -static int g_currentMessageResult = -1; -static ImVec2 g_joypadAxis = {}; -static int g_currentCursorIndex = -1; -static int g_currentCursorDefault = 0; -static bool g_currentCursorAccepted = false; -static bool g_currentCursorBack = false; -static std::vector> g_currentCursorRects; -static std::string g_creditsStr; + +static std::string g_currentMessagePrompt{}; +static MessagePromptSource g_currentMessagePromptSource{}; +static bool g_currentMessagePromptConfirmation{}; +static int g_currentMessageResult{ -1 }; + +static std::list g_currentPickerResults{}; +static std::atomic g_currentPickerResultsReady{}; +static std::string g_currentPickerErrorMessage{}; +static std::unique_ptr g_currentPickerThread{}; + +static bool g_pickerTutorialCleared[2]{}; +static bool g_pickerTutorialTriggered{}; +static bool g_pickerTutorialFolderMode{}; +static bool g_currentPickerVisible{}; +static bool g_currentPickerFolderMode{}; + +static int g_currentCursorIndex{ -1 }; +static int g_currentCursorDefault{}; +static bool g_currentCursorAccepted{}; +static bool g_currentCursorBack{}; +static std::vector> g_currentCursorRects{}; + +static std::string g_creditsStr{}; class SDLEventListenerForInstaller : public SDLEventListener { + ImVec2 m_joypadAxis = {}; + public: - bool OnSDLEvent(SDL_Event *event) override + bool OnSDLEvent(SDL_Event* event) override { - if (!InstallerWizard::s_isVisible) + if (!InstallerWizard::s_isVisible || g_isDisappearing) return false; - bool noModals = g_currentMessagePrompt.empty() && !g_currentPickerVisible; + auto noModals = g_currentMessagePrompt.empty() && !g_currentPickerVisible; + if (event->type == SDL_QUIT && g_currentPage == WizardPage::Installing) { // Pretend the back button was pressed if the user tried quitting during installation. @@ -172,10 +197,10 @@ class SDLEventListenerForInstaller : public SDLEventListener if (!noModals || !hid::IsInputAllowed()) return false; - constexpr float AxisValueRange = 32767.0f; - constexpr float AxisTapRange = 0.5f; - int newCursorIndex = -1; - ImVec2 tapDirection = {}; + constexpr auto axisValueRange = 32767.0f; + constexpr auto axisTapRange = 0.5f; + auto newCursorIndex = -1; + auto tapDirection = ImVec2(); switch (event->type) { @@ -187,14 +212,17 @@ class SDLEventListenerForInstaller : public SDLEventListener 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; + case SDL_SCANCODE_ESCAPE: g_currentCursorBack = true; break; @@ -210,18 +238,23 @@ class SDLEventListenerForInstaller : public SDLEventListener 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; + case SDL_CONTROLLER_BUTTON_B: g_currentCursorBack = true; break; @@ -234,16 +267,15 @@ class SDLEventListenerForInstaller : public SDLEventListener { 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; + auto newAxisValue = event->caxis.value / axisValueRange; + auto sameDirection = (newAxisValue * m_joypadAxis[event->caxis.axis]) > 0.0f; + auto wasInRange = abs(m_joypadAxis[event->caxis.axis]) > axisTapRange; + auto isInRange = abs(newAxisValue) > axisTapRange; + if (sameDirection && !wasInRange && isInRange) - { tapDirection[event->caxis.axis] = newAxisValue; - } - g_joypadAxis[event->caxis.axis] = newAxisValue; + m_joypadAxis[event->caxis.axis] = newAxisValue; } break; @@ -282,31 +314,33 @@ class SDLEventListenerForInstaller : public SDLEventListener } else { - auto ¤tRect = g_currentCursorRects[g_currentCursorIndex]; - ImVec2 currentPoint = ImVec2 + auto& currentRect = g_currentCursorRects[g_currentCursorIndex]; + + auto 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; + auto 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 + auto& targetRect = g_currentCursorRects[i]; + + auto 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); + auto delta = ImVec2(targetPoint.x - currentPoint.x, targetPoint.y - currentPoint.y); + auto projectedDistance = delta.x * tapDirection.x + delta.y * tapDirection.y; + auto manhattanDistance = abs(delta.x) + abs(delta.y); + if (projectedDistance > 0.0f && manhattanDistance < closestDistance) { newCursorIndex = int(i); @@ -319,7 +353,10 @@ class SDLEventListenerForInstaller : public SDLEventListener if (newCursorIndex >= 0) { if (g_currentCursorIndex != newCursorIndex) + { Game_PlaySound("move"); + g_cursorArrowsTime = ImGui::GetTime(); + } g_currentCursorIndex = newCursorIndex; } @@ -329,76 +366,33 @@ class SDLEventListenerForInstaller : public SDLEventListener } g_sdlEventListenerForInstaller; -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::SelectGame: return Localise("Installer_Page_SelectGame"); - 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, - 1, - 2, - 3, - 4, - 5, - 7, // Force Elise on InstallSucceeded. - 6 // Force Eggman on InstallFailed. -}; - -// These are ordered from bottom to top in a 3x2 grid. -const char* LANGUAGE_TEXT[] = +static void LeaveInstallerWizard(bool isQuitting = false) { - "FRANÇAIS", // French - "DEUTSCH", // German - "ENGLISH", // English - "ESPAÑOL", // Spanish - "ITALIANO", // Italian - "日本語", // Japanese -}; + g_isDisappearing = true; -const ELanguage LANGUAGE_ENUM[] = -{ - ELanguage::French, - ELanguage::German, - ELanguage::English, - ELanguage::Spanish, - ELanguage::Italian, - ELanguage::Japanese, -}; + Fader::FadeOut + ( + 1.0f, -const char *DLC_SOURCE_TEXT[] = -{ - "SONIC BOSS ATTACK", - "SHADOW BOSS ATTACK", - "SILVER BOSS ATTACK", - "TEAM ATTACK AMIGO", - "SONIC VERY HARD", - "SHADOW VERY HARD", - "SILVER VERY HARD", -}; + [=]() + { + g_isQuitting = isQuitting; + InstallerWizard::s_isVisible = false; + } + ); +} static int DLCIndex(DLC dlc) { assert(dlc != DLC::Unknown); + return (int)(dlc) - 1; } static void SetCurrentPage(WizardPage page) { g_currentPage = page; + g_cursorArrowsTime = ImGui::GetTime(); if (g_currentPage == WizardPage::InstallSucceeded) { @@ -423,388 +417,72 @@ static void SetCurrentPage(WizardPage page) } } -static double ComputeMotionInstaller(double timeAppear, double timeDisappear, double offset, double total) +static bool PushCursorRect(ImVec2 min, ImVec2 max, bool& isPressed, bool isDefault = false) { - return ComputeMotion(timeAppear, offset, total) * (1.0 - ComputeMotion(timeDisappear, ALL_ANIMATIONS_FULL_DURATION - offset - total, total)); -} + auto currentIndex = int(g_currentCursorRects.size()); -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) - { + if (isDefault) g_currentCursorDefault = currentIndex; - } if (g_currentCursorIndex == currentIndex) { if (g_currentCursorAccepted) { - Game_PlaySound("main_deside"); - cursorPressed = true; + isPressed = 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::GetBackgroundDrawList(); - - const uint32_t TOP = IM_COL32(0, 103, 255, 255); - const uint32_t BOTTOM = IM_COL32(0, 40, 100, 255); - drawList->AddRectFilledMultiColor({ 0.0, 0.0 }, res, TOP, TOP, BOTTOM, BOTTOM); + return false; } -static void DrawArrows() +static void DrawProgressBar(ImVec2 originMin, ImVec2 originMax, float progress) { auto& res = ImGui::GetIO().DisplaySize; - - DrawArrows({ 0, 0 }, res, g_chevronTime); -} - -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(140.0 * imageAlpha); - GuestTexture *guestTexture = g_installTextures[installTextureIndex % g_installTextures.size()].get(); - auto &res = ImGui::GetIO().DisplaySize; - auto drawList = ImGui::GetBackgroundDrawList(); - auto pos = g_installTexturePositions[installTextureIndex % g_installTextures.size()]; - ImVec2 min = { g_aspectRatioOffsetX + Scale(pos.x), g_aspectRatioOffsetY + Scale(pos.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 DrawContainer(ImVec2 min, ImVec2 max, bool isTextArea) -{ - auto &res = ImGui::GetIO().DisplaySize; - auto drawList = ImGui::GetBackgroundDrawList(); - - double gridOverlayAlpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_INNER_TIME, CONTAINER_INNER_DURATION); - - if (isTextArea) - { - auto padding = Scale(CONTAINER_PADDING); - - DrawContainerBox({ min.x - padding, min.y - padding }, { max.x + padding, max.y + padding }, gridOverlayAlpha); - } - - float gridSize = Scale(GRID_SIZE); - - // 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::GetBackgroundDrawList(); - auto fontSize = Scale(25.0f); - - ImVec2 descriptionMin = { round(g_aspectRatioOffsetX + Scale(CONTAINER_X + 0.5f)), round(g_aspectRatioOffsetY + Scale(CONTAINER_Y + 0.5f)) }; - ImVec2 descriptionMax = { round(g_aspectRatioOffsetX + Scale(CONTAINER_X + 0.5f + CONTAINER_WIDTH)), round(g_aspectRatioOffsetY + Scale(CONTAINER_Y + 0.5f + 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) - { - // Japanese needs text to be brought in by a normal width space - // as it allows for text to begin further than others for - // special characters. - if (Config::Language == ELanguage::Japanese) - { - strncat(descriptionText, " ", 1); - } - - 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(); - - float textX = clipRectMin.x + fontSize; - float textY = clipRectMin.y - Scale(1.0f); - - auto lineWidth = clipRectMax.x - (fontSize / 2.0f) - clipRectMin.x; - - clipRectMax.x += fontSize; - clipRectMax.y += Scale(1.0f); - - float lineMargin = 5.0f; - - drawList->PushClipRect(clipRectMin, clipRectMax, false); - - DrawTextParagraph - ( - g_pFntRodin, - fontSize, - lineWidth, - { textX, textY }, - lineMargin, - descriptionText, - - [=](const char* str, ImVec2 pos) - { - DrawTextBasic(g_pFntRodin, fontSize, pos, IM_COL32(255, 255, 255, 255 * textAlpha), str); - } - ); - - drawList->PopClipRect(); - drawList->PopClipRect(); - - if (g_currentPage == WizardPage::InstallSucceeded) - { - auto descTextSize = MeasureCentredParagraph(g_pFntRodin, fontSize, lineWidth, lineMargin, descriptionText); - - auto colWhite = IM_COL32(255, 255, 255, 255 * textAlpha); - - auto containerLeft = g_aspectRatioOffsetX + Scale(CONTAINER_X); - auto containerTop = g_aspectRatioOffsetY + Scale(CONTAINER_Y); - auto containerRight = containerLeft + Scale(CONTAINER_WIDTH); - auto containerBottom = containerTop + Scale(CONTAINER_HEIGHT); - - auto marqueeTextSize = g_pFntRodin->CalcTextSizeA(fontSize, FLT_MAX, 0, g_creditsStr.c_str()); - auto marqueeTextMarginX = Scale(5); - auto marqueeTextMarginY = Scale(15); - - ImVec2 marqueeTextPos = { descriptionMax.x, containerBottom - marqueeTextSize.y - marqueeTextMarginY }; - ImVec2 marqueeTextMin = { containerLeft, marqueeTextPos.y }; - ImVec2 marqueeTextMax = { containerRight, containerBottom }; - - auto imageWidth = Scale(524); - auto imageHeight = Scale(45); - - ImVec2 imageRegionMin = { containerLeft, textY + descTextSize.y }; - ImVec2 imageRegionMax = { containerRight, containerBottom - (marqueeTextMax.y - marqueeTextMin.y) }; - - ImVec2 imageMin = - { - /* X */ imageRegionMin.x + ((imageRegionMax.x - imageRegionMin.x) / 2) - (imageWidth / 2), - /* Y */ imageRegionMin.y + ((imageRegionMax.y - imageRegionMin.y) / 2) - (imageHeight / 2) - }; - - ImVec2 imageMax = { imageMin.x + imageWidth, imageMin.y + imageHeight }; - - drawList->AddImage(g_upSonicNextDev.get(), imageMin, imageMax, { 0, 0 }, { 1, 1 }, colWhite); - - // SetHorizontalMarqueeFade(marqueeTextMin, marqueeTextMax, Scale(32)); - // DrawTextWithMarquee(g_pFntRodin, fontSize, marqueeTextPos, marqueeTextMin, marqueeTextMax, colWhite, g_creditsStr.c_str(), g_installerEndTime, 0.9, Scale(200)); - // ResetMarqueeFade(); - } - - ImVec2 sideMin = { descriptionMax.x, descriptionMin.y }; - ImVec2 sideMax = { res.x, descriptionMax.y }; - - DrawContainer(sideMin, sideMax, false); - - drawList->PopClipRect(); - - ResetProceduralOrigin(); -} - -static void DrawButtonContainer(ImVec2 min, ImVec2 max, int baser, int baseg, float alpha) -{ - auto &res = ImGui::GetIO().DisplaySize; - auto drawList = ImGui::GetBackgroundDrawList(); - - 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)); -} - -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::GetBackgroundDrawList(); - 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 = g_pFntRodin; - float size = Scale(18.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; - - SetOrigin({ min.x + textSize.x / 2.0f, min.y }); - SetScale({ squashRatio, 1.0f }); - - DrawTextBasic - ( - font, - size, - min, - IM_COL32(255, 255, 255, 255 * alpha), - buttonText - ); - - 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 = g_aspectRatioOffsetX + Scale(CONTAINER_X + CONTAINER_BUTTON_GAP); - maxX = g_aspectRatioOffsetX + Scale(CONTAINER_X + CONTAINER_BUTTON_GAP + CONTAINER_BUTTON_WIDTH); - break; - case ButtonColumnMiddle: - minX = g_aspectRatioOffsetX + Scale(CONTAINER_X + CONTAINER_BUTTON_GAP); - maxX = g_aspectRatioOffsetX + Scale(CONTAINER_X + CONTAINER_WIDTH - CONTAINER_BUTTON_GAP); - break; - case ButtonColumnRight: - minX = g_aspectRatioOffsetX + Scale(CONTAINER_X + CONTAINER_WIDTH - CONTAINER_BUTTON_GAP - CONTAINER_BUTTON_WIDTH); - maxX = g_aspectRatioOffsetX + Scale(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, g_aspectRatioOffsetY + Scale(CONTAINER_Y + CONTAINER_HEIGHT - CONTAINER_BUTTON_GAP - BUTTON_HEIGHT - minusY) }; - ImVec2 max = { maxX, g_aspectRatioOffsetY + Scale(CONTAINER_Y + CONTAINER_HEIGHT - CONTAINER_BUTTON_GAP - minusY) }; - - auto alphaMotion = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_INNER_TIME, CONTAINER_INNER_DURATION); - auto lightSize = Scale(14); - - DrawButton(min, max, sourceText, true, sourceSet, buttonPressed, ((max.x - min.x) * 0.7) / g_aspectRatioScale); -} - -static void DrawProgressBar(float progressRatio) -{ - auto &res = ImGui::GetIO().DisplaySize; - auto drawList = ImGui::GetBackgroundDrawList(); - 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(4); - float yPadding = Scale(3); - ImVec2 min = { g_aspectRatioOffsetX + Scale(CONTAINER_X) + BOTTOM_X_GAP + Scale(1), g_aspectRatioOffsetY + Scale(CONTAINER_Y + CONTAINER_HEIGHT + BOTTOM_Y_GAP)}; - ImVec2 max = { g_aspectRatioOffsetX + Scale(CONTAINER_X + CONTAINER_WIDTH - BOTTOM_X_GAP), g_aspectRatioOffsetY + Scale(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.5f); - yPadding += Scale(1.5f); - - 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); + auto drawList = ImGui::GetBackgroundDrawList(); + + auto pos = ImVec2(originMin.x - Scale(8, true), originMax.y - Scale(NAV_BUTTON_OFFSET_Y, true)); + auto edgeUVs = PIXELS_TO_UV_COORDS(256, 256, 1, 0, 51, 45); + auto stretchUVs = PIXELS_TO_UV_COORDS(256, 256, 51, 0, 51, 45); + auto edgeWidth = Scale(50, true); + auto edgeHeight = Scale(45, true); + auto colour = IM_COL32(255, 255, 255, 255 * g_alphaMotion); + + auto leftEdgeMin = pos; + auto leftEdgeMax = ImVec2(leftEdgeMin.x + edgeWidth, leftEdgeMin.y + edgeHeight); + auto stretchMin = ImVec2(leftEdgeMax.x, leftEdgeMin.y); + auto stretchMax = ImVec2(stretchMin.x + Scale(400, true), leftEdgeMax.y); + auto rightEdgeMin = ImVec2(stretchMax.x, stretchMin.y); + auto rightEdgeMax = ImVec2(rightEdgeMin.x + edgeWidth, rightEdgeMin.y + edgeHeight); + + drawList->AddImage(g_upTexMainMenu8.get(), leftEdgeMin, leftEdgeMax, GET_UV_COORDS(edgeUVs), colour); + drawList->AddImage(g_upTexMainMenu8.get(), stretchMin, stretchMax, GET_UV_COORDS(stretchUVs), colour); + AddImageFlipped(g_upTexMainMenu8.get(), rightEdgeMin, rightEdgeMax, GET_UV_COORDS(edgeUVs), colour, true); + + auto gaugeOffsetX = Scale(41, true); + auto gaugeHeight = Scale(11, true); + auto gaugeMin = ImVec2(leftEdgeMin.x + gaugeOffsetX, (rightEdgeMin.y + edgeHeight / 2) - (gaugeHeight / 2) + Scale(1, true)); + auto gaugeMax = ImVec2(rightEdgeMax.x - gaugeOffsetX, gaugeMin.y + gaugeHeight / 2); + + drawList->AddRectFilled(gaugeMin, gaugeMax, IM_COL32(0, 0, 0, 255 * g_alphaMotion), Scale(10, true)); + drawList->AddRectFilled(gaugeMin, { gaugeMin.x + (gaugeMax.x - gaugeMin.x) * progress, gaugeMax.y }, IM_COL32(112, 250, 255, 255 * g_alphaMotion), Scale(10, true)); } static bool ConvertPathSet(const nfdpathset_t *pathSet, std::list &filePaths) { nfdpathsetsize_t pathSetCount = 0; + if (NFD_PathSet_GetCount(pathSet, &pathSetCount) != NFD_OKAY) - { return false; - } for (nfdpathsetsize_t i = 0; i < pathSetCount; i++) { - nfdnchar_t *pathSetPath = nullptr; + nfdnchar_t* pathSetPath = nullptr; + if (NFD_PathSet_GetPathN(pathSet, i, &pathSetPath) != NFD_OKAY) { filePaths.clear(); @@ -812,6 +490,7 @@ static bool ConvertPathSet(const nfdpathset_t *pathSet, std::listjoin(); @@ -856,12 +539,15 @@ static void PickerStart(bool 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. + // Optional single thread mode for testing on systems + // that do not interact well with the separate thread + // being used for NFD. #ifdef __APPLE__ constexpr bool singleThreadMode = true; #else constexpr bool singleThreadMode = false; #endif + if (singleThreadMode) PickerThreadProcess(); else @@ -887,12 +573,14 @@ static bool ParseSourcePaths(std::list &paths) { assert((g_currentPage == WizardPage::SelectGame) || (g_currentPage == WizardPage::SelectDLC)); - constexpr size_t failedPathLimit = 5; - bool isFailedPathsOverLimit = false; - std::list failedPaths; + constexpr auto failedPathLimit = 5; + auto isFailedPathsOverLimit = false; + + std::list failedPaths{}; + if (g_currentPage == WizardPage::SelectGame) { - for (const std::filesystem::path &path : paths) + for (const auto& path : paths) { if (Installer::parseGame(path)) { @@ -908,11 +596,12 @@ static bool ParseSourcePaths(std::list &paths) } } } - else if(g_currentPage == WizardPage::SelectDLC) + else if (g_currentPage == WizardPage::SelectDLC) { - for (const std::filesystem::path &path : paths) + for (const auto& path : paths) { - DLC dlc = Installer::parseDLC(path); + auto dlc = Installer::parseDLC(path); + if (dlc != DLC::Unknown) { g_dlcSourcePaths[DLCIndex(dlc)] = path; @@ -926,9 +615,11 @@ static bool ParseSourcePaths(std::list &paths) if (!failedPaths.empty()) { - std::stringstream stringStream; + std::stringstream stringStream{}; + stringStream << Localise("Installer_Message_InvalidFilesList") << std::endl; - for (const std::filesystem::path &path : failedPaths) + + for (const auto& path : failedPaths) { std::u8string filenameU8 = path.filename().u8string(); stringStream << std::endl << "- " << Truncate(std::string(filenameU8.begin(), filenameU8.end()), 32, true, true); @@ -944,114 +635,26 @@ static bool ParseSourcePaths(std::list &paths) return failedPaths.empty(); } -static void DrawLanguagePicker() +static void InstallerThread() { - bool buttonPressed = false; - if (g_currentPage == WizardPage::SelectLanguage) - { - float alphaMotion = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_INNER_TIME, CONTAINER_INNER_DURATION); - float minX, maxX; - bool buttonPressed; - - for (int i = 0; i < 6; i++) + auto result = Installer::install(g_installerSources, g_installPath, false, g_installerJournal, std::chrono::seconds(1), + // + [&]() { - ComputeButtonColumnCoordinates((i < 3) ? ButtonColumnLeft : ButtonColumnRight, minX, maxX); + g_installerProgressRatioTarget = float(double(g_installerJournal.progressCounter) / double(g_installerJournal.progressTotal)); - float minusY = (CONTAINER_BUTTON_GAP + BUTTON_HEIGHT) * (float(i % 3)); - ImVec2 min = { minX, g_aspectRatioOffsetY + Scale(CONTAINER_Y + CONTAINER_HEIGHT - CONTAINER_BUTTON_GAP - BUTTON_HEIGHT - minusY) }; - ImVec2 max = { maxX, g_aspectRatioOffsetY + Scale(CONTAINER_Y + CONTAINER_HEIGHT - CONTAINER_BUTTON_GAP - minusY) }; + // If user is being asked for confirmation on cancelling + // the installation, halt the installer from progressing further. + g_installerHalted.wait(true); - auto lightSize = Scale(14); + // If user has confirmed they wish to cancel the + // installation, return false to indicate the installer + // should fail and stop. + return !g_installerCancelled.load(); + } + ); - DrawButton(min, max, LANGUAGE_TEXT[i], false, true, buttonPressed, FLT_MAX, LANGUAGE_ENUM[i] == ELanguage::English); - - if (buttonPressed) - { - Config::Language = LANGUAGE_ENUM[i]; - g_commonMenu.SetTitle(Localise("Installer_Header_Installer"), false); - } - } - } -} - -static void DrawSourcePickers() -{ - bool buttonPressed = false; - std::list paths; - if (g_currentPage == WizardPage::SelectGame || g_currentPage == WizardPage::SelectDLC) - { - constexpr float ADD_BUTTON_MAX_TEXT_WIDTH = 168.0f; - const std::string &addFilesText = Localise("Installer_Button_AddFiles"); - float squashRatio; - ImVec2 textSize = ComputeTextSize(g_pFntRodin, addFilesText.c_str(), 20.0f, squashRatio, ADD_BUTTON_MAX_TEXT_WIDTH); - ImVec2 min = { g_aspectRatioOffsetX + Scale(CONTAINER_X + BOTTOM_X_GAP), g_aspectRatioOffsetY + Scale(CONTAINER_Y + CONTAINER_HEIGHT + BOTTOM_Y_GAP) }; - ImVec2 max = { g_aspectRatioOffsetX + Scale(CONTAINER_X + BOTTOM_X_GAP + textSize.x * squashRatio + BUTTON_TEXT_GAP), g_aspectRatioOffsetY + Scale(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 + BUTTON_TEXT_GAP); - - const std::string &addFolderText = Localise("Installer_Button_AddFolder"); - textSize = ComputeTextSize(g_pFntRodin, addFolderText.c_str(), 20.0f, squashRatio, ADD_BUTTON_MAX_TEXT_WIDTH); - max.x = min.x + Scale(textSize.x * squashRatio + BUTTON_TEXT_GAP); - 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::SelectGame) - { - DrawSourceButton(ButtonColumnMiddle, 0, Localise("Installer_Step_Game").c_str(), !g_gameSourcePath.empty()); - } - - if (g_currentPage == WizardPage::SelectDLC) - { - for (int i = 0; i < 7; i++) - { - DrawSourceButton((i == 3) ? ButtonColumnMiddle : (i < 3) ? ButtonColumnLeft : ButtonColumnRight, 3 - float(i % 4), DLC_SOURCE_TEXT[i], !g_dlcSourcePaths[i].empty() || g_dlcInstalled[i]); - } - } -} - -static void DrawInstallingProgress() -{ - if (g_currentPage == WizardPage::Installing) - { - constexpr float ProgressSpeed = 0.1f; - float ratioTarget = g_installerProgressRatioTarget.load(); - g_installerProgressRatioCurrent += std::min(ratioTarget - g_installerProgressRatioCurrent, ProgressSpeed * ImGui::GetIO().DeltaTime); - DrawProgressBar(g_installerProgressRatioCurrent); - - if (g_installerFinished) - { - g_installerThread->join(); - g_installerThread.reset(); - g_installerEndTime = ImGui::GetTime(); - SetCurrentPage(g_installerFailed ? WizardPage::InstallFailed : WizardPage::InstallSucceeded); - g_commonMenu.SetTitle(Localise("Installer_Header_Installer")); - } - } -} - -static void InstallerThread() -{ - if (!Installer::install(g_installerSources, g_installPath, false, g_installerJournal, std::chrono::seconds(1), [&]() { - g_installerProgressRatioTarget = float(double(g_installerJournal.progressCounter) / double(g_installerJournal.progressTotal)); - - // If user is being asked for confirmation on cancelling the installation, halt the installer from progressing further. - g_installerHalted.wait(true); - - // If user has confirmed they wish to cancel the installation, return false to indicate the installer should fail and stop. - return !g_installerCancelled.load(); - })) + if (!result) { g_installerFailed = true; g_installerErrorMessage = g_installerJournal.lastErrorMessage; @@ -1067,6 +670,7 @@ static void InstallerThread() static void InstallerStart() { SetCurrentPage(WizardPage::Installing); + g_installerStartTime = ImGui::GetTime(); g_installerEndTime = DBL_MAX; g_installerProgressRatioCurrent = 0.0f; @@ -1074,130 +678,40 @@ static void InstallerStart() g_installerFailed = false; g_installerFinished = false; g_installerThread = std::make_unique(InstallerThread); + g_commonMenu.SetTitle(Localise("Installer_Header_Installing")); } static bool InstallerParseSources(std::string &errorMessage) { - std::error_code spaceErrorCode; - std::filesystem::space_info spaceInfo = std::filesystem::space(g_installPath, spaceErrorCode); - if (!spaceErrorCode) - { + std::error_code spaceInfoErrorCode; + auto spaceInfo = std::filesystem::space(g_installPath, spaceInfoErrorCode); + + if (!spaceInfoErrorCode) g_installerAvailableSize = spaceInfo.available; - } - Installer::Input installerInput; + Installer::Input installerInput{}; installerInput.gameSource = g_gameSourcePath; - for (std::filesystem::path &path : g_dlcSourcePaths) + for (auto& 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; -} + if (path.empty()) + continue; -static void DrawNavigationButton() -{ - if (g_currentPage == WizardPage::Installing) - { - // Navigation buttons are not offered during installation at the moment. - return; + installerInput.dlcSources.push_back(path); } - bool nextButtonEnabled = !g_isDisappearing && (g_currentPage != WizardPage::Installing); - if (nextButtonEnabled && g_currentPage == WizardPage::SelectGame) - { - nextButtonEnabled = !g_gameSourcePath.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 NAV_BUTTON_MAX_TEXT_WIDTH = 90.0f; - std::string_view nextButtonKey = "Installer_Button_Next"; - if (skipButton) - { - nextButtonKey = "Installer_Button_Skip"; - } - else if (g_currentPage == WizardPage::InstallFailed) - { - nextButtonKey = "Installer_Button_Retry"; - } + auto sourcesParsed = Installer::parseSources(installerInput, g_installerJournal, g_installerSources); - const std::string &nextButtonText = Localise(nextButtonKey); - ImVec2 nextTextSize = ComputeTextSize(g_pFntRodin, nextButtonText.c_str(), 20.0f, squashRatio, NAV_BUTTON_MAX_TEXT_WIDTH); - ImVec2 min = { g_aspectRatioOffsetX + Scale(CONTAINER_X + CONTAINER_WIDTH - nextTextSize.x * squashRatio - BOTTOM_X_GAP - BUTTON_TEXT_GAP), g_aspectRatioOffsetY + Scale(CONTAINER_Y + CONTAINER_HEIGHT + BOTTOM_Y_GAP) }; - ImVec2 max = { g_aspectRatioOffsetX + Scale(CONTAINER_X + CONTAINER_WIDTH - BOTTOM_X_GAP), g_aspectRatioOffsetY + Scale(CONTAINER_Y + CONTAINER_HEIGHT + BOTTOM_Y_GAP + BUTTON_HEIGHT) }; - - bool buttonPressed = false; - DrawButton(min, max, nextButtonText.c_str(), false, nextButtonEnabled, buttonPressed, NAV_BUTTON_MAX_TEXT_WIDTH); - - if (buttonPressed) - { - if (g_currentPage == WizardPage::SelectDLC) - { - 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; - } + errorMessage = g_installerJournal.lastErrorMessage; - g_currentMessagePrompt = stringStream.str(); - g_currentMessagePromptConfirmation = false; - SetCurrentPage(dlcInstallerMode ? WizardPage::SelectDLC : WizardPage::SelectGame); - } - 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 - { - SetCurrentPage(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) - { - SetCurrentPage(g_firstPage); - } - else - { - SetCurrentPage(WizardPage(int(g_currentPage) + 1)); - } - } + return sourcesParsed; } static void CheckCancelAction() { if (!g_currentCursorBack) - { return; - } g_currentCursorBack = false; @@ -1208,11 +722,12 @@ static void CheckCancelAction() } if (g_currentPage == WizardPage::Installing && g_installerCancelled) { - // Installer's already been cancelled, no need for more confirmations. + // Installer's already been cancelled, + // no need for more confirmations. return; } - Game_PlaySound("cursor2"); + Game_PlaySound("window_close"); if (g_currentPage == g_firstPage || g_currentPage == WizardPage::InstallFailed) { @@ -1228,7 +743,8 @@ static void CheckCancelAction() g_currentMessagePromptSource = MessagePromptSource::Back; g_currentMessagePromptConfirmation = true; - // Indicate to the installer that all progress should stop until the user confirms if they wish to cancel. + // Indicate to the installer that all progress should + // stop until the user confirms if they wish to cancel. g_installerHalted = true; } else if (int(g_currentPage) > 0) @@ -1240,16 +756,25 @@ static void CheckCancelAction() static void DrawMessagePrompt() { + static auto s_messageWindowOpened = false; + if (g_currentMessagePrompt.empty()) - { return; + + auto messageWindowReturned = false; + + if (!s_messageWindowOpened) + { + // Update alpha time to fade out. + g_alphaTime = ImGui::GetTime(); + s_messageWindowOpened = true; } - bool messageWindowReturned = false; if (g_currentMessagePromptConfirmation) { - std::array YesNoButtons = { Localise("Common_Yes"), Localise("Common_No") }; - messageWindowReturned = MessageWindow::Open(g_currentMessagePrompt, &g_currentMessageResult, YesNoButtons, 1); + std::array buttons = { Localise("Common_Yes"), Localise("Common_No") }; + + messageWindowReturned = MessageWindow::Open(g_currentMessagePrompt, &g_currentMessageResult, buttons, 1); } else { @@ -1258,33 +783,36 @@ static void DrawMessagePrompt() if (messageWindowReturned) { - if (g_currentMessagePromptConfirmation && (g_currentMessageResult == 0)) + if (g_currentMessagePromptConfirmation && !g_currentMessageResult) { if (g_currentMessagePromptSource == MessagePromptSource::Back) { if (g_currentPage == WizardPage::Installing) { - // If user confirms they wish to cancel the installation, notify the installation thread it must finish as soon as possible. + // If user confirms they wish to cancel the + // installation, notify the installer thread + // it must finish as soon as possible. g_installerCancelled = true; } else { // In all cases, proceed to just quit the application. - g_isQuitting = true; - g_isDisappearing = true; - g_disappearTime = ImGui::GetTime(); + LeaveInstallerWizard(true); } } else if (g_currentPage == WizardPage::SelectDLC) { - // If user confirms the message prompt that they wish to skip installing the DLC, proceed to the next step. + // If user confirms the message prompt that + // they wish to skip installing the DLC, proceed + // to the next step. SetCurrentPage(WizardPage::CheckSpace); } } if (g_currentMessagePromptSource == MessagePromptSource::Back) { - // Regardless of the confirmation, the installation thread must be resumed. + // Regardless of the confirmation, the + // installation thread must be resumed. g_installerHalted = false; g_installerHalted.notify_all(); } @@ -1292,35 +820,38 @@ static void DrawMessagePrompt() g_currentMessagePrompt.clear(); g_currentMessagePromptSource = MessagePromptSource::Unknown; g_currentMessageResult = -1; + + // Update alpha time to fade in. + g_alphaTime = ImGui::GetTime(); + + s_messageWindowOpened = false; } } static void PickerDrawForeground() { - if (g_currentPickerVisible) - { - auto drawList = ImGui::GetBackgroundDrawList(); - drawList->AddRectFilled({ 0.0f, 0.0f }, ImGui::GetIO().DisplaySize, IM_COL32(0, 0, 0, 190)); - } + if (!g_currentPickerVisible) + return; + + auto drawList = ImGui::GetBackgroundDrawList(); + + drawList->AddRectFilled({ 0.0f, 0.0f }, ImGui::GetIO().DisplaySize, IM_COL32(0, 0, 0, 190)); } 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()) { @@ -1330,37 +861,286 @@ static void PickerCheckResults() } if (!g_currentPickerResults.empty() && ParseSourcePaths(g_currentPickerResults)) - { g_pickerTutorialCleared[g_pickerTutorialFolderMode] = true; - } g_currentPickerResultsReady = false; g_currentPickerVisible = false; } -static bool g_fadingOutMusic; +static void DrawLeftImage() +{ + // Don't display character renders at game and DLC + // select pages, as we draw the sources on the left side. + if (g_currentPage == WizardPage::SelectGame || g_currentPage == WizardPage::SelectDLC) + return; + + auto& res = ImGui::GetIO().DisplaySize; + auto drawList = ImGui::GetBackgroundDrawList(); + auto installTextureIndex = g_installTextureIndices[int(g_currentPage)]; + + // Cycle through the available images while time passes during installation. + if (g_currentPage == WizardPage::Installing) + { + constexpr auto installSpeed = 1.0 / 15.0; + auto installTime = (ImGui::GetTime() - g_installerStartTime) * installSpeed; + + installTextureIndex += int(installTime); + } -static void ProcessMusic() + auto pTexture = g_installTextures[installTextureIndex % g_installTextures.size()].get(); + + constexpr float imageSize = 1000.0f; + + auto pos = g_installTexturePositions[installTextureIndex % g_installTextures.size()]; + auto min = ImVec2(g_aspectRatioOffsetX + Scale(pos.x, true), g_aspectRatioOffsetY + Scale(pos.y, true)); + auto max = ImVec2(min.x + Scale(imageSize, true), min.y + Scale(imageSize, true)); + + drawList->AddImage(pTexture, min, max, { 0, 0 }, { 1, 1 }, IM_COL32(255, 255, 255, std::lround(140.0 * g_alphaMotion))); +} + +static std::string& GetWizardText(WizardPage page) { - if (g_isDisappearing) + switch (page) { - if (!g_fadingOutMusic) + case WizardPage::SelectLanguage: return Localise("Installer_Page_SelectLanguage"); + case WizardPage::Introduction: return Localise("Installer_Page_Introduction"); + case WizardPage::SelectGame: return Localise("Installer_Page_SelectGame"); + 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 bool DrawButton(ImVec2 pos, const char* text, bool& isHovered, bool isEnabled, bool isDefault = false) +{ + auto& res = ImGui::GetIO().DisplaySize; + auto drawList = ImGui::GetBackgroundDrawList(); + + auto edgeUVs = PIXELS_TO_UV_COORDS(256, 256, 1, 0, 51, 45); + auto stretchUVs = PIXELS_TO_UV_COORDS(256, 256, 51, 0, 51, 45); + auto width = Scale(50, true); + auto height = Scale(45, true); + auto colourMotion = isHovered && isEnabled ? 255 : 0; + auto colour = IM_COL32(colourMotion, colourMotion, colourMotion, 245 * g_alphaMotion); + auto fadeColour = IM_COL32(colourMotion, colourMotion, colourMotion, 45 * g_alphaMotion); + + auto edgeMin = ImVec2(pos.x - Scale(8, true), pos.y); + auto edgeMax = ImVec2(edgeMin.x + width, edgeMin.y + height); + auto stretchMin = ImVec2(edgeMax.x, edgeMin.y); + auto stretchMax = ImVec2(res.x, edgeMax.y); + auto fadeMin = ImVec2(stretchMax.x - ((stretchMax.x - edgeMin.x) / 2), stretchMax.y); + auto fadeMax = stretchMax; + + SetHorizontalGradient(fadeMin, fadeMax, colour, fadeColour); + drawList->AddImage(g_upTexMainMenu8.get(), edgeMin, edgeMax, GET_UV_COORDS(edgeUVs), colour); + drawList->AddImage(g_upTexMainMenu8.get(), stretchMin, stretchMax, GET_UV_COORDS(stretchUVs), colour); + ResetGradient(); + + auto textPos = ImVec2(edgeMin.x + Scale(51, true), edgeMin.y + Scale(8, true)); + + auto textColour = isEnabled + ? IM_COL32(255, 255, 255, 255 * g_alphaMotion) + : IM_COL32(137, 137, 137, 255 * g_alphaMotion); + + drawList->AddText(g_pFntRodin, g_fntRodinSize, textPos, textColour, text); + + if (isHovered && g_alphaMotion >= 1.0f) + DrawArrowCursor({ edgeMin.x + Scale(8, true), edgeMin.y + Scale(9, true) }, g_cursorArrowsTime, true, false); + + auto isPressed = false; + + isHovered = PushCursorRect(edgeMin, fadeMax, isPressed, isDefault); + + if (isPressed) + Game_PlaySound(isEnabled ? "main_deside" : "cannot_deside"); + + return isEnabled && isPressed; +} + +static void DrawSource(ImVec2 pos, const char* text, bool isEnabled) +{ + auto& res = ImGui::GetIO().DisplaySize; + auto drawList = ImGui::GetBackgroundDrawList(); + + auto selectedUVs = PIXELS_TO_UV_COORDS(1024, 1024, 443, 524, 560, 47); + auto unselectedUVs = PIXELS_TO_UV_COORDS(1024, 1024, 443, 579, 560, 47); + auto categoryWidth = Scale(560, true); + auto categoryHeight = Scale(47, true); + auto colour = IM_COL32(255, 255, 255, 255 * g_alphaMotion); + + ImVec2 categoryMin = { pos.x + Scale(42, true), pos.y + Scale(147, true) }; + ImVec2 categoryMax = { categoryMin.x + categoryWidth, categoryMin.y + categoryHeight }; + + SetHorizontalGradient(categoryMin, categoryMax, colour, IM_COL32_WHITE_TRANS); + drawList->AddImage(g_upTexMainMenu7.get(), categoryMin, categoryMax, GET_UV_COORDS(isEnabled ? selectedUVs : unselectedUVs)); + ResetGradient(); + + drawList->AddText(g_pFntRodin, g_fntRodinSize, { categoryMin.x + Scale(129, true), categoryMin.y + Scale(6, true) }, colour, text); + + if (isEnabled) + { + auto cursorOffsetX = Scale(80, true); + auto cursorOffsetY = Scale(8, true); + + DrawArrowCursor({ categoryMin.x + cursorOffsetX, categoryMin.y + cursorOffsetY }, 0, false, true); + } +} + +static ImVec2 GetNavButtonPosition(ImVec2 originMin, ImVec2 originMax, int index) +{ + return { originMin.x, originMax.y - Scale(NAV_BUTTON_OFFSET_Y + (NAV_BUTTON_MARGIN * index), true) }; +} + +static void DrawSelectLanguagePage(ImVec2 originMin, ImVec2 originMax) +{ + auto languageBtnIdx = 5; + + for (auto& language : g_languageEnum) + { + auto isHovered = Config::Language == language; + auto isPressed = DrawButton(GetNavButtonPosition(originMin, originMax, languageBtnIdx), g_languageText[int(language) - 1], isHovered, true); + + if (isHovered) { - EmbeddedPlayer::FadeOutMusic(); - g_fadingOutMusic = true; + Config::Language = language; + g_commonMenu.SetTitle(Localise("Installer_Header_Installer"), false); } + + if (isPressed) + SetCurrentPage(WizardPage::Introduction); + + languageBtnIdx--; } - else +} + +static void DrawSourcePickerPage(ImVec2 min, ImVec2 max, ImVec2 originMin, ImVec2 originMax) +{ + if (g_currentPage == WizardPage::SelectGame) { - EmbeddedPlayer::PlayMusic(); + DrawSource(min, Localise("Installer_Step_Game").c_str(), !g_gameSourcePath.empty()); + } + else if (g_currentPage == WizardPage::SelectDLC) + { + auto offsetY = 0.0f; + + for (int i = 0; i < 7; i++) + { + DrawSource({ min.x, min.y + offsetY }, g_dlcText[i], !g_dlcSourcePaths[i].empty() || g_dlcInstalled[i]); + offsetY += Scale(NAV_BUTTON_MARGIN, true); + } } + + static auto s_isAddFilesHovered = false; + static auto s_isAddFolderHovered = false; + + auto isAddFilesPressed = DrawButton(GetNavButtonPosition(originMin, originMax, 2), Localise("Installer_Button_AddFiles").c_str(), s_isAddFilesHovered, true); + auto isAddFolderPressed = DrawButton(GetNavButtonPosition(originMin, originMax, 1), Localise("Installer_Button_AddFolder").c_str(), s_isAddFolderHovered, true); + + if (isAddFilesPressed) + PickerShow(false); + + if (isAddFolderPressed) + PickerShow(true); } -void InstallerWizard::Init() +static void DrawInstallingPage(ImVec2 originMin, ImVec2 originMax) { - auto &io = ImGui::GetIO(); + constexpr auto progressSpeed = 0.1f; + auto ratioTarget = g_installerProgressRatioTarget.load(); + + g_installerProgressRatioCurrent += std::min(ratioTarget - g_installerProgressRatioCurrent, progressSpeed * ImGui::GetIO().DeltaTime); + + DrawProgressBar(originMin, originMax, g_installerProgressRatioCurrent); + if (g_installerFinished) + { + g_installerThread->join(); + g_installerThread.reset(); + + g_installerEndTime = ImGui::GetTime(); + + SetCurrentPage(g_installerFailed ? WizardPage::InstallFailed : WizardPage::InstallSucceeded); + + g_commonMenu.SetTitle(Localise("Installer_Header_Installer")); + } +} + +static void DrawInstallSucceededPage(ImVec2 originMin, ImVec2 originMax, ImVec2 descriptionTextSize) +{ + auto drawList = ImGui::GetBackgroundDrawList(); + + auto marqueeTextSize = g_pFntRodin->CalcTextSizeA(g_fntRodinSize, FLT_MAX, 0, g_creditsStr.c_str()); + auto marqueeTextMarginX = Scale(5, true); + auto marqueeTextMarginY = Scale(65, true); + auto marqueeTextPos = ImVec2(originMax.x, originMax.y - marqueeTextSize.y - marqueeTextMarginY); + auto marqueeTextMin = ImVec2(originMin.x, marqueeTextPos.y); + auto marqueeTextMax = ImVec2(originMax.x, originMax.y); + + // NOTE (Hyper): shifting the first four rows of pixels out of view + // fixes a dark line appearing at the top of the logo, along with a + // couple chunks being taken away towards the upper right. + // + // This issue seems to occur a lot with various other textures, + // needing at least one or two pixels shifted right or down to + // prevent this error. Weird. + auto imageUVs = PIXELS_TO_UV_COORDS(5243, 450, 0, 4, 5243, 446); + + auto imageWidth = Scale(524, true); + auto imageHeight = Scale(45, true); + auto imageRegionMin = ImVec2(originMin.x, originMin.y + descriptionTextSize.y); + auto imageRegionMax = ImVec2(originMax.x, originMax.y - (marqueeTextMax.y - marqueeTextMin.y)); + + auto imageMin = ImVec2 + ( + /* X */ imageRegionMin.x + ((imageRegionMax.x - imageRegionMin.x) / 2) - (imageWidth / 2), + /* Y */ imageRegionMin.y + ((imageRegionMax.y - imageRegionMin.y) / 2) - (imageHeight / 2) + ); + + auto imageMax = ImVec2(imageMin.x + imageWidth, imageMin.y + imageHeight); + + drawList->AddImage(g_upTexSonicNextDev.get(), imageMin, imageMax, GET_UV_COORDS(imageUVs), IM_COL32_WHITE); + + SetHorizontalMarqueeFade(marqueeTextMin, marqueeTextMax, Scale(32, true)); + DrawTextWithMarquee(g_pFntRodin, g_fntRodinSize, marqueeTextPos, marqueeTextMin, marqueeTextMax, IM_COL32_WHITE, g_creditsStr.c_str(), g_installerEndTime, 0.8, Scale(250, true)); + ResetMarqueeFade(); +} + +static void DrawMusicCredits() +{ + auto drawList = ImGui::GetBackgroundDrawList(); + + constexpr auto fadeTime = COMMON_FADE_TIME; + + // Wait 12 seconds before displaying music credits, + // as that's when the main melody begins. + constexpr auto fadeInOffset = 738; + + // Wait 10 seconds after fade in before fading out music credits. + constexpr auto fadeOutOffset = fadeInOffset + fadeTime + 600; + + auto fadeInMotion = ComputeLinearMotion(g_appearTime, fadeInOffset, fadeTime); + auto fadeOutMotion = ComputeLinearMotion(g_appearTime, fadeOutOffset, fadeTime); + auto fadeMotion = fadeInMotion - fadeOutMotion; + auto fadeAlphaMotion = IM_COL32(0, 0, 0, 70 * fadeMotion); + + auto fontSize = Scale(12, true); + auto offsetX = Scale(8, true); + auto offsetY = Scale(5, true); + + if (g_aspectRatio < NARROW_ASPECT_RATIO) + offsetY += g_vertCentre; + + drawList->AddText(g_pFntNewRodin, fontSize, { offsetX, offsetY }, fadeAlphaMotion, Localise("Installer_MusicCredits").c_str()); +} + +void InstallerWizard::Init() +{ g_commonMenu = CommonMenu(Localise("Installer_Header_Installer"), "", true); + 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); @@ -1369,12 +1149,13 @@ void InstallerWizard::Init() 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_upSonicNextDev = LOAD_ZSTD_TEXTURE(g_sonicnextdev); + g_upTexSonicNextDev = LOAD_ZSTD_TEXTURE(g_sonicnextdev); + // Add whitespace between credits for marquee. for (int i = 0; i < g_credits.size(); i++) { g_creditsStr += g_credits[i]; - g_creditsStr += " "; + g_creditsStr += " "; } } @@ -1383,42 +1164,174 @@ void InstallerWizard::Draw() if (!s_isVisible) return; - ResetCursorRects(); - DrawBackground(); - DrawArrows(); + g_alphaMotion = ComputeLinearMotion(g_alphaTime, g_isIntroAnim ? COMMON_MENU_INTRO_TIME : 0, COMMON_MENU_INTRO_TIME, !g_currentMessagePrompt.empty()); + + // Only offset fade motion for common menu intro. + if (g_alphaMotion >= 1.0) + g_isIntroAnim = false; + + auto& res = ImGui::GetIO().DisplaySize; + auto drawList = ImGui::GetBackgroundDrawList(); + + auto min = ImVec2(g_horzCentre + g_aspectRatioNarrowMargin, g_vertCentre); + auto max = ImVec2(res.x - min.x, res.y - min.y); + auto isMessageWindowOpen = !g_currentMessagePrompt.empty(); + + const auto bgGradientTop = IM_COL32(0, 103, 255, 255); + const auto bgGradientBottom = IM_COL32(0, 40, 100, 255); + + drawList->AddRectFilledMultiColor({ 0.0, 0.0 }, res, bgGradientTop, bgGradientTop, bgGradientBottom, bgGradientBottom); + + // Reset cursor rects. + g_currentCursorDefault = 0; + g_currentCursorRects.clear(); + + DrawArrows({ 0, 0 }, res, g_chevronTime); DrawLeftImage(); + g_commonMenu.Draw(); - DrawDescriptionContainer(); - DrawLanguagePicker(); - DrawSourcePickers(); - DrawSources(); - DrawInstallingProgress(); - DrawNavigationButton(); - CheckCancelAction(); - DrawMessagePrompt(); - PickerDrawForeground(); - PickerCheckTutorial(); - PickerCheckResults(); - if (g_isDisappearing) + DrawMusicCredits(); + + auto containerWidth = Scale(640, true); + auto containerHeight = Scale(405, true); + auto containerOffsetY = Scale(135, true); + auto containerMin = ImVec2(max.x - containerWidth, min.y + containerOffsetY); + auto containerMax = ImVec2(res.x, containerMin.y + containerHeight); + + DrawContainerBox(containerMin, containerMax, g_alphaMotion); + + auto originMargin = Scale(38, true); + auto originMin = ImVec2(containerMin.x + originMargin, containerMin.y + originMargin); + auto originMax = ImVec2(max.x - originMargin, containerMax.y - originMargin); + + auto text = GetWizardText(g_currentPage); + auto textMaxWidth = originMax.x - (g_fntRodinSize / 2.0f) - originMin.x; + auto textMargin = 5.0f; + auto textSize = MeasureCentredParagraph(g_pFntRodin, g_fntRodinSize, textMaxWidth, textMargin, text.c_str()); + + if (g_currentPage == WizardPage::Installing) + { + DrawInstallingPage(originMin, originMax); + } + else { - double disappearDuration = ALL_ANIMATIONS_FULL_DURATION / 60.0; - if (g_isQuitting) + auto isNavButtonSkip = false; + auto isNavButtonEnabled = true; + std::function navButtonFunction = []() { SetCurrentPage(WizardPage(int(g_currentPage) + 1)); }; + + switch (g_currentPage) { - // Add some extra waiting time when quitting the application altogether. - disappearDuration += QUITTING_EXTRA_DURATION / 60.0; + case WizardPage::SelectLanguage: + DrawSelectLanguagePage(originMin, originMax); + break; + + case WizardPage::SelectGame: + DrawSourcePickerPage(min, max, originMin, originMax); + isNavButtonEnabled = !g_gameSourcePath.empty(); + break; + + case WizardPage::SelectDLC: + { + DrawSourcePickerPage(min, max, originMin, originMax); + + isNavButtonSkip = std::all_of(g_dlcSourcePaths.begin(), g_dlcSourcePaths.end(), [](const std::filesystem::path& path) { return path.empty(); }); + + navButtonFunction = [&]() + { + auto isDLCInstallerMode = 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; + + SetCurrentPage(isDLCInstallerMode ? WizardPage::SelectDLC : WizardPage::SelectGame); + } + else if (isNavButtonSkip && isDLCInstallerMode) + { + LeaveInstallerWizard(); + } + else + { + SetCurrentPage(WizardPage::CheckSpace); + } + }; + + break; + } + + case WizardPage::CheckSpace: + { + char descriptionText[512]{}; + char requiredSpaceText[128]{}; + char availableSpaceText[128]{}; + + constexpr auto divisor = 1024.0 * 1024.0 * 1024.0; + auto requiredGiB = double(g_installerSources.totalSize) / divisor; + auto availableGiB = double(g_installerAvailableSize) / divisor; + + 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", text.c_str(), requiredSpaceText, availableSpaceText); + + text = descriptionText; + navButtonFunction = []() { InstallerStart(); }; + + break; + } + + case WizardPage::InstallSucceeded: + DrawInstallSucceededPage(originMin, originMax, textSize); + navButtonFunction = []() { LeaveInstallerWizard(); }; + break; + + case WizardPage::InstallFailed: + text += g_installerErrorMessage.c_str(); + navButtonFunction = []() { SetCurrentPage(g_firstPage); }; + break; } - if (ImGui::GetTime() > (g_disappearTime + disappearDuration)) + if (g_currentPage != WizardPage::SelectLanguage) { - s_isVisible = false; + std::string navButtonText{}; + + if (isNavButtonSkip) + navButtonText = Localise("Installer_Button_Skip"); + else + navButtonText = Localise("Installer_Button_Next"); + + static auto s_isNavButtonHovered = false; + + if (DrawButton({ originMin.x, originMax.y - Scale(NAV_BUTTON_OFFSET_Y, true) }, navButtonText.c_str(), s_isNavButtonHovered, isNavButtonEnabled, true) && navButtonFunction) + navButtonFunction(); } } + + DrawTextParagraph(g_pFntRodin, g_fntRodinSize, textMaxWidth, originMin, textMargin, text.c_str(), + [&](const char* str, ImVec2 pos) { DrawTextBasic(g_pFntRodin, g_fntRodinSize, pos, IM_COL32(255, 255, 255, 255 * g_alphaMotion), str); }); + + CheckCancelAction(); + DrawMessagePrompt(); + PickerDrawForeground(); + PickerCheckTutorial(); + PickerCheckResults(); } void InstallerWizard::Shutdown() { - // Wait for and erase the threads. + // Wait for and reset the threads. if (g_installerThread != nullptr) { g_installerThread->join(); @@ -1431,18 +1344,18 @@ void InstallerWizard::Shutdown() g_currentPickerThread.reset(); } - // Erase the sources. + // Free the sources. g_installerSources.game.reset(); g_installerSources.dlc.clear(); - // Make sure the GPU is not currently active before deleting these textures. + // Make sure the GPU is not currently active before deleting textures. Video::WaitForGPU(); - // Erase the textures. + // Free the textures. for (auto &texture : g_installTextures) - { texture.reset(); - } + + g_upTexSonicNextDev.reset(); } bool InstallerWizard::Run(std::filesystem::path installPath, bool skipGame) @@ -1452,40 +1365,40 @@ bool InstallerWizard::Run(std::filesystem::path installPath, bool skipGame) 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; + // Guarantee that one controller is initialised. + // 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; } SetCurrentPage(g_firstPage); - GameWindow::SetFullscreenCursorVisibility(true); + s_isVisible = true; while (s_isVisible) { Video::WaitOnSwapChain(); - ProcessMusic(); + EmbeddedPlayer::PlayMusic(); SDL_PumpEvents(); SDL_FlushEvents(SDL_FIRSTEVENT, SDL_LASTEVENT); GameWindow::Update(); Video::Present(); } + Fader::FadeIn(0); + ButtonWindow::Close(); GameWindow::SetFullscreenCursorVisibility(false); NFD_Quit(); - - InstallerWizard::Shutdown(); EmbeddedPlayer::Shutdown(); + InstallerWizard::Shutdown(); return !g_isQuitting; } diff --git a/MarathonRecomp/ui/installer_wizard.h b/MarathonRecomp/ui/installer_wizard.h index 509e1fff7..4890853dc 100644 --- a/MarathonRecomp/ui/installer_wizard.h +++ b/MarathonRecomp/ui/installer_wizard.h @@ -1,7 +1,5 @@ #pragma once -#include - struct InstallerWizard { static inline bool s_isVisible = false; diff --git a/MarathonRecomp/ui/message_window.cpp b/MarathonRecomp/ui/message_window.cpp index 796fabbdd..fc22baa63 100644 --- a/MarathonRecomp/ui/message_window.cpp +++ b/MarathonRecomp/ui/message_window.cpp @@ -157,8 +157,7 @@ void DrawButton(int rowIndex, float yOffset, float yPadding, float width, float DrawArrowCursor(min, g_time, false, true); } - auto fontSize = Scale(27, true); - auto textSize = g_pFntRodin->CalcTextSizeA(fontSize, FLT_MAX, 0, text.c_str()); + auto textSize = g_pFntRodin->CalcTextSizeA(g_fntRodinSize, FLT_MAX, 0, text.c_str()); // Show low quality text in-game. if (App::s_isInit) @@ -167,7 +166,7 @@ void DrawButton(int rowIndex, float yOffset, float yPadding, float width, float DrawTextBasic ( g_pFntRodin, - fontSize, + g_fntRodinSize, { /* X */ min.x + ((max.x - min.x) - textSize.x) / 2, /* Y */ min.y + ((max.y - min.y) - textSize.y) / 2 }, textColour, text.c_str() @@ -245,10 +244,20 @@ void MessageWindow::Draw() if (App::s_isInit) SetShaderModifier(IMGUI_SHADER_MODIFIER_LOW_QUALITY_TEXT); - auto fontSize = Scale(27, true); - auto textSize = g_pFntRodin->CalcTextSizeA(fontSize, FLT_MAX, 0, g_text.c_str()); + auto textMargin = Scale(270, true); + auto textMaxWidth = msgMax.x - msgMin.x - textMargin; + auto textSize = g_pFntRodin->CalcTextSizeA(g_fntRodinSize, textMaxWidth, textMaxWidth, g_text.c_str()); - DrawTextBasic(g_pFntRodin, fontSize, { msgCentre.x - textSize.x / 2, msgCentre.y - textSize.y / 2 }, IM_COL32_WHITE, g_text.c_str()); + DrawTextParagraph + ( + g_pFntRodin, + g_fntRodinSize, + textMaxWidth, + { msgCentre.x - textSize.x / 2, msgCentre.y - textSize.y / 2 }, + 1.0f, + g_text.c_str(), + [&](const char* str, ImVec2 pos) { DrawTextBasic(g_pFntRodin, g_fntRodinSize, pos, IM_COL32_WHITE, str); } + ); // Reset the shader modifier. if (App::s_isInit) diff --git a/MarathonRecomp/ui/options_menu.cpp b/MarathonRecomp/ui/options_menu.cpp index 82807573e..2ad28baa1 100644 --- a/MarathonRecomp/ui/options_menu.cpp +++ b/MarathonRecomp/ui/options_menu.cpp @@ -3,8 +3,6 @@ #include #include #include -#include -#include #include #include #include @@ -48,12 +46,8 @@ static int g_optionCount{}; static IConfigDef* g_optionCurrent{}; static bool g_optionCanReset{}; -static std::unique_ptr g_upTexMainMenu7{}; -static std::unique_ptr g_upTexMainMenu8{}; static std::unique_ptr g_upTexMainMenu9{}; -static float g_fntRodinSize{}; - static std::string& GetCategoryName(OptionsMenuCategory category) { switch (category) @@ -947,15 +941,6 @@ void OptionsMenu::Draw() return; } - g_fntRodinSize = Scale(Config::Language == ELanguage::Japanese ? 28 : 27, true); - - // Draw faded letterbox at tall aspect ratios. - if (g_aspectRatio < NARROW_ASPECT_RATIO) - { - BlackBar::Show(true); - BlackBar::SetBorderMargin(Scale(BlackBar::ms_MenuBorderMargin, true)); - } - auto* drawList = ImGui::GetBackgroundDrawList(); auto& res = ImGui::GetIO().DisplaySize; @@ -1235,8 +1220,6 @@ void OptionsMenu::Draw() void OptionsMenu::Init() { - g_upTexMainMenu7 = LOAD_ZSTD_TEXTURE(g_main_menu7); - g_upTexMainMenu8 = LOAD_ZSTD_TEXTURE(g_main_menu8); g_upTexMainMenu9 = LOAD_ZSTD_TEXTURE(g_main_menu9); } diff --git a/MarathonRecompResources b/MarathonRecompResources index ba675633c..763b3f8b5 160000 --- a/MarathonRecompResources +++ b/MarathonRecompResources @@ -1 +1 @@ -Subproject commit ba675633caaa69bebf402ff83b108f887b0139a3 +Subproject commit 763b3f8b5f37c77e6737d15dd31b28badd6d69a5