Skip to content

Add CLI installation check option. #1387

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 28, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 162 additions & 32 deletions UnleashedRecomp/install/installer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down
2 changes: 2 additions & 0 deletions UnleashedRecomp/install/installer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
22 changes: 22 additions & 0 deletions UnleashedRecomp/locale/locale.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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",
{
Expand Down
52 changes: 52 additions & 0 deletions UnleashedRecomp/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -200,13 +200,15 @@ 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++)
{
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)
{
Expand All @@ -226,6 +228,56 @@ int main(int argc, char *argv[])

Config::Load();

if (forceInstallationCheck)
{
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)
{
Expand Down
Loading