#include "patchinstaller.h" #include "legacyapplication.h" #include "models/patchdownloader.h" #include "models/filesystem.h" #include "models/settings.h" #include "models/datoriginalfilesdatabase.h" #include #include #include #include QString getComponentNameFromId(int id) { switch (id) { case 100: return "texts_main"; case 101: return "texts_items"; case 102: return "texts_emotes"; case 200: return "maps"; case 201: return "loadscreens"; case 202: return "textures"; case 300: return "sounds"; case 301: return "videos"; } return "none"; } PatchInstaller::PatchInstaller(QObject *parent) : QObject(parent) { client_local_file_ = new LOTRO_DAT::DatFile(100); client_general_file_ = new LOTRO_DAT::DatFile(101); connect(&PatchDownloader::instance(), &PatchDownloader::finished, this, &PatchInstaller::startPatchInstallationChain); } bool PatchInstaller::initialised() { return client_general_file_->Initialized() && client_local_file_->Initialized(); } PatchInstaller::~PatchInstaller() { deinit(); delete client_local_file_; delete client_general_file_; } // ############## PRIVATE ############## // bool PatchInstaller::datPathIsRelevant() { QString game_folder = Settings::getValue("Lotro/game_path").toString(); QString locale_prefix = Settings::getValue("Lotro/original_locale").toString(); QString client_local_filepath = game_folder + "/client_local_" + locale_prefix + ".dat"; QString client_general_filepath = game_folder + "/client_general.dat"; QString client_local_current_path = QString::fromStdString(client_local_file_->GetFilename()); QString client_general_current_path = QString::fromStdString(client_general_file_->GetFilename()); return QFileInfo(client_local_filepath) != QFileInfo(client_local_current_path) || QFileInfo(client_general_filepath) != QFileInfo(client_general_current_path); } void PatchInstaller::deinit() { client_local_file_->Deinit(); client_general_file_->Deinit(); emit deinitialized(); } PatchInstaller::AppliedPatchesInfo PatchInstaller::getPatchesInfo() { return _current_applied_patches_info; } void PatchInstaller::installPatch(QString patch_name, LOTRO_DAT::Database* database) { if (patch_name == "loadscreen") { installLoadscreens(database); return; } if (patch_name == "video") { installVideos(database); return; } DatOriginalFilesDatabase orig_files_db(QApplication::applicationDirPath() + "/data/" + patch_name + "_orig.db"); LOTRO_DAT::SubfileData file; qDebug() << "Total files in database " << database->CountRows(); qDebug() << "Patching all files from database..." << database; while (!(file = database->GetNextFile()).Empty()) { current_status.finished_parts++; if (current_status.finished_parts * 100 / current_status.total_parts != (current_status.finished_parts - 1) * 100 * 10 / current_status.total_parts) { // emitting if changed at least on 0.1% emit progressChanged(current_status); } if (!file.options["fid"]) { continue; } const int dat_id = file.options["did"] ? file.options["did"].as() : E_CLIENT_LOCAL; if (dat_id != E_CLIENT_LOCAL && dat_id != E_CLIENT_GENERAL) { qWarning() << "Unknown dat id parameter for file " << file.options["fid"].as() << " (dat id value = " << dat_id << "), SKIPPING!"; continue; } const int file_id = file.options["fid"].as(); int file_version = -1; int file_iteration = -1; // TODO: FILE ITERATION if (dat_id == E_CLIENT_LOCAL) { file_version = client_local_file_->GetFileVersion(file_id); } else if (dat_id == E_CLIENT_GENERAL) { file_version = client_general_file_->GetFileVersion(file_id); } if (file_version != _patch_files_versions && file_version != 0) { LOTRO_DAT::SubfileData data; if (dat_id == E_CLIENT_LOCAL) { data = client_local_file_->GetFile(file_id); } else if (dat_id == E_CLIENT_GENERAL) { data = client_general_file_->GetFile(file_id); } orig_files_db.addFile(file_id, dat_id, file_version, file_iteration, data); } if (dat_id == E_CLIENT_LOCAL) { client_local_file_->PatchFile(file, _patch_files_versions); } else if (dat_id == E_CLIENT_GENERAL) { client_general_file_->PatchFile(file, _patch_files_versions); } } _current_applied_patches_info.has_no_patch_mark = false; const QString hashsum = Settings::getValue("PatchDatabases/" + patch_name + "/hashsum").toString(); if (patch_name == "text") { _current_applied_patches_info.texts_patch_hashsum = hashsum; } else if (patch_name == "image") { _current_applied_patches_info.images_patch_hashsum = hashsum; } else if (patch_name == "sound") { _current_applied_patches_info.sounds_patch_hashsum = hashsum; } else if (patch_name == "texture") { _current_applied_patches_info.textures_patch_hashsum = hashsum; } else if (patch_name == "font") { _current_applied_patches_info.fonts_patch_hashsum = hashsum; } else if (patch_name == "video") { _current_applied_patches_info.videos_patch_hashsum = hashsum; } orig_files_db.closeDatabase(); insertPatchesInfoInDatFile(_current_applied_patches_info); return; } void PatchInstaller::installOriginalPatch(QString patch_name) { qDebug() << "Installing original version of patch " << patch_name; DatOriginalFilesDatabase orig_db(QApplication::applicationDirPath() + "/data/" + patch_name + "_orig.db"); DatOriginalFilesDatabase::FileOperation operation = [this](int file_id, int dat_id, int version, int iteration, const LOTRO_DAT::SubfileData& data) { if (dat_id == E_CLIENT_LOCAL) { client_local_file_->PatchFile(data, version, iteration); } else if (dat_id == E_CLIENT_GENERAL) { client_general_file_->PatchFile(data, version, iteration); } current_status.finished_parts++; if (current_status.finished_parts * 100 / current_status.total_parts != (current_status.finished_parts - 1) * 100 * 10 / current_status.total_parts) { // emitting if changed at least on 0.1% emit progressChanged(current_status); } }; orig_db.performOperationOnAllFiles(operation); if (patch_name == "text") { _current_applied_patches_info.texts_patch_hashsum = ""; } else if (patch_name == "image") { _current_applied_patches_info.images_patch_hashsum = ""; } else if (patch_name == "sound") { _current_applied_patches_info.sounds_patch_hashsum = ""; } else if (patch_name == "texture") { _current_applied_patches_info.textures_patch_hashsum = ""; } else if (patch_name == "font") { _current_applied_patches_info.fonts_patch_hashsum = ""; } else if (patch_name == "video") { _current_applied_patches_info.videos_patch_hashsum = ""; } else if (patch_name == "loadscreen") { _current_applied_patches_info.loadscreens_patch_hashsum = ""; } insertPatchesInfoInDatFile(_current_applied_patches_info); } void PatchInstaller::installLoadscreens(LOTRO_DAT::Database* database) { QString locale_prefix = Settings::getValue("Lotro/original_locale").toString(); const QStringList loadscreens_filenames = { locale_prefix == "English" ? "lotro_ad_pregame.jpg" : "lotro_ad_pregame_" + locale_prefix + ".jpg", "lotro_generic_teleport_screen_01.jpg", "lotro_generic_teleport_screen_02.jpg", "lotro_generic_teleport_screen_03.jpg", "lotro_generic_teleport_screen_04.jpg", "lotro_generic_teleport_screen_05.jpg", "lotro_generic_teleport_screen_06.jpg", "lotro_generic_teleport_screen_07.jpg", "lotro_generic_teleport_screen_08.jpg", "lotro_generic_teleport_screen_09.jpg", "lotro_generic_teleport_screen_10.jpg" }; LOTRO_DAT::SubfileData data; QString logo_path = Settings::getValue("Lotro/game_path").toString() + "/raw/" + (locale_prefix == "English" ? "en" : locale_prefix) + "/logo/"; for (size_t i = 0; i < qMin(size_t(loadscreens_filenames.size()), database->CountRows()); ++i) { data = database->GetNextFile(); QFile::remove(logo_path + loadscreens_filenames[i]); if (!data.binary_data.WriteToFile((logo_path + loadscreens_filenames[i]).toLocal8Bit())) { qWarning() << "InstallLoadscreens: Cannot write to file " << logo_path + loadscreens_filenames[i]; } current_status.finished_parts++; if (current_status.finished_parts * 100 / current_status.total_parts != (current_status.finished_parts - 1) * 100 * 10 / current_status.total_parts) { // emitting if changed at least on 0.1% emit progressChanged(current_status); } } _current_applied_patches_info.has_no_patch_mark = false; _current_applied_patches_info.loadscreens_patch_hashsum = Settings::getValue("PatchDatabases/loadscreen/hashsum").toString(); insertPatchesInfoInDatFile(_current_applied_patches_info); } void PatchInstaller::installVideos(LOTRO_DAT::Database* database) { current_status.finished_parts += database->CountRows(); emit progressChanged(current_status); download_video_total_videos = database->CountRows(); download_video_finished_videos = 0; LOTRO_DAT::SubfileData file; while (!(file = database->GetNextFile()).Empty()) { if (!file.options["name"] || !file.options["url"] || !file.options["hash"] || !file.options["folder"]) { download_video_finished_videos++; continue; } const QString filename = QString::fromStdString(file.options["name"].as()); const QString url = QString::fromStdString(file.options["url"].as()); const QString hash = QString::fromStdString(file.options["hash"].as()); const QString folder = QString::fromStdString(file.options["folder"].as()); const QString full_filename = Settings::getValue("Lotro/game_path").toString() + "/" + folder + "/" + filename; FileSystem::createFilePath(full_filename); if (FileSystem::fileExists(full_filename) && FileSystem::fileHash(full_filename) == hash) { download_video_finished_videos++; continue; } QFile* target_file = new QFile(full_filename); target_file->open(QIODevice::WriteOnly); Downloader* video_downloader = new Downloader(this); connect(video_downloader, &Downloader::progressChanged, this, &PatchInstaller::onDownloaderProgressChanged); video_downloader->setUrl(url); video_downloader->targetFile = target_file; video_downloader->start(); video_downloader->waitForDownloaded(); video_downloader->targetFile->close(); video_downloader->targetFile->deleteLater(); video_downloader->targetFile = nullptr; video_downloader->deleteLater(); download_video_finished_videos++; } _current_applied_patches_info.has_no_patch_mark = false; _current_applied_patches_info.videos_patch_hashsum = Settings::getValue("PatchDatabases/video/hashsum").toString(); insertPatchesInfoInDatFile(_current_applied_patches_info); } PatchInstaller::AppliedPatchesInfo PatchInstaller::getAppliedPatchesInfoFromDatFile() { PatchInstaller::AppliedPatchesInfo result; const LOTRO_DAT::SubfileData patch_versions_file = client_local_file_->GetFile(_applied_patches_file_id); if (patch_versions_file.Empty()) { result.has_no_patch_mark = true; } else { const QString text_data = QString::fromUtf8(reinterpret_cast(patch_versions_file.binary_data.data() + 4), patch_versions_file.binary_data.size() - 4); const QStringList text_data_splitted = text_data.split("\n"); if (text_data_splitted.length() < 0 || text_data_splitted[0] != _patch_mark_header) { return result; } result.has_no_patch_mark = false; for (const QString& str: text_data_splitted) { if (str.startsWith("TEXTS:")) { result.texts_patch_hashsum = QString(str).remove(0, 6); } else if (str.startsWith("IMAGES:")) { result.images_patch_hashsum = QString(str).remove(0, 7); } else if (str.startsWith("SOUNDS:")) { result.sounds_patch_hashsum = QString(str).remove(0, 7); } else if (str.startsWith("LOADSCREENS:")) { result.loadscreens_patch_hashsum = QString(str).remove(0, 12); } else if (str.startsWith("TEXTURES:")) { result.textures_patch_hashsum = QString(str).remove(0, 9); } else if (str.startsWith("FONTS:")) { result.fonts_patch_hashsum = QString(str).remove(0, 6); } else if (str.startsWith("VIDEOS:")) { result.videos_patch_hashsum = QString(str).remove(0, 7); } } } // TODO: PATCHED BY OLD LEGACY CHECK return result; } void PatchInstaller::insertPatchesInfoInDatFile(const PatchInstaller::AppliedPatchesInfo& info) { LOTRO_DAT::SubfileData file_data; file_data.options["fid"] = _applied_patches_file_id; file_data.options["ext"] = LOTRO_DAT::StringFromFileType(LOTRO_DAT::TEXT); QString patches_info_data = _patch_mark_header + "\n"; patches_info_data += "TEXTS:" + info.texts_patch_hashsum + "\n"; patches_info_data += "IMAGES:" + info.images_patch_hashsum + "\n"; patches_info_data += "SOUNDS:" + info.sounds_patch_hashsum + "\n"; patches_info_data += "LOADSCREENS:" + info.loadscreens_patch_hashsum + "\n"; patches_info_data += "TEXTURES:" + info.textures_patch_hashsum + "\n"; patches_info_data += "FONTS:" + info.fonts_patch_hashsum + "\n"; patches_info_data += "VIDEOS:" + info.videos_patch_hashsum + "\n"; QByteArray data = QByteArray((char*)&_applied_patches_file_id, 4) + patches_info_data.toUtf8(); file_data.binary_data = LOTRO_DAT::BinaryData(data.data(), data.size()); client_local_file_->PatchFile(file_data, _patch_files_versions, 1, true); } // ############## PUBLIC SLOTS ############## // void PatchInstaller::init() { if (client_local_file_->Initialized() || client_general_file_->Initialized()) { // Firstly - deinitializing existing client_local files. deinit(); } qDebug() << __FUNCTION__ << "Starting initialisation of LotroDatManager"; qRegisterMetaType(); QString game_folder = Settings::getValue("Lotro/game_path").toString(); QString locale_prefix = Settings::getValue("Lotro/original_locale").toString(); QString client_local_filepath = game_folder + "/client_local_" + locale_prefix + ".dat"; QString client_general_filepath = game_folder + "/client_general.dat"; // Initialising client_local_*.dat file and client_general.dat auto client_local_init_res = client_local_file_->Init(client_local_filepath.toStdString()); auto client_general_init_res = client_general_file_->Init(client_general_filepath.toStdString()); if (!client_local_init_res || !client_general_init_res) { client_local_file_->Deinit(); client_general_file_->Deinit(); qCritical() << __FUNCTION__ << "Finished LotroDatManager initialisation - error: DatFile initialisation error!"; return; } // Initializing db for original files backup QString database_path = game_folder + "/LotroLegacy/orig_files.db"; if (!FileSystem::fileExists(database_path)) { FileSystem::createFilePath(database_path); } qDebug() << "LotroDatManager initialisation successfull! Dat files: " << QString::fromStdString(client_general_file_->GetFilename()) << QString::fromStdString(client_local_file_->GetFilename()); _current_applied_patches_info = getAppliedPatchesInfoFromDatFile(); emit successfullyInitialized(); } void PatchInstaller::startGame() { QString game_folder = Settings::getValue("Lotro/game_path").toString(); if (game_folder == "none") { qCritical() << __FUNCTION__ << "Starting game FAILED - game folder wasnt set!"; return; } if (!FileSystem::fileExists(QApplication::applicationDirPath() + "/Launcher.exe")) { qCritical() << __FUNCTION__ << "Starting game FAILED - no game launcher in legacy directory found!"; return; } QStringList args; args << "-nosplashscreen"; if (_current_applied_patches_info.loadscreens_patch_hashsum != "") { args << "-skiprawdownload"; } client_general_file_->Deinit(); client_local_file_->Deinit(); QString username = Settings::getValue("Account/username").toString(); QString password = Settings::getValue("Account/password").toString(); if (!username.isEmpty() && !password.isEmpty()) { args << "-username" << username << "-password" << password; } qDebug() << __FUNCTION__ << "Starting game with arguments: " << args; QFile f(Settings::getValue("Lotro/game_path").toString() + "/LotroLauncher.exe"); QProcess process; if (FileSystem::fileExists(f.fileName())) { if (f.fileName().contains(" ")) { f.setFileName("\"" + f.fileName() + "\""); } deinit(); process.startDetached(f.fileName(), args); process.waitForFinished(-1); QMetaObject::invokeMethod(&LegacyApplication::instance(), &LegacyApplication::close, Qt::QueuedConnection); } } void PatchInstaller::startPatchInstallationChain() { emit started(); qInfo() << "PatchInstaller: Starting installation chain..."; const QVector patches = {"text", "font", "image", "loadscreen", "texture", "sound", "video", "micro"}; QMap patch_databases; current_status.total_parts = 0; current_status.finished_parts = 0; for (const QString& patch: patches) { if (!Settings::getValue("DatabaseDownload/" + patch).toBool()) { continue; } const QString patch_hashsum = Settings::getValue("PatchDatabases/" + patch + "/hashsum").toString(); const QString patch_filename = Settings::getValue("PatchDatabases/" + patch + "/path").toString(); QString hashsum_in_dat_file = ""; if (patch == "text") { hashsum_in_dat_file = _current_applied_patches_info.texts_patch_hashsum; } else if (patch == "image") { hashsum_in_dat_file = _current_applied_patches_info.images_patch_hashsum; } else if (patch == "sound") { hashsum_in_dat_file = _current_applied_patches_info.sounds_patch_hashsum; } else if (patch == "texture") { hashsum_in_dat_file = _current_applied_patches_info.textures_patch_hashsum; } else if (patch == "font") { hashsum_in_dat_file = _current_applied_patches_info.fonts_patch_hashsum; } else if (patch == "video") { hashsum_in_dat_file = _current_applied_patches_info.videos_patch_hashsum; } else if (patch == "loadscreen") { hashsum_in_dat_file = _current_applied_patches_info.loadscreens_patch_hashsum; } if (patch_hashsum == hashsum_in_dat_file) { qDebug() << "Skipping patch " << patch << " because its hashsum (" << patch_hashsum << ") is equal to hashsum in dat file (" << hashsum_in_dat_file << ")"; continue; } const QString real_file_hashsum = FileSystem::fileHash(patch_filename); if (!FileSystem::fileExists(patch_filename) || real_file_hashsum != patch_hashsum) { qCritical() << "PatchInstallation: Incorrect patch file: " << patch_filename << ", hashsum: " << real_file_hashsum << ", expected: " << patch_hashsum; continue; } LOTRO_DAT::Database* db = new LOTRO_DAT::Database(); if (!db->InitDatabase(patch_filename.toStdString())) { qCritical() << "PatchInstallation: failed to initialize db " << patch_filename; continue; } patch_databases[patch] = db; current_status.total_parts += db->CountRows(); } // Parsing info about patches which original versions should be installed QStringList patches_to_be_installed_orig_versions; for (const QString& patch: patches) { if (Settings::getValue("DatabaseDownload/" + patch).toBool()) { continue; } QString hashsum_in_dat_file = ""; if (patch == "text") { hashsum_in_dat_file = _current_applied_patches_info.texts_patch_hashsum; } else if (patch == "image") { hashsum_in_dat_file = _current_applied_patches_info.images_patch_hashsum; } else if (patch == "sound") { hashsum_in_dat_file = _current_applied_patches_info.sounds_patch_hashsum; } else if (patch == "texture") { hashsum_in_dat_file = _current_applied_patches_info.textures_patch_hashsum; } else if (patch == "font") { hashsum_in_dat_file = _current_applied_patches_info.fonts_patch_hashsum; } else if (patch == "video") { hashsum_in_dat_file = _current_applied_patches_info.videos_patch_hashsum; } else if (patch == "loadscreen") { hashsum_in_dat_file = _current_applied_patches_info.loadscreens_patch_hashsum; } if (hashsum_in_dat_file == "") { qDebug() << "Skipping installing original version of patch " << patch << " because hashsum in dat file is already empty"; continue; } patches_to_be_installed_orig_versions.append(patch); DatOriginalFilesDatabase db(QApplication::applicationDirPath() + "/data/" + patch + "_orig.db"); current_status.total_parts += db.getRowsCount(); } emit progressChanged(current_status); for (const QString patch_name: patch_databases.keys()) { qInfo() << "PatchInstaller: Installing patch " << patch_name; installPatch(patch_name, patch_databases[patch_name]); patch_databases[patch_name]->CloseDatabase(); delete patch_databases[patch_name]; } for (const QString& patch_name : patches_to_be_installed_orig_versions) { installOriginalPatch(patch_name); } insertPatchesInfoInDatFile(_current_applied_patches_info); qInfo() << "PatchInstaller: Finished installation chain..."; emit finished(); } // ############## PRIVATE SLOTS ############## // void PatchInstaller::onDownloaderProgressChanged(Downloader*, Downloader::Status progress) { emit videosDownloadProgressChanged(download_video_finished_videos, download_video_total_videos, progress); }