1 /*
2 * modmgr.cc - Mod manager for Exult.
3 *
4 * Copyright (C) 2006 The Exult Team
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 */
20
21 #ifdef HAVE_CONFIG_H
22 # include <config.h>
23 #endif
24
25 #include "modmgr.h"
26
27 #include "Configuration.h"
28 #include "Flex.h"
29 #include "crc.h"
30 #include "databuf.h"
31 #include "exult_constants.h"
32 #include "fnames.h"
33 #include "listfiles.h"
34 #include "utils.h"
35
36 #include <cstdlib>
37 #include <fstream>
38 #include <iomanip>
39 #include <iostream>
40 #include <string>
41 #include <vector>
42
43 #ifdef HAVE_ZIP_SUPPORT
44 # include "files/zip/unzip.h"
45 # include "files/zip/zip.h"
46 #endif
47
48 using std::cerr;
49 using std::cout;
50 using std::endl;
51 using std::ifstream;
52 using std::string;
53 using std::vector;
54
55 // BaseGameInfo: Generic information and functions common to mods and games
setup_game_paths()56 void BaseGameInfo::setup_game_paths() {
57 // Make aliases to the current game's paths.
58 clone_system_path("<STATIC>", "<" + path_prefix + "_STATIC>");
59 clone_system_path("<MODS>", "<" + path_prefix + "_MODS>");
60
61 string mod_path_tag = path_prefix;
62
63 if (!mod_title.empty())
64 mod_path_tag += ("_" + to_uppercase(static_cast<const string>(mod_title)));
65
66 clone_system_path("<GAMEDAT>", "<" + mod_path_tag + "_GAMEDAT>");
67 clone_system_path("<SAVEGAME>", "<" + mod_path_tag + "_SAVEGAME>");
68
69 if (is_system_path_defined("<" + mod_path_tag + "_PATCH>"))
70 clone_system_path("<PATCH>", "<" + mod_path_tag + "_PATCH>");
71 else
72 clear_system_path("<PATCH>");
73
74 if (is_system_path_defined("<" + mod_path_tag + "_SOURCE>"))
75 clone_system_path("<SOURCE>", "<" + mod_path_tag + "_SOURCE>");
76 else
77 clear_system_path("<SOURCE>");
78
79 if (type != EXULT_MENU_GAME) {
80 U7mkdir("<SAVEGAME>", 0755); // make sure savegame directory exists
81 U7mkdir("<GAMEDAT>", 0755); // make sure gamedat directory exists
82 }
83 }
84
ReplaceMacro(string & path,const string & srch,const string & repl)85 static inline void ReplaceMacro(
86 string &path,
87 const string &srch,
88 const string &repl
89 ) {
90 string::size_type pos = path.find(srch);
91 if (pos != string::npos)
92 path.replace(pos, srch.length(), repl);
93 }
94
95 // ModInfo: class that manages one mod's information
ModInfo(Exult_Game game,Game_Language lang,const string & name,const string & mod,const string & path,bool exp,bool sib,bool ed,const string & cfg)96 ModInfo::ModInfo(
97 Exult_Game game,
98 Game_Language lang,
99 const string &name,
100 const string &mod,
101 const string &path,
102 bool exp,
103 bool sib,
104 bool ed,
105 const string &cfg
106 ) : BaseGameInfo(game, lang, name, mod, path, "", exp, sib, false, ed, ""),
107 configfile(cfg), compatible(false) {
108 Configuration modconfig(configfile, "modinfo");
109
110 string config_path;
111 string default_dir;
112
113 config_path = "mod_info/mod_title";
114 default_dir = mod;
115 string modname;
116 modconfig.value(config_path, modname, default_dir.c_str());
117 mod_title = modname;
118
119 config_path = "mod_info/display_string";
120 default_dir = "Description missing!";
121 string menustr;
122 modconfig.value(config_path, menustr, default_dir.c_str());
123 menustring = menustr;
124
125 config_path = "mod_info/required_version";
126 default_dir = "0.0.00R";
127 string modversion;
128 modconfig.value(config_path, modversion, default_dir.c_str());
129 if (modversion == default_dir)
130 // Required version is missing; assume the mod to be incompatible
131 compatible = false;
132 else {
133 const char *ptrmod = modversion.c_str();
134 const char *ptrver = VERSION;
135 char *eptrmod;
136 char *eptrver;
137 int modver = strtol(ptrmod, &eptrmod, 0);
138 int exver = strtol(ptrver, &eptrver, 0);
139
140 // Assume compatibility:
141 compatible = true;
142 // Comparing major version number:
143 if (modver > exver)
144 compatible = false;
145 else if (modver == exver) {
146 modver = strtol(eptrmod + 1, &eptrmod, 0);
147 exver = strtol(eptrver + 1, &eptrver, 0);
148 // Comparing minor version number:
149 if (modver > exver)
150 compatible = false;
151 else if (modver == exver) {
152 modver = strtol(eptrmod + 1, &eptrmod, 0);
153 exver = strtol(eptrver + 1, &eptrver, 0);
154 // Comparing revision number:
155 if (modver > exver)
156 compatible = false;
157 else if (modver == exver) {
158 string mver(to_uppercase(eptrmod));
159 string ever(to_uppercase(eptrver));
160 // Release vs CVS:
161 if (mver == "CVS" && ever == "R")
162 compatible = false;
163 }
164 }
165 }
166 }
167
168 string tagstr(to_uppercase(static_cast<const string>(mod_title)));
169 string system_path_tag(path_prefix + "_" + tagstr);
170 string mods_dir("<" + path_prefix + "_MODS>");
171 string data_directory(mods_dir + "/" + mod_title);
172 string mods_save_dir("<" + path_prefix + "_SAVEGAME>/mods");
173 string savedata_directory(mods_save_dir + "/" + mod_title);
174 string mods_macro("__MODS__");
175 string mod_path_macro("__MOD_PATH__");
176
177 // Read codepage first.
178 config_path = "mod_info/codepage";
179 default_dir = "CP437"; // DOS code page.
180 modconfig.value(config_path, codepage, default_dir.c_str());
181
182 // Where game data is. This is defaults to a non-writable location because
183 // mods_dir does too.
184 config_path = "mod_info/patch";
185 default_dir = data_directory + "/patch";
186 string patchdir;
187 modconfig.value(config_path, patchdir, default_dir.c_str());
188 ReplaceMacro(patchdir, mods_macro, mods_dir);
189 ReplaceMacro(patchdir, mod_path_macro, data_directory);
190 add_system_path("<" + system_path_tag + "_PATCH>", get_system_path(patchdir));
191 // Where usecode source is found; defaults to same as patch.
192 config_path = "mod_info/source";
193 string sourcedir;
194 modconfig.value(config_path, sourcedir, default_dir.c_str());
195 ReplaceMacro(sourcedir, mods_macro, mods_dir);
196 ReplaceMacro(sourcedir, mod_path_macro, data_directory);
197 add_system_path("<" + system_path_tag + "_SOURCE>", get_system_path(sourcedir));
198
199 U7mkdir(mods_save_dir.c_str(), 0755);
200 U7mkdir(savedata_directory.c_str(), 0755);
201
202 // The following paths default to user-writable locations.
203 config_path = "mod_info/gamedat_path";
204 default_dir = savedata_directory + "/gamedat";
205 string gamedatdir;
206 modconfig.value(config_path, gamedatdir, default_dir.c_str());
207 // Path 'macros' for relative paths:
208 ReplaceMacro(gamedatdir, mods_macro, mods_save_dir);
209 ReplaceMacro(gamedatdir, mod_path_macro, savedata_directory);
210 add_system_path("<" + system_path_tag + "_GAMEDAT>", get_system_path(gamedatdir));
211 U7mkdir(gamedatdir.c_str(), 0755);
212
213 config_path = "mod_info/savegame_path";
214 string savedir;
215 modconfig.value(config_path, savedir, savedata_directory.c_str());
216 // Path 'macros' for relative paths:
217 ReplaceMacro(savedir, mods_macro, mods_save_dir);
218 ReplaceMacro(savedir, mod_path_macro, savedata_directory);
219 add_system_path("<" + system_path_tag + "_SAVEGAME>", get_system_path(savedir));
220 U7mkdir(savedir.c_str(), 0755);
221
222 #ifdef DEBUG_PATHS
223 cout << "path prefix of " << cfgname << " mod " << mod_title
224 << " is: " << system_path_tag << endl;
225 cout << "setting " << cfgname
226 << " gamedat directory to: " << get_system_path(gamedatdir) << endl;
227 cout << "setting " << cfgname
228 << " savegame directory to: " << get_system_path(savedir) << endl;
229 cout << "setting " << cfgname
230 << " patch directory to: " << get_system_path(patchdir) << endl;
231 cout << "setting " << cfgname
232 << " source directory to: " << get_system_path(sourcedir) << endl;
233 #endif
234 }
235
236 /*
237 * Return string from IDENTITY in a savegame.
238 * Also needed by ES.
239 *
240 * Output: identity if found.
241 * "" if error (or may throw exception).
242 * "*" if older savegame.
243 */
get_game_identity(const char * savename,const string & title)244 string get_game_identity(const char *savename, const string &title) {
245 char *game_identity = nullptr;
246 if (!U7exists(savename))
247 return title;
248 if (!Flex::is_flex(savename))
249 #ifdef HAVE_ZIP_SUPPORT
250 {
251 unzFile unzipfile = unzOpen(get_system_path(savename).c_str());
252 if (unzipfile) {
253 // Find IDENTITY, ignoring case.
254 if (unzLocateFile(unzipfile, "identity", 2) != UNZ_OK) {
255 unzClose(unzipfile);
256 return "*"; // Old game. Return wildcard.
257 } else {
258 unz_file_info file_info;
259 unzGetCurrentFileInfo(unzipfile, &file_info, nullptr,
260 0, nullptr, 0, nullptr, 0);
261 game_identity = new char[file_info.uncompressed_size + 1];
262
263 if (unzOpenCurrentFile(unzipfile) != UNZ_OK) {
264 unzClose(unzipfile);
265 delete [] game_identity;
266 throw file_read_exception(savename);
267 }
268 unzReadCurrentFile(unzipfile, game_identity,
269 file_info.uncompressed_size);
270 if (unzCloseCurrentFile(unzipfile) == UNZ_OK)
271 // 0-delimit.
272 game_identity[file_info.uncompressed_size] = 0;
273 }
274 }
275 }
276 #else
277 return title.c_str();
278 #endif
279 else {
280 IFileDataSource in(savename);
281
282 in.seek(0x54); // Get to where file count sits.
283 size_t numfiles = in.read4();
284 in.seek(0x80); // Get to file info.
285 // Read pos., length of each file.
286 auto finfo = std::make_unique<uint32[]>(2 * numfiles);
287 for (size_t i = 0; i < numfiles; i++) {
288 finfo[2 * i] = in.read4(); // The position, then the length.
289 finfo[2 * i + 1] = in.read4();
290 }
291 for (size_t i = 0; i < numfiles; i++) { // Now read each file.
292 // Get file length.
293 size_t len = finfo[2 * i + 1];
294 if (len <= 13)
295 continue;
296 len -= 13;
297 in.seek(finfo[2 * i]); // Get to it.
298 char fname[50]; // Set up name.
299 in.read(fname, 13);
300 if (!strcmp("identity", fname)) {
301 game_identity = new char[len];
302 in.read(game_identity, len);
303 break;
304 }
305 }
306 }
307 if (!game_identity)
308 return title;
309 // Truncate identity
310 char *ptr = game_identity;
311 for (; (*ptr != 0x1a && *ptr != 0x0d); ptr++)
312 ;
313 *ptr = 0;
314 string id = game_identity;
315 delete [] game_identity;
316 return id;
317 }
318
319 // ModManager: class that manages a game's modlist and paths
ModManager(const string & name,const string & menu,bool needtitle,bool silent)320 ModManager::ModManager(const string &name, const string &menu, bool needtitle,
321 bool silent) {
322 cfgname = name;
323 mod_title = "";
324
325 // We will NOT trust config with these values.
326 // We MUST NOT use path tags at this point yet!
327 string game_path;
328 string static_dir;
329 string base_cfg_path("config/disk/game/" + cfgname);
330 {
331 string default_dir;
332 string config_path;
333
334 // ++++ These path settings are for that game data which requires only
335 // ++++ read access. They default to a subdirectory of:
336 // ++++ *nix: /usr/local/share/exult or /usr/share/exult
337 // ++++ MacOS X: /Library/Application Support/Exult
338 // ++++ Windows, MacOS: program path.
339
340 // <path> setting: default is "$gameprefix".
341 config_path = base_cfg_path + "/path";
342 default_dir = get_system_path("<GAMEHOME>") + "/" + cfgname;
343 config->value(config_path.c_str(), game_path, default_dir.c_str());
344
345 // <static_path> setting: default is "$game_path/static".
346 config_path = base_cfg_path + "/static_path";
347 default_dir = game_path + "/static";
348 config->value(config_path.c_str(), static_dir, default_dir.c_str());
349
350 // Read codepage too.
351 config_path = base_cfg_path + "/codepage";
352 default_dir = "CP437"; // DOS code page.
353 config->value(config_path, codepage, default_dir.c_str());
354
355 // And edit flag.
356 string cfgediting;
357 config_path = base_cfg_path + "/editing";
358 default_dir = "no"; // Not editing.
359 config->value(config_path, cfgediting, default_dir.c_str());
360 editing = (cfgediting == "yes");
361 }
362
363 if (!silent)
364 cout << "Looking for '" << cfgname << "' at '" << game_path << "'... ";
365 string initgam_path(static_dir + "/initgame.dat");
366 found = U7exists(initgam_path);
367
368 string static_identity;
369 if (found) {
370 static_identity = get_game_identity(initgam_path.c_str(), cfgname);
371 if (!silent)
372 cout << "found game with identity '" << static_identity << "'" << endl;
373 } else { // New game still under development.
374 static_identity = "DEVEL GAME";
375 if (!silent)
376 cout << "but it wasn't there." << endl;
377 }
378
379 const string mainshp = static_dir + "/mainshp.flx";
380 const uint32 crc = crc32(mainshp.c_str());
381 auto unknown_crc = [crc](const char *game) {
382 cerr << "Warning: Unknown CRC for mainshp.flx: 0x"
383 << std::hex << crc << std::dec << std::endl;
384 cerr << "Note: Guessing hacked " << game << std::endl;
385 };
386 string new_title;
387 if (static_identity == "ULTIMA7") {
388 type = BLACK_GATE;
389 expansion = false;
390 sibeta = false;
391 switch (crc) {
392 case 0x36af707f:
393 // French BG
394 language = FRENCH;
395 path_prefix = to_uppercase(CFG_BG_FR_NAME);
396 if (needtitle)
397 new_title = CFG_BG_FR_TITLE;
398 break;
399 case 0x157ca514:
400 // German BG
401 language = GERMAN;
402 path_prefix = to_uppercase(CFG_BG_DE_NAME);
403 if (needtitle)
404 new_title = CFG_BG_DE_TITLE;
405 break;
406 case 0x6d7b7323:
407 // Spanish BG
408 language = SPANISH;
409 path_prefix = to_uppercase(CFG_BG_ES_NAME);
410 if (needtitle)
411 new_title = CFG_BG_ES_TITLE;
412 break;
413 default:
414 unknown_crc("Black Gate");
415 // FALLTHROUGH
416 case 0xafc35523:
417 // English BG
418 language = ENGLISH;
419 path_prefix = to_uppercase(CFG_BG_NAME);
420 if (needtitle)
421 new_title = CFG_BG_TITLE;
422 break;
423 };
424 } else if (static_identity == "FORGE") {
425 type = BLACK_GATE;
426 language = ENGLISH;
427 expansion = true;
428 sibeta = false;
429 if (crc != 0x8a74c26b) {
430 unknown_crc("Forge of Virtue");
431 }
432 path_prefix = to_uppercase(CFG_FOV_NAME);
433 if (needtitle)
434 new_title = CFG_FOV_TITLE;
435 } else if (static_identity == "SERPENT ISLE") {
436 type = SERPENT_ISLE;
437 expansion = false;
438 switch (crc) {
439 case 0x96f66a7a:
440 // Spanish SI
441 language = FRENCH;
442 path_prefix = to_uppercase(CFG_SI_ES_NAME);
443 if (needtitle)
444 new_title = CFG_SI_ES_TITLE;
445 sibeta = false;
446 break;
447 case 0xdbdc2676:
448 // SI Beta
449 language = ENGLISH;
450 path_prefix = to_uppercase(CFG_SIB_NAME);
451 if (needtitle)
452 new_title = CFG_SIB_TITLE;
453 sibeta = true;
454 break;
455 default:
456 unknown_crc("Serpent Isle");
457 // FALLTHROUGH
458 case 0xf98f5f3e:
459 // English SI
460 language = ENGLISH;
461 path_prefix = to_uppercase(CFG_SI_NAME);
462 if (needtitle)
463 new_title = CFG_SI_TITLE;
464 sibeta = false;
465 break;
466 };
467 } else if (static_identity == "SILVER SEED") {
468 type = SERPENT_ISLE;
469 language = ENGLISH;
470 expansion = true;
471 sibeta = false;
472 if (crc != 0x3e18f9a0) {
473 unknown_crc("Silver Seed");
474 }
475 path_prefix = to_uppercase(CFG_SS_NAME);
476 if (needtitle)
477 new_title = CFG_SS_TITLE;
478 } else {
479 type = EXULT_DEVEL_GAME;
480 language = ENGLISH;
481 expansion = false;
482 sibeta = false;
483 path_prefix = "DEVEL" + to_uppercase(name);
484 new_title = menu; // To be safe.
485 }
486
487 // If the "default" path selected above is already taken, then use a unique
488 // one based on the exult.cfg entry instead.
489 if (is_system_path_defined("<" + path_prefix + "_STATIC>")) {
490 path_prefix = to_uppercase(name);
491 }
492
493 menustring = needtitle ? new_title : menu;
494 // NOW we can store the path.
495 add_system_path("<" + path_prefix + "_PATH>", game_path);
496 add_system_path("<" + path_prefix + "_STATIC>", static_dir);
497
498 {
499 string src_dir;
500 string patch_dir;
501 string mods_dir;
502 string default_dir;
503 string config_path;
504
505 // <mods> setting: default is "$game_path/mods".
506 config_path = base_cfg_path + "/mods";
507 default_dir = game_path + "/mods";
508 config->value(config_path.c_str(), mods_dir, default_dir.c_str());
509 add_system_path("<" + path_prefix + "_MODS>", mods_dir);
510
511 // <patch> setting: default is "$game_path/patch".
512 config_path = base_cfg_path + "/patch";
513 default_dir = game_path + "/patch";
514 config->value(config_path.c_str(), patch_dir, default_dir.c_str());
515 add_system_path("<" + path_prefix + "_PATCH>", patch_dir);
516
517 // <source> setting: default is "$game_path/source".
518 config_path = base_cfg_path + "/source";
519 config->value(config_path.c_str(), patch_dir, default_dir.c_str());
520 add_system_path("<" + path_prefix + "_SOURCE>", src_dir);
521 #ifdef DEBUG_PATHS
522 if (!silent) {
523 cout << "path prefix of " << cfgname
524 << " is: " << path_prefix << endl;
525 cout << "setting " << cfgname
526 << " static directory to: " << static_dir << endl;
527 cout << "setting " << cfgname
528 << " patch directory to: " << patch_dir << endl;
529 cout << "setting " << cfgname
530 << " modifications directory to: " << mods_dir << endl;
531 cout << "setting " << cfgname
532 << " source directory to: " << src_dir << endl;
533 }
534 #endif
535 }
536
537 get_game_paths(game_path);
538 gather_mods();
539 }
540
gather_mods()541 void ModManager::gather_mods() {
542 modlist.clear(); // Just to be on the safe side.
543
544 FileList filenames;
545 string pathname("<" + path_prefix + "_MODS>");
546 int ptroff = get_system_path(pathname).length() + 1;
547
548 // If the dir doesn't exist, leave at once.
549 if (!U7exists(pathname))
550 return;
551
552 U7ListFiles(pathname + "/*.cfg", filenames);
553 int num_mods = filenames.size();
554
555 if (num_mods > 0) {
556 modlist.reserve(num_mods);
557 for (int i = 0; i < num_mods; i++) {
558 string modtitle = filenames[i].substr(ptroff,
559 filenames[i].size() - ptroff - 4);
560 modlist.emplace_back(type, language, cfgname,
561 modtitle, path_prefix, expansion, sibeta,
562 editing, filenames[i]);
563 }
564 }
565 }
566
find_mod(const string & name)567 ModInfo *ModManager::find_mod(const string &name) {
568 for (auto& mod : modlist)
569 if (mod.get_mod_title() == name)
570 return &mod;
571 return nullptr;
572 }
573
find_mod_index(const string & name)574 int ModManager::find_mod_index(const string &name) {
575 for (size_t i = 0; i < modlist.size(); i++)
576 if (modlist[i].get_mod_title() == name)
577 return i;
578 return -1;
579 }
580
add_mod(const string & mod,const string & modconfig)581 void ModManager::add_mod(const string &mod, const string &modconfig) {
582 modlist.emplace_back(type, language, cfgname, mod, path_prefix,
583 expansion, sibeta, editing, modconfig);
584 store_system_paths();
585 }
586
587
588 // Checks the game 'gam' for the presence of the mod given in 'arg_modname'
589 // and checks the mod's compatibility. If the mod exists and is compatible with
590 // Exult, returns a reference to the mod; otherwise, returns the mod's parent game.
591 // Outputs error messages is the mod is not found or is not compatible.
get_mod(const string & name,bool checkversion)592 BaseGameInfo *ModManager::get_mod(const string &name, bool checkversion) {
593 ModInfo *newgame = nullptr;
594 if (has_mods())
595 newgame = find_mod(name);
596 if (newgame) {
597 if (checkversion && !newgame->is_mod_compatible()) {
598 cerr << "Mod '" << name << "' is not compatible with this version of Exult." << endl;
599 return nullptr;
600 }
601 }
602 if (!newgame)
603 cerr << "Mod '" << name << "' not found." << endl;
604 return newgame;
605 }
606
607 /*
608 * Calculate paths for the given game, using the config file and
609 * falling back to defaults if necessary. These are stored in
610 * per-game system_path entries, which are then used later once the
611 * game is selected.
612 */
get_game_paths(const string & game_path)613 void ModManager::get_game_paths(const string &game_path) {
614 string saveprefix(get_system_path("<SAVEHOME>") + "/" + cfgname);
615 string default_dir;
616 string config_path;
617 string gamedat_dir;
618 string static_dir;
619 string savegame_dir;
620 string base_cfg_path("config/disk/game/" + cfgname);
621
622 // ++++ All of these are directories with read/write requirements.
623 // ++++ They default to a directory in the current user's profile,
624 // ++++ with Win9x and old MacOS (possibly others) being exceptions.
625
626 // Usually for Win9x:
627 if (saveprefix == ".")
628 saveprefix = game_path;
629
630 // <savegame_path> setting: default is "$dataprefix".
631 config_path = base_cfg_path + "/savegame_path";
632 default_dir = saveprefix;
633 config->value(config_path.c_str(), savegame_dir, default_dir.c_str());
634 add_system_path("<" + path_prefix + "_SAVEGAME>", savegame_dir);
635 U7mkdir(savegame_dir.c_str(), 0755);
636
637 // <gamedat_path> setting: default is "$dataprefix/gamedat".
638 config_path = base_cfg_path + "/gamedat_path";
639 default_dir = saveprefix + "/gamedat";
640 config->value(config_path.c_str(), gamedat_dir, default_dir.c_str());
641 add_system_path("<" + path_prefix + "_GAMEDAT>", gamedat_dir);
642 U7mkdir(gamedat_dir.c_str(), 0755);
643
644 #ifdef DEBUG_PATHS
645 cout << "setting " << cfgname
646 << " gamedat directory to: " << gamedat_dir << endl;
647 cout << "setting " << cfgname
648 << " savegame directory to: " << savegame_dir << endl;
649 #endif
650 }
651
652 // GameManager: class that manages the installed games
GameManager(bool silent)653 GameManager::GameManager(bool silent) {
654 games.clear();
655 bg = fov = si = ss = sib = nullptr;
656
657 // Search for games defined in exult.cfg:
658 string config_path("config/disk/game");
659 string game_title;
660 std::vector<string> gamestrs = config->listkeys(config_path, false);
661 std::vector<string> checkgames;
662 checkgames.reserve(checkgames.size()+5); // +5 in case the four below are not in the cfg.
663 // The original games plus expansions.
664 checkgames.emplace_back(CFG_BG_NAME);
665 checkgames.emplace_back(CFG_FOV_NAME);
666 checkgames.emplace_back(CFG_SI_NAME);
667 checkgames.emplace_back(CFG_SS_NAME);
668 checkgames.emplace_back(CFG_SIB_NAME);
669
670 for (auto& gamestr : gamestrs) {
671 if (gamestr != CFG_BG_NAME && gamestr != CFG_FOV_NAME &&
672 gamestr != CFG_SI_NAME && gamestr != CFG_SS_NAME &&
673 gamestr != CFG_SIB_NAME)
674 checkgames.push_back(gamestr);
675 }
676
677 games.reserve(checkgames.size());
678 int bgind = -1;
679 int fovind = -1;
680 int siind = -1;
681 int ssind = -1;
682 int sibind = -1;
683
684 for (const auto& gameentry : checkgames) {
685 // Load the paths for all games found:
686 string base_title = gameentry;
687 string new_title;
688 to_uppercase(base_title);
689 base_title += "\nMissing Title";
690 string base_conf = config_path;
691 base_conf += '/';
692 base_conf += gameentry;
693 base_conf += "/title";
694 config->value(base_conf, game_title, base_title.c_str());
695 bool need_title = game_title == base_title;
696 // This checks static identity and sets game type.
697 ModManager game = ModManager(gameentry, game_title, need_title, silent);
698 if (!game.being_edited() && !game.is_there())
699 continue;
700 if (game.get_game_type() == BLACK_GATE) {
701 if (game.have_expansion()) {
702 if (fovind == -1)
703 fovind = games.size();
704 } else if (bgind == -1)
705 bgind = games.size();
706 } else if (game.get_game_type() == SERPENT_ISLE) {
707 if (game.is_si_beta()) {
708 if (sibind == -1)
709 sibind = games.size();
710 } else if (game.have_expansion()) {
711 if (ssind == -1)
712 ssind = games.size();
713 } else if (siind == -1)
714 siind = games.size();
715 }
716
717 games.push_back(game);
718 }
719
720 if (bgind >= 0)
721 bg = &(games[bgind]);
722 if (fovind >= 0)
723 fov = &(games[fovind]);
724 if (siind >= 0)
725 si = &(games[siind]);
726 if (ssind >= 0)
727 ss = &(games[ssind]);
728 if (sibind >= 0)
729 sib = &(games[sibind]);
730
731 // Sane defaults.
732 add_system_path("<ULTIMA7_STATIC>", ".");
733 add_system_path("<SERPENT_STATIC>", ".");
734 print_found(bg, "exult_bg.flx", "Black Gate", CFG_BG_NAME, "ULTIMA7", silent);
735 print_found(fov, "exult_bg.flx", "Forge of Virtue", CFG_FOV_NAME, "ULTIMA7", silent);
736 print_found(si, "exult_si.flx", "Serpent Isle", CFG_SI_NAME, "SERPENT", silent);
737 print_found(ss, "exult_si.flx", "Silver Seed", CFG_SS_NAME, "SERPENT", silent);
738 store_system_paths();
739 }
740
print_found(ModManager * game,const char * flex,const char * title,const char * cfgname,const char * basepath,bool silent)741 void GameManager::print_found(
742 ModManager *game,
743 const char *flex,
744 const char *title,
745 const char *cfgname,
746 const char *basepath,
747 bool silent
748 ) {
749 char path[50];
750 string cfgstr(cfgname);
751 to_uppercase(cfgstr);
752 snprintf(path, sizeof(path), "<%s_STATIC>/", cfgstr.c_str());
753
754 if (game == nullptr) {
755 if (!silent)
756 cout << title << " : not found ("
757 << get_system_path(path) << ")" << endl;
758 return;
759 }
760 if (!silent)
761 cout << title << " : found" << endl;
762 // This stores the BG/SI static paths (preferring the expansions)
763 // for easier support of things like multiracial avatars in BG.
764 char staticpath[50];
765 snprintf(path, sizeof(path), "<%s_STATIC>", cfgstr.c_str());
766 snprintf(staticpath, sizeof(staticpath), "<%s_STATIC>", basepath);
767 clone_system_path(staticpath, path);
768 if (silent)
769 return;
770 snprintf(path, sizeof(path), "<DATA>/%s", flex);
771 if (U7exists(path))
772 cout << flex << " : found" << endl;
773 else
774 cout << flex << " : not found ("
775 << get_system_path(path)
776 << ")" << endl;
777 }
778
find_game(const string & name)779 ModManager *GameManager::find_game(const string &name) {
780 for (auto& game : games)
781 if (game.get_cfgname() == name)
782 return &game;
783 return nullptr;
784 }
785
find_game_index(const string & name)786 int GameManager::find_game_index(const string &name) {
787 for (size_t i = 0; i < games.size(); i++)
788 if (games[i].get_cfgname() == name)
789 return i;
790 return -1;
791 }
792
add_game(const string & name,const string & menu)793 void GameManager::add_game(const string &name, const string &menu) {
794 games.emplace_back(name, menu, false);
795 store_system_paths();
796 }
797