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