/* * This file is part of OpenTTD. * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . */ /** * @file base_media_func.h Generic function implementations for base data (graphics, sounds). */ #include "base_media_base.h" #include "debug.h" #include "ini_type.h" #include "string_func.h" #include "error_func.h" #include "core/string_consumer.hpp" #include "3rdparty/fmt/ranges.h" extern void CheckExternalFiles(); /** * Log error from reading basesets. * @param full_filename the full filename of the loaded file * @param detail detail log message * @param level debug level */ template void BaseSet::LogError(std::string_view full_filename, std::string_view detail, int level) const { Debug(misc, level, "Loading base {}set details failed: {}", BaseSet::SET_TYPE, full_filename); Debug(misc, level, " {}", detail); } /** * Try to read a single piece of metadata and return nullptr if it doesn't exist. * Log error, if the data is missing. * @param full_filename the full filename of the loaded file (for error reporting purposes) * @param group ini group to read from * @param name the name of the item to fetch. */ template const IniItem *BaseSet::GetMandatoryItem(std::string_view full_filename, const IniGroup &group, std::string_view name) const { auto *item = group.GetItem(name); if (item != nullptr && item->value.has_value() && !item->value->empty()) return item; this->LogError(full_filename, fmt::format("{}.{} field missing.", group.name, name)); return nullptr; } /** * Read the set information from a loaded ini. * @param ini the ini to read from * @param path the path to this ini file (for filenames) * @param full_filename the full filename of the loaded file (for error reporting purposes) * @param allow_empty_filename empty filenames are valid * @return true if loading was successful. */ template bool BaseSet::FillSetDetails(const IniFile &ini, const std::string &path, const std::string &full_filename, bool allow_empty_filename) { const IniGroup *metadata = ini.GetGroup("metadata"); if (metadata == nullptr) { this->LogError(full_filename, "Is the file readable for the user running OpenTTD?"); return false; } const IniItem *item; item = this->GetMandatoryItem(full_filename, *metadata, "name"); if (item == nullptr) return false; this->name = *item->value; item = this->GetMandatoryItem(full_filename, *metadata, "description"); if (item == nullptr) return false; this->description[std::string{}] = *item->value; item = metadata->GetItem("url"); if (item != nullptr) this->url = *item->value; /* Add the translations of the descriptions too. */ for (const IniItem &titem : metadata->items) { if (titem.name.compare(0, 12, "description.") != 0) continue; this->description[titem.name.substr(12)] = titem.value.value_or(""); } item = this->GetMandatoryItem(full_filename, *metadata, "shortname"); if (item == nullptr) return false; for (uint i = 0; (*item->value)[i] != '\0' && i < 4; i++) { this->shortname |= ((uint8_t)(*item->value)[i]) << (i * 8); } item = this->GetMandatoryItem(full_filename, *metadata, "version"); if (item == nullptr) return false; for (StringConsumer consumer{*item->value};;) { auto value = consumer.TryReadIntegerBase(10); bool valid = value.has_value(); if (valid) this->version.push_back(*value); if (valid && !consumer.AnyBytesLeft()) break; if (!valid || !consumer.ReadIf(".")) { this->LogError(full_filename, fmt::format("metadata.version field is invalid: {}", *item->value)); return false; } } item = metadata->GetItem("fallback"); this->fallback = (item != nullptr && item->value && *item->value != "0" && *item->value != "false"); /* For each of the file types we want to find the file, MD5 checksums and warning messages. */ const IniGroup *files = ini.GetGroup("files"); const IniGroup *md5s = ini.GetGroup("md5s"); const IniGroup *origin = ini.GetGroup("origin"); auto file_names = BaseSet::GetFilenames(); bool original_set = std::byteswap(this->shortname) == 'TTDD' || // TTD DOS graphics, TTD DOS music std::byteswap(this->shortname) == 'TTDW' || // TTD WIN graphics, TTD WIN music std::byteswap(this->shortname) == 'TTDO' || // TTD sound std::byteswap(this->shortname) == 'TTOD'; // TTO music for (uint i = 0; i < BaseSet::NUM_FILES; i++) { MD5File *file = &this->files[i]; /* Find the filename first. */ item = files != nullptr ? files->GetItem(file_names[i]) : nullptr; if (item == nullptr || (!item->value.has_value() && !allow_empty_filename)) { this->LogError(full_filename, fmt::format("files.{} field missing", file_names[i])); return false; } if (!item->value.has_value()) { file->filename.clear(); /* If we list no file, that file must be valid */ this->valid_files++; this->found_files++; continue; } const std::string &filename = item->value.value(); file->filename = path + filename; /* Then find the MD5 checksum */ item = md5s != nullptr ? md5s->GetItem(filename) : nullptr; if (item == nullptr || !item->value.has_value()) { this->LogError(full_filename, fmt::format("md5s.{} field missing", filename)); return false; } const char *c = item->value->c_str(); for (size_t i = 0; i < file->hash.size() * 2; i++, c++) { uint j; if ('0' <= *c && *c <= '9') { j = *c - '0'; } else if ('a' <= *c && *c <= 'f') { j = *c - 'a' + 10; } else if ('A' <= *c && *c <= 'F') { j = *c - 'A' + 10; } else { this->LogError(full_filename, fmt::format("md5s.{} is malformed: {}", filename, *item->value)); return false; } if (i % 2 == 0) { file->hash[i / 2] = j << 4; } else { file->hash[i / 2] |= j; } } /* Then find the warning message when the file's missing */ item = origin != nullptr ? origin->GetItem(filename) : nullptr; if (item == nullptr) item = origin != nullptr ? origin->GetItem("default") : nullptr; if (item == nullptr || !item->value.has_value()) { this->LogError(full_filename, fmt::format("origin.{} field missing", filename), 1); file->missing_warning.clear(); } else { file->missing_warning = item->value.value(); } file->check_result = T::CheckMD5(file, BASESET_DIR); switch (file->check_result) { case MD5File::CR_UNKNOWN: break; case MD5File::CR_MATCH: this->valid_files++; this->found_files++; break; case MD5File::CR_MISMATCH: /* This is normal for original sample.cat, which either matches with orig_dos or orig_win. */ this->LogError(full_filename, fmt::format("MD5 checksum mismatch for: {}", filename), original_set ? 1 : 0); this->found_files++; break; case MD5File::CR_NO_FILE: /* Missing files is normal for the original basesets. Use lower debug level */ this->LogError(full_filename, fmt::format("File is missing: {}", filename), original_set ? 1 : 0); break; } } return true; } template bool BaseMedia::AddFile(const std::string &filename, size_t basepath_length, const std::string &) { bool ret = false; Debug(misc, 1, "Checking {} for base {} set", filename, BaseSet::SET_TYPE); Tbase_set *set = new Tbase_set(); IniFile ini{}; std::string path{ filename, basepath_length }; ini.LoadFromDisk(path, BASESET_DIR); auto psep = path.rfind(PATHSEPCHAR); if (psep != std::string::npos) { path.erase(psep + 1); } else { path.clear(); } if (set->FillSetDetails(ini, path, filename)) { Tbase_set *duplicate = nullptr; for (Tbase_set *c = BaseMedia::available_sets; c != nullptr; c = c->next) { if (c->name == set->name || c->shortname == set->shortname) { duplicate = c; break; } } if (duplicate != nullptr) { /* The more complete set takes precedence over the version number. */ if ((duplicate->valid_files == set->valid_files && duplicate->version >= set->version) || duplicate->valid_files > set->valid_files) { Debug(misc, 1, "Not adding {} ({}) as base {} set (duplicate, {})", set->name, fmt::join(set->version, "."), BaseSet::SET_TYPE, duplicate->valid_files > set->valid_files ? "less valid files" : "lower version"); set->next = BaseMedia::duplicate_sets; BaseMedia::duplicate_sets = set; } else { Tbase_set **prev = &BaseMedia::available_sets; while (*prev != duplicate) prev = &(*prev)->next; *prev = set; set->next = duplicate->next; /* Keep baseset configuration, if compatible */ set->CopyCompatibleConfig(*duplicate); /* If the duplicate set is currently used (due to rescanning this can happen) * update the currently used set to the new one. This will 'lie' about the * version number until a new game is started which isn't a big problem */ if (BaseMedia::used_set == duplicate) BaseMedia::used_set = set; Debug(misc, 1, "Removing {} ({}) as base {} set (duplicate, {})", duplicate->name, fmt::join(duplicate->version, "."), BaseSet::SET_TYPE, duplicate->valid_files < set->valid_files ? "less valid files" : "lower version"); duplicate->next = BaseMedia::duplicate_sets; BaseMedia::duplicate_sets = duplicate; ret = true; } } else { Tbase_set **last = &BaseMedia::available_sets; while (*last != nullptr) last = &(*last)->next; *last = set; ret = true; } if (ret) { Debug(misc, 1, "Adding {} ({}) as base {} set", set->name, fmt::join(set->version, "."), BaseSet::SET_TYPE); } } else { delete set; } return ret; } /** * Set the set to be used. * @param set the set to use * @return true if it could be loaded */ template /* static */ bool BaseMedia::SetSet(const Tbase_set *set) { if (set == nullptr) { if (!BaseMedia::DetermineBestSet()) return false; } else { BaseMedia::used_set = set; } CheckExternalFiles(); return true; } /** * Set the set to be used. * @param name of the set to use * @return true if it could be loaded */ template /* static */ bool BaseMedia::SetSetByName(const std::string &name) { if (name.empty()) { return SetSet(nullptr); } for (const Tbase_set *s = BaseMedia::available_sets; s != nullptr; s = s->next) { if (name == s->name) { return SetSet(s); } } return false; } /** * Set the set to be used. * @param shortname of the set to use * @return true if it could be loaded */ template /* static */ bool BaseMedia::SetSetByShortname(uint32_t shortname) { if (shortname == 0) { return SetSet(nullptr); } for (const Tbase_set *s = BaseMedia::available_sets; s != nullptr; s = s->next) { if (shortname == s->shortname) { return SetSet(s); } } return false; } /** * Returns a list with the sets. * @param output_iterator The iterator to write the string to. */ template /* static */ void BaseMedia::GetSetsList(std::back_insert_iterator &output_iterator) { fmt::format_to(output_iterator, "List of {} sets:\n", BaseSet::SET_TYPE); for (const Tbase_set *s = BaseMedia::available_sets; s != nullptr; s = s->next) { fmt::format_to(output_iterator, "{:>18}: {}", s->name, s->GetDescription({})); int invalid = s->GetNumInvalid(); if (invalid != 0) { int missing = s->GetNumMissing(); if (missing == 0) { fmt::format_to(output_iterator, " ({} corrupt file{})\n", invalid, invalid == 1 ? "" : "s"); } else { fmt::format_to(output_iterator, " (unusable: {} missing file{})\n", missing, missing == 1 ? "" : "s"); } } else { fmt::format_to(output_iterator, "\n"); } } fmt::format_to(output_iterator, "\n"); } #include "network/core/tcp_content_type.h" template std::optional TryGetBaseSetFile(const ContentInfo &ci, bool md5sum, const Tbase_set *s) { for (; s != nullptr; s = s->next) { if (s->GetNumMissing() != 0) continue; if (s->shortname != ci.unique_id) continue; if (!md5sum) return s->files[0].filename; MD5Hash md5; for (const auto &file : s->files) { md5 ^= file.hash; } if (md5 == ci.md5sum) return s->files[0].filename; } return std::nullopt; } template /* static */ bool BaseMedia::HasSet(const ContentInfo &ci, bool md5sum) { return TryGetBaseSetFile(ci, md5sum, BaseMedia::available_sets).has_value() || TryGetBaseSetFile(ci, md5sum, BaseMedia::duplicate_sets).has_value(); } /** * Count the number of available graphics sets. * @return the number of sets */ template /* static */ int BaseMedia::GetNumSets() { int n = 0; for (const Tbase_set *s = BaseMedia::available_sets; s != nullptr; s = s->next) { if (s != BaseMedia::used_set && s->GetNumMissing() != 0) continue; n++; } return n; } /** * Get the index of the currently active graphics set * @return the current set's index */ template /* static */ int BaseMedia::GetIndexOfUsedSet() { int n = 0; for (const Tbase_set *s = BaseMedia::available_sets; s != nullptr; s = s->next) { if (s == BaseMedia::used_set) return n; if (s->GetNumMissing() != 0) continue; n++; } return -1; } /** * Get the name of the graphics set at the specified index * @return the name of the set */ template /* static */ const Tbase_set *BaseMedia::GetSet(int index) { for (const Tbase_set *s = BaseMedia::available_sets; s != nullptr; s = s->next) { if (s != BaseMedia::used_set && s->GetNumMissing() != 0) continue; if (index == 0) return s; index--; } FatalError("Base{}::GetSet(): index {} out of range", BaseSet::SET_TYPE, index); } /** * Return the used set. * @return the used set. */ template /* static */ const Tbase_set *BaseMedia::GetUsedSet() { return BaseMedia::used_set; } /** * Return the available sets. * @return The available sets. */ template /* static */ Tbase_set *BaseMedia::GetAvailableSets() { return BaseMedia::available_sets; }