1 /**
2  * @file
3  * @brief Handle cleanup during shutdown.
4  **/
5 
6 #include "AppHdr.h"
7 
8 #include "end.h"
9 
10 #include <cerrno>
11 
12 #include "abyss.h"
13 #include "chardump.h"
14 #include "colour.h"
15 #include "crash.h"
16 #include "database.h"
17 #include "describe.h"
18 #include "dungeon.h"
19 #include "files.h"
20 #include "god-passive.h"
21 #include "ghost.h"
22 #include "hints.h"
23 #include "initfile.h"
24 #include "invent.h"
25 #include "item-prop.h"
26 #include "los.h"
27 #include "macro.h"
28 #include "message.h"
29 #include "misc.h"
30 #include "prompt.h"
31 #include "religion.h"
32 #include "startup.h"
33 #include "state.h"
34 #include "stringutil.h"
35 #include "tag-version.h"
36 #include "tilepick.h"
37 #include "view.h"
38 #include "xom.h"
39 #include "ui.h"
40 #include "rltiles/tiledef-feat.h"
41 
42 #ifdef __ANDROID__
43 #include <android/log.h>
44 #endif
45 
46 using namespace ui;
47 
48 /**
49  * Should crawl restart on game end, depending on restart options and options
50  * (command-line or RC) that bypass the startup menu?
51  *
52  * @param saved whether the game ended by saving
53  */
crawl_should_restart(game_exit exit)54 bool crawl_should_restart(game_exit exit)
55 {
56 #ifdef DGAMELAUNCH
57     UNUSED(exit);
58     return false;
59 #else
60 #ifdef USE_TILE_WEB
61     if (is_tiles() && Options.name_bypasses_menu)
62         return false;
63 #endif
64     if (exit == game_exit::crash)
65         return false;
66     if (exit == game_exit::abort || exit == game_exit::unknown)
67         return true; // always restart on aborting out of a menu
68     bool ret =
69         tobool(Options.restart_after_game, !crawl_state.bypassed_startup_menu);
70     if (exit == game_exit::save)
71         ret = ret && Options.restart_after_save;
72     return ret;
73 #endif
74 }
75 
cio_cleanup()76 void cio_cleanup()
77 {
78     if (!crawl_state.io_inited)
79         return;
80 
81     console_shutdown();
82     crawl_state.io_inited = false;
83 }
84 
85 // Clear some globally defined variables.
_clear_globals_on_exit()86 static void _clear_globals_on_exit()
87 {
88     clear_rays_on_exit();
89     clear_zap_info_on_exit();
90     destroy_abyss();
91 }
92 
fatal_error_notification(string error_msg)93 bool fatal_error_notification(string error_msg)
94 {
95     if (error_msg.empty())
96         return false;
97 
98     // for local tiles, if there is no available ui, it's possible that wm
99     // initialisation has failed and there's nothing that can be done, so we
100     // don't try. On other builds, though, it's just probably early in the
101     // initialisation process, and cio_init should be fairly safe.
102 #ifndef USE_TILE_LOCAL
103     if (!ui::is_available() && !msg::uses_stderr(MSGCH_ERROR))
104         cio_init(); // this, however, should be fairly safe
105 #endif
106 
107     mprf(MSGCH_ERROR, "%s", error_msg.c_str());
108 
109     if (!ui::is_available() || msg::uses_stderr(MSGCH_ERROR))
110         return false;
111 
112     // do the linebreak here so webtiles has it, but it's needed below as well
113     linebreak_string(error_msg, 79);
114 #ifdef USE_TILE_WEB
115     tiles.send_exit_reason("error", error_msg);
116 #endif
117 
118     // this is a bit heavy to continue past here in the face of a real crash.
119     if (crawl_state.seen_hups)
120         return false;
121 
122 #if (!defined(DGAMELAUNCH)) || defined(DGL_PAUSE_AFTER_ERROR)
123 #ifdef USE_TILE_WEB
124     tiles_crt_popup show_as_popup;
125     tiles.set_ui_state(UI_CRT);
126 #endif
127 
128     // TODO: better formatting, maybe use a formatted_scroller?
129     // Escape '<'.
130     // NOTE: This assumes that the error message doesn't contain
131     //       any formatting!
132     error_msg = string("Fatal error:\n\n<lightred>")
133                        + replace_all(error_msg, "<", "<<");
134     error_msg += "</lightred>\n\n<cyan>Hit any key to exit, "
135                  "ctrl-p for the full log.</cyan>";
136     linebreak_string(error_msg, cgetsize(GOTO_CRT).x - 1);
137 
138     auto prompt_ui =
139                 make_shared<Text>(formatted_string::parse_string(error_msg));
140     auto popup = make_shared<ui::Popup>(prompt_ui);
141     bool done = false;
142 
143     popup->on_hotkey_event([&](const KeyEvent& ev) {
144         if (ev.key() == CONTROL('P'))
145         {
146             replay_messages();
147             return true;
148         }
149         return false;
150     });
151 
152     popup->on_keydown_event([&](const KeyEvent&) { return done = true; });
153 
154     mouse_control mc(MOUSE_MODE_MORE);
155     ui::run_layout(move(popup), done);
156 #endif
157 
158     return true;
159 }
160 
161 // Used by do_crash_dump() to tell if the crash happened during exit() hooks.
162 // Not a part of crawl_state, since that's a global C++ instance which is
163 // free'd by exit() hooks when exit() is called, and we don't want to reference
164 // free'd memory.
165 bool CrawlIsExiting = false;
166 bool CrawlIsCrashing = false;
167 
end(int exit_code,bool print_error,const char * format,...)168 NORETURN void end(int exit_code, bool print_error, const char *format, ...)
169 {
170     disable_other_crashes();
171 
172     // Let "error" go out of scope for valgrind's sake.
173     {
174         string error = print_error ? strerror(errno) : "";
175         if (format)
176         {
177             va_list arg;
178             va_start(arg, format);
179             char buffer[1024];
180             vsnprintf(buffer, sizeof buffer, format, arg);
181             va_end(arg);
182 
183             if (error.empty())
184                 error = string(buffer);
185             else
186                 error = string(buffer) + ": " + error;
187 
188             if (!error.empty() && error[error.length() - 1] != '\n')
189                 error += "\n";
190         }
191 
192         if (exit_code)
193             fatal_error_notification(error);
194 
195 #ifdef USE_TILE_WEB
196         tiles.shutdown();
197 #endif
198 
199         cio_cleanup();
200         msg::deinitialise_mpr_streams();
201         _clear_globals_on_exit();
202         databaseSystemShutdown();
203 #ifdef DEBUG_PROPS
204         dump_prop_accesses();
205 #endif
206 
207         if (!error.empty())
208         {
209 #ifdef __ANDROID__
210             __android_log_print(ANDROID_LOG_INFO, "Crawl", "%s", error.c_str());
211 #endif
212             error.clear();
213         }
214     }
215 
216     CrawlIsExiting = true;
217     if (exit_code)
218         CrawlIsCrashing = true;
219 
220 #ifdef DEBUG_GLOBALS
221     delete real_env;         real_env = 0;
222     delete real_crawl_state; real_crawl_state = 0;
223     delete real_dlua;        real_dlua = 0;
224     delete real_clua;        real_clua = 0;
225     delete real_you;         real_you = 0;
226     delete real_Options;     real_Options = 0;
227 #endif
228 
229     exit(exit_code);
230 }
231 
232 // Delete save files on game end.
233 // Non-static for catch2-tests.
delete_files()234 void delete_files()
235 {
236     crawl_state.need_save = false;
237     you.save->unlink();
238     delete you.save;
239     you.save = 0;
240 }
241 
screen_end_game(string text)242 NORETURN void screen_end_game(string text)
243 {
244 #ifdef USE_TILE_WEB
245     tiles.send_exit_reason("quit");
246 #endif
247     crawl_state.cancel_cmd_all();
248     delete_files();
249 
250     if (!text.empty())
251     {
252         auto prompt_ui = make_shared<Text>(
253                 formatted_string::parse_string(text));
254         auto popup = make_shared<ui::Popup>(prompt_ui);
255         bool done = false;
256         popup->on_keydown_event([&](const KeyEvent&) { return done = true; });
257 
258         mouse_control mc(MOUSE_MODE_MORE);
259         ui::run_layout(move(popup), done);
260     }
261 
262     game_ended(game_exit::abort); // TODO: is this the right exit condition?
263 }
264 
_kill_method_to_exit(kill_method_type kill)265 static game_exit _kill_method_to_exit(kill_method_type kill)
266 {
267     switch (kill)
268     {
269         case KILLED_BY_QUITTING: return game_exit::quit;
270         case KILLED_BY_WINNING:  return game_exit::win;
271         case KILLED_BY_LEAVING:  return game_exit::leave;
272         default:                 return game_exit::death;
273     }
274 }
275 
_exit_type_to_string(game_exit e)276 static string _exit_type_to_string(game_exit e)
277 {
278     // some of these may be used by webtiles, check before editing
279     switch (e)
280     {
281         case game_exit::unknown: return "unknown";
282         case game_exit::win:     return "won";
283         case game_exit::leave:   return "bailed out";
284         case game_exit::quit:    return "quit";
285         case game_exit::death:   return "dead";
286         case game_exit::save:    return "save";
287         case game_exit::abort:   return "abort";
288         case game_exit::crash:   return "crash";
289     }
290     return "BUGGY EXIT TYPE";
291 }
292 
293 class HiscoreScroller : public Scroller
294 {
295 public:
296     virtual void _allocate_region();
297     int scroll_target = 0;
298 };
299 
_allocate_region()300 void HiscoreScroller::_allocate_region()
301 {
302     m_scroll = scroll_target - m_region.height/2;
303     Scroller::_allocate_region();
304 }
305 
end_game(scorefile_entry & se)306 NORETURN void end_game(scorefile_entry &se)
307 {
308     //Update states
309     crawl_state.need_save       = false;
310     crawl_state.updating_scores = true;
311 
312     const kill_method_type death_type = (kill_method_type) se.get_death_type();
313 
314     const bool non_death = death_type == KILLED_BY_QUITTING
315                         || death_type == KILLED_BY_WINNING
316                         || death_type == KILLED_BY_LEAVING;
317 
318     int hiscore_index = -1;
319 #ifndef SCORE_WIZARD_CHARACTERS
320     if (!you.wizard && !you.explore)
321 #endif
322     {
323         // Add this highscore to the score file.
324         hiscore_index = hiscores_new_entry(se);
325         logfile_new_entry(se);
326     }
327 #ifndef SCORE_WIZARD_CHARACTERS
328     else
329         hiscores_read_to_memory();
330 #endif
331 
332     // Never generate bones files of wizard or tutorial characters -- bwr
333     if (!non_death && !crawl_state.game_is_tutorial() && !you.wizard)
334         save_ghosts(ghost_demon::find_ghosts());
335 
336     for (auto &item : you.inv)
337         if (item.defined() && item_type_unknown(item))
338             add_inscription(item, "unknown");
339 
340     identify_inventory();
341 
342     delete_files();
343 
344     // death message
345     if (!non_death)
346     {
347         canned_msg(MSG_YOU_DIE);
348         xom_death_message(death_type);
349 
350         switch (you.religion)
351         {
352         case GOD_FEDHAS:
353             simple_god_message(" appreciates your contribution to the "
354                                "ecosystem.");
355             break;
356 
357         case GOD_NEMELEX_XOBEH:
358             nemelex_death_message();
359             break;
360 
361         case GOD_KIKUBAAQUDGHA:
362         {
363             const mon_holy_type holi = you.holiness();
364 
365             if (holi & (MH_NONLIVING | MH_UNDEAD))
366             {
367                 simple_god_message(" rasps: \"You have failed me! "
368                                    "Welcome... oblivion!\"");
369             }
370             else
371             {
372                 simple_god_message(" rasps: \"You have failed me! "
373                                    "Welcome... death!\"");
374             }
375             break;
376         }
377 
378         case GOD_YREDELEMNUL:
379             if (you.undead_state() != US_ALIVE)
380                 simple_god_message(" claims you as an undead slave.");
381             else if (death_type != KILLED_BY_DISINT
382                   && death_type != KILLED_BY_LAVA)
383             {
384                 mprf(MSGCH_GOD, "Your body rises from the dead as a mindless "
385                      "zombie.");
386             }
387             // No message if you're not undead and your corpse is lost.
388             break;
389 
390         case GOD_BEOGH:
391             if (actor* killer = se.killer())
392             {
393                 if (killer->is_monster() && killer->deity() == GOD_BEOGH)
394                 {
395                     const string msg = " appreciates "
396                         + killer->name(DESC_ITS)
397                         + " killing of a heretic priest.";
398                     simple_god_message(msg.c_str());
399                 }
400             }
401             break;
402 
403 #if TAG_MAJOR_VERSION == 34
404         case GOD_PAKELLAS:
405         {
406             const string result = getSpeakString("Pakellas death");
407             god_speaks(GOD_PAKELLAS, result.c_str());
408             break;
409         }
410 #endif
411 
412         default:
413             if (will_have_passive(passive_t::goldify_corpses)
414                 && death_type != KILLED_BY_DISINT
415                 && death_type != KILLED_BY_LAVA)
416             {
417                 mprf(MSGCH_GOD, "Your body crumbles into a pile of gold.");
418             }
419             break;
420         }
421 
422         flush_prev_message();
423         viewwindow(); // don't do for leaving/winning characters
424         update_screen();
425 
426         if (crawl_state.game_is_hints())
427             hints_death_screen();
428     }
429 
430     string fname = morgue_name(you.your_name, se.get_death_time());
431     if (!dump_char(fname, true, true, &se))
432         mpr("Char dump unsuccessful! Sorry about that.");
433 #ifdef USE_TILE_WEB
434     else
435         tiles.send_dump_info("morgue", fname);
436 #endif
437 
438     const game_exit exit_reason = _kill_method_to_exit(death_type);
439 #if defined(DGL_WHEREIS) || defined(USE_TILE_WEB)
440     const string reason = _exit_type_to_string(exit_reason);
441 
442 # ifdef DGL_WHEREIS
443     whereis_record(reason.c_str());
444 # endif
445 #else
446     UNUSED(_exit_type_to_string);
447 #endif
448 
449 #ifndef DISABLE_STICKY_STARTUP_OPTIONS
450     // TODO: update all sticky prefs based on the dead char? Right now this
451     // would lose weapon choice, and random select, as far as I can tell.
452     save_seed_pref();
453 #endif
454 
455     if (!crawl_state.seen_hups)
456         more();
457 
458     if (!crawl_state.disables[DIS_CONFIRMATIONS])
459         display_inventory();
460 
461     clua.save_persist();
462 
463     if (crawl_state.unsaved_macros)
464         macro_save();
465 
466     auto title_hbox = make_shared<Box>(Widget::HORZ);
467 #ifdef USE_TILE
468     tile_def death_tile(TILEG_ERROR);
469     if (death_type == KILLED_BY_LEAVING || death_type == KILLED_BY_WINNING)
470         death_tile = tile_def(TILE_DNGN_EXIT_DUNGEON);
471     else
472         death_tile = tile_def(TILE_DNGN_GRAVESTONE+1);
473 
474     auto tile = make_shared<Image>(death_tile);
475     tile->set_margin_for_sdl(0, 10, 0, 0);
476     title_hbox->add_child(move(tile));
477 #endif
478     string goodbye_title = make_stringf("Goodbye, %s.", you.your_name.c_str());
479     title_hbox->add_child(make_shared<Text>(goodbye_title));
480     title_hbox->set_cross_alignment(Widget::CENTER);
481     title_hbox->set_margin_for_sdl(0, 0, 20, 0);
482     title_hbox->set_margin_for_crt(0, 0, 1, 0);
483 
484     auto vbox = make_shared<Box>(Box::VERT);
485     vbox->add_child(move(title_hbox));
486 
487     string goodbye_msg;
488     goodbye_msg += "    "; // Space padding where # would go in list format
489 
490     string hiscore = hiscores_format_single_long(se, true);
491 
492     goodbye_msg += hiscore;
493 
494     goodbye_msg += make_stringf("\nBest Crawlers - %s\n",
495             crawl_state.game_type_name().c_str());
496 
497 #ifdef USE_TILE_LOCAL
498         const int line_height = tiles.get_crt_font()->char_height();
499 #else
500         const int line_height = 1;
501 #endif
502 
503     int start;
504     int num_lines = 100;
505     string hiscores = hiscores_print_list(num_lines, SCORE_TERSE, hiscore_index, start);
506     auto scroller = make_shared<HiscoreScroller>();
507     auto hiscores_txt = make_shared<Text>(formatted_string::parse_string(hiscores));
508     scroller->set_child(hiscores_txt);
509     scroller->set_scrollbar_visible(false);
510     scroller->scroll_target = (hiscore_index - start)*line_height + (line_height/2);
511 
512     mouse_control mc(MOUSE_MODE_MORE);
513 
514     auto goodbye_txt = make_shared<Text>(formatted_string::parse_string(goodbye_msg));
515     vbox->add_child(goodbye_txt);
516     vbox->add_child(scroller);
517 
518 #ifndef DGAMELAUNCH
519     string morgue_dir = make_stringf("\nYou can find your morgue file in the '%s' directory.",
520             morgue_directory().c_str());
521     vbox->add_child(make_shared<Text>(formatted_string::parse_string(morgue_dir)));
522 #endif
523 
524     auto popup = make_shared<ui::Popup>(move(vbox));
525     bool done = false;
526     popup->on_keydown_event([&](const KeyEvent&) { return done = true; });
527 
528     if (!crawl_state.seen_hups && !crawl_state.disables[DIS_CONFIRMATIONS])
529     {
530 #ifdef USE_TILE_WEB
531     tiles.json_open_object();
532     tiles.json_open_object("tile");
533     tiles.json_write_int("t", death_tile.tile);
534     tiles.json_write_int("tex", get_tile_texture(death_tile.tile));
535     tiles.json_close_object();
536     tiles.json_write_string("title", goodbye_title);
537     tiles.json_write_string("body", goodbye_msg
538             + hiscores_print_list(11, SCORE_TERSE, hiscore_index, start));
539     tiles.push_ui_layout("game-over", 0);
540     popup->on_layout_pop([](){ tiles.pop_ui_layout(); });
541 #endif
542 
543         ui::run_layout(move(popup), done);
544     }
545 
546 #ifdef USE_TILE_WEB
547     tiles.send_exit_reason(reason, hiscore);
548 #endif
549 
550     game_ended(exit_reason);
551 }
552 
game_ended(game_exit exit,const string & message)553 NORETURN void game_ended(game_exit exit, const string &message)
554 {
555     if (crawl_state.marked_as_won &&
556         (exit == game_exit::death || exit == game_exit::leave))
557     {
558         // used in tutorials
559         exit = game_exit::win;
560     }
561     if (crawl_state.seen_hups ||
562         (exit == game_exit::crash && !crawl_should_restart(game_exit::crash)))
563     {
564         const int retval = exit == game_exit::crash ? 1 : 0;
565         if (message.size() > 0)
566         {
567 #ifdef USE_TILE_WEB
568             tiles.send_exit_reason("error", message);
569 #endif
570             end(retval, false, "%s\n", message.c_str());
571         }
572         else
573             end(retval);
574     }
575 #ifdef USE_TILE_WEB
576     else if (exit == game_exit::abort)
577         tiles.send_exit_reason("cancel", message);
578 #endif
579     throw game_ended_condition(exit, message);
580 }
581 
582 // note: this *will not* print a crash dump, and so should probably be avoided.
game_ended_with_error(const string & message)583 NORETURN void game_ended_with_error(const string &message)
584 {
585     game_ended(game_exit::crash, message);
586 }
587