OpenTTD Source 20260206-master-g4d4e37dbf1
base_media_func.h
Go to the documentation of this file.
1/*
2 * This file is part of OpenTTD.
3 * 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.
4 * 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.
5 * 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 <https://www.gnu.org/licenses/old-licenses/gpl-2.0>.
6 */
7
9
10#include "base_media_base.h"
11#include "debug.h"
12#include "ini_type.h"
13#include "string_func.h"
14#include "error_func.h"
16#include "3rdparty/fmt/ranges.h"
17
18extern void CheckExternalFiles();
19
26template <class T>
27void BaseSet<T>::LogError(std::string_view full_filename, std::string_view detail, int level) const
28{
29 Debug(misc, level, "Loading base {}set details failed: {}", BaseSet<T>::SET_TYPE, full_filename);
30 Debug(misc, level, " {}", detail);
31}
32
40template <class T>
41const IniItem *BaseSet<T>::GetMandatoryItem(std::string_view full_filename, const IniGroup &group, std::string_view name) const
42{
43 auto *item = group.GetItem(name);
44 if (item != nullptr && item->value.has_value() && !item->value->empty()) return item;
45 this->LogError(full_filename, fmt::format("{}.{} field missing.", group.name, name));
46 return nullptr;
47}
48
57template <class T>
58bool BaseSet<T>::FillSetDetails(const IniFile &ini, const std::string &path, const std::string &full_filename, bool allow_empty_filename)
59{
60 const IniGroup *metadata = ini.GetGroup("metadata");
61 if (metadata == nullptr) {
62 this->LogError(full_filename, "Is the file readable for the user running OpenTTD?");
63 return false;
64 }
65 const IniItem *item;
66
67 item = this->GetMandatoryItem(full_filename, *metadata, "name");
68 if (item == nullptr) return false;
69 this->name = *item->value;
70
71 item = this->GetMandatoryItem(full_filename, *metadata, "description");
72 if (item == nullptr) return false;
73 this->description[std::string{}] = *item->value;
74
75 item = metadata->GetItem("url");
76 if (item != nullptr) this->url = *item->value;
77
78 /* Add the translations of the descriptions too. */
79 for (const IniItem &titem : metadata->items) {
80 if (!titem.name.starts_with("description.")) continue;
81
82 this->description[titem.name.substr(12)] = titem.value.value_or("");
83 }
84
85 item = this->GetMandatoryItem(full_filename, *metadata, "shortname");
86 if (item == nullptr) return false;
87 for (uint i = 0; (*item->value)[i] != '\0' && i < 4; i++) {
88 this->shortname |= ((uint8_t)(*item->value)[i]) << (i * 8);
89 }
90
91 item = this->GetMandatoryItem(full_filename, *metadata, "version");
92 if (item == nullptr) return false;
93 for (StringConsumer consumer{*item->value};;) {
94 auto value = consumer.TryReadIntegerBase<uint32_t>(10);
95 bool valid = value.has_value();
96 if (valid) this->version.push_back(*value);
97 if (valid && !consumer.AnyBytesLeft()) break;
98 if (!valid || !consumer.ReadIf(".")) {
99 this->LogError(full_filename, fmt::format("metadata.version field is invalid: {}", *item->value));
100 return false;
101 }
102 }
103
104 item = metadata->GetItem("fallback");
105 this->fallback = (item != nullptr && item->value && *item->value != "0" && *item->value != "false");
106
107 /* For each of the file types we want to find the file, MD5 checksums and warning messages. */
108 const IniGroup *files = ini.GetGroup("files");
109 const IniGroup *md5s = ini.GetGroup("md5s");
110 const IniGroup *origin = ini.GetGroup("origin");
111 auto file_names = BaseSet<T>::GetFilenames();
112 bool original_set =
113 std::byteswap(this->shortname) == 'TTDD' || // TTD DOS graphics, TTD DOS music
114 std::byteswap(this->shortname) == 'TTDW' || // TTD WIN graphics, TTD WIN music
115 std::byteswap(this->shortname) == 'TTDO' || // TTD sound
116 std::byteswap(this->shortname) == 'TTOD'; // TTO music
117
118 for (uint i = 0; i < BaseSet<T>::NUM_FILES; i++) {
119 MD5File *file = &this->files[i];
120 /* Find the filename first. */
121 item = files != nullptr ? files->GetItem(file_names[i]) : nullptr;
122 if (item == nullptr || (!item->value.has_value() && !allow_empty_filename)) {
123 this->LogError(full_filename, fmt::format("files.{} field missing", file_names[i]));
124 return false;
125 }
126
127 if (!item->value.has_value()) {
128 file->filename.clear();
129 /* If we list no file, that file must be valid */
130 this->valid_files++;
131 this->found_files++;
132 continue;
133 }
134
135 const std::string &filename = item->value.value();
136 file->filename = path + filename;
137
138 /* Then find the MD5 checksum */
139 item = md5s != nullptr ? md5s->GetItem(filename) : nullptr;
140 if (item == nullptr || !item->value.has_value()) {
141 this->LogError(full_filename, fmt::format("md5s.{} field missing", filename));
142 return false;
143 }
144 if (!ConvertHexToBytes(*item->value, file->hash)) {
145 this->LogError(full_filename, fmt::format("md5s.{} is malformed: {}", filename, *item->value));
146 return false;
147 }
148
149 /* Then find the warning message when the file's missing */
150 item = origin != nullptr ? origin->GetItem(filename) : nullptr;
151 if (item == nullptr) item = origin != nullptr ? origin->GetItem("default") : nullptr;
152 if (item == nullptr || !item->value.has_value()) {
153 this->LogError(full_filename, fmt::format("origin.{} field missing", filename), 1);
154 file->missing_warning.clear();
155 } else {
156 file->missing_warning = item->value.value();
157 }
158
159 file->check_result = T::CheckMD5(file, BASESET_DIR);
160 switch (file->check_result) {
162 break;
163
165 this->valid_files++;
166 this->found_files++;
167 break;
168
170 /* This is normal for original sample.cat, which either matches with orig_dos or orig_win. */
171 this->LogError(full_filename, fmt::format("MD5 checksum mismatch for: {}", filename), original_set ? 1 : 0);
172 this->found_files++;
173 break;
174
176 /* Missing files is normal for the original basesets. Use lower debug level */
177 this->LogError(full_filename, fmt::format("File is missing: {}", filename), original_set ? 1 : 0);
178 break;
179 }
180 }
181
182 return true;
183}
184
185template <class Tbase_set>
186bool BaseMedia<Tbase_set>::AddFile(const std::string &filename, size_t basepath_length, const std::string &)
187{
188 Debug(misc, 1, "Checking {} for base {} set", filename, BaseSet<Tbase_set>::SET_TYPE);
189
190 auto set = std::make_unique<Tbase_set>();
191 IniFile ini{};
192 std::string path{ filename, basepath_length };
193 ini.LoadFromDisk(path, BASESET_DIR);
194
195 auto psep = path.rfind(PATHSEPCHAR);
196 if (psep != std::string::npos) {
197 path.erase(psep + 1);
198 } else {
199 path.clear();
200 }
201
202 if (!set->FillSetDetails(ini, path, filename)) return false;
203
204 auto existing = std::ranges::find_if(BaseMedia<Tbase_set>::available_sets, [&set](const auto &c) { return c->name == set->name || c->shortname == set->shortname; });
205 if (existing != std::end(BaseMedia<Tbase_set>::available_sets)) {
206 /* The more complete set takes precedence over the version number. */
207 if (((*existing)->valid_files == set->valid_files && (*existing)->version >= set->version) ||
208 (*existing)->valid_files > set->valid_files) {
210 Debug(misc, 1, "Not adding {} ({}) as base {} set (duplicate, {})", set->name, fmt::join(set->version, "."),
212 (*existing)->valid_files > set->valid_files ? "fewer valid files" : "lower version");
214 duplicate_sets.push_back(std::move(set));
215 return false;
216 }
217
218 /* If the duplicate set is currently used (due to rescanning this can happen)
219 * update the currently used set to the new one. This will 'lie' about the
220 * version number until a new game is started which isn't a big problem */
221 if (BaseMedia<Tbase_set>::used_set == existing->get()) BaseMedia<Tbase_set>::used_set = set.get();
223 /* Keep baseset configuration, if compatible */
224 set->CopyCompatibleConfig(**existing);
225
226 Debug(misc, 1, "Removing {} ({}) as base {} set (duplicate, {})", (*existing)->name, fmt::join((*existing)->version, "."), BaseSet<Tbase_set>::SET_TYPE,
227 (*existing)->valid_files < set->valid_files ? "fewer valid files" : "lower version");
228
229 /* Existing set is worse, move it to duplicates and replace with the current set. */
230 duplicate_sets.push_back(std::move(*existing));
231
232 Debug(misc, 1, "Adding {} ({}) as base {} set", set->name, fmt::join(set->version, "."), BaseSet<Tbase_set>::SET_TYPE);
233 *existing = std::move(set);
234 } else {
235 Debug(grf, 1, "Adding {} ({}) as base {} set", set->name, set->version, BaseSet<Tbase_set>::SET_TYPE);
236 available_sets.push_back(std::move(set));
237 }
238
239 return true;
240}
241
247template <class Tbase_set>
248/* static */ bool BaseMedia<Tbase_set>::SetSet(const Tbase_set *set)
249{
250 if (set == nullptr) {
251 if (!BaseMedia<Tbase_set>::DetermineBestSet()) return false;
252 } else {
254 }
256 return true;
257}
258
264template <class Tbase_set>
265/* static */ bool BaseMedia<Tbase_set>::SetSetByName(const std::string &name)
266{
267 if (name.empty()) {
268 return SetSet(nullptr);
269 }
270
271 for (const auto &s : BaseMedia<Tbase_set>::available_sets) {
272 if (name == s->name) {
273 return SetSet(s.get());
274 }
275 }
276 return false;
277}
278
284template <class Tbase_set>
285/* static */ bool BaseMedia<Tbase_set>::SetSetByShortname(uint32_t shortname)
286{
287 if (shortname == 0) {
288 return SetSet(nullptr);
289 }
290
291 for (const auto &s : BaseMedia<Tbase_set>::available_sets) {
292 if (shortname == s->shortname) {
293 return SetSet(s.get());
294 }
295 }
296 return false;
297}
298
303template <class Tbase_set>
304/* static */ void BaseMedia<Tbase_set>::GetSetsList(std::back_insert_iterator<std::string> &output_iterator)
305{
306 fmt::format_to(output_iterator, "List of {} sets:\n", BaseSet<Tbase_set>::SET_TYPE);
307 for (const auto &s : BaseMedia<Tbase_set>::available_sets) {
308 fmt::format_to(output_iterator, "{:>18}: {}", s->name, s->GetDescription({}));
309 int invalid = s->GetNumInvalid();
310 if (invalid != 0) {
311 int missing = s->GetNumMissing();
312 if (missing == 0) {
313 fmt::format_to(output_iterator, " ({} corrupt file{})\n", invalid, invalid == 1 ? "" : "s");
314 } else {
315 fmt::format_to(output_iterator, " (unusable: {} missing file{})\n", missing, missing == 1 ? "" : "s");
316 }
317 } else {
318 fmt::format_to(output_iterator, "\n");
319 }
320 }
321 fmt::format_to(output_iterator, "\n");
322}
323
325
326template <class Tbase_set> std::optional<std::string_view> TryGetBaseSetFile(const ContentInfo &ci, bool md5sum, std::span<const std::unique_ptr<Tbase_set>> sets)
327{
328 for (const auto &s : sets) {
329 if (s->GetNumMissing() != 0) continue;
330
331 if (s->shortname != ci.unique_id) continue;
332 if (!md5sum) return s->files[0].filename;
333
334 MD5Hash md5;
335 for (const auto &file : s->files) {
336 md5 ^= file.hash;
337 }
338 if (md5 == ci.md5sum) return s->files[0].filename;
339 }
340 return std::nullopt;
341}
342
343template <class Tbase_set>
344/* static */ bool BaseMedia<Tbase_set>::HasSet(const ContentInfo &ci, bool md5sum)
345{
346 return TryGetBaseSetFile(ci, md5sum, BaseMedia<Tbase_set>::GetAvailableSets()).has_value() ||
348}
349
354template <class Tbase_set>
356{
357 return std::ranges::count_if(BaseMedia<Tbase_set>::GetAvailableSets(), [](const auto &set) {
358 return set.get() == BaseMedia<Tbase_set>::used_set || set->GetNumMissing() == 0;
359 });
360}
361
366template <class Tbase_set>
368{
369 int n = 0;
370 for (const auto &s : BaseMedia<Tbase_set>::available_sets) {
371 if (s.get() == BaseMedia<Tbase_set>::used_set) return n;
372 if (s->GetNumMissing() != 0) continue;
373 n++;
374 }
375 return -1;
376}
377
382template <class Tbase_set>
383/* static */ const Tbase_set *BaseMedia<Tbase_set>::GetSet(int index)
384{
385 for (const auto &s : BaseMedia<Tbase_set>::available_sets) {
386 if (s.get() != BaseMedia<Tbase_set>::used_set && s->GetNumMissing() != 0) continue;
387 if (index == 0) return s.get();
388 index--;
389 }
390 FatalError("Base{}::GetSet(): index {} out of range", BaseSet<Tbase_set>::SET_TYPE, index);
391}
392
397template <class Tbase_set>
398/* static */ const Tbase_set *BaseMedia<Tbase_set>::GetUsedSet()
399{
401}
Generic functions for replacing base data (graphics, sounds).
void CheckExternalFiles()
Checks whether the MD5 checksums of the files are correct.
Definition gfxinit.cpp:113
std::optional< std::string_view > TryGetBaseSetFile(const ContentInfo &ci, bool md5sum, std::span< const std::unique_ptr< Tbase_set > > sets)
Check whether there's a base set matching some information.
constexpr enable_if_t< is_integral_v< T >, T > byteswap(T x) noexcept
Custom implementation of std::byteswap; remove once we build with C++23.
static const Tbase_set * GetUsedSet()
Return the used set.
static std::vector< std::unique_ptr< Tbase_set > > available_sets
All available sets.
bool AddFile(const std::string &filename, size_t basepath_length, const std::string &tar_filename) override
Add a file with the given filename.
static bool SetSetByShortname(uint32_t shortname)
Set the set to be used.
static void GetSetsList(std::back_insert_iterator< std::string > &output_iterator)
Returns a list with the sets.
static std::span< const std::unique_ptr< Tbase_set > > GetAvailableSets()
Return the available sets.
static int GetNumSets()
Count the number of available graphics sets.
static bool HasSet(const ContentInfo &ci, bool md5sum)
Check whether we have an set with the exact characteristics as ci.
static const Tbase_set * used_set
The currently used set.
static std::vector< std::unique_ptr< GraphicsSet > > duplicate_sets
static const Tbase_set * GetSet(int index)
Get the name of the graphics set at the specified index.
static int GetIndexOfUsedSet()
Get the index of the currently active graphics set.
static bool DetermineBestSet()
Determine the graphics pack that has to be used.
static bool SetSetByName(const std::string &name)
Set the set to be used.
static std::span< const std::unique_ptr< Tbase_set > > GetDuplicateSets()
Return the duplicate sets.
static bool SetSet(const Tbase_set *set)
Set the set to be used.
Parse data from a string / buffer.
Functions related to debugging.
#define Debug(category, level, format_string,...)
Output a line of debugging information.
Definition debug.h:37
Error reporting related functions.
@ BASESET_DIR
Subdirectory for all base data (base sets, intro game).
Definition fileio_type.h:96
Types related to reading/writing '*.ini' files.
bool ConvertHexToBytes(std::string_view hex, std::span< uint8_t > bytes)
Convert a hex-string to a byte-array, while validating it was actually hex.
Definition string.cpp:570
Parse strings.
Functions related to low-level strings.
void LogError(std::string_view full_filename, std::string_view detail, int level=0) const
Log error from reading basesets.
const IniItem * GetMandatoryItem(std::string_view full_filename, const IniGroup &group, std::string_view name) const
Try to read a single piece of metadata and return nullptr if it doesn't exist.
std::string url
URL for information about the base set.
TranslatedStrings description
Description of the base set.
static std::span< const std::string_view > GetFilenames()
Get the internal names of the files in this set.
static constexpr std::string_view SET_TYPE
BaseSet type name.
std::string name
The name of the base set.
bool FillSetDetails(const IniFile &ini, const std::string &path, const std::string &full_filename, bool allow_empty_filename=true)
Read the set information from a loaded ini.
std::vector< uint32_t > version
uint32_t shortname
Four letter short variant of the name.
Container for all important information about a piece of content.
uint32_t unique_id
Unique ID; either GRF ID or shortname.
MD5Hash md5sum
The MD5 checksum.
Ini file that supports both loading and saving.
Definition ini_type.h:86
A group within an ini file.
Definition ini_type.h:34
const IniItem * GetItem(std::string_view name) const
Get the item with the given name.
Definition ini_load.cpp:50
std::string name
name of group
Definition ini_type.h:37
std::list< IniItem > items
all items in the group
Definition ini_type.h:35
A single "line" in an ini file.
Definition ini_type.h:23
std::optional< std::string > value
The value of this item.
Definition ini_type.h:25
std::string name
The name of this item.
Definition ini_type.h:24
const IniGroup * GetGroup(std::string_view name) const
Get the group with the given name.
Definition ini_load.cpp:117
void LoadFromDisk(std::string_view filename, Subdirectory subdir)
Load the Ini file's data from the disk.
Definition ini_load.cpp:184
Structure holding filename and MD5 information about a single file.
std::string missing_warning
warning when this file is missing
ChecksumResult check_result
cached result of md5 check
@ CR_MATCH
The file did exist and the md5 checksum did match.
@ CR_MISMATCH
The file did exist, just the md5 checksum did not match.
@ CR_NO_FILE
The file did not exist.
@ CR_UNKNOWN
The file has not been checked yet.
MD5Hash hash
md5 sum of the file
std::string filename
filename
Basic types related to the content on the content server.