1 /** @file clientapp.cpp The client application.
2 *
3 * @authors Copyright © 2013-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4 * @authors Copyright © 2013-2015 Daniel Swanson <danij@dengine.net>
5 *
6 * @par License
7 * GPL: http://www.gnu.org/licenses/gpl.html
8 *
9 * <small>This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by the
11 * Free Software Foundation; either version 2 of the License, or (at your
12 * option) any later version. This program is distributed in the hope that it
13 * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
14 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
15 * Public License for more details. You should have received a copy of the GNU
16 * General Public License along with this program; if not, see:
17 * http://www.gnu.org/licenses</small>
18 */
19
20 #include "de_platform.h"
21
22 #include <cstdlib>
23 #include <QAction>
24 #include <QDebug>
25 #include <QDesktopServices>
26 #include <QFontDatabase>
27 #include <QMenuBar>
28 #include <QNetworkProxyFactory>
29 #include <QSplashScreen>
30
31 #include <de/c_wrapper.h>
32 #include <de/ArrayValue>
33 #include <de/ByteArrayFile>
34 #include <de/CallbackAction>
35 #include <de/CommandLine>
36 #include <de/Config>
37 #include <de/DictionaryValue>
38 #include <de/DisplayMode>
39 #include <de/Error>
40 #include <de/FileSystem>
41 #include <de/Garbage>
42 #include <de/Info>
43 #include <de/Log>
44 #include <de/LogSink>
45 #include <de/NativeFont>
46 #include <de/ScriptSystem>
47 #include <de/TextValue>
48 #include <de/VRConfig>
49
50 #include <doomsday/console/exec.h>
51 #include <doomsday/AbstractSession>
52 #include <doomsday/GameStateFolder>
53
54 #include "audio/audiosystem.h"
55 #include "busyrunner.h"
56 #include "clientapp.h"
57 #include "clientplayer.h"
58 #include "con_config.h"
59 #include "dd_def.h"
60 #include "dd_loop.h"
61 #include "dd_main.h"
62 #include "def_main.h"
63 #include "gl/gl_defer.h"
64 #include "gl/gl_main.h"
65 #include "gl/gl_texmanager.h"
66 #include "gl/svg.h"
67 #include "network/net_demo.h"
68 #include "network/net_main.h"
69 #include "network/serverlink.h"
70 #include "render/r_draw.h"
71 #include "render/rend_main.h"
72 #include "render/rend_particle.h"
73 #include "render/rendersystem.h"
74 #include "sys_system.h"
75 #include "ui/alertmask.h"
76 #include "ui/b_main.h"
77 #include "ui/clientwindow.h"
78 #include "ui/clientwindowsystem.h"
79 #include "ui/dialogs/alertdialog.h"
80 #include "ui/dialogs/packagecompatibilitydialog.h"
81 #include "ui/inputsystem.h"
82 #include "ui/nativemenu.h"
83 #include "ui/progress.h"
84 #include "ui/styledlogsinkformatter.h"
85 #include "ui/sys_input.h"
86 #include "ui/viewcompositor.h"
87 #include "ui/widgets/taskbarwidget.h"
88 #include "updater.h"
89 #include "updater/updatedownloaddialog.h"
90 #include "world/contact.h"
91 #include "world/map.h"
92 #include "world/p_players.h"
93
94 #if WIN32
95 # include "dd_winit.h"
96 #elif UNIX
97 # include "dd_uinit.h"
98 #endif
99
100 #include <de/timer.h>
101
102 #include "ui/splash.xpm"
103
104 using namespace de;
105
106 static ClientApp *clientAppSingleton = 0;
107
handleLegacyCoreTerminate(char const * msg)108 static void handleLegacyCoreTerminate(char const *msg)
109 {
110 App_Error("Application terminated due to exception:\n%s\n", msg);
111 }
112
continueInitWithEventLoopRunning()113 static void continueInitWithEventLoopRunning()
114 {
115 if (!ClientWindowSystem::mainExists()) return;
116
117 #if !defined (DENG_MOBILE)
118 // Show the main window. This causes initialization to finish (in busy mode)
119 // as the canvas is visible and ready for initialization.
120 ClientWindowSystem::main().show();
121 #endif
122
123 #if defined (DENG_HAVE_UPDATER)
124 ClientApp::updater().setupUI();
125 #endif
126 }
127
Function_App_ConsolePlayer(Context &,const Function::ArgumentValues &)128 static Value *Function_App_ConsolePlayer(Context &, const Function::ArgumentValues &)
129 {
130 return new RecordValue(DoomsdayApp::players().at(consolePlayer).objectNamespace());
131 }
132
Function_App_GamePlugin(Context &,Function::ArgumentValues const &)133 static Value *Function_App_GamePlugin(Context &, Function::ArgumentValues const &)
134 {
135 if (App_CurrentGame().isNull())
136 {
137 // The null game has no plugin.
138 return 0;
139 }
140 String name = DoomsdayApp::plugins().fileForPlugin(App_CurrentGame().pluginId())
141 .name().fileNameWithoutExtension();
142 if (name.startsWith("lib")) name.remove(0, 3);
143 return new TextValue(name);
144 }
145
Function_App_GetInteger(Context &,const Function::ArgumentValues & args)146 static Value *Function_App_GetInteger(Context &, const Function::ArgumentValues &args)
147 {
148 const int valueId = args.at(0)->asInt();
149 if (valueId >= DD_FIRST_VALUE && valueId < DD_LAST_VALUE)
150 {
151 return new NumberValue(DD_GetInteger(valueId));
152 }
153 throw Error("Function_App_GetInteger", "Invalid value ID");
154 }
155
Function_App_SetInteger(Context &,const Function::ArgumentValues & args)156 static Value *Function_App_SetInteger(Context &, const Function::ArgumentValues &args)
157 {
158 const int valueId = args.at(0)->asInt();
159 if (valueId >= DD_FIRST_VALUE && valueId < DD_LAST_VALUE)
160 {
161 DD_SetInteger(valueId, args.at(1)->asInt());
162 }
163 else
164 {
165 throw Error("Function_App_SetInteger", "Invalid value ID");
166 }
167 return nullptr;
168 }
169
Function_App_Quit(Context &,Function::ArgumentValues const &)170 static Value *Function_App_Quit(Context &, Function::ArgumentValues const &)
171 {
172 Sys_Quit();
173 return nullptr;
174 }
175
DENG2_PIMPL(ClientApp)176 DENG2_PIMPL(ClientApp)
177 , DENG2_OBSERVES(Plugins, PublishAPI)
178 , DENG2_OBSERVES(Plugins, Notification)
179 , DENG2_OBSERVES(Games, Progress)
180 , DENG2_OBSERVES(DoomsdayApp, GameChange)
181 , DENG2_OBSERVES(DoomsdayApp, GameUnload)
182 , DENG2_OBSERVES(DoomsdayApp, ConsoleRegistration)
183 , DENG2_OBSERVES(DoomsdayApp, PeriodicAutosave)
184 {
185 Binder binder;
186 #if defined (DENG_HAVE_UPDATER)
187 QScopedPointer<Updater> updater;
188 #endif
189 #if defined (DENG_HAVE_BUSYRUNNER)
190 BusyRunner busyRunner;
191 #endif
192 ConfigProfiles audioSettings;
193 ConfigProfiles networkSettings;
194 ConfigProfiles logSettings;
195 ConfigProfiles uiSettings;
196 std::unique_ptr<NativeMenu> nativeAppMenu;
197 InputSystem *inputSys = nullptr;
198 AudioSystem *audioSys = nullptr;
199 RenderSystem *rendSys = nullptr;
200 ClientResources *resources = nullptr;
201 ClientWindowSystem *winSys = nullptr;
202 InFineSystem infineSys; // instantiated at construction time
203 ServerLink *svLink = nullptr;
204 ClientServerWorld *world = nullptr;
205
206 /**
207 * Log entry sink that passes warning messages to the main window's alert
208 * notification dialog.
209 */
210 struct LogWarningAlarm : public LogSink
211 {
212 AlertMask alertMask;
213 StyledLogSinkFormatter formatter;
214
215 LogWarningAlarm()
216 : LogSink(formatter)
217 , formatter(LogEntry::Styled | LogEntry::OmitLevel | LogEntry::Simple)
218 {
219 //formatter.setOmitSectionIfNonDev(false); // always show section
220 setMode(OnlyWarningEntries);
221 }
222
223 LogSink &operator << (LogEntry const &entry)
224 {
225 if (alertMask.shouldRaiseAlert(entry.metadata()))
226 {
227 // Don't raise alerts if the console history is open; the
228 // warning/error will be shown there.
229 if (ClientWindow::mainExists() &&
230 ClientWindow::main().taskBar().isOpen() &&
231 ClientWindow::main().taskBar().console().isLogOpen())
232 {
233 return *this;
234 }
235
236 // We don't want to raise alerts about problems in id/Raven WADs,
237 // since these just have to be accepted by the user.
238 if ((entry.metadata() & LogEntry::Map) &&
239 ClientApp::world().hasMap())
240 {
241 world::Map const &map = ClientApp::world().map();
242 if (map.hasManifest() && !map.manifest().sourceFile()->hasCustom())
243 {
244 return *this;
245 }
246 }
247
248 foreach (String msg, formatter.logEntryToTextLines(entry))
249 {
250 ClientApp::alert(msg, entry.level());
251 }
252 }
253 return *this;
254 }
255
256 LogSink &operator << (String const &plainText)
257 {
258 ClientApp::alert(plainText);
259 return *this;
260 }
261
262 void flush() {} // not buffered
263 };
264
265 LogWarningAlarm logAlarm;
266
267 Impl(Public *i) : Base(i)
268 {
269 clientAppSingleton = thisPublic;
270
271 LogBuffer::get().addSink(logAlarm);
272 DoomsdayApp::plugins().audienceForPublishAPI() += this;
273 DoomsdayApp::plugins().audienceForNotification() += this;
274 self().audienceForGameChange() += this;
275 self().audienceForGameUnload() += this;
276 self().audienceForConsoleRegistration() += this;
277 self().games().audienceForProgress() += this;
278 self().audienceForPeriodicAutosave() += this;
279 }
280
281 ~Impl()
282 {
283 try
284 {
285 ClientWindow::glActiveMain(); // for GL deinit
286
287 LogBuffer::get().removeSink(logAlarm);
288
289 self().players().forAll([] (Player &p)
290 {
291 p.as<ClientPlayer>().viewCompositor().glDeinit();
292 return LoopContinue;
293 });
294 self().glDeinit();
295
296 Sys_Shutdown();
297 DD_Shutdown();
298 }
299 catch (Error const &er)
300 {
301 qWarning() << "Exception during ~ClientApp:" << er.asText();
302 DENG2_ASSERT("Unclean shutdown: exception in ~ClientApp"!=0);
303 }
304
305 #if defined (DENG_HAVE_UPDATER)
306 updater.reset();
307 #endif
308 delete inputSys;
309 delete resources;
310 delete winSys;
311 delete audioSys;
312 delete rendSys;
313 delete world;
314 delete svLink;
315 clientAppSingleton = 0;
316 }
317
318 void publishAPIToPlugin(::Library *plugin)
319 {
320 DD_PublishAPIs(plugin);
321 }
322
323 void pluginSentNotification(int notification, void *data)
324 {
325 LOG_AS("ClientApp::pluginSentNotification");
326
327 switch (notification)
328 {
329 case DD_NOTIFY_GAME_SAVED:
330 // If an update has been downloaded and is ready to go, we should
331 // re-show the dialog now that the user has saved the game as prompted.
332 LOG_DEBUG("Game saved");
333 #if defined (DENG_HAVE_UPDATER)
334 UpdateDownloadDialog::showCompletedDownload();
335 #endif
336 break;
337
338 case DD_NOTIFY_PSPRITE_STATE_CHANGED:
339 if (data)
340 {
341 auto const *args = (ddnotify_psprite_state_changed_t *) data;
342 self().player(args->player).weaponStateChanged(args->state);
343 }
344 break;
345
346 case DD_NOTIFY_PLAYER_WEAPON_CHANGED:
347 if (data)
348 {
349 auto const *args = (ddnotify_player_weapon_changed_t *) data;
350 self().player(args->player).setWeaponAssetId(args->weaponId);
351 }
352 break;
353
354 default:
355 break;
356 }
357 }
358
359 void gameWorkerProgress(int progress)
360 {
361 Con_SetProgress(progress);
362 }
363
364 void consoleRegistration()
365 {
366 DD_ConsoleRegister();
367 }
368
369 void aboutToUnloadGame(Game const &/*gameBeingUnloaded*/)
370 {
371 DENG_ASSERT(ClientWindow::mainExists());
372
373 // Quit netGame if one is in progress.
374 if (netGame)
375 {
376 Con_Execute(CMDS_DDAY, "net disconnect", true, false);
377 }
378
379 Demo_StopPlayback();
380 GL_PurgeDeferredTasks();
381
382 App_Resources().releaseAllGLTextures();
383 App_Resources().pruneUnusedTextureSpecs();
384 GL_LoadLightingSystemTextures();
385 GL_LoadFlareTextures();
386 Rend_ParticleLoadSystemTextures();
387 GL_ResetViewEffects();
388
389 infineSystem().reset();
390
391 if (App_GameLoaded())
392 {
393 // Write cvars and bindings to .cfg files.
394 Con_SaveDefaults();
395
396 // Disallow further saving of bindings until another game is loaded.
397 Con_SetAllowed(0);
398
399 R_ClearViewData();
400 world::R_DestroyContactLists();
401 P_ClearPlayerImpulses();
402
403 Con_Execute(CMDS_DDAY, "clearbindings", true, false);
404 inputSys->bindDefaults();
405 inputSys->initialContextActivations();
406 }
407
408 infineSys.deinitBindingContext();
409 }
410
411 void currentGameChanged(Game const &newGame)
412 {
413 if (Sys_IsShuttingDown()) return;
414
415 infineSys.initBindingContext();
416
417 // Process any GL-related tasks we couldn't while Busy.
418 Rend_ParticleLoadExtraTextures();
419
420 /**
421 * Clear any input events we may have accumulated during this process.
422 * @note Only necessary here because we might not have been able to use
423 * busy mode (which would normally do this for us on end).
424 */
425 inputSys->clearEvents();
426
427 if (newGame.isNull())
428 {
429 // The mouse is free while in the Home.
430 ClientWindow::main().eventHandler().trapMouse(false);
431 }
432
433 ClientWindow::main().console().zeroLogHeight();
434
435 if (!newGame.isNull())
436 {
437 // Auto-start the game?
438 auto const *prof = self().currentGameProfile();
439 if (prof && prof->autoStartMap())
440 {
441 LOG_NOTE("Starting in %s as configured in the game profile")
442 << prof->autoStartMap();
443
444 Con_Executef(CMDS_DDAY, false, "setdefaultskill %i; setmap %s",
445 prof->autoStartSkill(),
446 prof->autoStartMap().toUtf8().constData());
447 }
448 }
449 }
450
451 void periodicAutosave()
452 {
453 if (Config::exists())
454 {
455 Config::get().writeIfModified();
456 }
457 Con_SaveDefaultsIfChanged();
458 }
459
460 /**
461 * Set up an application-wide menu.
462 */
463 void setupAppMenu()
464 {
465 #if defined (MACOSX)
466 nativeAppMenu.reset(new NativeMenu);
467 #endif
468 }
469
470 void initSettings()
471 {
472 using Prof = ConfigProfiles; // convenience
473
474 // Log filter and alert settings.
475 for (int i = LogEntry::FirstDomainBit; i <= LogEntry::LastDomainBit; ++i)
476 {
477 String const name = LogFilter::domainRecordName(LogEntry::Context(1 << i));
478 logSettings
479 .define(Prof::ConfigVariable, String("log.filter.%1.minLevel").arg(name))
480 .define(Prof::ConfigVariable, String("log.filter.%1.allowDev").arg(name))
481 .define(Prof::ConfigVariable, String("alert.%1").arg(name));
482 }
483
484 uiSettings
485 .define(Prof::ConfigVariable, "ui.scaleFactor")
486 .define(Prof::ConfigVariable, "ui.showAnnotations")
487 .define(Prof::ConfigVariable, "home.showColumnDescription")
488 .define(Prof::ConfigVariable, "home.showUnplayableGames")
489 .define(Prof::ConfigVariable, "home.columns.doom")
490 .define(Prof::ConfigVariable, "home.columns.heretic")
491 .define(Prof::ConfigVariable, "home.columns.hexen")
492 .define(Prof::ConfigVariable, "home.columns.otherGames")
493 .define(Prof::ConfigVariable, "home.columns.multiplayer")
494 .define(Prof::ConfigVariable, "home.sortBy")
495 .define(Prof::ConfigVariable, "home.sortAscending")
496 .define(Prof::ConfigVariable, "home.sortCustomSeparately");
497
498 /// @todo These belong in their respective subsystems.
499
500 networkSettings
501 .define(Prof::ConfigVariable, "apiUrl")
502 .define(Prof::ConfigVariable, "resource.localPackages")
503 .define(Prof::IntCVar, "net-dev", 0);
504
505 audioSettings
506 .define(Prof::IntCVar, "sound-volume", 255 * 2/3)
507 .define(Prof::IntCVar, "music-volume", 255 * 2/3)
508 .define(Prof::FloatCVar, "sound-reverb-volume", 0.5f)
509 .define(Prof::IntCVar, "sound-info", 0)
510 //.define(Prof::IntCVar, "sound-rate", 11025)
511 //.define(Prof::IntCVar, "sound-16bit", 0)
512 .define(Prof::IntCVar, "sound-3d", 0)
513 .define(Prof::IntCVar, "sound-overlap-stop", 0)
514 .define(Prof::IntCVar, "music-source", AudioSystem::MUSP_EXT)
515 .define(Prof::StringCVar, "music-soundfont", "")
516 .define(Prof::ConfigVariable, "audio.soundPlugin")
517 .define(Prof::ConfigVariable, "audio.musicPlugin")
518 .define(Prof::ConfigVariable, "audio.cdPlugin")
519 .define(Prof::ConfigVariable, "audio.channels")
520 .define(Prof::ConfigVariable, "audio.pauseOnFocus")
521 .define(Prof::ConfigVariable, "audio.output");
522 }
523
524 #ifdef UNIX
525 void printVersionToStdOut()
526 {
527 printf("%s\n", String("%1 %2")
528 .arg(DOOMSDAY_NICENAME)
529 .arg(DOOMSDAY_VERSION_FULLTEXT)
530 .toLatin1().constData());
531 }
532
533 void printHelpToStdOut()
534 {
535 printVersionToStdOut();
536 printf("Usage: %s [options]\n", self().commandLine().at(0).toLatin1().constData());
537 printf(" -iwad (dir) Set directory containing IWAD files.\n");
538 printf(" -file (f) Load one or more PWAD files at startup.\n");
539 printf(" -game (id) Set game to load at startup.\n");
540 printf(" -nomaximize Do not maximize window at startup.\n");
541 printf(" -wnd Start in windowed mode.\n");
542 printf(" -wh (w) (h) Set window width and height.\n");
543 printf(" --version Print current version.\n");
544 printf("For more options and information, see \"man doomsday\".\n");
545 }
546 #endif
547
548 String mapClientStatePath(String const &mapId) const
549 {
550 return String("maps/%1ClientState").arg(mapId);
551 }
552
553 String mapObjectStatePath(String const &mapId) const
554 {
555 return String("maps/%1ObjectState").arg(mapId);
556 }
557 };
558
ClientApp(int & argc,char ** argv)559 ClientApp::ClientApp(int &argc, char **argv)
560 : BaseGuiApp(argc, argv)
561 , DoomsdayApp([] () -> Player * { return new ClientPlayer; })
562 , d(new Impl(this))
563 {
564 novideo = false;
565
566 // Use the host system's proxy configuration.
567 QNetworkProxyFactory::setUseSystemConfiguration(true);
568
569 // Metadata.
570 setMetadata("Deng Team", "dengine.net", "Doomsday Engine", DOOMSDAY_VERSION_BASE);
571 setUnixHomeFolderName(".doomsday");
572
573 // Callbacks.
574 setTerminateFunc(handleLegacyCoreTerminate);
575
576 // We must presently set the current game manually (the collection is global).
577 setGame(games().nullGame());
578
579 // Script bindings.
580 /// @todo ServerApp is missing these, but they belong in DoomsdayApp.
581 {
582 auto &appModule = scriptSystem()["App"];
583 d->binder.init(appModule)
584 << DENG2_FUNC_NOARG (App_ConsolePlayer, "consolePlayer")
585 << DENG2_FUNC_NOARG (App_GamePlugin, "gamePlugin")
586 << DENG2_FUNC_NOARG (App_Quit, "quit")
587 << DENG2_FUNC (App_GetInteger, "getInteger", "id")
588 << DENG2_FUNC (App_SetInteger, "setInteger", "id" << "value");
589 }
590
591 #if !defined (DENG_MOBILE)
592 /// @todo Remove the splash screen when file system indexing can be done as
593 /// a background task and the main window can be opened instantly. -jk
594 QPixmap const pixmap(doomsdaySplashXpm);
595 QSplashScreen *splash = new QSplashScreen(pixmap);
596 splash->show();
597 splash->showMessage(Version::currentBuild().asHumanReadableText(),
598 Qt::AlignHCenter | Qt::AlignBottom,
599 QColor(90, 110, 95));
600 processEvents();
601 splash->deleteLater();
602 #endif
603 }
604
initialize()605 void ClientApp::initialize()
606 {
607 Libdeng_Init();
608 DD_InitCommandLine();
609
610 #ifdef UNIX
611 // Some common Unix command line options.
612 if (commandLine().has("--version") || commandLine().has("-version"))
613 {
614 d->printVersionToStdOut();
615 ::exit(0);
616 }
617 if (commandLine().has("--help") || commandLine().has("-h") || commandLine().has("-?"))
618 {
619 d->printHelpToStdOut();
620 ::exit(0);
621 }
622 #endif
623
624 d->svLink = new ServerLink;
625
626 // Config needs DisplayMode, so let's initialize it before the libcore
627 // subsystems and Config.
628 DisplayMode_Init();
629
630 // Initialize definitions before the files are indexed.
631 Def_Init();
632
633 addInitPackage("net.dengine.client");
634 initSubsystems(); // loads Config
635 DoomsdayApp::initialize();
636
637 // Initialize players.
638 for (int i = 0; i < players().count(); ++i)
639 {
640 player(i).viewCompositor().setPlayerNumber(i);
641 }
642
643 // Set up the log alerts (observes Config variables).
644 d->logAlarm.alertMask.init();
645
646 // Create the user's configurations and settings folder, if it doesn't exist.
647 fileSystem().makeFolder("/home/configs");
648
649 d->initSettings();
650
651 // Initialize.
652 #if WIN32
653 if (!DD_Win32_Init())
654 {
655 throw Error("ClientApp::initialize", "DD_Win32_Init failed");
656 }
657 #elif UNIX
658 if (!DD_Unix_Init())
659 {
660 throw Error("ClientApp::initialize", "DD_Unix_Init failed");
661 }
662 #endif
663
664 // Create the world system.
665 d->world = new ClientServerWorld;
666 addSystem(*d->world);
667
668 // Create the render system.
669 d->rendSys = new RenderSystem;
670 addSystem(*d->rendSys);
671
672 // Create the audio system.
673 d->audioSys = new AudioSystem;
674 addSystem(*d->audioSys);
675
676 // Create the window system.
677 d->winSys = new ClientWindowSystem;
678 WindowSystem::setAppWindowSystem(*d->winSys);
679 addSystem(*d->winSys);
680
681 #if defined (DENG_HAVE_UPDATER)
682 // Check for updates automatically.
683 d->updater.reset(new Updater);
684 #endif
685
686 // Create the resource system.
687 d->resources = new ClientResources;
688 addSystem(*d->resources);
689
690 plugins().loadAll();
691
692 // On mobile, the window is instantiated via QML.
693 #if !defined (DENG_MOBILE)
694 d->winSys->createWindow()->setTitle(DD_ComposeMainWindowTitle());
695 #endif
696
697 d->setupAppMenu();
698
699 // Create the input system.
700 d->inputSys = new InputSystem;
701 addSystem(*d->inputSys);
702 B_Init();
703
704 // Finally, run the bootstrap script.
705 scriptSystem().importModule("bootstrap");
706
707 App_Timer(1, continueInitWithEventLoopRunning);
708 }
709
preFrame()710 void ClientApp::preFrame()
711 {
712 DGL_BeginFrame();
713
714 // Frame synchronous I/O operations.
715 App_AudioSystem().startFrame();
716
717 if (gx.BeginFrame) /// @todo Move to GameSystem::timeChanged().
718 {
719 gx.BeginFrame();
720 }
721 }
722
postFrame()723 void ClientApp::postFrame()
724 {
725 /// @todo Should these be here? Consider multiple windows, each having a postFrame?
726 /// Or maybe the frames need to be synced? Or only one of them has a postFrame?
727
728 // We will arrive here always at the same time in relation to the displayed
729 // frame: it is a good time to update the mouse state.
730 Mouse_Poll();
731
732 if (!BusyMode_Active())
733 {
734 if (gx.EndFrame)
735 {
736 gx.EndFrame();
737 }
738 }
739
740 App_AudioSystem().endFrame();
741
742 // This is a good time to recycle unneeded memory allocations, as we're just
743 // finished and shown a frame and there might be free time before we have to
744 // begin drawing the next frame.
745 Garbage_Recycle();
746 }
747
checkPackageCompatibility(StringList const & packageIds,String const & userMessageIfIncompatible,std::function<void ()> finalizeFunc)748 void ClientApp::checkPackageCompatibility(StringList const &packageIds,
749 String const &userMessageIfIncompatible,
750 std::function<void ()> finalizeFunc)
751 {
752 if (packageIds.isEmpty() || // Metadata did not specify packages.
753 GameProfiles::arePackageListsCompatible(packageIds, loadedPackagesAffectingGameplay()))
754 {
755 finalizeFunc();
756 }
757 else
758 {
759 auto *dlg = new PackageCompatibilityDialog;
760 dlg->setMessage(userMessageIfIncompatible);
761 dlg->setWantedPackages(packageIds);
762 dlg->setAcceptanceAction(new CallbackAction(finalizeFunc));
763
764 if (!dlg->isCompatible())
765 {
766 // Run the dialog's event loop in a separate timer callback so it doesn't
767 // interfere with the app's event loop.
768 Loop::timer(.01, [this, dlg] ()
769 {
770 dlg->setDeleteAfterDismissed(true);
771 dlg->exec(ClientWindow::main().root());
772 });
773 }
774 else
775 {
776 delete dlg;
777 }
778 }
779 }
780
gameSessionWasSaved(AbstractSession const & session,GameStateFolder & toFolder)781 void ClientApp::gameSessionWasSaved(AbstractSession const &session,
782 GameStateFolder &toFolder)
783 {
784 DoomsdayApp::gameSessionWasSaved(session, toFolder);
785
786 try
787 {
788 String const mapId = session.mapUri().path();
789
790 // Internal map state.
791 {
792 File &file = toFolder.replaceFile(d->mapClientStatePath(mapId));
793 Writer writer(file);
794 world().map().serializeInternalState(writer.withHeader());
795 }
796
797 // Object state.
798 {
799 File &file = toFolder.replaceFile(d->mapObjectStatePath(mapId));
800 file << world().map().objectsDescription().toUtf8(); // plain text
801 }
802 }
803 catch (Error const &er)
804 {
805 LOGDEV_MAP_WARNING("Internal map state was not serialized: %s") << er.asText();
806 }
807 }
808
gameSessionWasLoaded(AbstractSession const & session,GameStateFolder const & fromFolder)809 void ClientApp::gameSessionWasLoaded(AbstractSession const &session,
810 GameStateFolder const &fromFolder)
811 {
812 DoomsdayApp::gameSessionWasLoaded(session, fromFolder);
813
814 String const mapId = session.mapUri().path();
815
816 // Internal map state. This might be missing.
817 try
818 {
819 if (File const *file = fromFolder.tryLocate<File const>(d->mapClientStatePath(mapId)))
820 {
821 DENG2_ASSERT(session.thinkerMapping() != nullptr);
822
823 Reader reader(*file);
824 world().map().deserializeInternalState(reader.withHeader(),
825 *session.thinkerMapping());
826 }
827 }
828 catch (Error const &er)
829 {
830 LOGDEV_MAP_WARNING("Internal map state not deserialized: %s") << er.asText();
831 }
832
833 // Restore object state.
834 try
835 {
836 if (File const *file = fromFolder.tryLocate<File const>(d->mapObjectStatePath(mapId)))
837 {
838 // Parse the info and cross-check with current state.
839 world().map().restoreObjects(Info(*file), *session.thinkerMapping());
840 }
841 else
842 {
843 LOGDEV_MSG("\"%s\" not found") << d->mapObjectStatePath(mapId);
844 }
845 }
846 catch (Error const &er)
847 {
848 LOGDEV_MAP_WARNING("Object state check error: %s") << er.asText();
849 }
850 }
851
player(int console)852 ClientPlayer &ClientApp::player(int console) // static
853 {
854 return DoomsdayApp::players().at(console).as<ClientPlayer>();
855 }
856
forLocalPlayers(const std::function<LoopResult (ClientPlayer &)> & func)857 LoopResult ClientApp::forLocalPlayers(const std::function<LoopResult (ClientPlayer &)> &func) // static
858 {
859 auto const &players = DoomsdayApp::players();
860 for (int i = 0; i < players.count(); ++i)
861 {
862 ClientPlayer &player = players.at(i).as<ClientPlayer>();
863 if (player.isInGame() &&
864 player.publicData().flags & DDPF_LOCAL)
865 {
866 if (auto result = func(player))
867 {
868 return result;
869 }
870 }
871 }
872 return LoopContinue;
873 }
874
alert(String const & msg,LogEntry::Level level)875 void ClientApp::alert(String const &msg, LogEntry::Level level)
876 {
877 if (ClientWindow::mainExists())
878 {
879 ClientWindow::main().alerts()
880 .newAlert(msg, level >= LogEntry::Error? AlertDialog::Major :
881 level == LogEntry::Warning? AlertDialog::Normal :
882 AlertDialog::Minor);
883 }
884 /**
885 * @todo If there is no window, the alert could be stored until the window becomes
886 * available. -jk
887 */
888 }
889
app()890 ClientApp &ClientApp::app()
891 {
892 DENG2_ASSERT(clientAppSingleton != 0);
893 return *clientAppSingleton;
894 }
895
896 #if defined (DENG_HAVE_UPDATER)
updater()897 Updater &ClientApp::updater()
898 {
899 DENG2_ASSERT(!app().d->updater.isNull());
900 return *app().d->updater;
901 }
902 #endif
903
904 #if defined (DENG_HAVE_BUSYRUNNER)
busyRunner()905 BusyRunner &ClientApp::busyRunner()
906 {
907 return app().d->busyRunner;
908 }
909 #endif
910
logSettings()911 ConfigProfiles &ClientApp::logSettings()
912 {
913 return app().d->logSettings;
914 }
915
networkSettings()916 ConfigProfiles &ClientApp::networkSettings()
917 {
918 return app().d->networkSettings;
919 }
920
audioSettings()921 ConfigProfiles &ClientApp::audioSettings()
922 {
923 return app().d->audioSettings;
924 }
925
uiSettings()926 ConfigProfiles &ClientApp::uiSettings()
927 {
928 return app().d->uiSettings;
929 }
930
serverLink()931 ServerLink &ClientApp::serverLink()
932 {
933 ClientApp &a = ClientApp::app();
934 DENG2_ASSERT(a.d->svLink != 0);
935 return *a.d->svLink;
936 }
937
infineSystem()938 InFineSystem &ClientApp::infineSystem()
939 {
940 ClientApp &a = ClientApp::app();
941 //DENG2_ASSERT(a.d->infineSys != 0);
942 return a.d->infineSys;
943 }
944
inputSystem()945 InputSystem &ClientApp::inputSystem()
946 {
947 ClientApp &a = ClientApp::app();
948 DENG2_ASSERT(a.d->inputSys != 0);
949 return *a.d->inputSys;
950 }
951
hasInputSystem()952 bool ClientApp::hasInputSystem()
953 {
954 return ClientApp::app().d->inputSys != nullptr;
955 }
956
renderSystem()957 RenderSystem &ClientApp::renderSystem()
958 {
959 ClientApp &a = ClientApp::app();
960 DENG2_ASSERT(hasRenderSystem());
961 return *a.d->rendSys;
962 }
963
hasRenderSystem()964 bool ClientApp::hasRenderSystem()
965 {
966 return ClientApp::app().d->rendSys != nullptr;
967 }
968
audioSystem()969 AudioSystem &ClientApp::audioSystem()
970 {
971 ClientApp &a = ClientApp::app();
972 DENG2_ASSERT(hasAudioSystem());
973 return *a.d->audioSys;
974 }
975
hasAudioSystem()976 bool ClientApp::hasAudioSystem()
977 {
978 return ClientApp::app().d->audioSys != nullptr;
979 }
980
resources()981 ClientResources &ClientApp::resources()
982 {
983 ClientApp &a = ClientApp::app();
984 DENG2_ASSERT(a.d->resources != 0);
985 return *a.d->resources;
986 }
987
windowSystem()988 ClientWindowSystem &ClientApp::windowSystem()
989 {
990 ClientApp &a = ClientApp::app();
991 DENG2_ASSERT(a.d->winSys != 0);
992 return *a.d->winSys;
993 }
994
world()995 ClientServerWorld &ClientApp::world()
996 {
997 ClientApp &a = ClientApp::app();
998 DENG2_ASSERT(a.d->world != 0);
999 return *a.d->world;
1000 }
1001
openHomepageInBrowser()1002 void ClientApp::openHomepageInBrowser()
1003 {
1004 openInBrowser(QUrl(DOOMSDAY_HOMEURL));
1005 }
1006
openInBrowser(QUrl url)1007 void ClientApp::openInBrowser(QUrl url)
1008 {
1009 #if !defined (DENG_MOBILE)
1010 // Get out of fullscreen mode.
1011 int windowed[] = {
1012 ClientWindow::Fullscreen, false,
1013 ClientWindow::End
1014 };
1015 ClientWindow::main().changeAttributes(windowed);
1016 #endif
1017
1018 QDesktopServices::openUrl(url);
1019 }
1020
unloadGame(GameProfile const & upcomingGame)1021 void ClientApp::unloadGame(GameProfile const &upcomingGame)
1022 {
1023 DoomsdayApp::unloadGame(upcomingGame);
1024
1025 // Game has been set to null, update window.
1026 ClientWindow::main().setTitle(DD_ComposeMainWindowTitle());
1027
1028 if (!upcomingGame.gameId().isEmpty())
1029 {
1030 ClientWindow &mainWin = ClientWindow::main();
1031 mainWin.taskBar().close();
1032
1033 // Trap the mouse automatically when loading a game in fullscreen.
1034 if (mainWin.isFullScreen())
1035 {
1036 mainWin.eventHandler().trapMouse();
1037 }
1038 }
1039
1040 R_InitViewWindow();
1041 R_InitSvgs();
1042
1043 world::Map::initDummies();
1044 }
1045
makeGameCurrent(GameProfile const & newGame)1046 void ClientApp::makeGameCurrent(GameProfile const &newGame)
1047 {
1048 DoomsdayApp::makeGameCurrent(newGame);
1049
1050 // Game has been changed, update window.
1051 ClientWindow::main().setTitle(DD_ComposeMainWindowTitle());
1052 }
1053
reset()1054 void ClientApp::reset()
1055 {
1056 DoomsdayApp::reset();
1057
1058 Rend_ResetLookups();
1059 for (int i = 0; i < ClientApp::players().count(); ++i)
1060 {
1061 ClientApp::player(i).viewCompositor().glDeinit();
1062 }
1063 if (App_GameLoaded())
1064 {
1065 d->inputSys->initAllDevices();
1066 }
1067 }
1068