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