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>
29extern "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/menu_manager.hpp"
46#include "math/random.hpp"
47#include "object/player.hpp"
48#include "object/spawnpoint.hpp"
49#include "physfs/physfs_file_system.hpp"
50#include "physfs/physfs_sdl.hpp"
51#include "sprite/sprite_data.hpp"
52#include "sprite/sprite_manager.hpp"
53#include "supertux/command_line_arguments.hpp"
54#include "supertux/console.hpp"
55#include "supertux/game_manager.hpp"
56#include "supertux/game_session.hpp"
57#include "supertux/gameconfig.hpp"
58#include "supertux/globals.hpp"
59#include "supertux/level.hpp"
60#include "supertux/level_parser.hpp"
61#include "supertux/player_status.hpp"
62#include "supertux/resources.hpp"
63#include "supertux/savegame.hpp"
64#include "supertux/screen_fade.hpp"
65#include "supertux/screen_manager.hpp"
66#include "supertux/sector.hpp"
67#include "supertux/tile.hpp"
68#include "supertux/tile_manager.hpp"
69#include "supertux/title_screen.hpp"
70#include "supertux/world.hpp"
71#include "util/file_system.hpp"
72#include "util/gettext.hpp"
73#include "util/string_util.hpp"
74#include "util/timelog.hpp"
75#include "util/string_util.hpp"
76#include "video/sdl_surface.hpp"
77#include "video/sdl_surface_ptr.hpp"
78#include "video/ttf_surface_manager.hpp"
79#include "worldmap/worldmap.hpp"
80#include "worldmap/worldmap_screen.hpp"
81
82static Timelog s_timelog;
83
84class ConfigSubsystem final
85{
86public:
87 ConfigSubsystem()
88 {
89 g_config.reset(new Config);
90 try {
91 g_config->load();
92 }
93 catch(const std::exception& e)
94 {
95 log_info << "Couldn't load config file: " << e.what() << ", using default settings" << std::endl;
96 }
97
98 // init random number stuff
99 gameRandom.seed(g_config->random_seed);
100 graphicsRandom.seed(0);
101 //const char *how = config->random_seed? ", user fixed.": ", from time().";
102 //log_info << "Using random seed " << config->random_seed << how << std::endl;
103 }
104
105 ~ConfigSubsystem()
106 {
107 if (g_config)
108 {
109 try
110 {
111 g_config->save();
112 }
113 catch(std::exception& e)
114 {
115 log_warning << "Error saving config: " << e.what() << std::endl;
116 }
117 }
118 g_config.reset();
119 }
120};
121
122Main::Main()
123{
124}
125
126void
127Main::init_tinygettext()
128{
129 g_dictionary_manager.reset(new tinygettext::DictionaryManager(std::make_unique<PhysFSFileSystem>(), "UTF-8"));
130
131 tinygettext::Log::set_log_info_callback(log_info_callback);
132 tinygettext::Log::set_log_warning_callback(log_warning_callback);
133 tinygettext::Log::set_log_error_callback(log_error_callback);
134
135 g_dictionary_manager->add_directory("locale");
136
137 // Config setting "locale" overrides language detection
138 if (!g_config->locale.empty())
139 {
140 g_dictionary_manager->set_language(tinygettext::Language::from_name(g_config->locale));
141 }
142 else
143 {
144 FL_Locale *locale;
145 FL_FindLocale(&locale);
146 tinygettext::Language language = tinygettext::Language::from_spec( locale->lang?locale->lang:"", locale->country?locale->country:"", locale->variant?locale->variant:"");
147 FL_FreeLocale(&locale);
148 g_dictionary_manager->set_language(language);
149 }
150}
151
152class PhysfsSubsystem final
153{
154private:
155 boost::optional<std::string> m_forced_datadir;
156 boost::optional<std::string> m_forced_userdir;
157
158public:
159 PhysfsSubsystem(const char* argv0,
160 boost::optional<std::string> forced_datadir,
161 boost::optional<std::string> forced_userdir) :
162 m_forced_datadir(std::move(forced_datadir)),
163 m_forced_userdir(std::move(forced_userdir))
164 {
165 if (!PHYSFS_init(argv0))
166 {
167 std::stringstream msg;
168 msg << "Couldn't initialize physfs: " << PHYSFS_getLastErrorCode();
169 throw std::runtime_error(msg.str());
170 }
171 else
172 {
173 // allow symbolic links
174 PHYSFS_permitSymbolicLinks(1);
175
176 find_userdir();
177 find_datadir();
178 }
179 }
180
181 void find_datadir() const
182 {
183 std::string datadir;
184 if (m_forced_datadir)
185 {
186 datadir = *m_forced_datadir;
187 }
188 else if (const char* env_datadir = getenv("SUPERTUX2_DATA_DIR"))
189 {
190 datadir = env_datadir;
191 }
192 else
193 {
194 // check if we run from source dir
195 char* basepath_c = SDL_GetBasePath();
196 std::string basepath = basepath_c ? basepath_c : "./";
197 SDL_free(basepath_c);
198
199 if (FileSystem::exists(FileSystem::join(BUILD_DATA_DIR, "credits.stxt")))
200 {
201 datadir = BUILD_DATA_DIR;
202 // Add config dir for supplemental files
203 PHYSFS_mount(boost::filesystem::canonical(BUILD_CONFIG_DATA_DIR).string().c_str(), nullptr, 1);
204 }
205 else
206 {
207 // if the game is not run from the source directory, try to find
208 // the global install location
209 datadir = basepath.substr(0, basepath.rfind(INSTALL_SUBDIR_BIN));
210 datadir = FileSystem::join(datadir, INSTALL_SUBDIR_SHARE);
211 }
212 }
213
214 if (!PHYSFS_mount(boost::filesystem::canonical(datadir).string().c_str(), nullptr, 1))
215 {
216 log_warning << "Couldn't add '" << datadir << "' to physfs searchpath: " << PHYSFS_getLastErrorCode() << std::endl;
217 }
218 }
219
220 void find_userdir() const
221 {
222 std::string userdir;
223 if (m_forced_userdir)
224 {
225 userdir = *m_forced_userdir;
226 }
227 else if (const char* env_userdir = getenv("SUPERTUX2_USER_DIR"))
228 {
229 userdir = env_userdir;
230 }
231 else
232 {
233 userdir = PHYSFS_getPrefDir("SuperTux","supertux2");
234 }
235 //Kept for backwards-compatability only, hence the silence
236#ifdef __GNUC__
237 #pragma GCC diagnostic push
238 #pragma GCC diagnostic ignored "-Wdeprecated-declarations"
239#endif
240 std::string physfs_userdir = PHYSFS_getUserDir();
241#ifdef __GNUC__
242 #pragma GCC diagnostic pop
243#endif
244
245#ifdef _WIN32
246 std::string olduserdir = FileSystem::join(physfs_userdir, PACKAGE_NAME);
247#else
248 std::string olduserdir = FileSystem::join(physfs_userdir, "." PACKAGE_NAME);
249#endif
250 if (FileSystem::is_directory(olduserdir)) {
251 boost::filesystem::path olduserpath(olduserdir);
252 boost::filesystem::path userpath(userdir);
253
254 boost::filesystem::directory_iterator end_itr;
255
256 bool success = true;
257
258 // cycle through the directory
259 for (boost::filesystem::directory_iterator itr(olduserpath); itr != end_itr; ++itr) {
260 try
261 {
262 boost::filesystem::rename(itr->path().string().c_str(), userpath / itr->path().filename());
263 }
264 catch (const boost::filesystem::filesystem_error& err)
265 {
266 success = false;
267 log_warning << "Failed to move contents of config directory: " << err.what() << std::endl;
268 }
269 }
270 if (success) {
271 try
272 {
273 boost::filesystem::remove_all(olduserpath);
274 }
275 catch (const boost::filesystem::filesystem_error& err)
276 {
277 success = false;
278 log_warning << "Failed to remove old config directory: " << err.what();
279 }
280 }
281 if (success) {
282 log_info << "Moved old config dir " << olduserdir << " to " << userdir << std::endl;
283 }
284 }
285
286 if (!FileSystem::is_directory(userdir))
287 {
288 FileSystem::mkdir(userdir);
289 log_info << "Created SuperTux userdir: " << userdir << std::endl;
290 }
291
292 if (!PHYSFS_setWriteDir(userdir.c_str()))
293 {
294 std::ostringstream msg;
295 msg << "Failed to use userdir directory '"
296 << userdir << "': errorcode: " << PHYSFS_getLastErrorCode();
297 throw std::runtime_error(msg.str());
298 }
299
300 PHYSFS_mount(userdir.c_str(), nullptr, 0);
301 }
302
303 static void print_search_path()
304 {
305 const char* writedir = PHYSFS_getWriteDir();
306 log_info << "PhysfsWriteDir: " << (writedir ? writedir : "(null)") << std::endl;
307 log_info << "PhysfsSearchPath:" << std::endl;
308 char** searchpath = PHYSFS_getSearchPath();
309 for (char** i = searchpath; *i != nullptr; ++i)
310 {
311 log_info << " " << *i << std::endl;
312 }
313 PHYSFS_freeList(searchpath);
314 }
315
316 ~PhysfsSubsystem()
317 {
318 PHYSFS_deinit();
319 }
320};
321
322class SDLSubsystem final
323{
324public:
325 SDLSubsystem()
326 {
327 if (SDL_Init(SDL_INIT_TIMER | SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) < 0)
328 {
329 std::stringstream msg;
330 msg << "Couldn't initialize SDL: " << SDL_GetError();
331 throw std::runtime_error(msg.str());
332 }
333
334 if (TTF_Init() < 0)
335 {
336 std::stringstream msg;
337 msg << "Couldn't initialize SDL TTF: " << SDL_GetError();
338 throw std::runtime_error(msg.str());
339 }
340
341 // just to be sure
342 atexit(TTF_Quit);
343 atexit(SDL_Quit);
344 }
345
346 ~SDLSubsystem()
347 {
348 TTF_Quit();
349 SDL_Quit();
350 }
351};
352
353void
354Main::init_video()
355{
356 VideoSystem::current()->set_title("SuperTux " PACKAGE_VERSION);
357
358 const char* icon_fname = "images/engine/icons/supertux-256x256.png";
359
360 SDLSurfacePtr icon = SDLSurface::from_file(icon_fname);
361 VideoSystem::current()->set_icon(*icon);
362
363 SDL_ShowCursor(0);
364
365 log_info << (g_config->use_fullscreen?"fullscreen ":"window ")
366 << " Window: " << g_config->window_size
367 << " Fullscreen: " << g_config->fullscreen_size << "@" << g_config->fullscreen_refresh_rate
368 << " Area: " << g_config->aspect_size << std::endl;
369}
370
371void
372Main::resave(const std::string& input_filename, const std::string& output_filename)
373{
374 Editor::s_resaving_in_progress = true;
375 std::ifstream in(input_filename);
376 if (!in) {
377 log_fatal << input_filename << ": couldn't open file for reading" << std::endl;
378 } else {
379 log_info << "loading level: " << input_filename << std::endl;
380 auto level = LevelParser::from_stream(in, input_filename, StringUtil::has_suffix(input_filename, ".stwm"), true);
381 in.close();
382
383 std::ofstream out(output_filename);
384 if (!out) {
385 log_fatal << output_filename << ": couldn't open file for writing" << std::endl;
386 } else {
387 log_info << "saving level: " << output_filename << std::endl;
388 level->save(out);
389 }
390 }
391 Editor::s_resaving_in_progress = false;
392}
393
394void
395Main::launch_game(const CommandLineArguments& args)
396{
397 SDLSubsystem sdl_subsystem;
398 ConsoleBuffer console_buffer;
399
400 s_timelog.log("controller");
401 InputManager input_manager(g_config->keyboard_config, g_config->joystick_config);
402
403 s_timelog.log("commandline");
404
405 auto video = g_config->video;
406 if (args.resave && *args.resave) {
407 if (args.video) {
408 video = *args.video;
409 } else {
410 video = VideoSystem::VIDEO_NULL;
411 }
412 }
413 s_timelog.log("video");
414 std::unique_ptr<VideoSystem> video_system = VideoSystem::create(video);
415 init_video();
416
417 TTFSurfaceManager ttf_surface_manager;
418
419 s_timelog.log("audio");
420 SoundManager sound_manager;
421 sound_manager.enable_sound(g_config->sound_enabled);
422 sound_manager.enable_music(g_config->music_enabled);
423 sound_manager.set_sound_volume(g_config->sound_volume);
424 sound_manager.set_music_volume(g_config->music_volume);
425
426 s_timelog.log("scripting");
427 SquirrelVirtualMachine scripting(g_config->enable_script_debugger);
428
429 s_timelog.log("resources");
430 TileManager tile_manager;
431 SpriteManager sprite_manager;
432 Resources resources;
433
434 s_timelog.log("addons");
435 AddonManager addon_manager("addons", g_config->addons);
436
437 Console console(console_buffer);
438
439 s_timelog.log(nullptr);
440
441 const auto default_savegame = std::make_unique<Savegame>(std::string());
442
443 GameManager game_manager;
444 ScreenManager screen_manager(*video_system, input_manager);
445
446 if (!args.filenames.empty())
447 {
448 for(const auto& start_level : args.filenames)
449 {
450 // we have a normal path specified at commandline, not a physfs path.
451 // So we simply mount that path here...
452 std::string dir = FileSystem::dirname(start_level);
453 const std::string filename = FileSystem::basename(start_level);
454 const std::string fileProtocol = "file://";
455 const std::string::size_type position = dir.find(fileProtocol);
456 if (position != std::string::npos) {
457 dir = dir.replace(position, fileProtocol.length(), "");
458 }
459 log_debug << "Adding dir: " << dir << std::endl;
460 PHYSFS_mount(dir.c_str(), nullptr, true);
461
462 if (args.resave && *args.resave)
463 {
464 resave(start_level, start_level);
465 }
466 else if (args.editor)
467 {
468 if (PHYSFS_exists(start_level.c_str())) {
469 auto editor = std::make_unique<Editor>();
470 editor->set_level(start_level);
471 editor->setup();
472 editor->update(0, Controller());
473 screen_manager.push_screen(std::move(editor));
474 MenuManager::instance().clear_menu_stack();
475 sound_manager.stop_music(0.5);
476 } else {
477 log_warning << "Level " << start_level << " doesn't exist." << std::endl;
478 }
479 }
480 else if (StringUtil::has_suffix(start_level, ".stwm"))
481 {
482 screen_manager.push_screen(std::make_unique<worldmap::WorldMapScreen>(
483 std::make_unique<worldmap::WorldMap>(filename, *default_savegame)));
484 }
485 else
486 { // launch game
487 std::unique_ptr<GameSession> session (
488 new GameSession(filename, *default_savegame));
489
490 g_config->random_seed = session->get_demo_random_seed(g_config->start_demo);
491 gameRandom.seed(g_config->random_seed);
492 graphicsRandom.seed(0);
493
494 if (args.sector || args.spawnpoint)
495 {
496 std::string sectorname = args.sector.get_value_or("main");
497
498 const auto& spawnpoints = session->get_current_sector().get_objects_by_type<SpawnPointMarker>();
499 std::string default_spawnpoint = (spawnpoints.begin() != spawnpoints.end()) ?
500 "" : spawnpoints.begin()->get_name();
501 std::string spawnpointname = args.spawnpoint.get_value_or(default_spawnpoint);
502
503 session->respawn(sectorname, spawnpointname);
504 }
505
506 if (g_config->tux_spawn_pos)
507 {
508 session->get_current_sector().get_player().set_pos(*g_config->tux_spawn_pos);
509 }
510
511 if (!g_config->start_demo.empty())
512 session->play_demo(g_config->start_demo);
513
514 if (!g_config->record_demo.empty())
515 session->record_demo(g_config->record_demo);
516 screen_manager.push_screen(std::move(session));
517 }
518 }
519 }
520 else
521 {
522 if (args.editor)
523 {
524 screen_manager.push_screen(std::make_unique<Editor>());
525 }
526 else
527 {
528 screen_manager.push_screen(std::make_unique<TitleScreen>(*default_savegame));
529 }
530 }
531
532 screen_manager.run();
533}
534
535int
536Main::run(int argc, char** argv)
537{
538#ifdef WIN32
539 //SDL is used instead of PHYSFS because both create the same path in app data
540 //However, PHYSFS is not yet initizlized, and this should be run before anything is initialized
541 std::string prefpath = SDL_GetPrefPath("SuperTux", "supertux2");
542
543 std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
544
545 //All this conversion stuff is necessary to make this work for internationalized usernames
546 std::string outpath = prefpath + u8"/console.out";
547 std::wstring w_outpath = converter.from_bytes(outpath);
548 _wfreopen(w_outpath.c_str(), L"a", stdout);
549
550 std::string errpath = prefpath + u8"/console.err";
551 std::wstring w_errpath = converter.from_bytes(errpath);
552 _wfreopen(w_errpath.c_str(), L"a", stderr);
553#endif
554
555 // Create and install global locale
556 std::locale::global(boost::locale::generator().generate(""));
557 // Make boost.filesystem use it
558 boost::filesystem::path::imbue(std::locale());
559
560 int result = 0;
561
562 try
563 {
564 CommandLineArguments args;
565
566 try
567 {
568 args.parse_args(argc, argv);
569 g_log_level = args.get_log_level();
570 }
571 catch(const std::exception& err)
572 {
573 std::cout << "Error: " << err.what() << std::endl;
574 return EXIT_FAILURE;
575 }
576
577 PhysfsSubsystem physfs_subsystem(argv[0], args.datadir, args.userdir);
578 physfs_subsystem.print_search_path();
579
580 s_timelog.log("config");
581 ConfigSubsystem config_subsystem;
582 args.merge_into(*g_config);
583
584 s_timelog.log("tinygettext");
585 init_tinygettext();
586
587 switch (args.get_action())
588 {
589 case CommandLineArguments::PRINT_VERSION:
590 args.print_version();
591 return 0;
592
593 case CommandLineArguments::PRINT_HELP:
594 args.print_help(argv[0]);
595 return 0;
596
597 case CommandLineArguments::PRINT_DATADIR:
598 args.print_datadir();
599 return 0;
600
601 default:
602 launch_game(args);
603 break;
604 }
605 }
606 catch(const std::exception& e)
607 {
608 log_fatal << "Unexpected exception: " << e.what() << std::endl;
609 result = 1;
610 }
611 catch(...)
612 {
613 log_fatal << "Unexpected exception" << std::endl;
614 result = 1;
615 }
616
617 g_dictionary_manager.reset();
618
619 return result;
620}
621
622/* EOF */
623