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