• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ParadoxGameConverters / commonItems / 4126425446

pending completion
4126425446

Pull #223

github

GitHub
Merge 8284d329e into 85a0aed9f
Pull Request #223: Fix for completely broken ModLoader that Idhrendur broke.

16 of 16 new or added lines in 1 file covered. (100.0%)

2188 of 3638 relevant lines covered (60.14%)

148.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

70.31
/ModLoader/ModLoader.cpp
1
#include "ModLoader.h"
2
#include "../CommonFunctions.h"
3
#include "../Log.h"
4
#include "../OSCompatibilityLayer.h"
5
#include "../external/zip/src/zip.h"
6
#include "ModParser.h"
7
#include <filesystem>
8
#include <set>
9
#include <stdexcept>
10
#include <string>
11

12

13

14
void commonItems::ModLoader::loadMods(const std::string& gameDocumentsPath, const Mods& incomingMods)
7✔
15
{
16
        loadMods(std::vector{gameDocumentsPath + "/mod"}, incomingMods);
14✔
17
}
14✔
18

19

20
void commonItems::ModLoader::loadMods(const std::vector<std::string>& gameModsPaths, const Mods& incomingMods)
8✔
21
{
22
        if (gameModsPaths.empty())
8✔
23
        {
24
                Log(LogLevel::Info) << "No mod directories were provided. Skipping mod processing.";
×
25
                return;
1✔
26
        }
27

28

29
        if (incomingMods.empty())
8✔
30
        {
31
                // We shouldn't even be here if the save didn't have mods! Why were Mods called?
32
                Log(LogLevel::Info) << "No mods were detected in savegame. Skipping mod processing.";
1✔
33
                return;
1✔
34
        }
35

36
        // First see what we're up against. Load mod folders, and cache the mod names. We need the names as bare minimum in case
37
        // we're doing old-style name-recognition modfinding and don't have the paths in incomingMods.
38
        Log(LogLevel::Info) << "\tMods directories are:";
7✔
39
        for (const auto& gameDocumentsPath: gameModsPaths)
15✔
40
        {
41
                Log(LogLevel::Info) << "\t\t" << gameDocumentsPath;
8✔
42
                cacheModNames(gameDocumentsPath);
8✔
43
        }
44

45
        // We enter this function with a vector of (optional) mod names and (required) mod file locations from the savegame.
46
        // We need to read all the mod files, check their paths (and potential archives for ancient mods) unpack what's
47
        // necessary, and exit with a vector of updated mod names (savegame can differ from actual mod file) and mod folder
48
        // locations.
49

50
        // The function below reads all the incoming .mod files and verifies their internal paths/archives are correct and
51
        // point to something present on disk. No unpacking yet.
52
        loadModDirectories(gameModsPaths, incomingMods);
7✔
53

54
        // Now we merge all detected .mod files together.
55
        Log(LogLevel::Info) << "\tDetermining Mod Usability";
7✔
56
        auto allMods = possibleUncompressedMods;
7✔
57
        allMods.insert(allMods.end(), possibleCompressedMods.begin(), possibleCompressedMods.end());
7✔
58

59
        // With a list of all detected and matched mods, we unpack the compressed ones (if any) and store the results.
60
        for (const auto& mod: allMods)
16✔
61
        {
62
                // This invocation will unpack any compressed mods into our converter's folder, and skip already unpacked ones.
63
                const auto possibleModPath = uncompressAndReturnNewPath(mod.name);
9✔
64
                if (!possibleModPath)
9✔
65
                {
66
                        Log(LogLevel::Warning) << "\t\tFailure unpacking " << mod.name << ", skipping this mod at your risk.";
1✔
67
                        continue;
1✔
68
                }
69

70
                // All verified mods go into usableMods
71
                Log(LogLevel::Info) << "\t\t->> Found potentially useful [" << mod.name << "]: " << *possibleModPath + "/";
8✔
72
                usableMods.emplace_back(Mod(mod.name, *possibleModPath + "/", mod.dependencies, mod.replacedFolders));
8✔
73
        }
9✔
74
}
7✔
75

76
void commonItems::ModLoader::loadModDirectories(const std::vector<std::string>& gameModPaths, const Mods& incomingMods)
7✔
77
{
78
        std::set<std::string> diskModNames;
7✔
79
        for (const auto& modPath: gameModPaths)
15✔
80
        {
81
                for (const auto& diskModName: GetAllFilesInFolder(modPath))
71✔
82
                {
83
                        diskModNames.insert(diskModName);
63✔
84
                }
8✔
85
        }
86

87
        for (auto mod: incomingMods)
19✔
88
        {
89
                // If we don't have a loaded mod path but have it in our cache, might as well fix it.
90
                if (mod.path.empty() && modCache.contains(mod.name))
12✔
91
                        mod.path = modCache.at(mod.name);
5✔
92

93
                const auto trimmedModFileName = trimPath(mod.path);
12✔
94

95
                // We either have the path as the reference point (in which case name we'll read ourselves), or the name, in which case we looked up the
96
                // cached map for the path.
97
                // If we have neither, that's unusable.
98

99
                if (!diskModNames.contains(trimmedModFileName) && !modCache.contains(mod.name))
12✔
100
                {
101
                        if (mod.name.empty())
1✔
102
                                Log(LogLevel::Warning) << "\t\tSavegame uses mod at " << mod.path
×
103
                                                                                          << " which is not present on disk. Skipping at your risk, but this can greatly affect conversion.";
×
104
                        else if (mod.path.empty())
1✔
105
                                Log(LogLevel::Warning) << "\t\tSavegame uses [" << mod.name
×
106
                                                                                          << "] which is not present on disk. Skipping at your risk, but this can greatly affect conversion.";
×
107
                        else
108
                                Log(LogLevel::Warning) << "\t\tSavegame uses [" << mod.name << "] at " << mod.path
2✔
109
                                                                                          << " which is not present on disk. Skipping at your risk, but this can greatly affect conversion.";
1✔
110
                        continue;
1✔
111
                }
112

113
                // if we do have a path incoming from the save, just make sure it's not some abnormality.
114
                if (!trimmedModFileName.empty() && getExtension(trimmedModFileName) != "mod")
11✔
115
                {
116
                        // Vic3 mods won't have .mod, but they will be in the cache and lack .mod there
117
                        if (!modCache.contains(mod.name) || modCache.at(mod.name).ends_with(".mod"))
3✔
118
                                continue; // shouldn't be necessary but just in case.
×
119
                }
120

121
                // Attempt parsing .mod file
122
                for (const auto& gameModPath: gameModPaths)
12✔
123
                {
124
                        if (!trimmedModFileName.empty() && trimmedModFileName.ends_with(".mod"))
12✔
125
                        {
126
                                const std::string mod_file_location = gameModPath + "/" + trimmedModFileName;
8✔
127

128
                                if (!DoesFileExist(mod_file_location))
8✔
129
                                {
130
                                        continue;
×
131
                                }
132

133
                                ModParser theMod;
8✔
134
                                try
135
                                {
136
                                        theMod.parseMod(mod_file_location);
8✔
137
                                }
138
                                catch (std::exception&)
×
139
                                {
140
                                        Log(LogLevel::Warning) << "\t\tError while reading " << mod_file_location << "! Mod will not be useable for conversions.";
×
141
                                        continue;
×
142
                                }
×
143
                                processLoadedMod(theMod, mod.name, trimmedModFileName, mod.path, gameModPath);
8✔
144
                                break;
8✔
145
                        }
16✔
146
                        else
147
                        {
148
                                // Vic3 mods
149

150
                                std::string mod_folder = mod.path.substr(mod.path.find_last_of('/') + 1, mod.path.size());
4✔
151
                                const std::string metadata_location = gameModPath + "/" + mod_folder + "/.metadata/metadata.json";
4✔
152
                                if (!DoesFileExist(metadata_location))
4✔
153
                                {
154
                                        continue;
1✔
155
                                }
156

157
                                ModParser theMod;
3✔
158
                                try
159
                                {
160
                                        theMod.parseMetadata(metadata_location);
3✔
161
                                }
162
                                catch (std::exception&)
×
163
                                {
164
                                        Log(LogLevel::Warning) << "\t\tError while reading " << metadata_location << "! Mod will not be useable for conversions.";
×
165
                                        continue;
×
166
                                }
×
167
                                possibleUncompressedMods.emplace_back(Mod(theMod.getName(), theMod.getPath(), theMod.getDependencies(), theMod.getReplacedPaths()));
3✔
168
                                Log(LogLevel::Info) << "\t\tFound a potential mod [" << theMod.getName() << "] at " << theMod.getPath();
3✔
169
                                break;
3✔
170
                        }
11✔
171
                }
172
        }
13✔
173
}
7✔
174

175
void commonItems::ModLoader::cacheModNames(const std::string& gameDocumentsPath)
8✔
176
{
177
        if (!DoesFolderExist(gameDocumentsPath))
8✔
178
                throw std::invalid_argument("Mods directory path is invalid! Is it at: " + gameDocumentsPath + " ?");
×
179

180
        for (const auto& diskModFile: GetAllFilesInFolder(gameDocumentsPath))
71✔
181
        {
182
                if (getExtension(diskModFile) != "mod")
63✔
183
                        continue;
14✔
184
                ModParser theMod;
49✔
185
                const auto trimmedModFileName = trimPath(diskModFile);
49✔
186
                try
187
                {
188
                        theMod.parseMod(gameDocumentsPath + "/" + trimmedModFileName);
49✔
189
                }
190
                catch (std::exception&)
×
191
                {
192
                        Log(LogLevel::Warning) << "\t\tError while caching " << gameDocumentsPath << "/" << trimmedModFileName << "! Mod will not be useable for conversions.";
×
193
                        continue;
×
194
                }
×
195
                if (theMod.isValid())
49✔
196
                        modCache.emplace(theMod.getName(), diskModFile);
35✔
197
        }
57✔
198

199
        for (const auto& possible_mod_folder: GetAllSubfolders(gameDocumentsPath))
37✔
200
        {
201
                const std::string metadata_location = gameDocumentsPath + "/" + possible_mod_folder + "/.metadata/metadata.json";
29✔
202
                if (!DoesFileExist(metadata_location))
29✔
203
                {
204
                        continue;
7✔
205
                }
206

207
                ModParser theMod;
22✔
208
                try
209
                {
210
                        theMod.parseMetadata(metadata_location);
22✔
211
                }
212
                catch (std::exception&)
×
213
                {
214
                        Log(LogLevel::Warning) << "\t\tError while caching " << possible_mod_folder << "! Mod will not be useable for conversions.";
×
215
                        continue;
×
216
                }
×
217
                if (theMod.isValid())
22✔
218
                        modCache.emplace(theMod.getName(), theMod.getPath());
15✔
219
        }
37✔
220
}
8✔
221

222
void commonItems::ModLoader::processLoadedMod(ModParser& theMod,
8✔
223
         const std::string& modName,
224
         const std::string& modFileName,
225
         const std::string& modPath,
226
         const std::string& gameModPath)
227
{
228
        if (!theMod.isValid())
8✔
229
        {
230
                Log(LogLevel::Warning) << "\t\tMod at " << gameModPath + "/" + modFileName << " does not look valid.";
1✔
231
                return;
1✔
232
        }
233

234
        // Fix potential pathing issues.
235
        if (!theMod.isCompressed() && !DoesFolderExist(theMod.getPath()))
7✔
236
        {
237
                // Maybe we have a relative path
238
                if (DoesFolderExist(gameModPath + "/" + modFileName))
1✔
239
                {
240
                        // fix this.
241
                        theMod.setPath(gameModPath + "/" + modFileName);
×
242
                }
243
                else
244
                {
245
                        warnForInvalidPath(theMod, modName, modPath);
1✔
246
                        return;
1✔
247
                }
248
        }
249
        else if (theMod.isCompressed() && !DoesFileExist(theMod.getPath()))
6✔
250
        {
251
                // Maybe we have a relative path
252
                if (DoesFileExist(gameModPath + "/" + modFileName))
×
253
                {
254
                        // fix this.
255
                        theMod.setPath(gameModPath + "/" + modFileName);
×
256
                }
257
                else
258
                {
259
                        warnForInvalidPath(theMod, modName, modPath);
×
260
                        return;
×
261
                }
262
        }
263

264
        // file under category.
265
        fileUnderCategory(theMod, gameModPath + "/" + modFileName);
6✔
266
}
267

268
void commonItems::ModLoader::warnForInvalidPath(const ModParser& theMod, const std::string& name, const std::string& path)
1✔
269
{
270
        if (name.empty())
1✔
271
                Log(LogLevel::Warning) << "\t\tMod at " + path + " points to " + theMod.getPath() +
×
272
                                                                                                " which does not exist! Skipping at your risk, but this can greatly affect conversion.";
×
273
        else
274
                Log(LogLevel::Warning) << "\t\tMod [" << name
2✔
275
                                                                          << "] at " + path + " points to " + theMod.getPath() +
2✔
276
                                                                                                " which does not exist! Skipping at your risk, but this can greatly affect conversion.";
1✔
277
}
1✔
278

279
void commonItems::ModLoader::fileUnderCategory(const ModParser& theMod, const std::string& path)
6✔
280
{
281
        if (!theMod.isCompressed())
6✔
282
        {
283
                possibleUncompressedMods.emplace_back(Mod(theMod.getName(), theMod.getPath(), theMod.getDependencies(), theMod.getReplacedPaths()));
4✔
284
                Log(LogLevel::Info) << "\t\tFound a potential mod [" << theMod.getName() << "] with a mod file at " << path << " and itself at " << theMod.getPath();
4✔
285
        }
286
        else
287
        {
288
                possibleCompressedMods.emplace_back(Mod(theMod.getName(), theMod.getPath(), theMod.getDependencies(), theMod.getReplacedPaths()));
2✔
289
                Log(LogLevel::Info) << "\t\tFound a compressed mod [" << theMod.getName() << "] with a mod file at " << path << " and itself at " << theMod.getPath();
2✔
290
        }
291
}
6✔
292

293
std::optional<std::string> commonItems::ModLoader::uncompressAndReturnNewPath(const std::string& modName) const
9✔
294
{
295
        for (const auto& mod: possibleUncompressedMods)
12✔
296
        {
297
                if (mod.name == modName)
10✔
298
                        return mod.path;
7✔
299
        }
300

301
        for (const auto& compressedMod: possibleCompressedMods)
2✔
302
        {
303
                if (compressedMod.name != modName)
2✔
304
                        continue;
×
305

306
                const auto uncompressedName = trimPath(trimExtension(compressedMod.path));
2✔
307

308
                TryCreateFolder("mods/");
2✔
309

310
                if (!DoesFolderExist("mods/" + uncompressedName))
2✔
311
                {
312
                        Log(LogLevel::Info) << "\t\tUncompressing: " << compressedMod.path;
2✔
313
                        if (!extractZip(compressedMod.path, "mods/" + uncompressedName))
2✔
314
                        {
315
                                Log(LogLevel::Warning) << "We're having trouble automatically uncompressing your mod.";
1✔
316
                                Log(LogLevel::Warning) << "Please, manually uncompress: " << compressedMod.path;
1✔
317
                                Log(LogLevel::Warning) << "Into converter's folder, mods/" << uncompressedName << " subfolder.";
1✔
318
                                Log(LogLevel::Warning) << "Then run the converter again. Thank you and good luck.";
1✔
319
                                return std::nullopt;
1✔
320
                        }
321
                }
322

323
                if (DoesFolderExist("mods/" + uncompressedName))
1✔
324
                {
325
                        return "mods/" + uncompressedName;
1✔
326
                }
327
                return std::nullopt;
×
328
        }
2✔
329

330
        return std::nullopt;
×
331
}
332

333
bool commonItems::ModLoader::extractZip(const std::string& archive, const std::string& path) const
2✔
334
{
335
        TryCreateFolder(path);
2✔
336

337
        const int result = zip_extract(archive.c_str(), path.c_str(), NULL, NULL);
2✔
338

339
        if (result != 0)
2✔
340
        {
341
                DeleteFolder(path);
1✔
342
                return false;
1✔
343
        }
344

345
        return true;
1✔
346
}
347

348

349
void commonItems::ModLoader::sortMods()
×
350
{
351
        // using Kahn's algorithm - https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm
352
        auto unsortedMods = usableMods;
×
353

354
        // track incoming edges
355
        std::map<std::string, std::set<std::string>> incomingDependencies;
×
356
        for (const auto& mod: unsortedMods)
×
357
        {
358
                for (const auto& dependency: mod.dependencies)
×
359
                {
360
                        if (auto [itr, inserted] = incomingDependencies.emplace(dependency, std::set{mod.name}); !inserted)
×
361
                        {
362
                                itr->second.insert(mod.name);
×
363
                        }
364
                }
365
        }
366

367
        // add mods with no incoming edges to the sorted mods
368
        Mods sortedMods;
×
369
        while (!unsortedMods.empty())
×
370
        {
371
                auto itr = unsortedMods.begin();
×
372
                while (incomingDependencies.contains(itr->name))
×
373
                {
374
                        ++itr;
×
375
                        if (itr == unsortedMods.end())
×
376
                        {
377
                                throw std::invalid_argument("A mod dependency was missing.");
×
378
                        }
379
                }
380

381
                sortedMods.push_back(*itr);
×
382

383
                for (const auto& dependencyName: itr->dependencies)
×
384
                {
385
                        auto dependency = incomingDependencies.find(dependencyName);
×
386
                        dependency->second.erase(itr->name);
×
387
                        if (dependency->second.empty())
×
388
                        {
389
                                incomingDependencies.erase(dependencyName);
×
390
                        }
391
                }
392

393
                unsortedMods.erase(itr);
×
394
        }
395

396
        usableMods = sortedMods;
×
397
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc