#include #include #include #include #define DAT_LOCALE_DICT_VERSION 103 namespace LOTRO_DAT { DatLocaleManager::DatLocaleManager(DatFile *datFilePtr) : dat(datFilePtr), current_locale_(ORIGINAL) { } /*! * \Author Gi1dor * \date 06.07.2018 * Инициализация модуля. Должна происходить после инициализации модуля ввода-вывода и файловой системы. * Считывает словарь и проверяет корректность (если структура словаря нарушена и основной является локаль PATHCED, * то dat файл считается некорреткным (любое обновлене dat файла с активной локалью PATCHED нарушает структуру файла * и делает использование в игре невозможным. * * \warning Не должна вызываться вручную! Автоматически вызывается в функции Initialise класса DatFile * * Структура словарей локализации: * ======== LOCALE DICT STRUCTURE ========= * 4 bytes for dict size (in bytes) * 4 bytes for locale version * 4 bytes for .dat file size (with patches) * 4 bytes for header hash * 15 bytes for "Hi from Gi1dor" * 4 bytes for LOCALE mark ("PATC" or "ORIG") * 4 bytes for orig_dict.size() * (32 + 4) * orig_dict.size() bytes for orig_dict data * 4 bytes for patch_dict.size() * (32 + 4) * patch_dict.size() bytes for patch_dict data * 4 bytes for inactive_categories dict * 4 * inactive_categories.size() bytes for inactive_categories data * ======================================== * Помимо этого: * 0x128-0x12C - Флаг, установлена ли альтернативная версия (flag) * 0x12C-0x130 - Офсет начала словаря локализации (offset) * * \warning Если flag != 0 и offset == 0 - то dat файл считается повреждённым. Проверка осуществляется функцией CheckLocaleCorrectж */ DatOperationResult<> DatLocaleManager::Init() { if (!dat) return DatOperationResult<>(ERROR, "LOCALEINIT: no connection with Dat (dat is nullptr)"); orig_dict_.clear(); patch_dict_.clear(); inactive_categories.clear(); current_locale_ = ORIGINAL; LOG(INFO) << "Initialising locales..."; BinaryData locale_offset_data(4); dat->GetIO().ReadData(locale_offset_data, 4, 300); long long locale_offset = locale_offset_data.ToNumber<4>(0); if (locale_offset == 0 || locale_offset + 8 >= dat->GetIO().GetActualDatSize().value) { if (CheckLocaleCorrect()) { LOG(INFO) << "Dictionary offset is empty or incorrect. Passing."; return DatOperationResult<>(); } else { return DatOperationResult<>(ERROR, "Locale dict is incorrect, through patched mark is standing. Dat file may be corrupted"); } } BinaryData locale_info(16); dat->GetIO().ReadData(locale_info, 16, locale_offset); long long dict_size = locale_info.ToNumber<4>(0); long long dict_version = locale_info.ToNumber<4>(4); long long patched_file_end = locale_info.ToNumber<4>(8); long long dat_file_header_hash = locale_info.ToNumber<4>(12); if (dict_version != DAT_LOCALE_DICT_VERSION) { dat->GetIO().WriteData(BinaryData::FromNumber<4>(0), 4, 300); if (CheckLocaleCorrect()) return DatOperationResult<>(SUCCESS); else return DatOperationResult<>(ERROR, "Version of locales' dictionary is incorrect, through patched mark is standing. Dat file may be corrupted"); } if (dat_file_header_hash != dat->GetIO().GetHeaderHash()) { LOG(INFO) << "Header hash, written after last patch apply doesn't match current hash. Seems like dat-file was modified somewhere else, so removing all patch data"; dat->GetIO().WriteData(BinaryData::FromNumber<4>(0), 4, 300); if (CheckLocaleCorrect()) return DatOperationResult<>(SUCCESS); else return DatOperationResult<>(ERROR, "Version of locales' dictionary is incorrect, through patched mark is standing. Dat file may be corrupted"); } BinaryData dicts_data = BinaryData((unsigned) dict_size); dat->GetIO().ReadData(dicts_data, dict_size - 16, locale_offset + 16); if (dicts_data.size() < 15) { dat->GetIO().WriteData(BinaryData::FromNumber<4>(0), 4, 300); return DatOperationResult<>(ERROR, "INITLOCALE: Data in locales' dictionary is incorrect."); } BinaryData hi_data = dicts_data.CutData(0, 15) + BinaryData("\0", 1); std::string hi = std::string((char *) (hi_data.data())); if (hi != "Hi from Gi1dor!") return DatOperationResult<>(ERROR, "INITLOCALE: Data in locales' dictionary is incorrect (couldn't receive 'Hello')."); int offset = 15; BinaryData current_locale_data = dicts_data.CutData(offset, offset + 4) + BinaryData("\0", 1); std::string locale((char *) (current_locale_data.data())); offset += 4; if (locale != "PATC" && locale != "ORIG") return DatOperationResult<>(ERROR, "INITLOCALE: Data in locales' dictionary is incorrect (current locale mark is invalid)."); current_locale_ = (locale == "PATC" ? PATCHED : ORIGINAL); size_t orig_dict_size = size_t(dicts_data.CutData(offset, offset + 4).ToNumber<4>(0)); offset += 4; for (size_t i = 0; i < orig_dict_size; i++) { auto file = SubFile(*dat, dicts_data.CutData(offset, offset + 32)); file.category = dicts_data.ToNumber<4>(offset + 32); orig_dict_[file.file_id()] = file; offset += 36; } size_t patch_dict_size = size_t(dicts_data.CutData(offset, offset + 4).ToNumber<4>(0)); offset += 4; for (size_t i = 0; i < patch_dict_size; i++) { auto file = SubFile(*dat, dicts_data.CutData(offset, offset + 32)); file.category = dicts_data.ToNumber<4>(offset + 32); patch_dict_[file.file_id()] = file; offset += 36; } size_t inactive_categories_set_size = size_t(dicts_data.ToNumber<4>(offset)); offset += 4; for (size_t i = 0; i < inactive_categories_set_size; i++) { size_t category_id = size_t(dicts_data.ToNumber<4>(offset)); inactive_categories.insert(category_id); offset += 4; } LOG(INFO) << "There are " << patch_dict_.size() << " files in patch locale dictionary"; LOG(INFO) << "There are " << orig_dict_.size() << " files in original locale dictionary"; LOG(INFO) << "There are " << inactive_categories.size() << " categories inactive: "; LOG(INFO) << "Finished initialising locales"; if (CheckLocaleCorrect()) { dat->GetFileSystem().patched_file_end = patched_file_end; LOG(INFO) << "Locales initialisation success. Dictionary size is " << dict_size << ". Version is " << dict_version << ". Localed .dat size = " << patched_file_end; return DatOperationResult<>(SUCCESS); } else { orig_dict_.clear(); patch_dict_.clear(); inactive_categories.clear(); current_locale_ = ORIGINAL; return DatOperationResult<>(ERROR, "Locale dict is incorrect, through patched mark is standing. Dat file may be corrupted"); } } /*! * \Author Gi1dor * \date 06.07.2018 * Смена активной локали. Меняет информацию о файлах в файловой системе на соответствующую информацию в локали. * \param[in] locale Устанавливаемая локаль * \warning Изменения в файловую систему вносятся локально! В dat файл изменения будут записаны во время деинициализации модуля файловой системы */ DatOperationResult<> DatLocaleManager::SetLocale(DatLocaleManager::LOCALE locale) { if (dat->GetStatusModule().GetStatus() == DatStatus::E_FREE) dat->GetStatusModule().SetStatus(DatStatus::E_COMMITING); dat->GetStatusModule().SetDebugMessage("Changing locale to " + (locale == PATCHED ? std::string(" patched version") : std::string(" original version"))); LOG(INFO) << "Setting locale to " << (locale == PATCHED ? " PATCHED" : " ORIGINAL"); if (!dat) return DatOperationResult<>(ERROR, "SETLOCALE: no connection with Dat (dat is nullptr)"); if (current_locale_ == locale) { LOG(INFO) << "Locale is already " << locale << ", nothing to do."; if (dat->GetStatusModule().GetStatus() == DatStatus::E_COMMITING) dat->GetStatusModule().SetDefaultStatus(); return DatOperationResult<>(SUCCESS); } std::map &dict = GetLocaleDictReference(locale); size_t files_total = dict.size(); size_t files_proceeded = 0; dat->GetStatusModule().SetTotalParts(files_total); for (const auto &file : dict) { ++files_proceeded; dat->GetStatusModule().SetFinishedParts(files_proceeded); if (CategoryIsInactive(file.second.category) && locale == PATCHED) continue; long long file_id = file.first; auto dict_file_result = dat->GetFileSystem().GetFile(file_id); if (dict_file_result.result != SUCCESS) { LOG(WARNING) << "Unable to get file with id = " << file_id << "from datFileSystem!"; dict.erase(file_id); continue; } std::shared_ptr dict_file = dict_file_result.value; if (dict_file->MakeHeaderData().CutData(8, 16) == file.second.MakeHeaderData().CutData(8, 16)) continue; dat->GetFileSystem().UpdateFileInfo(file.second); } current_locale_ = locale; if (dat->GetStatusModule().GetStatus() == DatStatus::E_COMMITING) dat->GetStatusModule().SetDefaultStatus(); return DatOperationResult<>(SUCCESS); } /*! * \Author Gi1dor * \date 06.07.2018 * Получение текущей активной локали. Если локали не существуют, понимается, что используется оригинальная (ORIGINAL) */ DatLocaleManager::LOCALE DatLocaleManager::GetCurrentLocale() { return current_locale_; } /*! * \Author Gi1dor * \date 06.07.2018 * Деинициализация модуля. Должна происходить перед деинициализацией модулей ввода-вывода и файловой системы. */ DatOperationResult<> DatLocaleManager::DeInit() { LOG(INFO) << "Committing locales..."; if (!dat) return DatOperationResult<>(ERROR, "LOCALEDEINIT: no connection with Dat (dat is nullptr)"); auto result = CommitLocales(); ClearData(); return result; } /*! * \Author Gi1dor * \date 06.07.2018 * Обновление данных файла в локали * \param[in] locale Локаль, для которой файл данных будет обновлён * \param[in] file Новые данные файла. Старые данные будут перезаписаны */ void DatLocaleManager::UpdateLocaleFile(DatLocaleManager::LOCALE locale, const SubFile &file) { std::map &dict = GetLocaleDictReference(locale); dict[file.file_id()] = file; } /*! * \Author Gi1dor * \date 06.07.2018 * Получение данных файла в локали. Вернёт DatOperationResult.result = ERROR, если файла с таким id не существует. * \param[in] file_id id файла, для которого необходимо получить данных * \param[in] locale Локаль, данные которой будут получены */ DatOperationResult DatLocaleManager::GetLocaleFile(long long file_id, DatLocaleManager::LOCALE locale) { std::map &dict = GetLocaleDictReference(locale); if (dict.count(file_id) == 0) return DatOperationResult(SubFile(), ERROR, "GETLOCFILE: cannot get file with id = " + std::to_string(file_id) + " from dict " + std::to_string(locale)); return DatOperationResult(dict[file_id], SUCCESS); } /*! * \Author Gi1dor * \date 06.07.2018 * Получение словаря файлов локали * \param[in] locale Локаль, данные которой будут получены */ std::map &DatLocaleManager::GetLocaleDictReference(DatLocaleManager::LOCALE locale) { return locale == PATCHED ? patch_dict_ : orig_dict_; } /*! * \Author Gi1dor * \date 06.07.2018 * Выводит информацию о состоянии модуля в файл * \param[in] file указатель на объект файла, в конец которого будет напечатана информация * \warning Файл file должен быть открыт для записи. После завершения работы функции файл остаётся открытым */ void DatLocaleManager::PrintInformaion(FILE *file) { fprintf(file, "========= Locales info ========\n"); BinaryData locale_offset_data(4); dat->GetIO().ReadData(locale_offset_data, 4, 300); long long locale_offset = locale_offset_data.ToNumber<4>(0); fprintf(file, "Locales' dictionary offset = %lld\n", locale_offset); BinaryData locale_status_data(4); dat->GetIO().ReadData(locale_status_data, 4, 296); long long locale_status = locale_offset_data.ToNumber<4>(0); fprintf(file, "Locale status = %lld\n", locale_status); if (locale_offset != 0) { BinaryData locale_info(16); dat->GetIO().ReadData(locale_info, 16, locale_offset); long long dict_size = locale_info.ToNumber<4>(0); long long dict_version = locale_info.ToNumber<4>(4); fprintf(file, "Locales' dictionary size = %lld, version = %lld\n", dict_size, dict_version); } fprintf(file, "Current locale id = %d\n", current_locale_); fprintf(file, "Patch dictionary size = %d\n", patch_dict_.size()); fprintf(file, "Original dictionary size = %d\n", orig_dict_.size()); } /*! * \Author Gi1dor * \date 06.07.2018 * Осуществляет проверку корректности dat файла с позиции локалей * Файл считается некорректным, если активной является альтернативная локаль, причём нет возможности вернуть оригинальную (блок локалей неверный/повреждён) * Байты 0x128-0x12C равны 0, если на момент пользования патчером активна оригинальная локаль, и DatIO::file_size, если нет * Отличие значения в 0x128 от 0 и значения в 0x148 => файл ресурсов мог быть повреждён */ bool DatLocaleManager::CheckLocaleCorrect() { BinaryData locale_info(4); dat->GetIO().ReadData(locale_info, 4, 296); long long locale_status = locale_info.ToNumber<4>(0); BinaryData locale_offset_data(4); dat->GetIO().ReadData(locale_offset_data, 4, 300); long long locale_offset = locale_offset_data.ToNumber<4>(0); if (locale_offset == 0 || locale_offset + 8 >= dat->GetIO().GetActualDatSize().value) return locale_status == 0; BinaryData dicts_data = BinaryData(4); auto operation = dat->GetIO().ReadData(dicts_data, 4, locale_offset + 16 + 15); if (operation.result == ERROR) return locale_status == 0; BinaryData locale_data = dicts_data + BinaryData("\0", 1); std::string locale((char *) (locale_data.data())); LOCALE dat_locale = (locale == "PATC" ? PATCHED : ORIGINAL); if ((locale_status == 0 && dat_locale == PATCHED) || (locale_status != 0 && dat_locale == ORIGINAL)) { LOG(ERROR) << "CHCKLOCALECORRECT: Locale status and current_locale doesn't match!"; return false; } if (locale_status != 0 && locale_status != dat->GetIO().GetHeaderHash()){ LOG(ERROR) << "CHCKLOCALECORRECT: Locale hash doesn't match!"; return false; } return true; } /*! * \Author Gi1dor * \date 11.07.2018 * Проверка активности категории * \returns true, если категория активна и false - если нет */ bool DatLocaleManager::CategoryIsInactive(long long category) { return inactive_categories.count(category) > 0; } /*! * \Author Gi1dor * \date 11.07.2018 * Обновляет категорию у файлов в словарях локалей */ void DatLocaleManager::UpdateCategory(long long file_id, long long category) { if (orig_dict_.count(file_id)) orig_dict_[file_id].category = category; if (patch_dict_.count(file_id)) patch_dict_[file_id].category = category; } /*! * \Author Gi1dor * \date 21.10.2018 * Возвращает словарь, состоящий из id категорий, помеченных как неактивные */ const std::set &DatLocaleManager::GetInactiveCategories() { return inactive_categories; } /*! * \Author Gi1dor * \date 21.10.2018 * Записывает словарь. Если ранее словарь не существовал - новый будет записан в конец файла. * Для словаря выделяется блок не менее 20мб * * \warning Не должна вызываться вручную! Автоматически вызывается в функции Deinitialise класса DatFile * * Структура словарей локализации: * ======== LOCALE DICT STRUCTURE ========= * 4 bytes for dict size (in bytes) * 4 bytes for locale version * 4 bytes for .dat file size (with patches) * 4 bytes for dat file header hash * 15 bytes for "Hi from Gi1dor" * 4 bytes for LOCALE * 4 bytes for orig_dict.size() * (32 + 4) * orig_dict.size() bytes for orig_dict data * 4 bytes for patch_dict.size() * (32 + 4) * patch_dict.size() bytes for patch_dict data * 4 bytes for inactive_categories dict * 4 * inactive_categories.size() bytes for inactive_categories data * ======================================== * Помимо этого: * 0x128-0x12C - 0, если выбрана локаль ORIGINAL и обновление клиентом игры не испортит .dat file * хэш хедера файла в противном случае. * Отличие значения в 0x128 от 0 и значения в 0x148 => файл ресурсов мог быть повреждён * * 0x12C-0x130 - Офсет начала словаря локализации */ DatOperationResult<> DatLocaleManager::CommitLocales() { if (!dat->Initialized()) return DatOperationResult<>(SUCCESS); if (patch_dict_.empty()) { dat->GetIO().WriteData(BinaryData::FromNumber<4>(0), 4, 296); dat->GetIO().WriteData(BinaryData::FromNumber<4>(0), 4, 300); return DatOperationResult<>(SUCCESS); } dat->GetStatusModule().SetStatus(DatStatus::E_COMMITING); dat->GetStatusModule().SetTotalParts(orig_dict_.size() + patch_dict_.size()); dat->GetStatusModule().SetFinishedParts(0); BinaryData binary_data = BinaryData(4 + 4 + 4 + 4 + 14 + 15 + 4 + 4 + (32 + 4) * orig_dict_.size() + 4 + (32 + 4) * patch_dict_.size() + 4 + 4 * inactive_categories.size()); // First 16 bytes will be filled just before writing data to file size_t current_size = 16; binary_data.Append(BinaryData("Hi from Gi1dor!", 15), current_size); current_size += 15; binary_data.Append(BinaryData((current_locale_ == ORIGINAL ? "ORIG" : "PATC"), 4), current_size); current_size += 4; binary_data.Append(BinaryData::FromNumber<4>(orig_dict_.size()), current_size); current_size += 4; unsigned long long processed_files = 0; for (const auto &file : orig_dict_) { binary_data.Append(file.second.MakeHeaderData(), current_size); current_size += 32; binary_data.Append(BinaryData::FromNumber<4>(file.second.category), current_size); current_size += 4; dat->GetStatusModule().SetFinishedParts(++processed_files); } binary_data.Append(BinaryData::FromNumber<4>(patch_dict_.size()), current_size); current_size += 4; for (const auto &file : patch_dict_) { binary_data.Append(file.second.MakeHeaderData(), current_size); current_size += 32; binary_data.Append(BinaryData::FromNumber<4>(file.second.category), current_size); current_size += 4; dat->GetStatusModule().SetFinishedParts(++processed_files); } binary_data.Append(BinaryData::FromNumber<4>(inactive_categories.size()), current_size); current_size += 4; for (auto patch_id : inactive_categories) { binary_data.Append(BinaryData::FromNumber<4>(patch_id), current_size); current_size += 4; } BinaryData dicts_data(4); dat->GetIO().ReadData(dicts_data, 4, 300); long long dict_offset = dicts_data.ToNumber<4>(0); dat->GetIO().ReadData(dicts_data, 4, dict_offset); long long dict_size = dicts_data.ToNumber<4>(0); if (binary_data.size() > dict_size || dict_offset == 0) { long long new_dict_offset = dat->GetFileSystem().patched_file_end + 16; // Updating first 16 bytes binary_data.Append(BinaryData::FromNumber<4>(std::max(binary_data.size() + 4, 20u * 1024u * 1024u)), 0); binary_data.Append(BinaryData::FromNumber<4>(DAT_LOCALE_DICT_VERSION), 4); binary_data.Append( BinaryData::FromNumber<4>(dat->GetFileSystem().patched_file_end + binary_data.size() + 20 * 1024 * 1024), 8); binary_data.Append(BinaryData::FromNumber<4>(dat->GetIO().GetHeaderHash()), 12); auto operation = dat->GetIO().WriteData(binary_data, binary_data.size(), new_dict_offset); if (operation.result != SUCCESS) { return DatOperationResult<>(ERROR, "LOCALEDEINIT: Cannot write locales"); } dat->GetIO().WriteData(BinaryData::FromNumber<4>(new_dict_offset), 4, 300); if (current_locale_ == PATCHED) { dat->GetIO().WriteData(BinaryData::FromNumber<4>(dat->GetIO().GetHeaderHash()), 4, 296); } else { dat->GetIO().WriteData(BinaryData::FromNumber<4>(0), 4, 296); } dat->GetFileSystem().patched_file_end += binary_data.size(); LOG(INFO) << "Writing 20 mbytes to " << dat->GetFileSystem().patched_file_end; BinaryData nulls(unsigned(20 * 1024 * 1024)); dat->GetIO().WriteData(nulls, nulls.size(), dat->GetFileSystem().patched_file_end); dat->GetFileSystem().patched_file_end += nulls.size(); } else { binary_data.Append(BinaryData::FromNumber<4>(std::max(binary_data.size() + 4, 20u * 1024u * 1024u)), 0); binary_data.Append(BinaryData::FromNumber<4>(DAT_LOCALE_DICT_VERSION), 4); binary_data.Append(BinaryData::FromNumber<4>(dat->GetFileSystem().patched_file_end), 8); binary_data.Append(BinaryData::FromNumber<4>(dat->GetIO().GetHeaderHash()), 12); if (current_locale_ == PATCHED) { dat->GetIO().WriteData(BinaryData::FromNumber<4>(dat->GetIO().GetHeaderHash()), 4, 296); } else { dat->GetIO().WriteData(BinaryData::FromNumber<4>(0), 4, 296); } auto operation = dat->GetIO().WriteData(binary_data, binary_data.size(), dict_offset); if (operation.result != SUCCESS) { return DatOperationResult<>(ERROR, "LOCALEDEINIT: Cannot write locales. ERRMSG: " + operation.msg); } } dat->GetStatusModule().SetDefaultStatus(); LOG(INFO) << "Locales committed successfully"; return DatOperationResult<>(SUCCESS); } void DatLocaleManager::ClearData() { orig_dict_.clear(); patch_dict_.clear(); inactive_categories.clear(); current_locale_ = LOCALE(-1); } DatOperationResult<> DatLocaleManager::EnableCategory(long long category) { inactive_categories.erase(category); dat->GetStatusModule().SetStatus(DatStatus::E_COMMITING); size_t files_count = patch_dict_.size(); size_t files_processed = 0; dat->GetStatusModule().SetTotalParts(files_count); for (const auto& entry : patch_dict_) { SubFile file = entry.second; ++files_processed; dat->GetStatusModule().SetFinishedParts(files_processed); if (file.category == category) { dat->GetFileSystem().UpdateFileInfo(file); } } dat->GetStatusModule().SetDefaultStatus(); return DatOperationResult<>(SUCCESS); } DatOperationResult<> DatLocaleManager::DisableCategory(long long category) { inactive_categories.insert(category); dat->GetStatusModule().SetStatus(DatStatus::E_COMMITING); dat->GetStatusModule().SetFinishedParts(0); size_t files_count = orig_dict_.size(); size_t files_processed = 0; dat->GetStatusModule().SetTotalParts(files_count); for (const auto& entry : orig_dict_) { SubFile file = entry.second; ++files_processed; dat->GetStatusModule().SetFinishedParts(files_processed); if (file.category == category) { dat->GetFileSystem().UpdateFileInfo(file); } } dat->GetStatusModule().SetDefaultStatus(); return DatOperationResult<>(SUCCESS); } }