1 // SuperTux
2 // Copyright (C) 2006 Matthias Braun <matze@braunis.de>
3 //
4 // This program is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // This program is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17 #include "supertux/main.hpp"
18
19 #include <config.h>
20 #include <version.h>
21 #include <fstream>
22
23 #include <SDL_image.h>
24 #include <SDL_ttf.h>
25 #include <boost/filesystem.hpp>
26 #include <boost/locale.hpp>
27 #include <physfs.h>
28 #include <tinygettext/log.hpp>
29 extern "C" {
30 #include <findlocale.h>
31 }
32
33 #ifdef WIN32
34 #include <codecvt>
35 #endif
36
37 #include "addon/addon_manager.hpp"
38 #include "audio/sound_manager.hpp"
39 #include "editor/editor.hpp"
40 #include "editor/layer_icon.hpp"
41 #include "editor/object_info.hpp"
42 #include "editor/tile_selection.hpp"
43 #include "editor/tip.hpp"
44 #include "editor/tool_icon.hpp"
45 #include "gui/dialog.hpp"
46 #include "gui/menu_manager.hpp"
47 #include "math/random.hpp"
48 #include "object/player.hpp"
49 #include "object/spawnpoint.hpp"
50 #include "physfs/physfs_file_system.hpp"
51 #include "physfs/physfs_sdl.hpp"
52 #include "port/emscripten.hpp"
53 #include "sdk/integration.hpp"
54 #include "sprite/sprite_data.hpp"
55 #include "sprite/sprite_manager.hpp"
56 #include "supertux/command_line_arguments.hpp"
57 #include "supertux/console.hpp"
58 #include "supertux/error_handler.hpp"
59 #include "supertux/game_manager.hpp"
60 #include "supertux/game_session.hpp"
61 #include "supertux/gameconfig.hpp"
62 #include "supertux/globals.hpp"
63 #include "supertux/level.hpp"
64 #include "supertux/level_parser.hpp"
65 #include "supertux/player_status.hpp"
66 #include "supertux/resources.hpp"
67 #include "supertux/savegame.hpp"
68 #include "supertux/screen_fade.hpp"
69 #include "supertux/screen_manager.hpp"
70 #include "supertux/sector.hpp"
71 #include "supertux/tile.hpp"
72 #include "supertux/tile_manager.hpp"
73 #include "supertux/title_screen.hpp"
74 #include "supertux/world.hpp"
75 #include "util/file_system.hpp"
76 #include "util/gettext.hpp"
77 #include "util/string_util.hpp"
78 #include "util/timelog.hpp"
79 #include "util/string_util.hpp"
80 #include "video/sdl_surface.hpp"
81 #include "video/sdl_surface_ptr.hpp"
82 #include "video/ttf_surface_manager.hpp"
83 #include "worldmap/worldmap.hpp"
84 #include "worldmap/worldmap_screen.hpp"
85
86 static Timelog s_timelog;
87
ConfigSubsystem()88 ConfigSubsystem::ConfigSubsystem() :
89 m_config()
90 {
91 g_config = &m_config;
92 try {
93 m_config.load();
94 }
95 catch(const std::exception& e)
96 {
97 log_info << "Couldn't load config file: " << e.what() << ", using default settings" << std::endl;
98 }
99
100 // init random number stuff
101 gameRandom.seed(m_config.random_seed);
102 graphicsRandom.seed(0);
103 //const char *how = config->random_seed? ", user fixed.": ", from time().";
104 //log_info << "Using random seed " << config->random_seed << how << std::endl;
105 }
106
~ConfigSubsystem()107 ConfigSubsystem::~ConfigSubsystem()
108 {
109 try
110 {
111 m_config.save();
112 }
113 catch(std::exception& e)
114 {
115 log_warning << "Error saving config: " << e.what() << std::endl;
116 }
117 }
118
Main()119 Main::Main() :
120 m_physfs_subsystem(),
121 m_config_subsystem(),
122 m_sdl_subsystem(),
123 m_console_buffer(),
124 m_input_manager(),
125 m_video_system(),
126 m_ttf_surface_manager(),
127 m_sound_manager(),
128 m_squirrel_virtual_machine(),
129 m_tile_manager(),
130 m_sprite_manager(),
131 m_resources(),
132 m_addon_manager(),
133 m_console(),
134 m_game_manager(),
135 m_screen_manager(),
136 m_savegame()
137 {
138 }
139
140 void
init_tinygettext()141 Main::init_tinygettext()
142 {
143 g_dictionary_manager.reset(new tinygettext::DictionaryManager(std::make_unique<PhysFSFileSystem>(), "UTF-8"));
144
145 tinygettext::Log::set_log_info_callback(log_info_callback);
146 tinygettext::Log::set_log_warning_callback(log_warning_callback);
147 tinygettext::Log::set_log_error_callback(log_error_callback);
148
149 g_dictionary_manager->add_directory("locale");
150
151 // Config setting "locale" overrides language detection
152 if (!g_config->locale.empty())
153 {
154 g_dictionary_manager->set_language(tinygettext::Language::from_name(g_config->locale));
155 }
156 else
157 {
158 FL_Locale *locale;
159 FL_FindLocale(&locale);
160 tinygettext::Language language = tinygettext::Language::from_spec( locale->lang?locale->lang:"", locale->country?locale->country:"", locale->variant?locale->variant:"");
161 FL_FreeLocale(&locale);
162 g_dictionary_manager->set_language(language);
163 }
164 }
165
PhysfsSubsystem(const char * argv0,boost::optional<std::string> forced_datadir,boost::optional<std::string> forced_userdir)166 PhysfsSubsystem::PhysfsSubsystem(const char* argv0,
167 boost::optional<std::string> forced_datadir,
168 boost::optional<std::string> forced_userdir) :
169 m_forced_datadir(std::move(forced_datadir)),
170 m_forced_userdir(std::move(forced_userdir))
171 {
172 if (!PHYSFS_init(argv0))
173 {
174 std::stringstream msg;
175 msg << "Couldn't initialize physfs: " << PHYSFS_getLastErrorCode();
176 throw std::runtime_error(msg.str());
177 }
178 else
179 {
180 // allow symbolic links
181 PHYSFS_permitSymbolicLinks(1);
182
183 find_userdir();
184 find_datadir();
185 }
186 }
187
find_datadir() const188 void PhysfsSubsystem::find_datadir() const
189 {
190 #ifndef __EMSCRIPTEN__
191 std::string datadir;
192 if (m_forced_datadir)
193 {
194 datadir = *m_forced_datadir;
195 }
196 else if (const char* env_datadir = getenv("SUPERTUX2_DATA_DIR"))
197 {
198 datadir = env_datadir;
199 }
200 else
201 {
202 // check if we run from source dir
203 char* basepath_c = SDL_GetBasePath();
204 std::string basepath = basepath_c ? basepath_c : "./";
205 SDL_free(basepath_c);
206
207 if (FileSystem::exists(FileSystem::join(BUILD_DATA_DIR, "credits.stxt")))
208 {
209 datadir = BUILD_DATA_DIR;
210 // Add config dir for supplemental files
211 PHYSFS_mount(boost::filesystem::canonical(BUILD_CONFIG_DATA_DIR).string().c_str(), nullptr, 1);
212 }
213 else
214 {
215 // if the game is not run from the source directory, try to find
216 // the global install location
217 datadir = basepath.substr(0, basepath.rfind(INSTALL_SUBDIR_BIN));
218 datadir = FileSystem::join(datadir, INSTALL_SUBDIR_SHARE);
219 }
220 }
221
222 if (!PHYSFS_mount(boost::filesystem::canonical(datadir).string().c_str(), nullptr, 1))
223 {
224 log_warning << "Couldn't add '" << datadir << "' to physfs searchpath: " << PHYSFS_getLastErrorCode() << std::endl;
225 }
226 #else
227 if (!PHYSFS_mount(BUILD_CONFIG_DATA_DIR, nullptr, 1))
228 {
229 log_warning << "Couldn't add '" << BUILD_CONFIG_DATA_DIR << "' to physfs searchpath: " << PHYSFS_getLastErrorCode() << std::endl;
230 }
231 #endif
232 }
233
find_userdir() const234 void PhysfsSubsystem::find_userdir() const
235 {
236 std::string userdir;
237 if (m_forced_userdir)
238 {
239 userdir = *m_forced_userdir;
240 }
241 else if (const char* env_userdir = getenv("SUPERTUX2_USER_DIR"))
242 {
243 userdir = env_userdir;
244 }
245 else
246 {
247 userdir = PHYSFS_getPrefDir("SuperTux","supertux2");
248 }
249 //Kept for backwards-compatability only, hence the silence
250 #ifdef __GNUC__
251 #pragma GCC diagnostic push
252 #pragma GCC diagnostic ignored "-Wdeprecated-declarations"
253 #endif
254 std::string physfs_userdir = PHYSFS_getUserDir();
255 #ifdef __GNUC__
256 #pragma GCC diagnostic pop
257 #endif
258
259 #ifndef __HAIKU__
260 #ifdef _WIN32
261 std::string olduserdir = FileSystem::join(physfs_userdir, PACKAGE_NAME);
262 #else
263 std::string olduserdir = FileSystem::join(physfs_userdir, "." PACKAGE_NAME);
264 #endif
265 if (FileSystem::is_directory(olduserdir)) {
266 boost::filesystem::path olduserpath(olduserdir);
267 boost::filesystem::path userpath(userdir);
268
269 boost::filesystem::directory_iterator end_itr;
270
271 bool success = true;
272
273 // cycle through the directory
274 for (boost::filesystem::directory_iterator itr(olduserpath); itr != end_itr; ++itr) {
275 try
276 {
277 boost::filesystem::rename(itr->path().string().c_str(), userpath / itr->path().filename());
278 }
279 catch (const boost::filesystem::filesystem_error& err)
280 {
281 success = false;
282 log_warning << "Failed to move contents of config directory: " << err.what() << std::endl;
283 }
284 }
285 if (success) {
286 try
287 {
288 boost::filesystem::remove_all(olduserpath);
289 }
290 catch (const boost::filesystem::filesystem_error& err)
291 {
292 success = false;
293 log_warning << "Failed to remove old config directory: " << err.what();
294 }
295 }
296 if (success) {
297 log_info << "Moved old config dir " << olduserdir << " to " << userdir << std::endl;
298 }
299 }
300 #endif
301
302 #ifdef EMSCRIPTEN
303 userdir = "/home/web_user/.local/share/supertux2/";
304 #endif
305
306 if (!FileSystem::is_directory(userdir))
307 {
308 FileSystem::mkdir(userdir);
309 log_info << "Created SuperTux userdir: " << userdir << std::endl;
310 }
311
312 #ifdef EMSCRIPTEN
313 EM_ASM({
314 FS.mount(IDBFS, {}, "/home/web_user/.local/share/supertux2/");
315 FS.syncfs(true, (err) => { console.log(err); });
316 }, 0); // EM_ASM is a variadic macro and Clang requires at least 1 value for the variadic argument
317 #endif
318
319 if (!PHYSFS_setWriteDir(userdir.c_str()))
320 {
321 std::ostringstream msg;
322 msg << "Failed to use userdir directory '"
323 << userdir << "': errorcode: " << PHYSFS_getLastErrorCode();
324 throw std::runtime_error(msg.str());
325 }
326
327 PHYSFS_mount(userdir.c_str(), nullptr, 0);
328 }
329
print_search_path()330 void PhysfsSubsystem::print_search_path()
331 {
332 const char* writedir = PHYSFS_getWriteDir();
333 log_info << "PhysfsWriteDir: " << (writedir ? writedir : "(null)") << std::endl;
334 log_info << "PhysfsSearchPath:" << std::endl;
335 char** searchpath = PHYSFS_getSearchPath();
336 for (char** i = searchpath; *i != nullptr; ++i)
337 {
338 log_info << " " << *i << std::endl;
339 }
340 PHYSFS_freeList(searchpath);
341 }
342
~PhysfsSubsystem()343 PhysfsSubsystem::~PhysfsSubsystem()
344 {
345 PHYSFS_deinit();
346 }
347
SDLSubsystem()348 SDLSubsystem::SDLSubsystem()
349 {
350 Uint32 flags = SDL_INIT_TIMER | SDL_INIT_VIDEO;
351 #ifndef UBUNTU_TOUCH
352 flags |= SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER;
353 #endif
354 if (SDL_Init(flags) < 0)
355 {
356 std::stringstream msg;
357 msg << "Couldn't initialize SDL: " << SDL_GetError();
358 throw std::runtime_error(msg.str());
359 }
360
361 if (TTF_Init() < 0)
362 {
363 std::stringstream msg;
364 msg << "Couldn't initialize SDL TTF: " << SDL_GetError();
365 throw std::runtime_error(msg.str());
366 }
367
368 // just to be sure
369 atexit(TTF_Quit);
370 atexit(SDL_Quit);
371 }
372
~SDLSubsystem()373 SDLSubsystem::~SDLSubsystem()
374 {
375 TTF_Quit();
376 SDL_Quit();
377 }
378
379 void
init_video()380 Main::init_video()
381 {
382 VideoSystem::current()->set_title("SuperTux " PACKAGE_VERSION);
383
384 const char* icon_fname = "images/engine/icons/supertux-256x256.png";
385
386 SDLSurfacePtr icon = SDLSurface::from_file(icon_fname);
387 VideoSystem::current()->set_icon(*icon);
388
389 SDL_ShowCursor(g_config->custom_mouse_cursor ? 0 : 1);
390
391 log_info << (g_config->use_fullscreen?"fullscreen ":"window ")
392 << " Window: " << g_config->window_size
393 << " Fullscreen: " << g_config->fullscreen_size << "@" << g_config->fullscreen_refresh_rate
394 << " Area: " << g_config->aspect_size << std::endl;
395 }
396
397 void
resave(const std::string & input_filename,const std::string & output_filename)398 Main::resave(const std::string& input_filename, const std::string& output_filename)
399 {
400 Editor::s_resaving_in_progress = true;
401 std::ifstream in(input_filename);
402 if (!in) {
403 log_fatal << input_filename << ": couldn't open file for reading" << std::endl;
404 } else {
405 log_info << "loading level: " << input_filename << std::endl;
406 auto level = LevelParser::from_stream(in, input_filename, StringUtil::has_suffix(input_filename, ".stwm"), true);
407 in.close();
408
409 std::ofstream out(output_filename);
410 if (!out) {
411 log_fatal << output_filename << ": couldn't open file for writing" << std::endl;
412 } else {
413 log_info << "saving level: " << output_filename << std::endl;
414 level->save(out);
415 }
416 }
417 Editor::s_resaving_in_progress = false;
418 }
419
420 void
launch_game(const CommandLineArguments & args)421 Main::launch_game(const CommandLineArguments& args)
422 {
423 m_sdl_subsystem.reset(new SDLSubsystem());
424 m_console_buffer.reset(new ConsoleBuffer());
425
426 s_timelog.log("controller");
427 m_input_manager.reset(new InputManager(g_config->keyboard_config, g_config->joystick_config));
428
429 s_timelog.log("commandline");
430
431 #ifndef EMSCRIPTEN
432 auto video = g_config->video;
433 if (args.resave && *args.resave) {
434 if (args.video) {
435 video = *args.video;
436 } else {
437 video = VideoSystem::VIDEO_NULL;
438 }
439 }
440 s_timelog.log("video");
441
442 m_video_system = VideoSystem::create(video);
443 #else
444 // Force SDL for WASM builds, as OpenGL is reportedly slow on some devices
445 m_video_system = VideoSystem::create(VideoSystem::VIDEO_SDL);
446 #endif
447 init_video();
448
449 m_ttf_surface_manager.reset(new TTFSurfaceManager());
450
451 s_timelog.log("audio");
452 m_sound_manager.reset(new SoundManager());
453 m_sound_manager->enable_sound(g_config->sound_enabled);
454 m_sound_manager->enable_music(g_config->music_enabled);
455 m_sound_manager->set_sound_volume(g_config->sound_volume);
456 m_sound_manager->set_music_volume(g_config->music_volume);
457
458 s_timelog.log("scripting");
459 m_squirrel_virtual_machine.reset(new SquirrelVirtualMachine(g_config->enable_script_debugger));
460
461 s_timelog.log("resources");
462 m_tile_manager.reset(new TileManager());
463 m_sprite_manager.reset(new SpriteManager());
464 m_resources.reset(new Resources());
465
466 s_timelog.log("integrations");
467 Integration::setup();
468
469 s_timelog.log("addons");
470 m_addon_manager.reset(new AddonManager("addons", g_config->addons));
471
472 m_console.reset(new Console(*m_console_buffer));
473
474 s_timelog.log(nullptr);
475
476 m_savegame = std::make_unique<Savegame>(std::string());
477
478 m_game_manager.reset(new GameManager());
479 m_screen_manager.reset(new ScreenManager(*m_video_system, *m_input_manager));
480
481 if (!args.filenames.empty())
482 {
483 for(const auto& start_level : args.filenames)
484 {
485 // we have a normal path specified at commandline, not a physfs path.
486 // So we simply mount that path here...
487 std::string dir = FileSystem::dirname(start_level);
488 const std::string filename = FileSystem::basename(start_level);
489 const std::string fileProtocol = "file://";
490 const std::string::size_type position = dir.find(fileProtocol);
491 if (position != std::string::npos) {
492 dir = dir.replace(position, fileProtocol.length(), "");
493 }
494 log_debug << "Adding dir: " << dir << std::endl;
495 PHYSFS_mount(dir.c_str(), nullptr, true);
496
497 if (args.resave && *args.resave)
498 {
499 resave(start_level, start_level);
500 }
501 else if (args.editor)
502 {
503 if (PHYSFS_exists(start_level.c_str())) {
504 auto editor = std::make_unique<Editor>();
505 editor->set_level(start_level);
506 editor->setup();
507 editor->update(0, Controller());
508 m_screen_manager->push_screen(std::move(editor));
509 MenuManager::instance().clear_menu_stack();
510 m_sound_manager->stop_music(0.5);
511 } else {
512 log_warning << "Level " << start_level << " doesn't exist." << std::endl;
513 }
514 }
515 else if (StringUtil::has_suffix(start_level, ".stwm"))
516 {
517 m_screen_manager->push_screen(std::make_unique<worldmap::WorldMapScreen>(
518 std::make_unique<worldmap::WorldMap>(filename, *m_savegame)));
519 }
520 else
521 { // launch game
522 std::unique_ptr<GameSession> session (
523 new GameSession(filename, *m_savegame));
524
525 g_config->random_seed = session->get_demo_random_seed(g_config->start_demo);
526 gameRandom.seed(g_config->random_seed);
527 graphicsRandom.seed(0);
528
529 if (args.sector || args.spawnpoint)
530 {
531 std::string sectorname = args.sector.get_value_or("main");
532
533 const auto& spawnpoints = session->get_current_sector().get_objects_by_type<SpawnPointMarker>();
534 std::string default_spawnpoint = (spawnpoints.begin() != spawnpoints.end()) ?
535 "" : spawnpoints.begin()->get_name();
536 std::string spawnpointname = args.spawnpoint.get_value_or(default_spawnpoint);
537
538 session->set_start_point(sectorname, spawnpointname);
539 session->restart_level();
540 }
541
542 if (g_config->tux_spawn_pos)
543 {
544 session->get_current_sector().get_player().set_pos(*g_config->tux_spawn_pos);
545 }
546
547 if (!g_config->start_demo.empty())
548 session->play_demo(g_config->start_demo);
549
550 if (!g_config->record_demo.empty())
551 session->record_demo(g_config->record_demo);
552 m_screen_manager->push_screen(std::move(session));
553 }
554 }
555 }
556 else
557 {
558 if (args.editor)
559 {
560 m_screen_manager->push_screen(std::make_unique<Editor>());
561 }
562 else
563 {
564 m_screen_manager->push_screen(std::make_unique<TitleScreen>(*m_savegame));
565 }
566 }
567
568 #ifdef UBUNTU_TOUCH
569 Dialog::show_message(_("The UBports version is under heavy development!\n"
570 "If you encounter issues, PLEASE contact the maintainter\n"
571 "at https://github.com/supertux/supertux/issues or on the\n"
572 "Open Store's Telegram at https://open-store.io/telegram"));
573 #endif
574
575 m_screen_manager->run();
576 }
577
578 int
run(int argc,char ** argv)579 Main::run(int argc, char** argv)
580 {
581 // First and foremost, set error handlers (to print stack trace on SIGSEGV, etc.)
582 ErrorHandler::set_handlers();
583
584 #ifdef __EMSCRIPTEN__
585 init_emscripten();
586 #endif
587
588 #ifdef WIN32
589 //SDL is used instead of PHYSFS because both create the same path in app data
590 //However, PHYSFS is not yet initizlized, and this should be run before anything is initialized
591 std::string prefpath = SDL_GetPrefPath("SuperTux", "supertux2");
592
593 std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
594
595 //All this conversion stuff is necessary to make this work for internationalized usernames
596 std::string outpath = prefpath + u8"/console.out";
597 std::wstring w_outpath = converter.from_bytes(outpath);
598 _wfreopen(w_outpath.c_str(), L"a", stdout);
599
600 std::string errpath = prefpath + u8"/console.err";
601 std::wstring w_errpath = converter.from_bytes(errpath);
602 _wfreopen(w_errpath.c_str(), L"a", stderr);
603
604 // Create and install global locale - this can fail on some situations:
605 // - with bad values for env vars (LANG, LC_ALL, ...)
606 // - targets where libstdc++ uses its generic locales code (https://gcc.gnu.org/legacy-ml/libstdc++/2003-02/msg00345.html)
607 // NOTE: when moving to C++ >= 17, keep the try-catch block, but use std::locale:global(std::locale(""));
608 //
609 // This should not be necessary on *nix, so only try it on Windows.
610 try
611 {
612 std::locale::global(boost::locale::generator().generate(""));
613 // Make boost.filesystem use it
614 boost::filesystem::path::imbue(std::locale());
615 }
616 catch(const std::runtime_error& err)
617 {
618 std::cout << "Warning: " << err.what() << std::endl;
619 }
620 #endif
621
622 int result = 0;
623
624 try
625 {
626 CommandLineArguments args;
627 try
628 {
629 args.parse_args(argc, argv);
630 g_log_level = args.get_log_level();
631 }
632 catch(const std::exception& err)
633 {
634 std::cout << "Error: " << err.what() << std::endl;
635 return EXIT_FAILURE;
636 }
637
638 m_physfs_subsystem.reset(new PhysfsSubsystem(argv[0], args.datadir, args.userdir));
639 m_physfs_subsystem->print_search_path();
640
641 s_timelog.log("config");
642 m_config_subsystem.reset(new ConfigSubsystem());
643 args.merge_into(*g_config);
644
645 s_timelog.log("tinygettext");
646 init_tinygettext();
647 switch (args.get_action())
648 {
649 case CommandLineArguments::PRINT_VERSION:
650 args.print_version();
651 return 0;
652
653 case CommandLineArguments::PRINT_HELP:
654 args.print_help(argv[0]);
655 return 0;
656
657 case CommandLineArguments::PRINT_DATADIR:
658 args.print_datadir();
659 return 0;
660
661 case CommandLineArguments::PRINT_ACKNOWLEDGEMENTS:
662 args.print_acknowledgements();
663 return 0;
664
665 default:
666 launch_game(args);
667 break;
668 }
669 }
670 catch(const std::exception& e)
671 {
672 log_fatal << "Unexpected exception: " << e.what() << std::endl;
673 result = 1;
674 }
675 catch(...)
676 {
677 log_fatal << "Unexpected exception" << std::endl;
678 result = 1;
679 }
680
681 g_dictionary_manager.reset();
682
683 return result;
684 }
685
686 /* EOF */
687