1 #include "logo32.png.h"
2 #include "Logger.h"
3 #include "RustInterface.h"
4 #include "Types.h"
5 #include "GameRes.h"
6 #include "Video.h"
7 
8 #include "Launcher.h"
9 
10 #include "FL/Fl_Native_File_Chooser.H"
11 #include <FL/Fl_PNG_Image.H>
12 #include <FL/fl_ask.H>
13 #include <string_theory/string>
14 
15 #include <algorithm>
16 #include <vector>
17 
18 #define RESOLUTION_SEPARATOR "x"
19 
20 
21 const char* defaultResolution = "640x480";
22 
23 const std::vector<GameVersion> predefinedVersions = {
24 	GameVersion::DUTCH,
25 	GameVersion::ENGLISH,
26 	GameVersion::FRENCH,
27 	GameVersion::GERMAN,
28 	GameVersion::ITALIAN,
29 	GameVersion::POLISH,
30 	GameVersion::RUSSIAN,
31 	GameVersion::RUSSIAN_GOLD
32 };
33 const std::vector< std::pair<int, int> > predefinedResolutions = {
34 	std::make_pair(640,  480),
35 	std::make_pair(800,  600),
36 	std::make_pair(1024, 768),
37 	std::make_pair(1280, 720),
38 	std::make_pair(1600, 900),
39 	std::make_pair(1920, 1080)
40 };
41 const std::vector<VideoScaleQuality> scalingModes = {
42 	VideoScaleQuality::LINEAR,
43 	VideoScaleQuality::NEAR_PERFECT,
44 	VideoScaleQuality::PERFECT,
45 };
46 
showRustError()47 void showRustError() {
48 	RustPointer<char> err(getRustError());
49 	if (err) {
50 		SLOGE("%s", err.get());
51 		fl_message_title("Rust error");
52 		fl_alert("%s", err.get());
53 	} else {
54 		RustPointer<char> err(getRustError());
55 		SLOGE("showRustError: no rust error");
56 		fl_message_title("showRustError");
57 		fl_alert("no rust error");
58 	}
59 }
60 
encodePath(const char * path)61 ST::string encodePath(const char* path) {
62 	if (path == nullptr) {
63 		return ST::string();
64 	}
65 	RustPointer<char> encodedPath(Path_encodeU8(reinterpret_cast<const uint8_t*>(path), strlen(path)));
66 	return ST::string(encodedPath.get());
67 }
68 
decodePath(const char * path)69 ST::char_buffer decodePath(const char* path) {
70 	if (path == nullptr) {
71 		return ST::char_buffer{};
72 	}
73 	ST::char_buffer buf{ST::char_buffer::strlen(path), '\0'}; // the decoded size always fits in the original size
74 	size_t len = Path_decodeU8(path, reinterpret_cast<uint8_t*>(buf.data()), buf.size());
75 	if (len > buf.size()) {
76 		showRustError();
77 		return ST::char_buffer{};
78 	}
79 	return ST::char_buffer{buf.c_str(), len};
80 }
81 
Launcher(int argc,char * argv[])82 Launcher::Launcher(int argc, char* argv[]) : StracciatellaLauncher() {
83 	this->argc = argc;
84 	this->argv = argv;
85 }
86 
~Launcher()87 Launcher::~Launcher() {
88 }
89 
loadJa2Json()90 void Launcher::loadJa2Json() {
91 	RustPointer<char> configFolderPath(EngineOptions_getStracciatellaHome());
92 	if (configFolderPath.get() == NULL) {
93 		auto rustError = getRustError();
94 		if (rustError != NULL) {
95 			SLOGE("Failed to find home directory: %s", rustError);
96 		}
97 	}
98 
99 	this->engine_options.reset(EngineOptions_create(configFolderPath.get(), argv, argc));
100 
101 	if (this->engine_options == NULL) {
102 		exit(EXIT_FAILURE);
103 	}
104 	if (EngineOptions_shouldShowHelp(this->engine_options.get())) {
105 		exit(EXIT_SUCCESS);
106 	}
107 }
108 
show()109 void Launcher::show() {
110 	editorButton->callback( (Fl_Callback*)startEditor, (void*)(this) );
111 	playButton->callback( (Fl_Callback*)startGame, (void*)(this) );
112 	gameDirectoryInput->callback( (Fl_Callback*)widgetChanged, (void*)(this) );
113 	browseJa2DirectoryButton->callback((Fl_Callback *) openGameDirectorySelector, (void *) (this));
114 	gameVersionInput->callback( (Fl_Callback*)widgetChanged, (void*)(this) );
115 	guessVersionButton->callback( (Fl_Callback*)guessVersion, (void*)(this) );
116 	scalingModeChoice->callback( (Fl_Callback*)widgetChanged, (void*)(this) );
117 	resolutionXInput->callback( (Fl_Callback*)widgetChanged, (void*)(this) );
118 	resolutionYInput->callback( (Fl_Callback*)widgetChanged, (void*)(this) );
119 	RustPointer<char> game_json_path(findPathFromAssetsDir("externalized/game.json", true, true));
120 	if (game_json_path) {
121 		gameSettingsOutput->value(game_json_path.get());
122 	} else {
123 		gameSettingsOutput->value("failed to find path to game.json");
124 	}
125 	fullscreenCheckbox->callback( (Fl_Callback*)widgetChanged, (void*)(this) );
126 	playSoundsCheckbox->callback( (Fl_Callback*)widgetChanged, (void*)(this) );
127 	RustPointer<char> ja2_json_path(findPathFromStracciatellaHome(this->engine_options.get(), "ja2.json", false, true));
128 	if (ja2_json_path) {
129 		ja2JsonPathOutput->value(ja2_json_path.get());
130 	} else {
131 		ja2JsonPathOutput->value("failed to find path to ja2.json");
132 	}
133 	ja2JsonReloadBtn->callback( (Fl_Callback*)reloadJa2Json, (void*)(this) );
134 	ja2JsonSaveBtn->callback( (Fl_Callback*)saveJa2Json, (void*)(this) );
135 	addModMenuButton->callback( (Fl_Callback*)addMod, (void*)(this) );
136 	moveDownModsButton->callback( (Fl_Callback*)moveDownMods, (void*)(this) );
137 	moveUpModsButton->callback( (Fl_Callback*)moveUpMods, (void*)(this) );
138 	removeModsButton->callback( (Fl_Callback*)removeMods, (void*)(this) );
139 
140 	populateChoices();
141 	initializeInputsFromDefaults();
142 
143 	const Fl_PNG_Image icon("logo32.png", logo32_png, 1374);
144 	stracciatellaLauncher->icon(&icon);
145 	stracciatellaLauncher->show();
146 }
147 
initializeInputsFromDefaults()148 void Launcher::initializeInputsFromDefaults() {
149 	RustPointer<char> rustResRootPath(EngineOptions_getVanillaGameDir(this->engine_options.get()));
150 	gameDirectoryInput->value(rustResRootPath.get());
151 
152 	uint32_t n = EngineOptions_getModsLength(this->engine_options.get());
153 	modsCheckBrowser->clear();
154 	for (uint32_t i = 0; i < n; ++i) {
155 		modsCheckBrowser->add(EngineOptions_getMod(this->engine_options.get(), i));
156 	}
157 
158 	GameVersion rustResVersion = EngineOptions_getResourceVersion(this->engine_options.get());
159 	int resourceVersionIndex = 0;
160 	for (GameVersion version : predefinedVersions) {
161 		if (version == rustResVersion) {
162 			break;
163 		}
164 		resourceVersionIndex += 1;
165 	}
166 	gameVersionInput->value(resourceVersionIndex);
167 
168 	int x = EngineOptions_getResolutionX(this->engine_options.get());
169 	int y = EngineOptions_getResolutionY(this->engine_options.get());
170 
171 	resolutionXInput->value(x);
172 	resolutionYInput->value(y);
173 
174 	VideoScaleQuality quality = EngineOptions_getScalingQuality(this->engine_options.get());
175 	int scalingModeIndex = 0;
176 	for (VideoScaleQuality scalingMode : scalingModes) {
177 		if (scalingMode == quality) {
178 			break;
179 		}
180 		scalingModeIndex += 1;
181 	}
182 	this->scalingModeChoice->value(scalingModeIndex);
183 
184 	fullscreenCheckbox->value(EngineOptions_shouldStartInFullscreen(this->engine_options.get()) ? 1 : 0);
185 	playSoundsCheckbox->value(EngineOptions_shouldStartWithoutSound(this->engine_options.get()) ? 0 : 1);
186 	update(false, nullptr);
187 }
188 
writeJsonFile()189 int Launcher::writeJsonFile() {
190 	EngineOptions_setStartInFullscreen(this->engine_options.get(), fullscreenCheckbox->value());
191 	EngineOptions_setStartWithoutSound(this->engine_options.get(), !playSoundsCheckbox->value());
192 
193 	EngineOptions_setVanillaGameDir(this->engine_options.get(), gameDirectoryInput->value());
194 
195 	EngineOptions_clearMods(this->engine_options.get());
196 	int nitems = modsCheckBrowser->nitems();
197 	for (int item = 1; item <= nitems; ++item) {
198 		EngineOptions_pushMod(this->engine_options.get(), modsCheckBrowser->text(item));
199 	}
200 
201 	int x = (int)resolutionXInput->value();
202 	int y = (int)resolutionYInput->value();
203 	EngineOptions_setResolution(this->engine_options.get(), x, y);
204 
205 	int currentResourceVersionIndex = gameVersionInput->value();
206 	GameVersion currentResourceVersion = predefinedVersions.at(currentResourceVersionIndex);
207 	EngineOptions_setResourceVersion(this->engine_options.get(), currentResourceVersion);
208 
209 	VideoScaleQuality currentScalingMode = scalingModes[this->scalingModeChoice->value()];
210 	EngineOptions_setScalingQuality(this->engine_options.get(), currentScalingMode);
211 
212 	bool success = EngineOptions_write(this->engine_options.get());
213 
214 	if (success) {
215 		update(false, nullptr);
216 		SLOGD("Succeeded writing config file");
217 		return 0;
218 	}
219 	SLOGD("Failed writing config file");
220 	return 1;
221 }
222 
populateChoices()223 void Launcher::populateChoices() {
224 	RustPointer<VecCString> mods(findAvailableMods(this->engine_options.get()));
225 	size_t nmods = VecCString_len(mods.get());
226 	for (size_t i = 0; i < nmods; ++i) {
227 		RustPointer<char> mod(VecCString_get(mods.get(), i));
228 		addModMenuButton->insert(-1, mod.get(), 0, addMod, this, 0);
229 	}
230 
231 	for(GameVersion version : predefinedVersions) {
232 		RustPointer<char> resourceVersionString(VanillaVersion_toString(version));
233 		gameVersionInput->add(resourceVersionString.get());
234 	}
235 	for (std::pair<int,int> res : predefinedResolutions) {
236 		char resolutionString[255];
237 		sprintf(resolutionString, "%dx%d", res.first, res.second);
238 		predefinedResolutionMenuButton->insert(-1, resolutionString, 0, setPredefinedResolution, this, 0);
239 	}
240 
241 	for (VideoScaleQuality scalingMode : scalingModes) {
242 		RustPointer<char> scalingModeString(ScalingQuality_toString(scalingMode));
243 		this->scalingModeChoice->add(scalingModeString.get());
244 	}
245 }
246 
openGameDirectorySelector(Fl_Widget * btn,void * userdata)247 void Launcher::openGameDirectorySelector(Fl_Widget *btn, void *userdata) {
248 	Launcher* window = static_cast< Launcher* >( userdata );
249 	Fl_Native_File_Chooser fnfc;
250 	fnfc.title("Select the original Jagged Alliance 2 install directory");
251 	fnfc.type(Fl_Native_File_Chooser::BROWSE_DIRECTORY);
252 	ST::char_buffer decoded = decodePath(window->gameDirectoryInput->value());
253 	fnfc.directory(decoded.empty() ? nullptr : decoded.c_str());
254 
255 	switch ( fnfc.show() ) {
256 		case -1:
257 			break; // ERROR
258 		case  1:
259 			break; // CANCEL
260 		default:
261 		{
262 			ST::string encoded = encodePath(fnfc.filename());
263 			window->gameDirectoryInput->value(encoded.c_str());
264 			window->update(true, window->gameDirectoryInput);
265 			break; // FILE CHOSEN
266 		}
267 	}
268 }
269 
startExecutable(bool asEditor)270 void Launcher::startExecutable(bool asEditor) {
271 	// check minimal resolution:
272 	if (resolutionIsInvalid()) {
273 		fl_message_title("Invalid resolution");
274 		fl_alert("Invalid custom resolution %dx%d.\nJA2 Stracciatella needs a resolution of at least 640x480.",
275 			(int) resolutionXInput->value(),
276 			(int) resolutionYInput->value());
277 		return;
278 	}
279 
280 	RustPointer<char> exePath(Env_currentExe());
281 	if (!exePath) {
282 		showRustError();
283 		return;
284 	}
285 	RustPointer<char> filename(Path_filename(exePath.get()));
286 	if (!filename) {
287 		fl_message_title("No filename");
288 		fl_alert("%s", exePath.get());
289 		return;
290 	}
291 	ST::string target("-launcher");
292 	ST::string newFilename(filename.get());
293 	auto pos = newFilename.find(target);
294 	if (pos == -1) {
295 		fl_message_title("Not launcher");
296 		fl_alert("%s", exePath.get());
297 		return;
298 	}
299 	newFilename = newFilename.replace(target, "");
300 	exePath.reset(Path_setFilename(exePath.get(), newFilename.c_str()));
301 	if (!Fs_exists(exePath.get())) {
302 		fl_message_title("Not found");
303 		fl_alert("%s", exePath.get());
304 		return;
305 	}
306 	RustPointer<VecCString> args(VecCString_create());
307 	if (asEditor) {
308 		VecCString_push(args.get(), "-editor");
309 	}
310 	bool ok = Command_execute(exePath.get(), args.get());
311 	if (!ok) {
312 		showRustError();
313 	}
314 }
315 
resolutionIsInvalid()316 bool Launcher::resolutionIsInvalid() {
317 	return resolutionXInput->value() < 640 || resolutionYInput->value() < 480;
318 }
319 
update(bool changed,Fl_Widget * widget)320 void Launcher::update(bool changed, Fl_Widget *widget) {
321 	// invalid resolution warning
322 	if (resolutionIsInvalid()) {
323 		invalidResolutionLabel->show();
324 	} else {
325 		invalidResolutionLabel->hide();
326 	}
327 
328 	// something changed indicator
329 	if (changed && ja2JsonPathOutput->value()[0] != '*') {
330 		ST::string tmp("*"); // add '*'
331 		tmp += ja2JsonPathOutput->value();
332 		ja2JsonPathOutput->value(tmp.c_str());
333 	} else if (!changed && ja2JsonPathOutput->value()[0] == '*') {
334 		ST::string tmp(ja2JsonPathOutput->value() + 1); // remove '*'
335 		ja2JsonPathOutput->value(tmp.c_str());
336 	}
337 }
338 
startGame(Fl_Widget * btn,void * userdata)339 void Launcher::startGame(Fl_Widget* btn, void* userdata) {
340 	Launcher* window = static_cast< Launcher* >( userdata );
341 
342 	window->writeJsonFile();
343 	if (!checkIfRelativePathExists(window->gameDirectoryInput->value(), "Data", true)) {
344 		fl_message_title(window->playButton->label());
345 		int choice = fl_choice("Data dir not found.\nAre you sure you want to continue?", "Stop", "Continue", 0);
346 		if (choice != 1) {
347 			return;
348 		}
349 	}
350 	window->startExecutable(false);
351 }
352 
startEditor(Fl_Widget * btn,void * userdata)353 void Launcher::startEditor(Fl_Widget* btn, void* userdata) {
354 	Launcher* window = static_cast< Launcher* >( userdata );
355 
356 	window->writeJsonFile();
357 	bool has_editor_slf = checkIfRelativePathExists(window->gameDirectoryInput->value(), "Data/Editor.slf", true);
358 	if (!has_editor_slf) {
359 		RustPointer<char> assets_dir(findPathFromAssetsDir(nullptr, false, false));
360 		if (assets_dir) {
361 			// free editor.slf
362 			has_editor_slf = checkIfRelativePathExists(assets_dir.get(), "externalized/editor.slf", true);
363 		}
364 	}
365 	if (!has_editor_slf) {
366 		fl_message_title(window->editorButton->label());
367 		int choice = fl_choice("Editor.slf not found.\nAre you sure you want to continue?", "Stop", "Continue", 0);
368 		if (choice != 1) {
369 			return;
370 		}
371 	}
372 	window->startExecutable(true);
373 }
374 
guessVersion(Fl_Widget * btn,void * userdata)375 void Launcher::guessVersion(Fl_Widget* btn, void* userdata) {
376 	Launcher* window = static_cast< Launcher* >( userdata );
377 	fl_message_title("Guess Game Version");
378 	int choice = fl_choice("Comparing resources packs can take a long time.\nAre you sure you want to continue?", "Stop", "Continue", 0);
379 	if (choice != 1) {
380 		return;
381 	}
382 
383 	const char* gamedir = window->gameDirectoryInput->value();
384 	int guessedVersion = guessResourceVersion(gamedir);
385 	if (guessedVersion != -1) {
386 		int resourceVersionIndex = 0;
387 		for (GameVersion version : predefinedVersions) {
388 			if (static_cast<int>(version) == guessedVersion) {
389 				break;
390 			}
391 			resourceVersionIndex += 1;
392 		}
393 		window->gameVersionInput->value(resourceVersionIndex);
394 		window->update(true, window->gameVersionInput);
395 		fl_message_title(window->guessVersionButton->label());
396 		fl_message("Success!");
397 	} else {
398 		fl_message_title(window->guessVersionButton->label());
399 		fl_alert("Failure!");
400 	}
401 }
402 
setPredefinedResolution(Fl_Widget * btn,void * userdata)403 void Launcher::setPredefinedResolution(Fl_Widget* btn, void* userdata) {
404 	Fl_Menu_Button* menuBtn = static_cast< Fl_Menu_Button* >( btn );
405 	Launcher* window = static_cast< Launcher* >( userdata );
406 	ST::string res = menuBtn->mvalue()->label();
407 	int x = 0;
408 	int y = 0;
409 	(void)sscanf(res.c_str(), "%d" RESOLUTION_SEPARATOR "%d", &x, &y);
410 	window->resolutionXInput->value(x);
411 	window->resolutionYInput->value(y);
412 	window->update(true, btn);
413 }
414 
widgetChanged(Fl_Widget * widget,void * userdata)415 void Launcher::widgetChanged(Fl_Widget* widget, void* userdata) {
416 	Launcher* window = static_cast< Launcher* >( userdata );
417 	window->update(true, widget);
418 }
419 
reloadJa2Json(Fl_Widget * widget,void * userdata)420 void Launcher::reloadJa2Json(Fl_Widget* widget, void* userdata) {
421 	Launcher* window = static_cast< Launcher* >( userdata );
422 	window->loadJa2Json();
423 	window->initializeInputsFromDefaults();
424 }
425 
saveJa2Json(Fl_Widget * widget,void * userdata)426 void Launcher::saveJa2Json(Fl_Widget* widget, void* userdata) {
427 	Launcher* window = static_cast< Launcher* >( userdata );
428 	window->writeJsonFile();
429 }
430 
addMod(Fl_Widget * widget,void * userdata)431 void Launcher::addMod(Fl_Widget* widget, void* userdata) {
432 	Fl_Menu_Button* menuButton = static_cast< Fl_Menu_Button* >( widget );
433 	Launcher* window = static_cast< Launcher* >( userdata );
434 
435 	const char* mod = menuButton->mvalue()->label();
436 	window->modsCheckBrowser->add(mod);
437 	window->modsCheckBrowser->redraw();
438 	window->update(true, widget);
439 }
440 
moveUpMods(Fl_Widget * widget,void * userdata)441 void Launcher::moveUpMods(Fl_Widget* widget, void* userdata) {
442 	Launcher* window = static_cast< Launcher* >( userdata );
443 	int nitems = window->modsCheckBrowser->nitems();
444 	int nchecked = window->modsCheckBrowser->nchecked();
445 	if (nchecked == 0 || nchecked == nitems) {
446 		return; // nothing to do
447 	}
448 
449 	std::vector<int> order;
450 	for (int item = 1; item <= nitems; ++item) {
451 		if (window->modsCheckBrowser->checked(item)) {
452 			if (!order.empty() && !window->modsCheckBrowser->checked(order.back())) {
453 				order.insert(order.end() - 1, item); // move up
454 				continue;
455 			}
456 		}
457 		order.emplace_back(item);
458 	}
459 
460 	std::vector<ST::string> text;
461 	std::vector<int> checked;
462 	for (int item : order) {
463 		text.emplace_back(window->modsCheckBrowser->text(item));
464 		checked.emplace_back(window->modsCheckBrowser->checked(item));
465 	}
466 
467 	window->modsCheckBrowser->clear();
468 	for (int i = 0; i < nitems; ++i) {
469 		window->modsCheckBrowser->add(text[i].c_str(), checked[i]);
470 	}
471 	window->update(true, widget);
472 }
473 
moveDownMods(Fl_Widget * widget,void * userdata)474 void Launcher::moveDownMods(Fl_Widget* widget, void* userdata) {
475 	Launcher* window = static_cast< Launcher* >( userdata );
476 	int nitems = window->modsCheckBrowser->nitems();
477 	int nchecked = window->modsCheckBrowser->nchecked();
478 	if (nchecked == 0 || nchecked == nitems) {
479 		return; // nothing to do
480 	}
481 
482 	std::vector<int> order;
483 	for (int item = nitems; item >= 1; --item) {
484 		if (window->modsCheckBrowser->checked(item)) {
485 			if (!order.empty() && !window->modsCheckBrowser->checked(order.back())) {
486 				order.insert(order.end() - 1, item); // move down
487 				continue;
488 			}
489 		}
490 		order.emplace_back(item);
491 	}
492 	std::reverse(order.begin(), order.end());
493 
494 	std::vector<ST::string> text;
495 	std::vector<int> checked;
496 	for (int item : order) {
497 		text.emplace_back(window->modsCheckBrowser->text(item));
498 		checked.emplace_back(window->modsCheckBrowser->checked(item));
499 	}
500 
501 	window->modsCheckBrowser->clear();
502 	for (int i = 0; i < nitems; ++i) {
503 		window->modsCheckBrowser->add(text[i].c_str(), checked[i]);
504 	}
505 	window->update(true, widget);
506 }
507 
removeMods(Fl_Widget * widget,void * userdata)508 void Launcher::removeMods(Fl_Widget* widget, void* userdata) {
509 	Launcher* window = static_cast< Launcher* >( userdata );
510 	int nchecked = window->modsCheckBrowser->nchecked();
511 	if (nchecked == 0) {
512 		return; // nothing to do
513 	}
514 
515 	std::vector<ST::string> text;
516 	int nitems = window->modsCheckBrowser->nitems();
517 	for (int item = 1; item <= nitems; ++item) {
518 		if (!window->modsCheckBrowser->checked(item)) {
519 			text.emplace_back(window->modsCheckBrowser->text(item));
520 		}
521 	}
522 
523 	window->modsCheckBrowser->clear();
524 	for (size_t i = 0; i < text.size(); ++i) {
525 		window->modsCheckBrowser->add(text[i].c_str());
526 	}
527 	window->update(true, widget);
528 }
529