OpenTTD Source 20260721-master-g25ec12c62d
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
41template <class T>
42const IniItem *BaseSet<T>::GetMandatoryItem(std::string_view full_filename, const IniGroup &group, std::string_view name) const
43{
44 auto *item = group.GetItem(name);
45 if (item != nullptr && item->value.has_value() && !item->value->empty()) return item;
46 this->LogError(full_filename, fmt::format("{}.{} field missing.", group.name, name));
47 return nullptr;
48}
49
55constexpr uint32_t DecodeShortName(std::string_view shortname)
56{
57 /* The short name is documented to be exactly 4 characters long, see docs/ob[gms]_format.txt. */
58 shortname = shortname.substr(0, 4);
59 uint32_t encoded = 0;
60 for (size_t i = 0; i < shortname.size(); i++) {
61 encoded |= static_cast<uint8_t>(shortname[i]) << (i * 8);
62 }
63 return encoded;
64}
65
74template <class T>
75bool BaseSet<T>::FillSetDetails(const IniFile &ini, const std::string &path, const std::string &full_filename, bool allow_empty_filename)
76{
77 const IniGroup *metadata = ini.GetGroup("metadata");
78 if (metadata == nullptr) {
79 this->LogError(full_filename, "Is the file readable for the user running OpenTTD?");
80 return false;
81 }
82 const IniItem *item;
83
84 item = this->GetMandatoryItem(full_filename, *metadata, "name");
85 if (item == nullptr) return false;
86 this->name = *item->value;
87
88 item = this->GetMandatoryItem(full_filename, *metadata, "description");
89 if (item == nullptr) return false;
90 this->description[std::string{}] = *item->value;
91
92 item = metadata->GetItem("url");
93 if (item != nullptr) this->url = *item->value;
94
95 /* Add the translations of the descriptions too. */
96 for (const IniItem &titem : metadata->items) {
97 if (!titem.name.starts_with("description.")) continue;
98
99 this->description[titem.name.substr(12)] = titem.value.value_or("");
100 }
101
102 item = this->GetMandatoryItem(full_filename, *metadata, "shortname");
103 if (item == nullptr) return false;
104 this->shortname = DecodeShortName(*item->value);
105
106 item = this->GetMandatoryItem(full_filename, *metadata, "version");
107 if (item == nullptr) return false;
108 for (StringConsumer consumer{*item->value};;) {
109 auto value = consumer.TryReadIntegerBase<uint32_t>(10);
110 bool valid = value.has_value();
111 if (valid) this->version.push_back(*value);
112 if (valid && !consumer.AnyBytesLeft()) break;
113 if (!valid || !consumer.ReadIf(".")) {
114 this->LogError(full_filename, fmt::format("metadata.version field is invalid: {}", *item->value));
115 return false;
116 }
117 }
118
119 item = metadata->GetItem("fallback");
120 this->fallback = (item != nullptr && item->value && *item->value != "0" && *item->value != "false");
121
122 /* For each of the file types we want to find the file, MD5 checksums and warning messages. */
123 const IniGroup *files = ini.GetGroup("files");
124 const IniGroup *md5s = ini.GetGroup("md5s");
125 const IniGroup *origin = ini.GetGroup("origin");
126 auto file_names = BaseSet<T>::GetFilenames();
127 bool original_set =
128 this->shortname == DecodeShortName("TTDD") || // TTD DOS graphics, TTD DOS music
129 this->shortname == DecodeShortName("TTDW") || // TTD WIN graphics, TTD WIN music
130 this->shortname == DecodeShortName("TTDO") || // TTD sound
131 this->shortname == DecodeShortName("TTOD"); // TTO music
132
133 for (uint i = 0; i < BaseSet<T>::NUM_FILES; i++) {
134 MD5File *file = &this->files[i];
135 /* Find the filename first. */
136 item = files != nullptr ? files->GetItem(file_names[i]) : nullptr;
137 if (item == nullptr || (!item->value.has_value() && !allow_empty_filename)) {
138 this->LogError(full_filename, fmt::format("files.{} field missing", file_names[i]));
139 return false;
140 }
141
142 if (!item->value.has_value()) {
143 file->filename.clear();
144 /* If we list no file, that file must be valid */
145 this->valid_files++;
146 this->found_files++;
147 continue;
148 }
149
150 const std::string &filename = item->value.value();
151 file->filename = path + filename;
152
153 /* Then find the MD5 checksum */
154 item = md5s != nullptr ? md5s->GetItem(filename) : nullptr;
155 if (item == nullptr || !item->value.has_value()) {
156 this->LogError(full_filename, fmt::format("md5s.{} field missing", filename));
157 return false;
158 }
159 if (!ConvertHexToBytes(*item->value, file->hash)) {
160 this->LogError(full_filename, fmt::format("md5s.{} is malformed: {}", filename, *item->value));
161 return false;
162 }
163
164 /* Then find the warning message when the file's missing */
165 item = origin != nullptr ? origin->GetItem(filename) : nullptr;
166 if (item == nullptr) item = origin != nullptr ? origin->GetItem("default") : nullptr;
167 if (item == nullptr || !item->value.has_value()) {
168 this->LogError(full_filename, fmt::format("origin.{} field missing", filename), 1);
169 file->missing_warning.clear();
170 } else {
171 file->missing_warning = item->value.value();
172 }
173
174 file->check_result = T::CheckMD5(file, Subdirectory::Baseset);
175 switch (file->check_result) {
177 break;
180 this->valid_files++;
181 this->found_files++;
182 break;
183
185 /* This is normal for original sample.cat, which either matches with orig_dos or orig_win. */
186 this->LogError(full_filename, fmt::format("MD5 checksum mismatch for: {}", filename), original_set ? 1 : 0);
187 this->found_files++;
188 break;
189
191 /* Missing files is normal for the original basesets. Use lower debug level */
192 this->LogError(full_filename, fmt::format("File is missing: {}", filename), original_set ? 1 : 0);
193 break;
194 }
195 }
196
197 return true;
198}
199
200template <class Tbase_set>
201bool BaseMedia<Tbase_set>::AddFile(const std::string &filename, size_t basepath_length, const std::string &)
202{
203 Debug(misc, 1, "Checking {} for base {} set", filename, BaseSet<Tbase_set>::SET_TYPE);
204
205 auto set = std::make_unique<Tbase_set>();
206 IniFile ini{};
207 std::string path{ filename, basepath_length };
209
210 auto psep = path.rfind(PATHSEPCHAR);
211 if (psep != std::string::npos) {
212 path.erase(psep + 1);
213 } else {
214 path.clear();
215 }
216
217 if (!set->FillSetDetails(ini, path, filename)) return false;
219 auto existing = std::ranges::find_if(BaseMedia<Tbase_set>::available_sets, [&set](const auto &c) { return c->name == set->name || c->shortname == set->shortname; });
220 if (existing != std::end(BaseMedia<Tbase_set>::available_sets)) {
221 /* The more complete set takes precedence over the version number. */
222 if (((*existing)->valid_files == set->valid_files && (*existing)->version >= set->version) ||
223 (*existing)->valid_files > set->valid_files) {
225 Debug(misc, 1, "Not adding {} ({}) as base {} set (duplicate, {})", set->name, fmt::join(set->version, "."),
227 (*existing)->valid_files > set->valid_files ? "fewer valid files" : "lower version");
228
229 duplicate_sets.push_back(std::move(set));
230 return false;
231 }
233 /* If the duplicate set is currently used (due to rescanning this can happen)
234 * update the currently used set to the new one. This will 'lie' about the
235 * version number until a new game is started which isn't a big problem */
236 if (BaseMedia<Tbase_set>::used_set == existing->get()) BaseMedia<Tbase_set>::used_set = set.get();
237
238 /* Keep baseset configuration, if compatible */
239 set->CopyCompatibleConfig(**existing);
240
241 Debug(misc, 1, "Removing {} ({}) as base {} set (duplicate, {})", (*existing)->name, fmt::join((*existing)->version, "."), BaseSet<Tbase_set>::SET_TYPE,
242 (*existing)->valid_files < set->valid_files ? "fewer valid files" : "lower version");
243
244 /* Existing set is worse, move it to duplicates and replace with the current set. */
245 duplicate_sets.push_back(std::move(*existing));
246
247 Debug(misc, 1, "Adding {} ({}) as base {} set", set->name, fmt::join(set->version, "."), BaseSet<Tbase_set>::SET_TYPE);
248 *existing = std::move(set);
249 } else {
250 Debug(grf, 1, "Adding {} ({}) as base {} set", set->name, set->version, BaseSet<Tbase_set>::SET_TYPE);
251 available_sets.push_back(std::move(set));
252 }
253
254 return true;
255}
256
262template <class Tbase_set>
263/* static */ bool BaseMedia<Tbase_set>::SetSet(const Tbase_set *set)
264{
265 if (set == nullptr) {
266 if (!BaseMedia<Tbase_set>::DetermineBestSet()) return false;
267 } else {
269 }
271 return true;
272}
273
279template <class Tbase_set>
280/* static */ bool BaseMedia<Tbase_set>::SetSetByName(const std::string &name)
281{
282 if (name.empty()) {
283 return SetSet(nullptr);
284 }
285
286 for (const auto &s : BaseMedia<Tbase_set>::available_sets) {
287 if (name == s->name) {
288 return SetSet(s.get());
289 }
290 }
291 return false;
292}
293
299template <class Tbase_set>
300/* static */ bool BaseMedia<Tbase_set>::SetSetByShortname(uint32_t shortname)
301{
302 if (shortname == 0) {
303 return SetSet(nullptr);
304 }
305
306 for (const auto &s : BaseMedia<Tbase_set>::available_sets) {
307 if (shortname == s->shortname) {
308 return SetSet(s.get());
309 }
310 }
311 return false;
312}
313
318template <class Tbase_set>
319/* static */ void BaseMedia<Tbase_set>::GetSetsList(std::back_insert_iterator<std::string> &output_iterator)
320{
321 fmt::format_to(output_iterator, "List of {} sets:\n", BaseSet<Tbase_set>::SET_TYPE);
322 for (const auto &s : BaseMedia<Tbase_set>::available_sets) {
323 fmt::format_to(output_iterator, "{:>18}: {}", s->name, s->GetDescription({}));
324 int invalid = s->GetNumInvalid();
325 if (invalid != 0) {
326 int missing = s->GetNumMissing();
327 if (missing == 0) {
328 fmt::format_to(output_iterator, " ({} corrupt file{})\n", invalid, invalid == 1 ? "" : "s");
329 } else {
330 fmt::format_to(output_iterator, " (unusable: {} missing file{})\n", missing, missing == 1 ? "" : "s");
331 }
332 } else {
333 fmt::format_to(output_iterator, "\n");
334 }
335 }
336 fmt::format_to(output_iterator, "\n");
337}
338
340
341template <class Tbase_set> std::optional<std::string_view> TryGetBaseSetFile(const ContentInfo &ci, bool md5sum, std::span<const std::unique_ptr<Tbase_set>> sets)
342{
343 for (const auto &s : sets) {
344 if (s->GetNumMissing() != 0) continue;
345
346 if (s->shortname != ci.unique_id) continue;
347 if (!md5sum) return s->files[0].filename;
348
349 MD5Hash md5;
350 for (const auto &file : s->files) {
351 md5 ^= file.hash;
352 }
353 if (md5 == ci.md5sum) return s->files[0].filename;
354 }
355 return std::nullopt;
356}
357
358template <class Tbase_set>
359/* static */ bool BaseMedia<Tbase_set>::HasSet(const ContentInfo &ci, bool md5sum)
360{
361 return TryGetBaseSetFile(ci, md5sum, BaseMedia<Tbase_set>::GetAvailableSets()).has_value() ||
363}
364
369template <class Tbase_set>
371{
372 return std::ranges::count_if(BaseMedia<Tbase_set>::GetAvailableSets(), [](const auto &set) {
373 return set.get() == BaseMedia<Tbase_set>::used_set || set->GetNumMissing() == 0;
374 });
375}
376
381template <class Tbase_set>
383{
384 int n = 0;
385 for (const auto &s : BaseMedia<Tbase_set>::available_sets) {
386 if (s.get() == BaseMedia<Tbase_set>::used_set) return n;
387 if (s->GetNumMissing() != 0) continue;
388 n++;
389 }
390 return -1;
391}
392
398template <class Tbase_set>
399/* static */ const Tbase_set *BaseMedia<Tbase_set>::GetSet(int index)
400{
401 for (const auto &s : BaseMedia<Tbase_set>::available_sets) {
402 if (s.get() != BaseMedia<Tbase_set>::used_set && s->GetNumMissing() != 0) continue;
403 if (index == 0) return s.get();
404 index--;
405 }
406 FatalError("Base{}::GetSet(): index {} out of range", BaseSet<Tbase_set>::SET_TYPE, index);
407}
408
413template <class Tbase_set>
414/* static */ const Tbase_set *BaseMedia<Tbase_set>::GetUsedSet()
415{
417}
Generic functions for replacing base data (graphics, sounds).
constexpr uint32_t DecodeShortName(std::string_view shortname)
Helper to decode a 4 character short name into an uint32_t representation.
void CheckExternalFiles()
Checks whether the MD5 checksums of the files are correct.
Definition gfxinit.cpp:112
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.
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 base set at a 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
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:572
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.
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:87
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
@ NoFile
The file did not exist.
@ Mismatch
The file did exist, just the md5 checksum did not match.
@ Match
The file did exist and the md5 checksum did match.
@ 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.