Make early protocol auth mechanism generic, and add SRP
[oweals/minetest.git] / src / client / clientlauncher.cpp
1 /*
2 Minetest
3 Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU Lesser General Public License as published by
7 the Free Software Foundation; either version 2.1 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU Lesser General Public License for more details.
14
15 You should have received a copy of the GNU Lesser General Public License along
16 with this program; if not, write to the Free Software Foundation, Inc.,
17 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20 #include "mainmenumanager.h"
21 #include "debug.h"
22 #include "clouds.h"
23 #include "server.h"
24 #include "filesys.h"
25 #include "guiMainMenu.h"
26 #include "game.h"
27 #include "chat.h"
28 #include "gettext.h"
29 #include "profiler.h"
30 #include "log.h"
31 #include "serverlist.h"
32 #include "guiEngine.h"
33 #include "player.h"
34 #include "fontengine.h"
35 #include "clientlauncher.h"
36
37 /* mainmenumanager.h
38  */
39 gui::IGUIEnvironment *guienv = NULL;
40 gui::IGUIStaticText *guiroot = NULL;
41 MainMenuManager g_menumgr;
42
43 bool noMenuActive()
44 {
45         return g_menumgr.menuCount() == 0;
46 }
47
48 // Passed to menus to allow disconnecting and exiting
49 MainGameCallback *g_gamecallback = NULL;
50
51
52 // Instance of the time getter
53 static TimeGetter *g_timegetter = NULL;
54
55 u32 getTimeMs()
56 {
57         if (g_timegetter == NULL)
58                 return 0;
59         return g_timegetter->getTime(PRECISION_MILLI);
60 }
61
62 u32 getTime(TimePrecision prec) {
63         if (g_timegetter == NULL)
64                 return 0;
65         return g_timegetter->getTime(prec);
66 }
67
68 ClientLauncher::~ClientLauncher()
69 {
70         if (receiver)
71                 delete receiver;
72
73         if (input)
74                 delete input;
75
76         if (g_fontengine)
77                 delete g_fontengine;
78
79         if (device)
80                 device->drop();
81 }
82
83
84 bool ClientLauncher::run(GameParams &game_params, const Settings &cmd_args)
85 {
86         init_args(game_params, cmd_args);
87
88         // List video modes if requested
89         if (list_video_modes)
90                 return print_video_modes();
91
92         if (!init_engine(game_params.log_level)) {
93                 errorstream << "Could not initialize game engine." << std::endl;
94                 return false;
95         }
96
97         // Create time getter
98         g_timegetter = new IrrlichtTimeGetter(device);
99
100         // Speed tests (done after irrlicht is loaded to get timer)
101         if (cmd_args.getFlag("speedtests")) {
102                 dstream << "Running speed tests" << std::endl;
103                 speed_tests();
104                 return true;
105         }
106
107         video::IVideoDriver *video_driver = device->getVideoDriver();
108         if (video_driver == NULL) {
109                 errorstream << "Could not initialize video driver." << std::endl;
110                 return false;
111         }
112
113         porting::setXorgClassHint(video_driver->getExposedVideoData(), PROJECT_NAME_C);
114
115         /*
116                 This changes the minimum allowed number of vertices in a VBO.
117                 Default is 500.
118         */
119         //driver->setMinHardwareBufferVertexCount(50);
120
121         // Create game callback for menus
122         g_gamecallback = new MainGameCallback(device);
123
124         device->setResizable(true);
125
126         if (random_input)
127                 input = new RandomInputHandler();
128         else
129                 input = new RealInputHandler(device, receiver);
130
131         smgr = device->getSceneManager();
132         smgr->getParameters()->setAttribute(scene::ALLOW_ZWRITE_ON_TRANSPARENT, true);
133
134         guienv = device->getGUIEnvironment();
135         skin = guienv->getSkin();
136         skin->setColor(gui::EGDC_BUTTON_TEXT, video::SColor(255, 255, 255, 255));
137         skin->setColor(gui::EGDC_3D_LIGHT, video::SColor(0, 0, 0, 0));
138         skin->setColor(gui::EGDC_3D_HIGH_LIGHT, video::SColor(255, 30, 30, 30));
139         skin->setColor(gui::EGDC_3D_SHADOW, video::SColor(255, 0, 0, 0));
140         skin->setColor(gui::EGDC_HIGH_LIGHT, video::SColor(255, 70, 120, 50));
141         skin->setColor(gui::EGDC_HIGH_LIGHT_TEXT, video::SColor(255, 255, 255, 255));
142
143         g_fontengine = new FontEngine(g_settings, guienv);
144         FATAL_ERROR_IF(g_fontengine == NULL, "Font engine creation failed.");
145
146 #if (IRRLICHT_VERSION_MAJOR >= 1 && IRRLICHT_VERSION_MINOR >= 8) || IRRLICHT_VERSION_MAJOR >= 2
147         // Irrlicht 1.8 input colours
148         skin->setColor(gui::EGDC_EDITABLE, video::SColor(255, 128, 128, 128));
149         skin->setColor(gui::EGDC_FOCUSED_EDITABLE, video::SColor(255, 96, 134, 49));
150 #endif
151
152         // Create the menu clouds
153         if (!g_menucloudsmgr)
154                 g_menucloudsmgr = smgr->createNewSceneManager();
155         if (!g_menuclouds)
156                 g_menuclouds = new Clouds(g_menucloudsmgr->getRootSceneNode(),
157                                 g_menucloudsmgr, -1, rand(), 100);
158         g_menuclouds->update(v2f(0, 0), video::SColor(255, 200, 200, 255));
159         scene::ICameraSceneNode* camera;
160         camera = g_menucloudsmgr->addCameraSceneNode(0,
161                                 v3f(0, 0, 0), v3f(0, 60, 100));
162         camera->setFarValue(10000);
163
164         /*
165                 GUI stuff
166         */
167
168         ChatBackend chat_backend;
169
170         // If an error occurs, this is set to something by menu().
171         // It is then displayed before  the menu shows on the next call to menu()
172         std::string error_message;
173
174         bool first_loop = true;
175
176         /*
177                 Menu-game loop
178         */
179         bool retval = true;
180         bool *kill = porting::signal_handler_killstatus();
181
182         while (device->run() && !*kill && !g_gamecallback->shutdown_requested)
183         {
184                 // Set the window caption
185                 const wchar_t *text = wgettext("Main Menu");
186                 device->setWindowCaption((narrow_to_wide(PROJECT_NAME_C) + L" [" + text + L"]").c_str());
187                 delete[] text;
188
189                 try {   // This is used for catching disconnects
190
191                         guienv->clear();
192
193                         /*
194                                 We need some kind of a root node to be able to add
195                                 custom gui elements directly on the screen.
196                                 Otherwise they won't be automatically drawn.
197                         */
198                         guiroot = guienv->addStaticText(L"", core::rect<s32>(0, 0, 10000, 10000));
199
200                         bool game_has_run = launch_game(error_message, game_params, cmd_args);
201
202                         // If skip_main_menu, we only want to startup once
203                         if (skip_main_menu && !first_loop)
204                                 break;
205
206                         first_loop = false;
207
208                         if (!game_has_run) {
209                                 if (skip_main_menu)
210                                         break;
211                                 else
212                                         continue;
213                         }
214
215                         // Break out of menu-game loop to shut down cleanly
216                         if (!device->run() || *kill) {
217                                 if (g_settings_path != "")
218                                         g_settings->updateConfigFile(g_settings_path.c_str());
219                                 break;
220                         }
221
222                         if (current_playername.length() > PLAYERNAME_SIZE-1) {
223                                 error_message = gettext("Player name too long.");
224                                 playername = current_playername.substr(0, PLAYERNAME_SIZE-1);
225                                 g_settings->set("name", playername);
226                                 continue;
227                         }
228
229                         device->getVideoDriver()->setTextureCreationFlag(
230                                         video::ETCF_CREATE_MIP_MAPS, g_settings->getBool("mip_map"));
231
232 #ifdef HAVE_TOUCHSCREENGUI
233                         receiver->m_touchscreengui = new TouchScreenGUI(device, receiver);
234                         g_touchscreengui = receiver->m_touchscreengui;
235 #endif
236                         the_game(
237                                 kill,
238                                 random_input,
239                                 input,
240                                 device,
241                                 worldspec.path,
242                                 current_playername,
243                                 current_password,
244                                 current_address,
245                                 current_port,
246                                 error_message,
247                                 chat_backend,
248                                 gamespec,
249                                 simple_singleplayer_mode
250                         );
251                         smgr->clear();
252
253 #ifdef HAVE_TOUCHSCREENGUI
254                         delete g_touchscreengui;
255                         g_touchscreengui = NULL;
256                         receiver->m_touchscreengui = NULL;
257 #endif
258
259                 } //try
260                 catch (con::PeerNotFoundException &e) {
261                         error_message = gettext("Connection error (timed out?)");
262                         errorstream << error_message << std::endl;
263                 }
264
265 #ifdef NDEBUG
266                 catch (std::exception &e) {
267                         std::string error_message = "Some exception: \"";
268                         error_message += e.what();
269                         error_message += "\"";
270                         errorstream << error_message << std::endl;
271                 }
272 #endif
273
274                 // If no main menu, show error and exit
275                 if (skip_main_menu) {
276                         if (!error_message.empty()) {
277                                 verbosestream << "error_message = "
278                                               << error_message << std::endl;
279                                 retval = false;
280                         }
281                         break;
282                 }
283         } // Menu-game loop
284
285         g_menuclouds->drop();
286         g_menucloudsmgr->drop();
287
288         return retval;
289 }
290
291 void ClientLauncher::init_args(GameParams &game_params, const Settings &cmd_args)
292 {
293
294         skip_main_menu = cmd_args.getFlag("go");
295
296         // FIXME: This is confusing (but correct)
297
298         /* If world_path is set then override it unless skipping the main menu using
299          * the --go command line param. Else, give preference to the address
300          * supplied on the command line
301          */
302         address = g_settings->get("address");
303         if (game_params.world_path != "" && !skip_main_menu)
304                 address = "";
305         else if (cmd_args.exists("address"))
306                 address = cmd_args.get("address");
307
308         playername = g_settings->get("name");
309         if (cmd_args.exists("name"))
310                 playername = cmd_args.get("name");
311
312         list_video_modes = cmd_args.getFlag("videomodes");
313
314         use_freetype = g_settings->getBool("freetype");
315
316         random_input = g_settings->getBool("random_input")
317                         || cmd_args.getFlag("random-input");
318 }
319
320 bool ClientLauncher::init_engine(int log_level)
321 {
322         receiver = new MyEventReceiver();
323         create_engine_device(log_level);
324         return device != NULL;
325 }
326
327 bool ClientLauncher::launch_game(std::string &error_message,
328                 GameParams &game_params, const Settings &cmd_args)
329 {
330         // Initialize menu data
331         MainMenuData menudata;
332         menudata.address      = address;
333         menudata.name         = playername;
334         menudata.port         = itos(game_params.socket_port);
335         menudata.errormessage = error_message;
336
337         error_message.clear();
338
339         if (cmd_args.exists("password"))
340                 menudata.password = cmd_args.get("password");
341
342         menudata.enable_public = g_settings->getBool("server_announce");
343
344         // If a world was commanded, append and select it
345         if (game_params.world_path != "") {
346                 worldspec.gameid = getWorldGameId(game_params.world_path, true);
347                 worldspec.name = _("[--world parameter]");
348
349                 if (worldspec.gameid == "") {   // Create new
350                         worldspec.gameid = g_settings->get("default_game");
351                         worldspec.name += " [new]";
352                 }
353                 worldspec.path = game_params.world_path;
354         }
355
356         /* Show the GUI menu
357          */
358         if (!skip_main_menu) {
359                 main_menu(&menudata);
360
361                 // Skip further loading if there was an exit signal.
362                 if (*porting::signal_handler_killstatus())
363                         return false;
364
365                 address = menudata.address;
366                 int newport = stoi(menudata.port);
367                 if (newport != 0)
368                         game_params.socket_port = newport;
369
370                 simple_singleplayer_mode = menudata.simple_singleplayer_mode;
371
372                 std::vector<WorldSpec> worldspecs = getAvailableWorlds();
373
374                 if (menudata.selected_world >= 0
375                                 && menudata.selected_world < (int)worldspecs.size()) {
376                         g_settings->set("selected_world_path",
377                                         worldspecs[menudata.selected_world].path);
378                         worldspec = worldspecs[menudata.selected_world];
379                 }
380         }
381
382         if (!menudata.errormessage.empty()) {
383                 /* The calling function will pass this back into this function upon the
384                  * next iteration (if any) causing it to be displayed by the GUI
385                  */
386                 error_message = menudata.errormessage;
387                 return false;
388         }
389
390         if (menudata.name == "")
391                 menudata.name = std::string("Guest") + itos(myrand_range(1000, 9999));
392         else
393                 playername = menudata.name;
394
395         password = menudata.password;
396
397         g_settings->set("name", playername);
398
399         current_playername = playername;
400         current_password   = password;
401         current_address    = address;
402         current_port       = game_params.socket_port;
403
404         // If using simple singleplayer mode, override
405         if (simple_singleplayer_mode) {
406                 assert(skip_main_menu == false);
407                 current_playername = "singleplayer";
408                 current_password = "";
409                 current_address = "";
410                 current_port = myrand_range(49152, 65535);
411         } else if (address != "") {
412                 ServerListSpec server;
413                 server["name"] = menudata.servername;
414                 server["address"] = menudata.address;
415                 server["port"] = menudata.port;
416                 server["description"] = menudata.serverdescription;
417                 ServerList::insert(server);
418         }
419
420         infostream << "Selected world: " << worldspec.name
421                    << " [" << worldspec.path << "]" << std::endl;
422
423         if (current_address == "") { // If local game
424                 if (worldspec.path == "") {
425                         error_message = gettext("No world selected and no address "
426                                         "provided. Nothing to do.");
427                         errorstream << error_message << std::endl;
428                         return false;
429                 }
430
431                 if (!fs::PathExists(worldspec.path)) {
432                         error_message = gettext("Provided world path doesn't exist: ")
433                                         + worldspec.path;
434                         errorstream << error_message << std::endl;
435                         return false;
436                 }
437
438                 // Load gamespec for required game
439                 gamespec = findWorldSubgame(worldspec.path);
440                 if (!gamespec.isValid() && !game_params.game_spec.isValid()) {
441                         error_message = gettext("Could not find or load game \"")
442                                         + worldspec.gameid + "\"";
443                         errorstream << error_message << std::endl;
444                         return false;
445                 }
446
447                 if (porting::signal_handler_killstatus())
448                         return true;
449
450                 if (game_params.game_spec.isValid() &&
451                                 game_params.game_spec.id != worldspec.gameid) {
452                         errorstream << "WARNING: Overriding gamespec from \""
453                                     << worldspec.gameid << "\" to \""
454                                     << game_params.game_spec.id << "\"" << std::endl;
455                         gamespec = game_params.game_spec;
456                 }
457
458                 if (!gamespec.isValid()) {
459                         error_message = gettext("Invalid gamespec.");
460                         error_message += " (world.gameid=" + worldspec.gameid + ")";
461                         errorstream << error_message << std::endl;
462                         return false;
463                 }
464         }
465
466         return true;
467 }
468
469 void ClientLauncher::main_menu(MainMenuData *menudata)
470 {
471         bool *kill = porting::signal_handler_killstatus();
472         video::IVideoDriver *driver = device->getVideoDriver();
473
474         infostream << "Waiting for other menus" << std::endl;
475         while (device->run() && *kill == false) {
476                 if (noMenuActive())
477                         break;
478                 driver->beginScene(true, true, video::SColor(255, 128, 128, 128));
479                 guienv->drawAll();
480                 driver->endScene();
481                 // On some computers framerate doesn't seem to be automatically limited
482                 sleep_ms(25);
483         }
484         infostream << "Waited for other menus" << std::endl;
485
486         // Cursor can be non-visible when coming from the game
487 #ifndef ANDROID
488         device->getCursorControl()->setVisible(true);
489 #endif
490
491         /* show main menu */
492         GUIEngine mymenu(device, guiroot, &g_menumgr, smgr, menudata, *kill);
493
494         smgr->clear();  /* leave scene manager in a clean state */
495 }
496
497 bool ClientLauncher::create_engine_device(int log_level)
498 {
499         static const irr::ELOG_LEVEL irr_log_level[5] = {
500                 ELL_NONE,
501                 ELL_ERROR,
502                 ELL_WARNING,
503                 ELL_INFORMATION,
504 #if (IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR < 8)
505                 ELL_INFORMATION
506 #else
507                 ELL_DEBUG
508 #endif
509         };
510
511         // Resolution selection
512         bool fullscreen = g_settings->getBool("fullscreen");
513         u16 screenW = g_settings->getU16("screenW");
514         u16 screenH = g_settings->getU16("screenH");
515
516         // bpp, fsaa, vsync
517         bool vsync = g_settings->getBool("vsync");
518         u16 bits = g_settings->getU16("fullscreen_bpp");
519         u16 fsaa = g_settings->getU16("fsaa");
520
521         // Determine driver
522         video::E_DRIVER_TYPE driverType = video::EDT_OPENGL;
523         std::string driverstring = g_settings->get("video_driver");
524         std::vector<video::E_DRIVER_TYPE> drivers
525                 = porting::getSupportedVideoDrivers();
526         u32 i;
527         for (i = 0; i != drivers.size(); i++) {
528                 if (!strcasecmp(driverstring.c_str(),
529                         porting::getVideoDriverName(drivers[i]))) {
530                         driverType = drivers[i];
531                         break;
532                 }
533         }
534         if (i == drivers.size()) {
535                 errorstream << "Invalid video_driver specified; "
536                         "defaulting to opengl" << std::endl;
537         }
538
539         SIrrlichtCreationParameters params = SIrrlichtCreationParameters();
540         params.DriverType    = driverType;
541         params.WindowSize    = core::dimension2d<u32>(screenW, screenH);
542         params.Bits          = bits;
543         params.AntiAlias     = fsaa;
544         params.Fullscreen    = fullscreen;
545         params.Stencilbuffer = false;
546         params.Vsync         = vsync;
547         params.EventReceiver = receiver;
548         params.HighPrecisionFPU = g_settings->getBool("high_precision_fpu");
549 #ifdef __ANDROID__
550         params.PrivateData = porting::app_global;
551         params.OGLES2ShaderPath = std::string(porting::path_user + DIR_DELIM +
552                         "media" + DIR_DELIM + "Shaders" + DIR_DELIM).c_str();
553 #endif
554
555         device = createDeviceEx(params);
556
557         if (device) {
558                 // Map our log level to irrlicht engine one.
559                 ILogger* irr_logger = device->getLogger();
560                 irr_logger->setLogLevel(irr_log_level[log_level]);
561
562                 porting::initIrrlicht(device);
563         }
564
565         return device != NULL;
566 }
567
568 void ClientLauncher::speed_tests()
569 {
570         // volatile to avoid some potential compiler optimisations
571         volatile static s16 temp16;
572         volatile static f32 tempf;
573         static v3f tempv3f1;
574         static v3f tempv3f2;
575         static std::string tempstring;
576         static std::string tempstring2;
577
578         tempv3f1 = v3f();
579         tempv3f2 = v3f();
580         tempstring = std::string();
581         tempstring2 = std::string();
582
583         {
584                 infostream << "The following test should take around 20ms." << std::endl;
585                 TimeTaker timer("Testing std::string speed");
586                 const u32 jj = 10000;
587                 for (u32 j = 0; j < jj; j++) {
588                         tempstring = "";
589                         tempstring2 = "";
590                         const u32 ii = 10;
591                         for (u32 i = 0; i < ii; i++) {
592                                 tempstring2 += "asd";
593                         }
594                         for (u32 i = 0; i < ii+1; i++) {
595                                 tempstring += "asd";
596                                 if (tempstring == tempstring2)
597                                         break;
598                         }
599                 }
600         }
601
602         infostream << "All of the following tests should take around 100ms each."
603                    << std::endl;
604
605         {
606                 TimeTaker timer("Testing floating-point conversion speed");
607                 tempf = 0.001;
608                 for (u32 i = 0; i < 4000000; i++) {
609                         temp16 += tempf;
610                         tempf += 0.001;
611                 }
612         }
613
614         {
615                 TimeTaker timer("Testing floating-point vector speed");
616
617                 tempv3f1 = v3f(1, 2, 3);
618                 tempv3f2 = v3f(4, 5, 6);
619                 for (u32 i = 0; i < 10000000; i++) {
620                         tempf += tempv3f1.dotProduct(tempv3f2);
621                         tempv3f2 += v3f(7, 8, 9);
622                 }
623         }
624
625         {
626                 TimeTaker timer("Testing std::map speed");
627
628                 std::map<v2s16, f32> map1;
629                 tempf = -324;
630                 const s16 ii = 300;
631                 for (s16 y = 0; y < ii; y++) {
632                         for (s16 x = 0; x < ii; x++) {
633                                 map1[v2s16(x, y)] =  tempf;
634                                 tempf += 1;
635                         }
636                 }
637                 for (s16 y = ii - 1; y >= 0; y--) {
638                         for (s16 x = 0; x < ii; x++) {
639                                 tempf = map1[v2s16(x, y)];
640                         }
641                 }
642         }
643
644         {
645                 infostream << "Around 5000/ms should do well here." << std::endl;
646                 TimeTaker timer("Testing mutex speed");
647
648                 JMutex m;
649                 u32 n = 0;
650                 u32 i = 0;
651                 do {
652                         n += 10000;
653                         for (; i < n; i++) {
654                                 m.Lock();
655                                 m.Unlock();
656                         }
657                 }
658                 // Do at least 10ms
659                 while(timer.getTimerTime() < 10);
660
661                 u32 dtime = timer.stop();
662                 u32 per_ms = n / dtime;
663                 infostream << "Done. " << dtime << "ms, " << per_ms << "/ms" << std::endl;
664         }
665 }
666
667 bool ClientLauncher::print_video_modes()
668 {
669         IrrlichtDevice *nulldevice;
670
671         bool vsync = g_settings->getBool("vsync");
672         u16 fsaa = g_settings->getU16("fsaa");
673         MyEventReceiver* receiver = new MyEventReceiver();
674
675         SIrrlichtCreationParameters params = SIrrlichtCreationParameters();
676         params.DriverType    = video::EDT_NULL;
677         params.WindowSize    = core::dimension2d<u32>(640, 480);
678         params.Bits          = 24;
679         params.AntiAlias     = fsaa;
680         params.Fullscreen    = false;
681         params.Stencilbuffer = false;
682         params.Vsync         = vsync;
683         params.EventReceiver = receiver;
684         params.HighPrecisionFPU = g_settings->getBool("high_precision_fpu");
685
686         nulldevice = createDeviceEx(params);
687
688         if (nulldevice == NULL) {
689                 delete receiver;
690                 return false;
691         }
692
693         dstream << _("Available video modes (WxHxD):") << std::endl;
694
695         video::IVideoModeList *videomode_list = nulldevice->getVideoModeList();
696
697         if (videomode_list != NULL) {
698                 s32 videomode_count = videomode_list->getVideoModeCount();
699                 core::dimension2d<u32> videomode_res;
700                 s32 videomode_depth;
701                 for (s32 i = 0; i < videomode_count; ++i) {
702                         videomode_res = videomode_list->getVideoModeResolution(i);
703                         videomode_depth = videomode_list->getVideoModeDepth(i);
704                         dstream << videomode_res.Width << "x" << videomode_res.Height
705                                 << "x" << videomode_depth << std::endl;
706                 }
707
708                 dstream << _("Active video mode (WxHxD):") << std::endl;
709                 videomode_res = videomode_list->getDesktopResolution();
710                 videomode_depth = videomode_list->getDesktopDepth();
711                 dstream << videomode_res.Width << "x" << videomode_res.Height
712                         << "x" << videomode_depth << std::endl;
713
714         }
715
716         nulldevice->drop();
717         delete receiver;
718
719         return videomode_list != NULL;
720 }