mirror of
https://github.com/hedge-dev/UnleashedRecomp.git
synced 2025-04-28 13:27:58 +03:00
Add CLI installation check option. (#1387)
* Add CLI installation check option. * Show the console for Windows Release builds. --------- Co-authored-by: Skyth <19259897+blueskythlikesclouds@users.noreply.github.com>
This commit is contained in:
parent
1dd5ba7fcd
commit
53596b8e5c
4 changed files with 241 additions and 32 deletions
|
@ -67,6 +67,73 @@ static std::unique_ptr<VirtualFileSystem> createFileSystemFromPath(const std::fi
|
|||
}
|
||||
}
|
||||
|
||||
static bool checkFile(const FilePair &pair, const uint64_t *fileHashes, const std::filesystem::path &targetDirectory, std::vector<uint8_t> &fileData, Journal &journal, const std::function<bool()> &progressCallback, bool checkSizeOnly) {
|
||||
const std::string fileName(pair.first);
|
||||
const uint32_t hashCount = pair.second;
|
||||
const std::filesystem::path filePath = targetDirectory / fileName;
|
||||
if (!std::filesystem::exists(filePath))
|
||||
{
|
||||
journal.lastResult = Journal::Result::FileMissing;
|
||||
journal.lastErrorMessage = fmt::format("File {} does not exist.", fileName);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
size_t fileSize = std::filesystem::file_size(filePath, ec);
|
||||
if (ec)
|
||||
{
|
||||
journal.lastResult = Journal::Result::FileReadFailed;
|
||||
journal.lastErrorMessage = fmt::format("Failed to read file size for {}.", fileName);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (checkSizeOnly)
|
||||
{
|
||||
journal.progressTotal += fileSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::ifstream fileStream(filePath, std::ios::binary);
|
||||
if (fileStream.is_open())
|
||||
{
|
||||
fileData.resize(fileSize);
|
||||
fileStream.read((char *)(fileData.data()), fileSize);
|
||||
}
|
||||
|
||||
if (!fileStream.is_open() || fileStream.bad())
|
||||
{
|
||||
journal.lastResult = Journal::Result::FileReadFailed;
|
||||
journal.lastErrorMessage = fmt::format("Failed to read file {}.", fileName);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t fileHash = XXH3_64bits(fileData.data(), fileSize);
|
||||
bool fileHashFound = false;
|
||||
for (uint32_t i = 0; i < hashCount && !fileHashFound; i++)
|
||||
{
|
||||
fileHashFound = fileHash == fileHashes[i];
|
||||
}
|
||||
|
||||
if (!fileHashFound)
|
||||
{
|
||||
journal.lastResult = Journal::Result::FileHashFailed;
|
||||
journal.lastErrorMessage = fmt::format("File {} did not match any of the known hashes.", fileName);
|
||||
return false;
|
||||
}
|
||||
|
||||
journal.progressCounter += fileSize;
|
||||
}
|
||||
|
||||
if (!progressCallback())
|
||||
{
|
||||
journal.lastResult = Journal::Result::Cancelled;
|
||||
journal.lastErrorMessage = "Check was cancelled.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool copyFile(const FilePair &pair, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, bool skipHashChecks, std::vector<uint8_t> &fileData, Journal &journal, const std::function<bool()> &progressCallback) {
|
||||
const std::string filename(pair.first);
|
||||
const uint32_t hashCount = pair.second;
|
||||
|
@ -204,6 +271,45 @@ static DLC detectDLC(const std::filesystem::path &sourcePath, VirtualFileSystem
|
|||
}
|
||||
}
|
||||
|
||||
static bool fillDLCSource(DLC dlc, Installer::DLCSource &dlcSource)
|
||||
{
|
||||
switch (dlc)
|
||||
{
|
||||
case DLC::Spagonia:
|
||||
dlcSource.filePairs = { SpagoniaFiles, SpagoniaFilesSize };
|
||||
dlcSource.fileHashes = SpagoniaHashes;
|
||||
dlcSource.targetSubDirectory = SpagoniaDirectory;
|
||||
return true;
|
||||
case DLC::Chunnan:
|
||||
dlcSource.filePairs = { ChunnanFiles, ChunnanFilesSize };
|
||||
dlcSource.fileHashes = ChunnanHashes;
|
||||
dlcSource.targetSubDirectory = ChunnanDirectory;
|
||||
return true;
|
||||
case DLC::Mazuri:
|
||||
dlcSource.filePairs = { MazuriFiles, MazuriFilesSize };
|
||||
dlcSource.fileHashes = MazuriHashes;
|
||||
dlcSource.targetSubDirectory = MazuriDirectory;
|
||||
return true;
|
||||
case DLC::Holoska:
|
||||
dlcSource.filePairs = { HoloskaFiles, HoloskaFilesSize };
|
||||
dlcSource.fileHashes = HoloskaHashes;
|
||||
dlcSource.targetSubDirectory = HoloskaDirectory;
|
||||
return true;
|
||||
case DLC::ApotosShamar:
|
||||
dlcSource.filePairs = { ApotosShamarFiles, ApotosShamarFilesSize };
|
||||
dlcSource.fileHashes = ApotosShamarHashes;
|
||||
dlcSource.targetSubDirectory = ApotosShamarDirectory;
|
||||
return true;
|
||||
case DLC::EmpireCityAdabat:
|
||||
dlcSource.filePairs = { EmpireCityAdabatFiles, EmpireCityAdabatFilesSize };
|
||||
dlcSource.fileHashes = EmpireCityAdabatHashes;
|
||||
dlcSource.targetSubDirectory = EmpireCityAdabatDirectory;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool Installer::checkGameInstall(const std::filesystem::path &baseDirectory, std::filesystem::path &modulePath)
|
||||
{
|
||||
modulePath = baseDirectory / PatchedDirectory / GameExecutableFile;
|
||||
|
@ -254,6 +360,40 @@ bool Installer::checkAllDLC(const std::filesystem::path& baseDirectory)
|
|||
return result;
|
||||
}
|
||||
|
||||
bool Installer::checkInstallIntegrity(const std::filesystem::path &baseDirectory, Journal &journal, const std::function<bool()> &progressCallback)
|
||||
{
|
||||
// Run the file checks twice: once to fill out the progress counter and the file sizes, and another pass to do the hash integrity checks.
|
||||
for (uint32_t checkPass = 0; checkPass < 2; checkPass++)
|
||||
{
|
||||
bool checkSizeOnly = (checkPass == 0);
|
||||
if (!checkFiles({ GameFiles, GameFilesSize }, GameHashes, baseDirectory / GameDirectory, journal, progressCallback, checkSizeOnly))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!checkFiles({ UpdateFiles, UpdateFilesSize }, UpdateHashes, baseDirectory / UpdateDirectory, journal, progressCallback, checkSizeOnly))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 1; i < (int)DLC::Count; i++)
|
||||
{
|
||||
if (checkDLCInstall(baseDirectory, (DLC)i))
|
||||
{
|
||||
Installer::DLCSource dlcSource;
|
||||
fillDLCSource((DLC)i, dlcSource);
|
||||
|
||||
if (!checkFiles(dlcSource.filePairs, dlcSource.fileHashes, baseDirectory / dlcSource.targetSubDirectory, journal, progressCallback, checkSizeOnly))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Installer::computeTotalSize(std::span<const FilePair> filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, Journal &journal, uint64_t &totalSize)
|
||||
{
|
||||
for (FilePair pair : filePairs)
|
||||
|
@ -272,6 +412,27 @@ bool Installer::computeTotalSize(std::span<const FilePair> filePairs, const uint
|
|||
return true;
|
||||
}
|
||||
|
||||
bool Installer::checkFiles(std::span<const FilePair> filePairs, const uint64_t *fileHashes, const std::filesystem::path &targetDirectory, Journal &journal, const std::function<bool()> &progressCallback, bool checkSizeOnly)
|
||||
{
|
||||
FilePair validationPair = {};
|
||||
uint32_t validationHashIndex = 0;
|
||||
uint32_t hashIndex = 0;
|
||||
uint32_t hashCount = 0;
|
||||
std::vector<uint8_t> fileData;
|
||||
for (FilePair pair : filePairs)
|
||||
{
|
||||
hashIndex = hashCount;
|
||||
hashCount += pair.second;
|
||||
|
||||
if (!checkFile(pair, &fileHashes[hashIndex], targetDirectory, fileData, journal, progressCallback, checkSizeOnly))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Installer::copyFiles(std::span<const FilePair> filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, const std::string &validationFile, bool skipHashChecks, Journal &journal, const std::function<bool()> &progressCallback)
|
||||
{
|
||||
std::error_code ec;
|
||||
|
@ -387,39 +548,8 @@ bool Installer::parseSources(const Input &input, Journal &journal, Sources &sour
|
|||
}
|
||||
|
||||
DLC dlc = detectDLC(path, *dlcSource.sourceVfs, journal);
|
||||
switch (dlc)
|
||||
if (!fillDLCSource(dlc, dlcSource))
|
||||
{
|
||||
case DLC::Spagonia:
|
||||
dlcSource.filePairs = { SpagoniaFiles, SpagoniaFilesSize };
|
||||
dlcSource.fileHashes = SpagoniaHashes;
|
||||
dlcSource.targetSubDirectory = SpagoniaDirectory;
|
||||
break;
|
||||
case DLC::Chunnan:
|
||||
dlcSource.filePairs = { ChunnanFiles, ChunnanFilesSize };
|
||||
dlcSource.fileHashes = ChunnanHashes;
|
||||
dlcSource.targetSubDirectory = ChunnanDirectory;
|
||||
break;
|
||||
case DLC::Mazuri:
|
||||
dlcSource.filePairs = { MazuriFiles, MazuriFilesSize };
|
||||
dlcSource.fileHashes = MazuriHashes;
|
||||
dlcSource.targetSubDirectory = MazuriDirectory;
|
||||
break;
|
||||
case DLC::Holoska:
|
||||
dlcSource.filePairs = { HoloskaFiles, HoloskaFilesSize };
|
||||
dlcSource.fileHashes = HoloskaHashes;
|
||||
dlcSource.targetSubDirectory = HoloskaDirectory;
|
||||
break;
|
||||
case DLC::ApotosShamar:
|
||||
dlcSource.filePairs = { ApotosShamarFiles, ApotosShamarFilesSize };
|
||||
dlcSource.fileHashes = ApotosShamarHashes;
|
||||
dlcSource.targetSubDirectory = ApotosShamarDirectory;
|
||||
break;
|
||||
case DLC::EmpireCityAdabat:
|
||||
dlcSource.filePairs = { EmpireCityAdabatFiles, EmpireCityAdabatFilesSize };
|
||||
dlcSource.fileHashes = EmpireCityAdabatHashes;
|
||||
dlcSource.targetSubDirectory = EmpireCityAdabatDirectory;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -75,7 +75,9 @@ struct Installer
|
|||
static bool checkGameInstall(const std::filesystem::path &baseDirectory, std::filesystem::path &modulePath);
|
||||
static bool checkDLCInstall(const std::filesystem::path &baseDirectory, DLC dlc);
|
||||
static bool checkAllDLC(const std::filesystem::path &baseDirectory);
|
||||
static bool checkInstallIntegrity(const std::filesystem::path &baseDirectory, Journal &journal, const std::function<bool()> &progressCallback);
|
||||
static bool computeTotalSize(std::span<const FilePair> filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, Journal &journal, uint64_t &totalSize);
|
||||
static bool checkFiles(std::span<const FilePair> filePairs, const uint64_t *fileHashes, const std::filesystem::path &targetDirectory, Journal &journal, const std::function<bool()> &progressCallback, bool checkSizeOnly);
|
||||
static bool copyFiles(std::span<const FilePair> filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, const std::string &validationFile, bool skipHashChecks, Journal &journal, const std::function<bool()> &progressCallback);
|
||||
static bool parseContent(const std::filesystem::path &sourcePath, std::unique_ptr<VirtualFileSystem> &targetVfs, Journal &journal);
|
||||
static bool parseSources(const Input &input, Journal &journal, Sources &sources);
|
||||
|
|
|
@ -703,6 +703,28 @@ std::unordered_map<std::string_view, std::unordered_map<ELanguage, std::string>>
|
|||
{ ELanguage::Italian, "Impossibile trovare il modulo \"%s\".\n\nAssicurati che:\n\n- Hai estratto questa copia di Unleashed Recompiled correttamente e non solo il file *.exe.\n- Non stai eseguendo Unleashed Recompiled da un file *.zip." }
|
||||
}
|
||||
},
|
||||
{
|
||||
"IntegrityCheck_Success",
|
||||
{
|
||||
{ ELanguage::English, "Installation check has finished.\n\nAll files seem to be correct.\n\nThe game will now close. Remove the launch argument to play the game." },
|
||||
{ ELanguage::Japanese, "インストールチェックが終了しました\n\nすべてのファイルは正しいようです\n\nゲームは終了します、ゲームをプレイするには起動引数を削除してください" },
|
||||
{ ELanguage::German, "Die Installation wurde überprüft.\n\nAlle Dateien scheinen korrekt zu sein.\n\nDas Spiel wird nun geschlossen. Entferne die Startoption, um das Spiel zu spielen." },
|
||||
{ ELanguage::French, "La vérification de l'installation est terminée.\n\nTous les fichiers semblent corrects.\n\nL'application va maintenant se fermer. Retirez l'argument de lancement pour pouvoir lancer le jeu." },
|
||||
{ ELanguage::Spanish, "La verificación de la instalación ha terminado.\n\nTodos los archivos parecen correctos.\n\nEl juego se cerrará ahora. Elimina el argumento de lanzamiento para jugar al juego." },
|
||||
{ ELanguage::Italian, "La verifica dei file d'installazione è terminata.\n\nTutti i file sembrano corretti.\n\nIl gioco si chiuderà. Rimuovi l'argomento di avvio per poter giocare." }
|
||||
}
|
||||
},
|
||||
{
|
||||
"IntegrityCheck_Failed",
|
||||
{
|
||||
{ ELanguage::English, "Installation check has failed.\n\nError: %s\n\nThe game will now close. Try reinstalling the game by using the --install launch argument." },
|
||||
{ ELanguage::Japanese, "インストールチェックに失敗しました\n\nエラー:%s\n\nゲームは終了します、--install 起動引数を使用してゲームを再インストールしてください" },
|
||||
{ ELanguage::German, "Die Installationsprüfung ist fehlgeschlagen.\n\nFehler: %s\n\nDas Spiel wird nun geschlossen. Versuche das Spiel durch Verwendung der Startoption --install neu zu installieren." },
|
||||
{ ELanguage::French, "La vérification de l'installation a échoué.\n\nErreur : %s\n\nL'application va maintenant se fermer. Essayez de réinstaller le jeu en utilisant l'argument de lancement --install." },
|
||||
{ ELanguage::Spanish, "La verificación de la instalación ha fallado.\n\nError: %s\n\nEl juego se cerrará ahora. Intenta reinstalar el juego utilizando el argumento de lanzamiento --install." },
|
||||
{ ELanguage::Italian, "La verifica dei file d'installazione non è andata a buon fine.\n\nErrore: %s\n\nIl gioco si chiuderà. Prova a reinstallare il gioco utilizzando l'argomento di avvio --install." }
|
||||
}
|
||||
},
|
||||
{
|
||||
"Common_On",
|
||||
{
|
||||
|
|
|
@ -201,6 +201,7 @@ int main(int argc, char *argv[])
|
|||
bool forceInstaller = false;
|
||||
bool forceDLCInstaller = false;
|
||||
bool useDefaultWorkingDirectory = false;
|
||||
bool forceInstallationCheck = false;
|
||||
const char *sdlVideoDriver = nullptr;
|
||||
|
||||
for (uint32_t i = 1; i < argc; i++)
|
||||
|
@ -208,6 +209,7 @@ int main(int argc, char *argv[])
|
|||
forceInstaller = forceInstaller || (strcmp(argv[i], "--install") == 0);
|
||||
forceDLCInstaller = forceDLCInstaller || (strcmp(argv[i], "--install-dlc") == 0);
|
||||
useDefaultWorkingDirectory = useDefaultWorkingDirectory || (strcmp(argv[i], "--use-cwd") == 0);
|
||||
forceInstallationCheck = forceInstallationCheck || (strcmp(argv[i], "--install-check") == 0);
|
||||
|
||||
if (strcmp(argv[i], "--sdl-video-driver") == 0)
|
||||
{
|
||||
|
@ -230,6 +232,59 @@ int main(int argc, char *argv[])
|
|||
if (!PersistentStorageManager::LoadBinary())
|
||||
LOGFN_ERROR("Failed to load persistent storage binary... (status code {})", (int)PersistentStorageManager::BinStatus);
|
||||
|
||||
if (forceInstallationCheck)
|
||||
{
|
||||
// Create the console to show progress to the user, otherwise it will seem as if the game didn't boot at all.
|
||||
os::process::ShowConsole();
|
||||
|
||||
Journal journal;
|
||||
double lastProgressMiB = 0.0;
|
||||
double lastTotalMib = 0.0;
|
||||
Installer::checkInstallIntegrity(GAME_INSTALL_DIRECTORY, journal, [&]()
|
||||
{
|
||||
constexpr double MiBDivisor = 1024.0 * 1024.0;
|
||||
constexpr double MiBProgressThreshold = 128.0;
|
||||
double progressMiB = double(journal.progressCounter) / MiBDivisor;
|
||||
double totalMiB = double(journal.progressTotal) / MiBDivisor;
|
||||
if (journal.progressCounter > 0)
|
||||
{
|
||||
if ((progressMiB - lastProgressMiB) > MiBProgressThreshold)
|
||||
{
|
||||
fprintf(stdout, "Checking files: %0.2f MiB / %0.2f MiB\n", progressMiB, totalMiB);
|
||||
lastProgressMiB = progressMiB;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((totalMiB - lastTotalMib) > MiBProgressThreshold)
|
||||
{
|
||||
fprintf(stdout, "Scanning files: %0.2f MiB\n", totalMiB);
|
||||
lastTotalMib = totalMiB;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
char resultText[512];
|
||||
uint32_t messageBoxStyle;
|
||||
if (journal.lastResult == Journal::Result::Success)
|
||||
{
|
||||
snprintf(resultText, sizeof(resultText), "%s", Localise("IntegrityCheck_Success").c_str());
|
||||
fprintf(stdout, "%s\n", resultText);
|
||||
messageBoxStyle = SDL_MESSAGEBOX_INFORMATION;
|
||||
}
|
||||
else
|
||||
{
|
||||
snprintf(resultText, sizeof(resultText), Localise("IntegrityCheck_Failed").c_str(), journal.lastErrorMessage.c_str());
|
||||
fprintf(stderr, "%s\n", resultText);
|
||||
messageBoxStyle = SDL_MESSAGEBOX_ERROR;
|
||||
}
|
||||
|
||||
SDL_ShowSimpleMessageBox(messageBoxStyle, GameWindow::GetTitle(), resultText, GameWindow::s_pWindow);
|
||||
std::_Exit(int(journal.lastResult));
|
||||
}
|
||||
|
||||
#if defined(_WIN32) && defined(UNLEASHED_RECOMP_D3D12)
|
||||
for (auto& dll : g_D3D12RequiredModules)
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue