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