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/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 | |
82 | static Timelog s_timelog; |
83 | |
84 | class ConfigSubsystem final |
85 | { |
86 | public: |
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 | |
122 | Main::Main() |
123 | { |
124 | } |
125 | |
126 | void |
127 | Main::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 | |
152 | class PhysfsSubsystem final |
153 | { |
154 | private: |
155 | boost::optional<std::string> m_forced_datadir; |
156 | boost::optional<std::string> m_forced_userdir; |
157 | |
158 | public: |
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 | |
322 | class SDLSubsystem final |
323 | { |
324 | public: |
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 | |
353 | void |
354 | Main::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 | |
371 | void |
372 | Main::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 | |
394 | void |
395 | Main::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 | |
535 | int |
536 | Main::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 | |