1 /**
2 * @file
3 * @brief deal with reading and writing of highscore file
4 **/
5
6 /*
7 * ----------- MODIFYING THE PRINTED SCORE FORMAT ---------------------
8 * Do this at your leisure. Change hiscores_format_single() as much
9 * as you like.
10 *
11 */
12
13 #include "AppHdr.h"
14
15 #include "hiscores.h"
16
17 #include <algorithm>
18 #include <cctype>
19 #include <cstdio>
20 #include <cstdlib>
21 #include <memory>
22 #if defined(UNIX) || defined(TARGET_COMPILER_MINGW)
23 #include <unistd.h>
24 #endif
25
26 #include "branch.h"
27 #include "chardump.h"
28 #include "cio.h"
29 #include "dungeon.h"
30 #include "end.h"
31 #include "english.h"
32 #include "files.h"
33 #include "initfile.h"
34 #include "item-prop.h"
35 #include "item-status-flag-type.h"
36 #include "items.h"
37 #include "jobs.h"
38 #include "kills.h"
39 #include "libutil.h"
40 #include "menu.h"
41 #include "misc.h"
42 #include "mon-util.h"
43 #include "options.h"
44 #include "ouch.h"
45 #include "place.h"
46 #include "religion.h"
47 #include "scroller.h"
48 #include "skills.h"
49 #include "state.h"
50 #include "status.h"
51 #include "stringutil.h"
52 #ifdef USE_TILE
53 #include "tilepick.h"
54 #endif
55 #ifdef USE_TILE_LOCAL
56 #include "tilereg-crt.h"
57 #endif
58 #include "unwind.h"
59 #include "version.h"
60 #include "outer-menu.h"
61
62 using namespace ui;
63
64 #define SCORE_VERSION "0.1"
65
66 // enough memory allocated to snarf in the scorefile entries
67 static unique_ptr<scorefile_entry> hs_list[SCORE_FILE_ENTRIES];
68 static int hs_list_size = 0;
69 static bool hs_list_initalized = false;
70
71 static FILE *_hs_open(const char *mode, const string &filename);
72 static void _hs_close(FILE *handle);
73 static bool _hs_read(FILE *scores, scorefile_entry &dest);
74 static void _hs_write(FILE *scores, scorefile_entry &entry);
75 static time_t _parse_time(const string &st);
76 static string _xlog_escape(const string &s);
77 static string _xlog_unescape(const string &s);
78 static vector<string> _xlog_split_fields(const string &s);
79
_score_file_name()80 static string _score_file_name()
81 {
82 string ret;
83 if (!SysEnv.scorefile.empty())
84 ret = SysEnv.scorefile;
85 else
86 ret = catpath(Options.shared_dir, "scores");
87
88 ret += crawl_state.game_type_qualifier();
89 if (crawl_state.game_is_sprint() && !crawl_state.map.empty())
90 ret += "-" + crawl_state.map;
91
92 return ret;
93 }
94
_log_file_name()95 static string _log_file_name()
96 {
97 return catpath(Options.shared_dir,
98 "logfile" + crawl_state.game_type_qualifier());
99 }
100
hiscores_new_entry(const scorefile_entry & ne)101 int hiscores_new_entry(const scorefile_entry &ne)
102 {
103 unwind_bool score_update(crawl_state.updating_scores, true);
104
105 FILE *scores;
106 int i;
107 bool inserted = false;
108 int newest_entry = -1;
109
110 // open highscore file (reading) -- nullptr is fatal!
111 //
112 // Opening as a+ instead of r+ to force an exclusive lock (see
113 // hs_open) and to create the file if it's not there already.
114 scores = _hs_open("a+", _score_file_name());
115 if (scores == nullptr)
116 end(1, true, "failed to open score file for writing");
117
118 // we're at the end of the file, seek back to beginning.
119 fseek(scores, 0, SEEK_SET);
120
121 // read highscore file, inserting new entry at appropriate point,
122 for (i = 0; i < SCORE_FILE_ENTRIES; i++)
123 {
124 hs_list[i].reset(new scorefile_entry);
125 if (_hs_read(scores, *hs_list[i]) == false)
126 break;
127
128 // compare points..
129 if (!inserted && ne.get_score() >= hs_list[i]->get_score())
130 {
131 newest_entry = i; // for later printing
132 inserted = true;
133 // copy read entry to i+1th position
134 // Fixed a nasty overflow bug here -- Sharp
135 if (i+1 < SCORE_FILE_ENTRIES)
136 {
137 hs_list[i + 1] = move(hs_list[i]);
138 hs_list[i].reset(new scorefile_entry(ne));
139 i++;
140 }
141 else
142 *hs_list[i] = ne; // copy new entry to current position
143 }
144 }
145
146 // special case: lowest score, with room
147 if (!inserted && i < SCORE_FILE_ENTRIES)
148 {
149 newest_entry = i;
150 inserted = true;
151 // copy new entry
152 hs_list[i].reset(new scorefile_entry(ne));
153 i++;
154 }
155
156 hs_list_size = i;
157 hs_list_initalized = true;
158
159 // If we've still not inserted it, it's not a highscore.
160 if (!inserted)
161 {
162 _hs_close(scores);
163 return -1;
164 }
165
166 // The old code closed and reopened the score file, leading to a
167 // race condition where one Crawl process could overwrite the
168 // other's highscore. Now we truncate and rewrite the file without
169 // closing it.
170 if (ftruncate(fileno(scores), 0))
171 end(1, true, "unable to truncate scorefile");
172
173 rewind(scores);
174
175 // write scorefile entries.
176 for (i = 0; i < hs_list_size; i++)
177 {
178 _hs_write(scores, *hs_list[i]);
179
180 // Leave in memory. Does this anyway if !inserted.
181 // Can write cleanup function if nessicary??
182 // hs_list[i].reset(nullptr);
183 }
184
185 // close scorefile.
186 _hs_close(scores);
187 return newest_entry;
188 }
189
logfile_new_entry(const scorefile_entry & ne)190 void logfile_new_entry(const scorefile_entry &ne)
191 {
192 unwind_bool logfile_update(crawl_state.updating_scores, true);
193
194 FILE *logfile;
195 scorefile_entry le = ne;
196
197 // open logfile (appending) -- nullptr *is* fatal here.
198 logfile = _hs_open("a", _log_file_name());
199 if (logfile == nullptr)
200 {
201 mprf(MSGCH_ERROR, "ERROR: failure writing to the logfile.");
202 return;
203 }
204
205 _hs_write(logfile, le);
206
207 // close logfile.
208 _hs_close(logfile);
209 }
210
211 template <class t_printf>
_hiscores_print_entry(const scorefile_entry & se,int index,int format,t_printf pf)212 static void _hiscores_print_entry(const scorefile_entry &se,
213 int index, int format, t_printf pf)
214 {
215 char buf[200];
216 // print position (tracked implicitly by order score file)
217 snprintf(buf, sizeof buf, "%3d.", index + 1);
218
219 pf("%s", buf);
220
221 string entry;
222 // format the entry
223 if (format == SCORE_TERSE)
224 entry = hiscores_format_single(se);
225 else
226 entry = hiscores_format_single_long(se, (format == SCORE_VERBOSE));
227
228 entry += "\n";
229 pf("%s", entry.c_str());
230 }
231
232 // Reads hiscores file to memory
hiscores_read_to_memory()233 void hiscores_read_to_memory()
234 {
235 FILE *scores;
236 int i;
237
238 // open highscore file (reading)
239 scores = _hs_open("r", _score_file_name());
240 if (scores == nullptr)
241 return;
242
243 // read highscore file
244 for (i = 0; i < SCORE_FILE_ENTRIES; i++)
245 {
246 hs_list[i].reset(new scorefile_entry);
247 if (_hs_read(scores, *hs_list[i]) == false)
248 break;
249 }
250
251 hs_list_size = i;
252 hs_list_initalized = true;
253
254 //close off
255 _hs_close(scores);
256 }
257
258 // Writes all entries in the scorefile to stdout in human-readable form.
hiscores_print_all(int display_count,int format)259 void hiscores_print_all(int display_count, int format)
260 {
261 unwind_bool scorefile_display(crawl_state.updating_scores, true);
262
263 FILE *scores = _hs_open("r", _score_file_name());
264 if (scores == nullptr)
265 {
266 // will only happen from command line
267 puts("No scores.");
268 return;
269 }
270
271 for (int entry = 0; display_count <= 0 || entry < display_count; ++entry)
272 {
273 scorefile_entry se;
274 if (!_hs_read(scores, se))
275 break;
276
277 if (format == -1)
278 printf("%s", se.raw_string().c_str());
279 else
280 _hiscores_print_entry(se, entry, format, printf);
281 }
282
283 _hs_close(scores);
284 }
285
286 // Displays high scores using curses. For output to the console, use
287 // hiscores_print_all.
hiscores_print_list(int display_count,int format,int newest_entry,int & start_out)288 string hiscores_print_list(int display_count, int format, int newest_entry, int& start_out)
289 {
290 unwind_bool scorefile_display(crawl_state.updating_scores, true);
291 string ret;
292
293 // Additional check to preserve previous functionality
294 if (!hs_list_initalized)
295 hiscores_read_to_memory();
296
297 int i, total_entries;
298
299 if (display_count <= 0)
300 return "";
301
302 total_entries = hs_list_size;
303
304 int start = newest_entry - display_count / 2;
305
306 if (start + display_count > total_entries)
307 start = total_entries - display_count;
308
309 if (start < 0)
310 start = 0;
311
312 const int finish = start + display_count;
313
314 for (i = start; i < finish && i < total_entries; i++)
315 {
316 // check for recently added entry
317 if (i == newest_entry)
318 ret += "<yellow>";
319
320 _hiscores_print_entry(*hs_list[i], i, format, [&ret](const char */*fmt*/, const char *s){
321 ret += string(s);
322 });
323
324 // return to normal color for next entry
325 if (i == newest_entry)
326 ret += "<lightgrey>";
327 }
328
329 start_out = start;
330 return ret;
331 }
332
_show_morgue(scorefile_entry & se)333 static void _show_morgue(scorefile_entry& se)
334 {
335 int flags = FS_PREWRAPPED_TEXT;
336 formatted_scroller morgue_file(flags);
337
338 morgue_file.set_tag("morgue");
339 morgue_file.set_more();
340
341 string morgue_base = morgue_name(se.get_name(), se.get_death_time());
342 string morgue_path = catpath(morgue_directory(),
343 strip_filename_unsafe_chars(morgue_base) + ".txt");
344 FILE* morgue = lk_open("r", morgue_path);
345
346 if (!morgue) // TODO: add an error message
347 return;
348
349 char buf[200];
350 string morgue_text = "";
351
352 while (fgets(buf, sizeof buf, morgue) != nullptr)
353 {
354 string line = string(buf);
355 size_t newline_pos = line.find_last_of('\n');
356 if (newline_pos != string::npos)
357 line.erase(newline_pos);
358 morgue_text += "<w>" + replace_all(line, "<", "<<") + "</w>" + '\n';
359 }
360
361 lk_close(morgue);
362
363 column_composer cols(2, 40);
364 cols.add_formatted(
365 0,
366 morgue_text,
367 true);
368
369 vector<formatted_string> blines = cols.formatted_lines();
370
371 unsigned i;
372 for (i = 0; i < blines.size(); ++i)
373 morgue_file.add_formatted_string(blines[i], true);
374
375 morgue_file.show();
376 }
377
378 class UIHiscoresMenu : public Widget
379 {
380 public:
381 UIHiscoresMenu();
382
get_child_at_offset(int,int)383 virtual shared_ptr<Widget> get_child_at_offset(int, int) override {
384 return static_pointer_cast<Widget>(m_root);
385 }
386
387 virtual void _render() override;
388 virtual SizeReq _get_preferred_size(Direction dim, int prosp_width) override;
389 virtual void _allocate_region() override;
390
391 void on_show();
392
393 bool done = false;
394
395 private:
396 void _construct_hiscore_table();
397 void _add_hiscore_row(scorefile_entry& se, int id);
398
399 Widget* initial_focus = nullptr;
400 bool have_allocated {false};
401
402 shared_ptr<Box> m_root;
403 shared_ptr<Text> m_description;
404 shared_ptr<OuterMenu> m_score_entries;
405 };
406
407 static int nhsr;
408
UIHiscoresMenu()409 UIHiscoresMenu::UIHiscoresMenu()
410 {
411 m_root = make_shared<Box>(Widget::VERT);
412 add_internal_child(m_root);
413 m_root->set_cross_alignment(Widget::STRETCH);
414
415 auto title_hbox = make_shared<Box>(Widget::HORZ);
416 title_hbox->set_margin_for_sdl(0, 0, 20, 0);
417 title_hbox->set_margin_for_crt(0, 0, 1, 0);
418
419 #ifdef USE_TILE
420 auto tile = make_shared<Image>();
421 tile->set_tile(tile_def(TILEG_STARTUP_HIGH_SCORES));
422 title_hbox->add_child(move(tile));
423 #endif
424
425 auto title = make_shared<Text>(formatted_string(
426 "Dungeon Crawl Stone Soup: High Scores", YELLOW));
427 title->set_margin_for_sdl(0, 0, 0, 16);
428 title_hbox->add_child(move(title));
429
430 title_hbox->set_main_alignment(Widget::CENTER);
431 title_hbox->set_cross_alignment(Widget::CENTER);
432
433 m_description = make_shared<Text>(string(9, '\n'));
434
435 m_score_entries= make_shared<OuterMenu>(true, 1, 100);
436 nhsr = 0;
437 _construct_hiscore_table();
438
439 m_root->add_child(move(title_hbox));
440 if (initial_focus)
441 {
442 m_root->add_child(m_description);
443 m_root->add_child(m_score_entries);
444 }
445 else
446 {
447 auto placeholder = formatted_string("No high scores yet...", DARKGRAY);
448 m_root->add_child(make_shared<Text>(placeholder));
449 initial_focus = this;
450 }
451
452 on_hotkey_event([this](const KeyEvent& ev) {
453 return done = (key_is_escape(ev.key()) || ev.key() == CK_MOUSE_CMD);
454 });
455 }
456
_construct_hiscore_table()457 void UIHiscoresMenu::_construct_hiscore_table()
458 {
459 FILE *scores = _hs_open("r", _score_file_name());
460
461 if (scores == nullptr)
462 return;
463
464 int i;
465 // read highscore file
466 for (i = 0; i < SCORE_FILE_ENTRIES; i++)
467 {
468 hs_list[i].reset(new scorefile_entry);
469 if (_hs_read(scores, *hs_list[i]) == false)
470 break;
471 }
472
473 _hs_close(scores);
474
475 for (int j=0; j<i; j++)
476 _add_hiscore_row(*hs_list[j], j);
477 }
478
_add_hiscore_row(scorefile_entry & se,int id)479 void UIHiscoresMenu::_add_hiscore_row(scorefile_entry& se, int id)
480 {
481 auto tmp = make_shared<Text>();
482
483 tmp->set_text(hiscores_format_single(se));
484 auto btn = make_shared<MenuButton>();
485 tmp->set_margin_for_sdl(2);
486 btn->set_child(move(tmp));
487 btn->on_activate_event([id](const ActivateEvent&) {
488 _show_morgue(*hs_list[id]);
489 return true;
490 });
491 btn->on_focusin_event([this, se](const FocusEvent&) {
492 formatted_string desc(hiscores_format_single_long(se, true));
493 desc.cprintf(string(max(0, 9-count_linebreaks(desc)), '\n'));
494 m_description->set_text(move(desc));
495 return false;
496 });
497
498 if (!initial_focus)
499 initial_focus = btn.get();
500 m_score_entries->add_button(move(btn), 0, nhsr++);
501 }
502
_render()503 void UIHiscoresMenu::_render()
504 {
505 m_root->render();
506 }
507
on_show()508 void UIHiscoresMenu::on_show()
509 {
510 ui::set_focused_widget(initial_focus);
511 }
512
_get_preferred_size(Direction dim,int prosp_width)513 SizeReq UIHiscoresMenu::_get_preferred_size(Direction dim, int prosp_width)
514 {
515 return m_root->get_preferred_size(dim, prosp_width);
516 }
517
_allocate_region()518 void UIHiscoresMenu::_allocate_region()
519 {
520 if (!have_allocated)
521 {
522 have_allocated = true;
523 on_show();
524 }
525 m_root->allocate_region(m_region);
526 }
527
show_hiscore_table()528 void show_hiscore_table()
529 {
530 unwind_var<string> sprintmap(crawl_state.map, crawl_state.sprint_map);
531 auto hiscore_ui = make_shared<UIHiscoresMenu>();
532 auto popup = make_shared<ui::Popup>(hiscore_ui);
533 ui::run_layout(move(popup), hiscore_ui->done);
534 }
535
536 // Trying to supply an appropriate verb for the attack type. -- bwr
_range_type_verb(const char * const aux)537 static const char *_range_type_verb(const char *const aux)
538 {
539 if (strncmp(aux, "Shot ", 5) == 0) // launched
540 return "shot";
541 else if (aux[0] == 0 // unknown
542 || strncmp(aux, "Hit ", 4) == 0 // thrown
543 || strncmp(aux, "volley ", 7) == 0) // manticore spikes
544 {
545 return "hit from afar";
546 }
547
548 return "blasted"; // spells, wands
549 }
550
hiscores_format_single(const scorefile_entry & se)551 string hiscores_format_single(const scorefile_entry &se)
552 {
553 return se.hiscore_line(scorefile_entry::DDV_ONELINE);
554 }
555
_hiscore_same_day(time_t t1,time_t t2)556 static bool _hiscore_same_day(time_t t1, time_t t2)
557 {
558 struct tm *d1 = TIME_FN(&t1);
559 const int year = d1->tm_year;
560 const int mon = d1->tm_mon;
561 const int day = d1->tm_mday;
562
563 struct tm *d2 = TIME_FN(&t2);
564
565 return d2->tm_mday == day && d2->tm_mon == mon && d2->tm_year == year;
566 }
567
_hiscore_date_string(time_t time)568 static string _hiscore_date_string(time_t time)
569 {
570 struct tm *date = TIME_FN(&time);
571
572 const char *mons[12] = { "Jan", "Feb", "Mar", "Apr", "May", "June",
573 "July", "Aug", "Sept", "Oct", "Nov", "Dec" };
574
575 return make_stringf("%s %d, %d", mons[date->tm_mon], date->tm_mday,
576 date->tm_year + 1900);
577 }
578
_hiscore_newline_string()579 static string _hiscore_newline_string()
580 {
581 return "\n ";
582 }
583
hiscores_format_single_long(const scorefile_entry & se,bool verbose)584 string hiscores_format_single_long(const scorefile_entry &se, bool verbose)
585 {
586 return se.hiscore_line(verbose ? scorefile_entry::DDV_VERBOSE
587 : scorefile_entry::DDV_NORMAL);
588 }
589
590 // --------------------------------------------------------------------------
591 // BEGIN private functions
592 // --------------------------------------------------------------------------
593
_hs_open(const char * mode,const string & scores)594 static FILE *_hs_open(const char *mode, const string &scores)
595 {
596 // allow reading from standard input
597 if (scores == "-")
598 return stdin;
599
600 return lk_open(mode, scores);
601 }
602
_hs_close(FILE * handle)603 static void _hs_close(FILE *handle)
604 {
605 lk_close(handle);
606 }
607
_hs_read(FILE * scores,scorefile_entry & dest)608 static bool _hs_read(FILE *scores, scorefile_entry &dest)
609 {
610 char inbuf[1300];
611 if (!scores || feof(scores))
612 return false;
613
614 memset(inbuf, 0, sizeof inbuf);
615 dest.reset();
616
617 if (!fgets(inbuf, sizeof inbuf, scores))
618 return false;
619
620 return dest.parse(inbuf);
621 }
622
_val_char(char digit)623 static int _val_char(char digit)
624 {
625 return digit - '0';
626 }
627
_parse_time(const string & st)628 static time_t _parse_time(const string &st)
629 {
630 struct tm date;
631
632 if (st.length() < 15)
633 return static_cast<time_t>(0);
634
635 date.tm_year = _val_char(st[0]) * 1000 + _val_char(st[1]) * 100
636 + _val_char(st[2]) * 10 + _val_char(st[3]) - 1900;
637
638 date.tm_mon = _val_char(st[4]) * 10 + _val_char(st[5]);
639 date.tm_mday = _val_char(st[6]) * 10 + _val_char(st[7]);
640 date.tm_hour = _val_char(st[8]) * 10 + _val_char(st[9]);
641 date.tm_min = _val_char(st[10]) * 10 + _val_char(st[11]);
642 date.tm_sec = _val_char(st[12]) * 10 + _val_char(st[13]);
643 date.tm_isdst = (st[14] == 'D');
644
645 return mktime(&date);
646 }
647
_hs_write(FILE * scores,scorefile_entry & se)648 static void _hs_write(FILE *scores, scorefile_entry &se)
649 {
650 fprintf(scores, "%s", se.raw_string().c_str());
651 }
652
653 static const char *kill_method_names[] =
654 {
655 "mon", "pois", "cloud", "beam", "lava", "water",
656 "stupidity", "weakness", "clumsiness", "trap", "leaving", "winning",
657 "quitting", "wizmode", "draining", "starvation", "freezing", "burning",
658 "wild_magic", "xom", "rotting", "targeting", "spore",
659 "tso_smiting", "petrification", "something",
660 "falling_down_stairs", "acid", "curare",
661 "beogh_smiting", "divine_wrath", "bounce", "reflect", "self_aimed",
662 "falling_through_gate", "disintegration", "headbutt", "rolling",
663 "mirror_damage", "spines", "frailty", "barbs", "being_thrown",
664 "collision", "zot", "constriction",
665 };
666
_kill_method_name(kill_method_type kmt)667 static const char *_kill_method_name(kill_method_type kmt)
668 {
669 COMPILE_CHECK(NUM_KILLBY == ARRAYSZ(kill_method_names));
670
671 if (kmt == NUM_KILLBY)
672 return "";
673
674 return kill_method_names[kmt];
675 }
676
_str_to_kill_method(const string & s)677 static kill_method_type _str_to_kill_method(const string &s)
678 {
679 COMPILE_CHECK(NUM_KILLBY == ARRAYSZ(kill_method_names));
680
681 for (int i = 0; i < NUM_KILLBY; ++i)
682 {
683 if (s == kill_method_names[i])
684 return static_cast<kill_method_type>(i);
685 }
686
687 return NUM_KILLBY;
688 }
689
690 //////////////////////////////////////////////////////////////////////////
691 // scorefile_entry
692
scorefile_entry(int dam,mid_t dsource,int dtype,const char * aux,bool death_cause_only,const char * dsource_name,time_t dt)693 scorefile_entry::scorefile_entry(int dam, mid_t dsource, int dtype,
694 const char *aux, bool death_cause_only,
695 const char *dsource_name, time_t dt)
696 {
697 reset();
698
699 init_death_cause(dam, dsource, dtype, aux, dsource_name);
700 if (!death_cause_only)
701 init(dt);
702 }
703
scorefile_entry()704 scorefile_entry::scorefile_entry()
705 {
706 // Completely uninitialised, caveat user.
707 reset();
708 }
709
scorefile_entry(const scorefile_entry & se)710 scorefile_entry::scorefile_entry(const scorefile_entry &se)
711 {
712 init_from(se);
713 }
714
operator =(const scorefile_entry & se)715 scorefile_entry &scorefile_entry::operator = (const scorefile_entry &se)
716 {
717 init_from(se);
718 return *this;
719 }
720
init_from(const scorefile_entry & se)721 void scorefile_entry::init_from(const scorefile_entry &se)
722 {
723 version = se.version;
724 save_rcs_version = se.save_rcs_version;
725 save_tag_version = se.save_tag_version;
726 tiles = se.tiles;
727 points = se.points;
728 name = se.name;
729 race = se.race;
730 job = se.job;
731 race_class_name = se.race_class_name;
732 lvl = se.lvl;
733 best_skill = se.best_skill;
734 best_skill_lvl = se.best_skill_lvl;
735 title = se.title;
736 death_type = se.death_type;
737 death_source = se.death_source;
738 death_source_name = se.death_source_name;
739 death_source_flags = se.death_source_flags;
740 auxkilldata = se.auxkilldata;
741 indirectkiller = se.indirectkiller;
742 killerpath = se.killerpath;
743 last_banisher = se.last_banisher;
744 dlvl = se.dlvl;
745 absdepth = se.absdepth;
746 branch = se.branch;
747 map = se.map;
748 mapdesc = se.mapdesc;
749 killer_map = se.killer_map;
750 final_hp = se.final_hp;
751 final_max_hp = se.final_max_hp;
752 final_max_max_hp = se.final_max_max_hp;
753 final_mp = se.final_mp;
754 final_max_mp = se.final_max_mp;
755 final_base_max_mp = se.final_base_max_mp;
756 damage = se.damage;
757 source_damage = se.source_damage;
758 turn_damage = se.turn_damage;
759 str = se.str;
760 intel = se.intel;
761 dex = se.dex;
762 ac = se.ac;
763 ev = se.ev;
764 sh = se.sh;
765 god = se.god;
766 piety = se.piety;
767 penance = se.penance;
768 wiz_mode = se.wiz_mode;
769 explore_mode = se.explore_mode;
770 birth_time = se.birth_time;
771 death_time = se.death_time;
772 real_time = se.real_time;
773 num_turns = se.num_turns;
774 num_aut = se.num_aut;
775 num_diff_runes = se.num_diff_runes;
776 num_runes = se.num_runes;
777 kills = se.kills;
778 maxed_skills = se.maxed_skills;
779 fifteen_skills = se.fifteen_skills;
780 status_effects = se.status_effects;
781 gold = se.gold;
782 gold_spent = se.gold_spent;
783 gold_found = se.gold_found;
784 zigs = se.zigs;
785 zigmax = se.zigmax;
786 scrolls_used = se.scrolls_used;
787 potions_used = se.potions_used;
788 seed = se.seed;
789 fixup_char_name();
790
791 // We could just reset raw_line to "" instead.
792 raw_line = se.raw_line;
793 }
794
killer() const795 actor* scorefile_entry::killer() const
796 {
797 return actor_by_mid(death_source);
798 }
799
get_fields() const800 xlog_fields scorefile_entry::get_fields() const
801 {
802 if (!fields)
803 return xlog_fields();
804 else
805 return *fields;
806 }
807
parse(const string & line)808 bool scorefile_entry::parse(const string &line)
809 {
810 // Scorefile formats down the ages:
811 //
812 // 1) old-style lines which were 80 character blocks
813 // 2) 4.0 pr1 through pr7 versions which were newline terminated
814 // 3) 4.0 pr8 and onwards which are colon-separated fields (and
815 // start with a colon), and may exceed 80 characters!
816 // 4) 0.2 and onwards, which are xlogfile format - no leading
817 // colon, fields separated by colons, each field specified as
818 // key=value. Colons are not allowed in key names, must be escaped to
819 // :: in values.
820 //
821 // 0.3 only reads and writes entries of type (4).
822
823 // Leading colon implies 4.0 style line:
824 if (line[0] == ':')
825 {
826 dprf("Corrupted xlog-line: %s", line.c_str());
827 return false;
828 }
829
830 raw_line = line;
831 return parse_scoreline(line);
832 }
833
raw_string() const834 string scorefile_entry::raw_string() const
835 {
836 if (!raw_line.empty())
837 return raw_line;
838
839 set_score_fields();
840
841 if (!fields)
842 return "";
843
844 return fields->xlog_line() + "\n";
845 }
846
parse_scoreline(const string & line)847 bool scorefile_entry::parse_scoreline(const string &line)
848 {
849 fields.reset(new xlog_fields(line));
850 init_with_fields();
851
852 return true;
853 }
854
_short_branch_name(int branch)855 static const char* _short_branch_name(int branch)
856 {
857 if (branch >= 0 && branch < NUM_BRANCHES)
858 return branches[branch].abbrevname;
859 return "";
860 }
861
862 enum old_job_type
863 {
864 OLD_JOB_THIEF = -1,
865 OLD_JOB_DEATH_KNIGHT = -2,
866 OLD_JOB_PALADIN = -3,
867 OLD_JOB_REAVER = -4,
868 OLD_JOB_STALKER = -5,
869 OLD_JOB_JESTER = -6,
870 OLD_JOB_PRIEST = -7,
871 OLD_JOB_HEALER = -8,
872 OLD_JOB_SKALD = -9,
873 NUM_OLD_JOBS = -OLD_JOB_SKALD
874 };
875
_job_name(int job)876 static const char* _job_name(int job)
877 {
878 switch (job)
879 {
880 case OLD_JOB_THIEF:
881 return "Thief";
882 case OLD_JOB_DEATH_KNIGHT:
883 return "Death Knight";
884 case OLD_JOB_PALADIN:
885 return "Paladin";
886 case OLD_JOB_REAVER:
887 return "Reaver";
888 case OLD_JOB_STALKER:
889 return "Stalker";
890 case OLD_JOB_JESTER:
891 return "Jester";
892 case OLD_JOB_PRIEST:
893 return "Priest";
894 case OLD_JOB_HEALER:
895 return "Healer";
896 case OLD_JOB_SKALD:
897 return "Skald";
898 }
899
900 return get_job_name(static_cast<job_type>(job));
901 }
902
_job_abbrev(int job)903 static const char* _job_abbrev(int job)
904 {
905 switch (job)
906 {
907 case OLD_JOB_THIEF:
908 return "Th";
909 case OLD_JOB_DEATH_KNIGHT:
910 return "DK";
911 case OLD_JOB_PALADIN:
912 return "Pa";
913 case OLD_JOB_REAVER:
914 return "Re";
915 case OLD_JOB_STALKER:
916 return "St";
917 case OLD_JOB_JESTER:
918 return "Jr";
919 case OLD_JOB_PRIEST:
920 return "Pr";
921 case OLD_JOB_HEALER:
922 return "He";
923 case OLD_JOB_SKALD:
924 return "Sk";
925 }
926
927 return get_job_abbrev(static_cast<job_type>(job));
928 }
929
_job_by_name(const string & name)930 static int _job_by_name(const string& name)
931 {
932 int job = get_job_by_name(name.c_str());
933
934 if (job != JOB_UNKNOWN)
935 return job;
936
937 for (job = -1; job >= -NUM_OLD_JOBS; job--)
938 if (name == _job_name(job))
939 return job;
940
941 return JOB_UNKNOWN;
942 }
943
944 enum old_species_type
945 {
946 OLD_SP_ELF = -1,
947 OLD_SP_HILL_DWARF = -2,
948 OLD_SP_OGRE_MAGE = -3,
949 OLD_SP_GREY_ELF = -4,
950 OLD_SP_GNOME = -5,
951 OLD_SP_MOUNTAIN_DWARF = -6,
952 OLD_SP_SLUDGE_ELF = -7,
953 OLD_SP_DJINNI = -8,
954 OLD_SP_LAVA_ORC = -9,
955 NUM_OLD_SPECIES = -OLD_SP_LAVA_ORC
956 };
957
_species_name(int race)958 static string _species_name(int race)
959 {
960 switch (race)
961 {
962 case OLD_SP_ELF: return "Elf";
963 case OLD_SP_HILL_DWARF: return "Hill Dwarf";
964 case OLD_SP_OGRE_MAGE: return "Ogre-Mage";
965 case OLD_SP_GREY_ELF: return "Grey Elf";
966 case OLD_SP_GNOME: return "Gnome";
967 case OLD_SP_MOUNTAIN_DWARF: return "Mountain Dwarf";
968 case OLD_SP_SLUDGE_ELF: return "Sludge Elf";
969 case OLD_SP_DJINNI: return "Djinni";
970 case OLD_SP_LAVA_ORC: return "Lava Orc";
971 }
972
973 // Guard against an ASSERT in get_species_def; it's really bad if the game
974 // crashes at this point while trying to clean up a dead/quit player.
975 // (This doesn't seem to even impact what is shown in the score list?)
976 if (race < 0 || race >= NUM_SPECIES)
977 return "Unknown (buggy) species!";
978
979 return species::name(static_cast<species_type>(race));
980 }
981
_species_abbrev(int race)982 static const char* _species_abbrev(int race)
983 {
984 switch (race)
985 {
986 case OLD_SP_ELF: return "El";
987 case OLD_SP_HILL_DWARF: return "HD";
988 case OLD_SP_OGRE_MAGE: return "OM";
989 case OLD_SP_GREY_ELF: return "GE";
990 case OLD_SP_GNOME: return "Gn";
991 case OLD_SP_MOUNTAIN_DWARF: return "MD";
992 case OLD_SP_SLUDGE_ELF: return "SE";
993 case OLD_SP_DJINNI: return "Dj";
994 case OLD_SP_LAVA_ORC: return "LO";
995 }
996
997 // see note in _species_name: don't ASSERT in get_species_def.
998 if (race < 0 || race >= NUM_SPECIES)
999 return "??";
1000
1001 return species::get_abbrev(static_cast<species_type>(race));
1002 }
1003
_species_by_name(const string & name)1004 static int _species_by_name(const string& name)
1005 {
1006 int race = species::from_str(name);
1007
1008 if (race != SP_UNKNOWN)
1009 return race;
1010
1011 for (race = -1; race >= -NUM_OLD_SPECIES; race--)
1012 if (name == _species_name(race))
1013 return race;
1014
1015 return SP_UNKNOWN;
1016 }
1017
init_with_fields()1018 void scorefile_entry::init_with_fields()
1019 {
1020 version = fields->str_field("v");
1021 save_rcs_version = fields->str_field("vsavrv");
1022 save_tag_version = fields->str_field("vsav");
1023
1024 tiles = fields->int_field("tiles");
1025 points = fields->int_field("sc");
1026
1027 name = fields->str_field("name");
1028 race = _species_by_name(fields->str_field("race"));
1029 job = _job_by_name(fields->str_field("cls"));
1030 lvl = fields->int_field("xl");
1031 race_class_name = fields->str_field("char");
1032
1033 best_skill = str_to_skill_safe(fields->str_field("sk"));
1034 best_skill_lvl = fields->int_field("sklev");
1035 title = fields->str_field("title");
1036
1037 death_type = _str_to_kill_method(fields->str_field("ktyp"));
1038 death_source_name = fields->str_field("killer");
1039 const vector<string> kflags =
1040 split_string(" ", fields->str_field("killer_flags"));
1041 death_source_flags = set<string>(kflags.begin(), kflags.end());
1042
1043 auxkilldata = fields->str_field("kaux");
1044 indirectkiller = fields->str_field("ikiller");
1045 if (indirectkiller.empty())
1046 indirectkiller = death_source_name;
1047 killerpath = fields->str_field("kpath");
1048 last_banisher = fields->str_field("banisher");
1049
1050 branch = branch_by_abbrevname(fields->str_field("br"), BRANCH_DUNGEON);
1051 dlvl = fields->int_field("lvl");
1052 absdepth = fields->int_field("absdepth");
1053
1054 map = fields->str_field("map");
1055 mapdesc = fields->str_field("mapdesc");
1056 killer_map = fields->str_field("killermap");
1057
1058 final_hp = fields->int_field("hp");
1059 final_max_hp = fields->int_field("mhp");
1060 final_max_max_hp = fields->int_field("mmhp");
1061 final_mp = fields->int_field("mp");
1062 final_max_mp = fields->int_field("mmp");
1063 final_base_max_mp = fields->int_field("bmmp");
1064
1065 damage = fields->int_field("dam");
1066 source_damage = fields->int_field("sdam");
1067 turn_damage = fields->int_field("tdam");
1068
1069 str = fields->int_field("str");
1070 intel = fields->int_field("int");
1071 dex = fields->int_field("dex");
1072
1073 ac = fields->int_field("ac");
1074 ev = fields->int_field("ev");
1075 sh = fields->int_field("sh");
1076
1077 god = str_to_god(fields->str_field("god"));
1078 piety = fields->int_field("piety");
1079 penance = fields->int_field("pen");
1080 wiz_mode = fields->int_field("wiz");
1081 explore_mode = fields->int_field("explore");
1082
1083 birth_time = _parse_time(fields->str_field("start"));
1084 death_time = _parse_time(fields->str_field("end"));
1085 real_time = fields->int_field("dur");
1086 num_turns = fields->int_field("turn");
1087 num_aut = fields->int_field("aut");
1088
1089 num_diff_runes = fields->int_field("urune");
1090 num_runes = fields->int_field("nrune");
1091
1092 kills = fields->int_field("kills");
1093 maxed_skills = fields->str_field("maxskills");
1094 fifteen_skills = fields->str_field("fifteenskills");
1095 status_effects = fields->str_field("status");
1096
1097 gold = fields->int_field("gold");
1098 gold_found = fields->int_field("goldfound");
1099 gold_spent = fields->int_field("goldspent");
1100
1101 zigs = fields->int_field("zigscompleted");
1102 zigmax = fields->int_field("zigdeepest");
1103
1104 scrolls_used = fields->int_field("scrollsused");
1105 potions_used = fields->int_field("potionsused");
1106
1107 seed = fields->str_field("seed");
1108
1109 fixup_char_name();
1110 }
1111
set_base_xlog_fields() const1112 void scorefile_entry::set_base_xlog_fields() const
1113 {
1114 if (!fields)
1115 fields.reset(new xlog_fields);
1116
1117 string score_version = SCORE_VERSION;
1118 if (crawl_state.game_is_sprint())
1119 {
1120 /* XXX: hmmm, something better here? */
1121 score_version += "-sprint.1";
1122 }
1123 fields->add_field("v", "%s", Version::Short);
1124 fields->add_field("vlong", "%s", Version::Long);
1125 fields->add_field("lv", "%s", score_version.c_str());
1126 if (!save_rcs_version.empty())
1127 fields->add_field("vsavrv", "%s", save_rcs_version.c_str());
1128 if (!save_tag_version.empty())
1129 fields->add_field("vsav", "%s", save_tag_version.c_str());
1130
1131 #ifdef EXPERIMENTAL_BRANCH
1132 fields->add_field("explbr", EXPERIMENTAL_BRANCH);
1133 #endif
1134 if (tiles)
1135 fields->add_field("tiles", "%d", tiles);
1136 fields->add_field("name", "%s", name.c_str());
1137 fields->add_field("race", "%s", _species_name(race).c_str());
1138 fields->add_field("cls", "%s", _job_name(job));
1139 fields->add_field("char", "%s", race_class_name.c_str());
1140 fields->add_field("xl", "%d", lvl);
1141 fields->add_field("sk", "%s", skill_name(best_skill));
1142 fields->add_field("sklev", "%d", best_skill_lvl);
1143 fields->add_field("title", "%s", title.c_str());
1144
1145 fields->add_field("place", "%s",
1146 level_id(branch, dlvl).describe().c_str());
1147
1148 if (!last_banisher.empty())
1149 fields->add_field("banisher", "%s", last_banisher.c_str());
1150
1151 // Note: "br", "lvl" (and former "ltyp") are redundant with "place"
1152 // but may still be used by DGL logs.
1153 fields->add_field("br", "%s", _short_branch_name(branch));
1154 fields->add_field("lvl", "%d", dlvl);
1155 fields->add_field("absdepth", "%d", absdepth);
1156
1157 fields->add_field("hp", "%d", final_hp);
1158 fields->add_field("mhp", "%d", final_max_hp);
1159 fields->add_field("mmhp", "%d", final_max_max_hp);
1160 fields->add_field("mp", "%d", final_mp);
1161 fields->add_field("mmp", "%d", final_max_mp);
1162 fields->add_field("bmmp", "%d", final_base_max_mp);
1163 fields->add_field("str", "%d", str);
1164 fields->add_field("int", "%d", intel);
1165 fields->add_field("dex", "%d", dex);
1166 fields->add_field("ac", "%d", ac);
1167 fields->add_field("ev", "%d", ev);
1168 fields->add_field("sh", "%d", sh);
1169
1170 fields->add_field("god", "%s", god == GOD_NO_GOD ? "" :
1171 god_name(god).c_str());
1172
1173 if (wiz_mode)
1174 fields->add_field("wiz", "%d", wiz_mode);
1175 if (explore_mode)
1176 fields->add_field("explore", "%d", explore_mode);
1177
1178 fields->add_field("start", "%s", make_date_string(birth_time).c_str());
1179 fields->add_field("dur", "%d", (int)real_time);
1180 fields->add_field("turn", "%d", num_turns);
1181 fields->add_field("aut", "%d", num_aut);
1182
1183 if (num_diff_runes)
1184 fields->add_field("urune", "%d", num_diff_runes);
1185
1186 if (num_runes)
1187 fields->add_field("nrune", "%d", num_runes);
1188
1189 fields->add_field("kills", "%d", kills);
1190 if (!maxed_skills.empty())
1191 fields->add_field("maxskills", "%s", maxed_skills.c_str());
1192 if (!fifteen_skills.empty())
1193 fields->add_field("fifteenskills", "%s", fifteen_skills.c_str());
1194 if (!status_effects.empty())
1195 fields->add_field("status", "%s", status_effects.c_str());
1196
1197 fields->add_field("gold", "%d", gold);
1198 fields->add_field("goldfound", "%d", gold_found);
1199 fields->add_field("goldspent", "%d", gold_spent);
1200 if (zigs)
1201 fields->add_field("zigscompleted", "%d", zigs);
1202 if (zigmax)
1203 fields->add_field("zigdeepest", "%d", zigmax);
1204 fields->add_field("scrollsused", "%d", scrolls_used);
1205 fields->add_field("potionsused", "%d", potions_used);
1206 }
1207
set_score_fields() const1208 void scorefile_entry::set_score_fields() const
1209 {
1210 fields.reset(new xlog_fields);
1211
1212 if (!fields)
1213 return;
1214
1215 set_base_xlog_fields();
1216
1217 fields->add_field("sc", "%d", points);
1218 fields->add_field("ktyp", "%s", _kill_method_name(kill_method_type(death_type)));
1219
1220 fields->add_field("killer", "%s", death_source_desc().c_str());
1221 if (!death_source_flags.empty())
1222 {
1223 const string kflags = comma_separated_line(
1224 death_source_flags.begin(),
1225 death_source_flags.end(),
1226 " ", " ");
1227 fields->add_field("killer_flags", "%s", kflags.c_str());
1228 }
1229 fields->add_field("dam", "%d", damage);
1230 fields->add_field("sdam", "%d", source_damage);
1231 fields->add_field("tdam", "%d", turn_damage);
1232
1233 fields->add_field("kaux", "%s", auxkilldata.c_str());
1234
1235 if (indirectkiller != death_source_desc())
1236 fields->add_field("ikiller", "%s", indirectkiller.c_str());
1237
1238 if (!killerpath.empty())
1239 fields->add_field("kpath", "%s", killerpath.c_str());
1240
1241 if (piety > 0)
1242 fields->add_field("piety", "%d", piety);
1243 if (penance > 0)
1244 fields->add_field("pen", "%d", penance);
1245
1246 fields->add_field("end", "%s", make_date_string(death_time).c_str());
1247
1248 if (!map.empty())
1249 {
1250 fields->add_field("map", "%s", map.c_str());
1251 if (!mapdesc.empty())
1252 fields->add_field("mapdesc", "%s", mapdesc.c_str());
1253 }
1254
1255 if (!killer_map.empty())
1256 fields->add_field("killermap", "%s", killer_map.c_str());
1257
1258 fields->add_field("seed", "%s", seed.c_str());
1259
1260 #ifdef DGL_EXTENDED_LOGFILES
1261 const string short_msg = short_kill_message();
1262 fields->add_field("tmsg", "%s", short_msg.c_str());
1263 const string long_msg = long_kill_message();
1264 if (long_msg != short_msg)
1265 fields->add_field("vmsg", "%s", long_msg.c_str());
1266 #endif
1267 }
1268
make_oneline(const string & ml) const1269 string scorefile_entry::make_oneline(const string &ml) const
1270 {
1271 vector<string> lines = split_string("\n", ml);
1272 for (string &s : lines)
1273 {
1274 if (starts_with(s, "..."))
1275 {
1276 s = s.substr(3);
1277 trim_string(s);
1278 }
1279 }
1280 return comma_separated_line(lines.begin(), lines.end(), " ", " ");
1281 }
1282
long_kill_message() const1283 string scorefile_entry::long_kill_message() const
1284 {
1285 string msg = death_description(DDV_LOGVERBOSE);
1286 msg = make_oneline(msg);
1287 msg[0] = tolower_safe(msg[0]);
1288 trim_string(msg);
1289 return msg;
1290 }
1291
short_kill_message() const1292 string scorefile_entry::short_kill_message() const
1293 {
1294 string msg = death_description(DDV_ONELINE);
1295 msg = make_oneline(msg);
1296 msg[0] = tolower_safe(msg[0]);
1297 trim_string(msg);
1298 return msg;
1299 }
1300
1301 /**
1302 * Remove from a string everything up to and including a given infix.
1303 *
1304 * @param[in,out] str The string to modify.
1305 * @param[in] infix The infix to remove.
1306 * @post If \c infix occurred as a substring of <tt>str</tt>, \c str is updated
1307 * by removing all characters up to and including the last character
1308 * of the the first occurrence. Otherwise, \c str is unchanged.
1309 * @return \c true if \c str was modified, \c false otherwise.
1310 */
_strip_to(string & str,const char * infix)1311 static bool _strip_to(string &str, const char *infix)
1312 {
1313 // Don't treat stripping the empty string as a change.
1314 if (*infix == '\0')
1315 return false;
1316
1317 size_t pos = str.find(infix);
1318 if (pos != string::npos)
1319 {
1320 str.erase(0, pos + strlen(infix));
1321 return true;
1322 }
1323 return false;
1324 }
1325
init_death_cause(int dam,mid_t dsrc,int dtype,const char * aux,const char * dsrc_name)1326 void scorefile_entry::init_death_cause(int dam, mid_t dsrc,
1327 int dtype, const char *aux,
1328 const char *dsrc_name)
1329 {
1330 death_source = dsrc;
1331 death_type = dtype;
1332 damage = dam;
1333
1334 const monster *source_monster = monster_by_mid(death_source);
1335 if (source_monster)
1336 killer_map = source_monster->originating_map();
1337
1338 // Set the default aux data value...
1339 // If aux is passed in (ie for a trap), we'll default to that.
1340 if (aux == nullptr)
1341 auxkilldata.clear();
1342 else
1343 auxkilldata = aux;
1344
1345 // for death by monster
1346 if ((death_type == KILLED_BY_MONSTER
1347 || death_type == KILLED_BY_HEADBUTT
1348 || death_type == KILLED_BY_BEAM
1349 || death_type == KILLED_BY_FREEZING
1350 || death_type == KILLED_BY_DISINT
1351 || death_type == KILLED_BY_ACID
1352 || death_type == KILLED_BY_DRAINING
1353 || death_type == KILLED_BY_BURNING
1354 || death_type == KILLED_BY_DEATH_EXPLOSION
1355 || death_type == KILLED_BY_CLOUD
1356 || death_type == KILLED_BY_ROTTING
1357 || death_type == KILLED_BY_REFLECTION
1358 || death_type == KILLED_BY_ROLLING
1359 || death_type == KILLED_BY_SPINES
1360 || death_type == KILLED_BY_WATER
1361 || death_type == KILLED_BY_BEING_THROWN
1362 || death_type == KILLED_BY_COLLISION
1363 || death_type == KILLED_BY_CONSTRICTION)
1364 && monster_by_mid(death_source))
1365 {
1366 const monster* mons = monster_by_mid(death_source);
1367 ASSERT(mons);
1368
1369 // Previously the weapon was only used for dancing weapons,
1370 // but now we pass it in as a string through the scorefile
1371 // entry to be appended in hiscores_format_single in long or
1372 // medium scorefile formats.
1373 if (death_type == KILLED_BY_MONSTER
1374 && mons->inv[MSLOT_WEAPON] != NON_ITEM)
1375 {
1376 // [ds] The highscore entry may be constructed while the player
1377 // is alive (for notes), so make sure we don't reveal info we
1378 // shouldn't.
1379 if (you.hp <= 0)
1380 {
1381 set_ident_flags(env.item[mons->inv[MSLOT_WEAPON]],
1382 ISFLAG_IDENT_MASK);
1383 }
1384
1385 // Setting this is redundant for dancing weapons, however
1386 // we do care about the above indentification. -- bwr
1387 if (!mons_class_is_animated_weapon(mons->type)
1388 && mons->get_defining_object())
1389 {
1390 auxkilldata = mons->get_defining_object()->name(DESC_A);
1391 }
1392 }
1393
1394 const bool death = (you.hp <= 0 || death_type == KILLED_BY_DRAINING);
1395
1396 const description_level_type desc =
1397 death_type == KILLED_BY_DEATH_EXPLOSION ? DESC_PLAIN : DESC_A;
1398
1399 death_source_name = mons->name(desc, death);
1400
1401 if (death || you.can_see(*mons))
1402 death_source_name = mons->full_name(desc);
1403
1404 // Some shadows have names
1405 if (mons_is_player_shadow(*mons) && mons->mname.empty())
1406 death_source_name = "their own shadow"; // heh
1407
1408 if (mons->mid == MID_YOU_FAULTLESS)
1409 death_source_name = "themself";
1410
1411 if (mons->has_ench(ENCH_SHAPESHIFTER))
1412 death_source_name += " (shapeshifter)";
1413 else if (mons->has_ench(ENCH_GLOWING_SHAPESHIFTER))
1414 death_source_name += " (glowing shapeshifter)";
1415
1416 if (mons->type == MONS_PANDEMONIUM_LORD)
1417 death_source_name += " the pandemonium lord";
1418
1419 if (mons->has_ench(ENCH_PHANTOM_MIRROR))
1420 death_source_name += " (illusionary)";
1421
1422 if (mons_is_unique(mons->type))
1423 death_source_flags.insert("unique");
1424
1425 if (mons->props.exists("blame"))
1426 {
1427 const CrawlVector& blame = mons->props["blame"].get_vector();
1428
1429 indirectkiller = blame[blame.size() - 1].get_string();
1430 _strip_to(indirectkiller, " by ");
1431 _strip_to(indirectkiller, "ed to "); // "attached to" and similar
1432
1433 killerpath = "";
1434
1435 for (const auto &bl : blame)
1436 killerpath = killerpath + ":" + _xlog_escape(bl.get_string());
1437
1438 killerpath.erase(killerpath.begin());
1439 }
1440 else
1441 {
1442 indirectkiller = death_source_name;
1443 killerpath = "";
1444 }
1445 }
1446 else if (death_type == KILLED_BY_DISINT
1447 || death_type == KILLED_BY_CLOUD)
1448 {
1449 death_source_name = dsrc_name ? dsrc_name :
1450 dsrc == MHITYOU ? "you" :
1451 "";
1452 indirectkiller = killerpath = "";
1453 }
1454 else
1455 {
1456 if (dsrc_name)
1457 death_source_name = dsrc_name;
1458 else
1459 death_source_name.clear();
1460 indirectkiller = killerpath = "";
1461 }
1462
1463 if (death_type == KILLED_BY_WEAKNESS
1464 || death_type == KILLED_BY_STUPIDITY
1465 || death_type == KILLED_BY_CLUMSINESS)
1466 {
1467 if (auxkilldata.empty())
1468 auxkilldata = "unknown source";
1469 }
1470
1471 if (death_type == KILLED_BY_POISON)
1472 {
1473 death_source_name = you.props["poisoner"].get_string();
1474 auxkilldata = you.props["poison_aux"].get_string();
1475 }
1476
1477 if (death_type == KILLED_BY_BURNING)
1478 {
1479 death_source_name = you.props["sticky_flame_source"].get_string();
1480 auxkilldata = you.props["sticky_flame_aux"].get_string();
1481 }
1482 }
1483
reset()1484 void scorefile_entry::reset()
1485 {
1486 // simple init
1487 raw_line.clear();
1488 version.clear();
1489 save_rcs_version.clear();
1490 save_tag_version.clear();
1491 tiles = 0;
1492 points = -1;
1493 name.clear();
1494 race = SP_UNKNOWN;
1495 job = JOB_UNKNOWN;
1496 lvl = 0;
1497 race_class_name.clear();
1498 best_skill = SK_NONE;
1499 best_skill_lvl = 0;
1500 title.clear();
1501 death_type = KILLED_BY_SOMETHING;
1502 death_source = MID_NOBODY;
1503 death_source_name.clear();
1504 auxkilldata.clear();
1505 indirectkiller.clear();
1506 killerpath.clear();
1507 last_banisher.clear();
1508 dlvl = 0;
1509 absdepth = 1;
1510 branch = BRANCH_DUNGEON;
1511 map.clear();
1512 mapdesc.clear();
1513 final_hp = -1;
1514 final_max_hp = -1;
1515 final_max_max_hp = -1;
1516 final_mp = -1;
1517 final_max_mp = -1;
1518 final_base_max_mp = -1;
1519 str = -1;
1520 intel = -1;
1521 dex = -1;
1522 ac = -1;
1523 ev = -1;
1524 sh = -1;
1525 damage = -1;
1526 source_damage = -1;
1527 turn_damage = -1;
1528 god = GOD_NO_GOD;
1529 piety = -1;
1530 penance = -1;
1531 wiz_mode = 0;
1532 explore_mode = 0;
1533 birth_time = 0;
1534 death_time = 0;
1535 real_time = -1;
1536 num_turns = -1;
1537 num_aut = -1;
1538 num_diff_runes = 0;
1539 num_runes = 0;
1540 kills = 0;
1541 maxed_skills.clear();
1542 fifteen_skills.clear();
1543 status_effects.clear();
1544 gold = 0;
1545 gold_found = 0;
1546 gold_spent = 0;
1547 zigs = 0;
1548 zigmax = 0;
1549 scrolls_used = 0;
1550 potions_used = 0;
1551 seed.clear();
1552 }
1553
_award_modified_experience()1554 static int _award_modified_experience()
1555 {
1556 int xp = you.experience;
1557 int result = 0;
1558
1559 if (xp <= 250000)
1560 return xp * 7 / 10;
1561
1562 result += 250000 * 7 / 10;
1563 xp -= 250000;
1564
1565 if (xp <= 750000)
1566 {
1567 result += xp * 4 / 10;
1568 return result;
1569 }
1570
1571 result += 750000 * 4 / 10;
1572 xp -= 750000;
1573
1574 if (xp <= 2000000)
1575 {
1576 result += xp * 2 / 10;
1577 return result;
1578 }
1579
1580 result += 2000000 * 2 / 10;
1581 xp -= 2000000;
1582
1583 result += xp / 10;
1584
1585 return result;
1586 }
1587
init(time_t dt)1588 void scorefile_entry::init(time_t dt)
1589 {
1590 // Score file entry version:
1591 //
1592 // 4.0 - original versioned entry
1593 // 4.1 - added real_time and num_turn fields
1594 // 4.2 - stats and god info
1595
1596 version = Version::Short;
1597 #ifdef USE_TILE_LOCAL
1598 tiles = 1;
1599 #elif defined (USE_TILE_WEB)
1600 tiles = ::tiles.is_controlled_from_web();
1601 #else
1602 tiles = 0;
1603 #endif
1604 name = you.your_name;
1605
1606 save_rcs_version = crawl_state.save_rcs_version;
1607 if (crawl_state.minor_version > 0)
1608 {
1609 save_tag_version = make_stringf("%d.%d", TAG_MAJOR_VERSION,
1610 crawl_state.minor_version);
1611 }
1612
1613 /*
1614 * old scoring system (0.1-0.3):
1615 *
1616 * Gold
1617 * + 0.7 * Experience
1618 * + (distinct Runes +2)^2 * 1000, winners with distinct runes >= 3 only
1619 * + value of Inventory, for winners only
1620 *
1621 *
1622 * 0.4 scoring system, as suggested by Lemuel:
1623 *
1624 * Gold
1625 * + 0.7 * Experience up to 250,000
1626 * + 0.4 * Experience between 250,000 and 1,000,000
1627 * + 0.2 * Experience between 1,000,000 and 3,000,000
1628 * + 0.1 * Experience above 3,000,000
1629 * + (distinct Runes +2)^2 * 1000, winners with distinct runes >= 3 only
1630 * + value of Inventory, for winners only
1631 * + (250,000 * d. runes) * (25,000/(turns/d. runes)), for winners only
1632 *
1633 * current scoring system (mostly the same as above):
1634 *
1635 * Experience terms as above
1636 * + runes * (runes + 12) * 1000 (for everyone)
1637 * + (250000 + 2 * (runes + 2) * 1000) (winners only)
1638 * + 250000 * 25000 * runes^2 / turns (winners only)
1639 */
1640
1641 // do points first.
1642 points = 0;
1643 bool base_score = true;
1644
1645 dlua.pushglobal("dgn.persist.calc_score");
1646 lua_pushboolean(dlua, death_type == KILLED_BY_WINNING);
1647 if (dlua.callfn(nullptr, 1, 2))
1648 dlua.fnreturns(">db", &points, &base_score);
1649
1650 // If calc_score didn't exist, or returned true as its second value,
1651 // use the default formula.
1652 if (base_score)
1653 {
1654 // sprint games could overflow a 32 bit value
1655 uint64_t pt = points + _award_modified_experience();
1656
1657 num_runes = runes_in_pack();
1658 num_diff_runes = num_runes;
1659
1660 // There's no point in rewarding lugging artefacts. Thus, no points
1661 // for the value of the inventory. -- 1KB
1662 if (death_type == KILLED_BY_WINNING)
1663 {
1664 pt += 250000; // the Orb
1665 pt += num_runes * 2000 + 4000;
1666 pt += ((uint64_t)250000) * 25000 * num_runes * num_runes
1667 / (1+you.num_turns);
1668 }
1669 pt += num_runes * 10000;
1670 pt += num_runes * (num_runes + 2) * 1000;
1671
1672 points = pt;
1673 }
1674 else
1675 ASSERT(crawl_state.game_is_sprint());
1676 // only sprint should use custom scores
1677
1678 race = you.species;
1679 job = you.char_class;
1680
1681 race_class_name.clear();
1682 fixup_char_name();
1683
1684 lvl = you.experience_level;
1685 best_skill = ::best_skill(SK_FIRST_SKILL, SK_LAST_SKILL);
1686 best_skill_lvl = you.skills[ best_skill ];
1687 title = player_title(false);
1688
1689 // Note all skills at level 27, and also all skills at level >= 15.
1690 for (skill_type sk = SK_FIRST_SKILL; sk < NUM_SKILLS; ++sk)
1691 {
1692 if (you.skills[sk] == 27)
1693 {
1694 if (!maxed_skills.empty())
1695 maxed_skills += ",";
1696 maxed_skills += skill_name(sk);
1697 }
1698 if (you.skills[sk] >= 15)
1699 {
1700 if (!fifteen_skills.empty())
1701 fifteen_skills += ",";
1702 fifteen_skills += skill_name(sk);
1703 }
1704 }
1705
1706 status_info inf;
1707 for (unsigned i = 0; i <= STATUS_LAST_STATUS; ++i)
1708 {
1709 if (fill_status_info(i, inf) && !inf.short_text.empty())
1710 {
1711 if (!status_effects.empty())
1712 status_effects += ",";
1713 status_effects += inf.short_text;
1714 }
1715 }
1716
1717 kills = you.kills.total_kills();
1718
1719 final_hp = you.hp;
1720 final_max_hp = you.hp_max;
1721 final_max_max_hp = get_real_hp(true, false);
1722
1723 final_mp = you.magic_points;
1724 final_max_mp = you.max_magic_points;
1725 final_base_max_mp = get_real_mp(false);
1726
1727 source_damage = you.source_damage;
1728 turn_damage = you.turn_damage;
1729
1730 // Use possibly negative stat values.
1731 str = you.stat(STAT_STR, false);
1732 intel = you.stat(STAT_INT, false);
1733 dex = you.stat(STAT_DEX, false);
1734
1735 ac = you.armour_class();
1736 ev = you.evasion();
1737 sh = player_displayed_shield_class();
1738
1739 god = you.religion;
1740 if (!you_worship(GOD_NO_GOD))
1741 {
1742 piety = you.piety;
1743 penance = you.penance[you.religion];
1744 }
1745
1746 branch = you.where_are_you; // no adjustments necessary.
1747 dlvl = you.depth;
1748
1749 absdepth = env.absdepth0 + 1; // 1-based absolute depth.
1750
1751 last_banisher = you.banished_by;
1752
1753 if (const vault_placement *vp = dgn_vault_at(you.pos()))
1754 {
1755 map = vp->map_name_at(you.pos());
1756 mapdesc = vp->map.description;
1757 }
1758
1759 birth_time = you.birth_time; // start time of game
1760 death_time = (dt != 0 ? dt : time(nullptr)); // end time of game
1761
1762 handle_real_time(chrono::system_clock::from_time_t(death_time));
1763 real_time = you.real_time();
1764
1765 num_turns = you.num_turns;
1766 num_aut = you.elapsed_time;
1767
1768 gold = you.gold;
1769 gold_found = you.attribute[ATTR_GOLD_FOUND];
1770 gold_spent = you.attribute[ATTR_PURCHASES];
1771
1772 zigs = you.zigs_completed;
1773 zigmax = you.zig_max;
1774
1775 scrolls_used = 0;
1776 pair<caction_type, int> p(CACT_USE, caction_compound(OBJ_SCROLLS));
1777
1778 const int maxlev = min<int>(you.max_level, 27);
1779 if (you.action_count.count(p))
1780 for (int i = 0; i < maxlev; i++)
1781 scrolls_used += you.action_count[p][i];
1782
1783 potions_used = 0;
1784 p = make_pair(CACT_USE, caction_compound(OBJ_POTIONS));
1785 if (you.action_count.count(p))
1786 for (int i = 0; i < maxlev; i++)
1787 potions_used += you.action_count[p][i];
1788
1789 wiz_mode = (you.wizard || you.suppress_wizard ? 1 : 0);
1790 explore_mode = (you.explore ? 1 : 0);
1791 seed = make_stringf("%" PRIu64, crawl_state.seed);
1792 }
1793
hiscore_line(death_desc_verbosity verbosity) const1794 string scorefile_entry::hiscore_line(death_desc_verbosity verbosity) const
1795 {
1796 string line = character_description(verbosity);
1797 line += death_description(verbosity);
1798 line += death_place(verbosity);
1799 line += game_time(verbosity);
1800
1801 return line;
1802 }
1803
game_time(death_desc_verbosity verbosity) const1804 string scorefile_entry::game_time(death_desc_verbosity verbosity) const
1805 {
1806 string line;
1807
1808 if (verbosity == DDV_VERBOSE)
1809 {
1810 line += make_stringf("The game lasted %s (%d turns).",
1811 make_time_string(real_time).c_str(), num_turns);
1812
1813 line += _hiscore_newline_string();
1814 }
1815
1816 return line;
1817 }
1818
damage_verb() const1819 const char *scorefile_entry::damage_verb() const
1820 {
1821 // GDL: here's an example of using final_hp. Verbiage could be better.
1822 // bwr: changed "blasted" since this is for melee
1823 return (final_hp > -6) ? "Slain" :
1824 (final_hp > -14) ? "Mangled" :
1825 (final_hp > -22) ? "Demolished"
1826 : "Annihilated";
1827 }
1828
death_source_desc() const1829 string scorefile_entry::death_source_desc() const
1830 {
1831 return death_source_name;
1832 }
1833
damage_string(bool terse) const1834 string scorefile_entry::damage_string(bool terse) const
1835 {
1836 return make_stringf("(%d%s)", damage,
1837 terse? "" : " damage");
1838 }
1839
strip_article_a(const string & s) const1840 string scorefile_entry::strip_article_a(const string &s) const
1841 {
1842 if (starts_with(s, "a "))
1843 return s.substr(2);
1844 else if (starts_with(s, "an "))
1845 return s.substr(3);
1846 return s;
1847 }
1848
terse_missile_name() const1849 string scorefile_entry::terse_missile_name() const
1850 {
1851 const string pre_post[][2] =
1852 {
1853 { "Shot with ", " by " },
1854 { "Hit by ", " thrown by " }
1855 };
1856 const string &aux = auxkilldata;
1857 string missile;
1858
1859 for (const string (&affixes)[2] : pre_post)
1860 {
1861 if (!starts_with(aux, affixes[0]))
1862 continue;
1863
1864 string::size_type end = aux.rfind(affixes[1]);
1865 if (end == string::npos)
1866 continue;
1867
1868 int istart = affixes[0].length();
1869 int nchars = end - istart;
1870 missile = aux.substr(istart, nchars);
1871
1872 // Was this prefixed by "a" or "an"?
1873 // (This should only ever not be the case with Robin and Ijyb.)
1874 missile = strip_article_a(missile);
1875 }
1876 return missile;
1877 }
1878
terse_missile_cause() const1879 string scorefile_entry::terse_missile_cause() const
1880 {
1881 const string &aux = auxkilldata;
1882
1883 string monster_prefix = " by ";
1884 // We're looking for Shot with a%s %s by %s/ Hit by a%s %s thrown by %s
1885 string::size_type by = aux.rfind(monster_prefix);
1886 if (by == string::npos)
1887 return "???";
1888
1889 string mcause = aux.substr(by + monster_prefix.length());
1890 mcause = strip_article_a(mcause);
1891
1892 string missile = terse_missile_name();
1893
1894 if (!missile.empty())
1895 mcause += "/" + missile;
1896
1897 return mcause;
1898 }
1899
terse_beam_cause() const1900 string scorefile_entry::terse_beam_cause() const
1901 {
1902 string cause = auxkilldata;
1903 if (starts_with(cause, "by ") || starts_with(cause, "By "))
1904 cause = cause.substr(3);
1905 return cause;
1906 }
1907
terse_wild_magic() const1908 string scorefile_entry::terse_wild_magic() const
1909 {
1910 return terse_beam_cause();
1911 }
1912
fixup_char_name()1913 void scorefile_entry::fixup_char_name()
1914 {
1915 if (race_class_name.empty())
1916 {
1917 race_class_name = make_stringf("%s%s",
1918 _species_abbrev(race),
1919 _job_abbrev(job));
1920 }
1921 }
1922
single_cdesc() const1923 string scorefile_entry::single_cdesc() const
1924 {
1925 string scname;
1926 scname = chop_string(name, 10);
1927
1928 return make_stringf("%8d %s %s-%02d%s", points, scname.c_str(),
1929 race_class_name.c_str(), lvl,
1930 (wiz_mode == 1) ? "W" : (explore_mode == 1) ? "E" : "");
1931 }
1932
_append_sentence_delimiter(const string & sentence,const string & delimiter)1933 static string _append_sentence_delimiter(const string &sentence,
1934 const string &delimiter)
1935 {
1936 if (sentence.empty())
1937 return sentence;
1938
1939 const char lastch = sentence[sentence.length() - 1];
1940 if (lastch == '!' || lastch == '.')
1941 return sentence;
1942
1943 return sentence + delimiter;
1944 }
1945
1946 string
character_description(death_desc_verbosity verbosity) const1947 scorefile_entry::character_description(death_desc_verbosity verbosity) const
1948 {
1949 bool single = verbosity == DDV_TERSE || verbosity == DDV_ONELINE;
1950
1951 if (single)
1952 return single_cdesc();
1953
1954 bool verbose = verbosity == DDV_VERBOSE;
1955
1956 string desc;
1957 // Please excuse the following bit of mess in the name of flavour ;)
1958 if (verbose)
1959 {
1960 desc = make_stringf("%8d %s the %s (level %d",
1961 points, name.c_str(), title.c_str(), lvl);
1962 }
1963 else
1964 {
1965 desc = make_stringf("%8d %s the %s %s (level %d",
1966 points, name.c_str(),
1967 _species_name(race).c_str(),
1968 _job_name(job), lvl);
1969 }
1970
1971 if (final_max_max_hp > 0) // as the other two may be negative
1972 {
1973 desc += make_stringf(", %d/%d", final_hp, final_max_hp);
1974
1975 if (final_max_hp < final_max_max_hp)
1976 desc += make_stringf(" (%d)", final_max_max_hp);
1977
1978 desc += " HPs";
1979 }
1980
1981 desc += wiz_mode ? ") *WIZ*" : explore_mode ? ") *EXPLORE*" : ")";
1982 desc += _hiscore_newline_string();
1983
1984 if (verbose)
1985 {
1986 string srace = _species_name(race);
1987 desc += make_stringf("Began as a%s %s %s",
1988 is_vowel(srace[0]) ? "n" : "",
1989 srace.c_str(),
1990 _job_name(job));
1991
1992 ASSERT(birth_time);
1993 desc += " on ";
1994 desc += _hiscore_date_string(birth_time);
1995 // TODO: show seed here?
1996
1997 desc = _append_sentence_delimiter(desc, ".");
1998 desc += _hiscore_newline_string();
1999
2000 if (god != GOD_NO_GOD
2001 // XX is this check really needed?
2002 && !species::mutation_level(static_cast<species_type>(race), MUT_FORLORN))
2003 {
2004 if (god == GOD_XOM)
2005 {
2006 desc += make_stringf("Was a %sPlaything of Xom.",
2007 (lvl >= 20) ? "Favourite " : "");
2008
2009 desc += _hiscore_newline_string();
2010 }
2011 else
2012 {
2013 // Not exactly the same as the religion screen, but
2014 // good enough to fill this slot for now.
2015 desc += make_stringf("Was %s of %s%s",
2016 (piety >= piety_breakpoint(5)) ? "the Champion" :
2017 (piety >= piety_breakpoint(4)) ? "a High Priest" :
2018 (piety >= piety_breakpoint(3)) ? "an Elder" :
2019 (piety >= piety_breakpoint(2)) ? "a Priest" :
2020 (piety >= piety_breakpoint(1)) ? "a Believer" :
2021 (piety >= piety_breakpoint(0)) ? "a Follower"
2022 : "an Initiate",
2023 god_name(god).c_str(),
2024 (penance > 0) ? " (penitent)." : ".");
2025
2026 desc += _hiscore_newline_string();
2027 }
2028 }
2029 }
2030
2031 return desc;
2032 }
2033
death_place(death_desc_verbosity verbosity) const2034 string scorefile_entry::death_place(death_desc_verbosity verbosity) const
2035 {
2036 bool verbose = (verbosity == DDV_VERBOSE);
2037 string place;
2038
2039 if (death_type == KILLED_BY_LEAVING || death_type == KILLED_BY_WINNING)
2040 return "";
2041
2042 if (verbosity == DDV_ONELINE || verbosity == DDV_TERSE)
2043 return " (" + level_id(branch, dlvl).describe() + ")";
2044
2045 if (verbose && death_type != KILLED_BY_QUITTING && death_type != KILLED_BY_WIZMODE)
2046 place += "...";
2047
2048 // where did we die?
2049 place += " " + prep_branch_level_name(level_id(branch, dlvl));
2050
2051 if (!mapdesc.empty())
2052 place += make_stringf(" (%s)", mapdesc.c_str());
2053
2054 if (verbose && death_time
2055 && !_hiscore_same_day(birth_time, death_time))
2056 {
2057 place += " on ";
2058 place += _hiscore_date_string(death_time);
2059 }
2060
2061 place = _append_sentence_delimiter(place, ".");
2062 place += _hiscore_newline_string();
2063
2064 return place;
2065 }
2066
2067 /**
2068 * Describes the cause of the player's death.
2069 *
2070 * @param verbosity The verbosity of the description.
2071 * @return A description of the cause of death.
2072 */
death_description(death_desc_verbosity verbosity) const2073 string scorefile_entry::death_description(death_desc_verbosity verbosity) const
2074 {
2075 bool needs_beam_cause_line = false;
2076 bool needs_called_by_monster_line = false;
2077 bool needs_damage = false;
2078
2079 const bool terse = (verbosity == DDV_TERSE);
2080 const bool semiverbose = (verbosity == DDV_LOGVERBOSE);
2081 const bool verbose = (verbosity == DDV_VERBOSE || semiverbose);
2082 const bool oneline = (verbosity == DDV_ONELINE);
2083
2084 string desc;
2085
2086 if (oneline)
2087 desc = " ";
2088
2089 switch (death_type)
2090 {
2091 case KILLED_BY_MONSTER:
2092 if (terse)
2093 desc += death_source_desc();
2094 else if (oneline)
2095 desc += "slain by " + death_source_desc();
2096 else
2097 {
2098 desc += damage_verb();
2099 desc += " by ";
2100 desc += death_source_desc();
2101 }
2102
2103 // put the damage on the weapon line if there is one
2104 if (auxkilldata.empty())
2105 needs_damage = true;
2106 break;
2107
2108 case KILLED_BY_HEADBUTT:
2109 if (terse)
2110 desc += apostrophise(death_source_desc()) + " headbutt";
2111 else
2112 desc += "Headbutted by " + death_source_desc();
2113 needs_damage = true;
2114 break;
2115
2116 case KILLED_BY_ROLLING:
2117 if (terse)
2118 desc += "squashed by " + death_source_desc();
2119 else
2120 desc += "Rolled over by " + death_source_desc();
2121 needs_damage = true;
2122 break;
2123
2124 case KILLED_BY_SPINES:
2125 if (terse)
2126 desc += apostrophise(death_source_desc()) + " spines";
2127 else
2128 desc += "Impaled on " + apostrophise(death_source_desc()) + " spines" ;
2129 needs_damage = true;
2130 break;
2131
2132 case KILLED_BY_POISON:
2133 if (death_source_name.empty() || terse)
2134 {
2135 if (!terse)
2136 desc += "Succumbed to poison";
2137 else if (!death_source_name.empty())
2138 desc += "poisoned by " + death_source_name;
2139 else
2140 desc += "poison";
2141 if (!auxkilldata.empty())
2142 desc += " (" + auxkilldata + ")";
2143 }
2144 else if (auxkilldata.empty()
2145 && death_source_name.find("poison") != string::npos)
2146 {
2147 desc += "Succumbed to " + death_source_name;
2148 }
2149 else
2150 {
2151 desc += "Succumbed to " + ((death_source_name == "you")
2152 ? "their own" : apostrophise(death_source_name)) + " "
2153 + (auxkilldata.empty()? "poison" : auxkilldata);
2154 }
2155 break;
2156
2157 case KILLED_BY_CLOUD:
2158 ASSERT(!auxkilldata.empty()); // there are no nameless clouds
2159 if (terse)
2160 if (death_source_name.empty())
2161 desc += "cloud of " + auxkilldata;
2162 else
2163 desc += "cloud of " +auxkilldata + " [" +
2164 death_source_name == "you" ? "self" : death_source_name
2165 + "]";
2166 else
2167 {
2168 desc += make_stringf("Engulfed by %s%s %s",
2169 death_source_name.empty() ? "a" :
2170 death_source_name == "you" ? "their own" :
2171 apostrophise(death_source_name).c_str(),
2172 death_source_name.empty() ? " cloud of" : "",
2173 auxkilldata.c_str());
2174 }
2175 needs_damage = true;
2176 break;
2177
2178 case KILLED_BY_BEAM:
2179 if (oneline || semiverbose)
2180 {
2181 // keeping this short to leave room for the deep elf spellcasters:
2182 desc += make_stringf("%s by ",
2183 _range_type_verb(auxkilldata.c_str()));
2184 desc += (death_source_name == "you") ? "themself"
2185 : death_source_desc();
2186
2187 if (semiverbose)
2188 {
2189 string beam = terse_missile_name();
2190 if (beam.empty())
2191 beam = terse_beam_cause();
2192 trim_string(beam);
2193 if (!beam.empty())
2194 desc += make_stringf(" (%s)", beam.c_str());
2195 }
2196 }
2197 else if (isupper(auxkilldata[0])) // already made (ie shot arrows)
2198 {
2199 // If terse we have to parse the information from the string.
2200 // Darn it to heck.
2201 desc += terse? terse_missile_cause() : auxkilldata;
2202 needs_damage = true;
2203 }
2204 else if (verbose && starts_with(auxkilldata, "by "))
2205 {
2206 // "by" is used for priest attacks where the effect is indirect
2207 // in verbose format we have another line for the monster
2208 if (death_source_name == "you")
2209 {
2210 needs_damage = true;
2211 desc += make_stringf("Killed by their own %s",
2212 auxkilldata.substr(3).c_str());
2213 }
2214 else
2215 {
2216 needs_called_by_monster_line = true;
2217 desc += make_stringf("Killed %s",
2218 auxkilldata.c_str());
2219 }
2220 }
2221 else
2222 {
2223 // Note: This is also used for the "by" cases in non-verbose
2224 // mode since listing the monster is more imporatant.
2225 if (semiverbose)
2226 desc += "Killed by ";
2227 else if (!terse)
2228 desc += "Killed from afar by ";
2229
2230 if (death_source_name == "you")
2231 desc += "themself";
2232 else
2233 desc += death_source_desc();
2234
2235 if (!auxkilldata.empty())
2236 needs_beam_cause_line = true;
2237
2238 needs_damage = true;
2239 }
2240 break;
2241
2242 case KILLED_BY_LAVA:
2243 if (terse)
2244 desc += "lava";
2245 else
2246 {
2247 if (starts_with(species::skin_name(
2248 static_cast<species_type>(race)), "bandage"))
2249 {
2250 desc += "Turned to ash by lava";
2251 }
2252 else
2253 desc += "Took a swim in molten lava";
2254 }
2255 break;
2256
2257 case KILLED_BY_WATER:
2258 if (species::is_undead(static_cast<species_type>(race)))
2259 {
2260 if (terse)
2261 desc = "fell apart";
2262 else if (starts_with(species::skin_name(
2263 static_cast<species_type>(race)), "bandage"))
2264 {
2265 desc = "Soaked and fell apart";
2266 }
2267 else
2268 desc = "Sank and fell apart";
2269 }
2270 else
2271 {
2272 if (!death_source_name.empty())
2273 {
2274 desc += terse? "drowned by " : "Drowned by ";
2275 desc += death_source_name;
2276 needs_damage = true;
2277 }
2278 else
2279 desc += terse? "drowned" : "Drowned";
2280 }
2281 break;
2282
2283 case KILLED_BY_STUPIDITY:
2284 if (terse)
2285 desc += "stupidity";
2286 else if (race >= 0 && // not a removed race
2287 (species::is_undead(static_cast<species_type>(race))
2288 || species::is_nonliving(static_cast<species_type>(race))))
2289 {
2290 desc += "Forgot to exist";
2291 }
2292 else
2293 desc += "Forgot to breathe";
2294 break;
2295
2296 case KILLED_BY_WEAKNESS:
2297 desc += terse? "collapsed" : "Collapsed under their own weight";
2298 break;
2299
2300 case KILLED_BY_CLUMSINESS:
2301 desc += terse? "clumsiness" : "Slipped on a banana peel";
2302 break;
2303
2304 case KILLED_BY_TRAP:
2305 if (terse)
2306 desc += auxkilldata.c_str();
2307 else
2308 {
2309 desc += make_stringf("Killed by triggering %s",
2310 auxkilldata.c_str());
2311 }
2312 needs_damage = true;
2313 break;
2314
2315 case KILLED_BY_LEAVING:
2316 if (terse)
2317 desc += "left";
2318 else
2319 {
2320 if (num_runes > 0)
2321 desc += "Got out of the dungeon";
2322 else if (species::is_undead(static_cast<species_type>(race)))
2323 desc += "Safely got out of the dungeon";
2324 else
2325 desc += "Got out of the dungeon alive";
2326 }
2327 break;
2328
2329 case KILLED_BY_WINNING:
2330 desc += terse? "escaped" : "Escaped with the Orb";
2331 if (num_runes < 1)
2332 desc += "!";
2333 break;
2334
2335 case KILLED_BY_QUITTING:
2336 desc += terse? "quit" : "Quit the game";
2337 break;
2338
2339 case KILLED_BY_WIZMODE:
2340 desc += terse? "wizmode" : "Entered wizard mode";
2341 break;
2342
2343 case KILLED_BY_DRAINING:
2344 if (terse)
2345 desc += "drained";
2346 else
2347 {
2348 desc += "Drained of all life";
2349 if (!death_source_desc().empty())
2350 {
2351 desc += " by " + death_source_desc();
2352
2353 if (!auxkilldata.empty())
2354 needs_beam_cause_line = true;
2355 }
2356 else if (!auxkilldata.empty())
2357 desc += " by " + auxkilldata;
2358 }
2359 break;
2360
2361 case KILLED_BY_STARVATION:
2362 desc += terse? "starvation" : "Starved to death";
2363 break;
2364
2365 case KILLED_BY_FREEZING: // Freeze, Fridge spells
2366 desc += terse? "frozen" : "Frozen to death";
2367 if (!terse && !death_source_desc().empty())
2368 desc += " by " + death_source_desc();
2369 needs_damage = true;
2370 break;
2371
2372 case KILLED_BY_BURNING: // sticky flame
2373 if (terse)
2374 desc += "burnt";
2375 else if (!death_source_desc().empty())
2376 {
2377 desc += "Incinerated by " + death_source_desc();
2378
2379 if (!auxkilldata.empty())
2380 needs_beam_cause_line = true;
2381 }
2382 else
2383 desc += "Burnt to a crisp";
2384
2385 needs_damage = true;
2386 break;
2387
2388 case KILLED_BY_WILD_MAGIC:
2389 if (auxkilldata.empty())
2390 desc += terse? "wild magic" : "Killed by wild magic";
2391 else
2392 {
2393 if (terse)
2394 desc += terse_wild_magic();
2395 else
2396 {
2397 // A lot of sources for this case... some have "by" already.
2398 desc += make_stringf("Killed %s%s",
2399 (auxkilldata.find("by ") != 0) ? "by " : "",
2400 auxkilldata.c_str());
2401 }
2402 }
2403
2404 needs_damage = true;
2405 break;
2406
2407 case KILLED_BY_XOM:
2408 if (terse)
2409 desc += "xom";
2410 else
2411 desc += auxkilldata.empty() ? "Killed for Xom's enjoyment"
2412 : "Killed by " + auxkilldata;
2413 needs_damage = true;
2414 break;
2415
2416 case KILLED_BY_ROTTING:
2417 desc += terse? "rotting" : "Rotted away";
2418 if (!auxkilldata.empty())
2419 desc += " (" + auxkilldata + ")";
2420 if (!death_source_desc().empty())
2421 desc += " (" + death_source_desc() + ")";
2422 break;
2423
2424 case KILLED_BY_TARGETING:
2425 if (terse)
2426 desc += "shot self";
2427 else
2428 {
2429 desc += "Killed themself with ";
2430 if (auxkilldata.empty())
2431 desc += "bad targeting";
2432 else
2433 desc += "a badly aimed " + auxkilldata;
2434 }
2435 needs_damage = true;
2436 break;
2437
2438 case KILLED_BY_REFLECTION:
2439 needs_damage = true;
2440 if (terse)
2441 desc += "reflected bolt";
2442 else
2443 {
2444 desc += "Killed by a reflected ";
2445 if (auxkilldata.empty())
2446 desc += "bolt";
2447 else
2448 desc += auxkilldata;
2449
2450 if (!death_source_name.empty() && !oneline && !semiverbose)
2451 {
2452 desc += "\n";
2453 desc += " ";
2454 desc += "... reflected by ";
2455 desc += death_source_name;
2456 needs_damage = false;
2457 }
2458 }
2459 break;
2460
2461 case KILLED_BY_BOUNCE:
2462 if (terse)
2463 desc += "bounced beam";
2464 else
2465 {
2466 desc += "Killed themself with a bounced ";
2467 if (auxkilldata.empty())
2468 desc += "beam";
2469 else
2470 desc += auxkilldata;
2471 }
2472 needs_damage = true;
2473 break;
2474
2475 case KILLED_BY_SELF_AIMED:
2476 if (terse)
2477 desc += "suicidal targeting";
2478 else
2479 {
2480 desc += "Shot themself with ";
2481 if (auxkilldata.empty())
2482 desc += "a beam";
2483 else
2484 desc += article_a(auxkilldata, true);
2485 }
2486 needs_damage = true;
2487 break;
2488
2489 case KILLED_BY_DEATH_EXPLOSION:
2490 if (terse)
2491 {
2492 if (death_source_name.empty())
2493 desc += "spore";
2494 else
2495 desc += death_source_name;
2496 }
2497 else
2498 {
2499 desc += "Killed by an exploding ";
2500 if (death_source_name.empty())
2501 desc += "spore";
2502 else
2503 desc += death_source_name;
2504 }
2505 needs_damage = true;
2506 break;
2507
2508 case KILLED_BY_TSO_SMITING:
2509 desc += terse? "smitten by Shining One" : "Smitten by the Shining One";
2510 needs_damage = true;
2511 break;
2512
2513 case KILLED_BY_BEOGH_SMITING:
2514 desc += terse? "smitten by Beogh" : "Smitten by Beogh";
2515 needs_damage = true;
2516 break;
2517
2518 case KILLED_BY_PETRIFICATION:
2519 desc += terse? "petrified" : "Turned to stone";
2520 break;
2521
2522 case KILLED_BY_SOMETHING:
2523 if (!auxkilldata.empty())
2524 desc += (terse ? "" : "Killed by ") + auxkilldata;
2525 else
2526 desc += terse? "died" : "Died";
2527 needs_damage = true;
2528 break;
2529
2530 case KILLED_BY_FALLING_DOWN_STAIRS:
2531 desc += terse? "fell downstairs" : "Fell down a flight of stairs";
2532 needs_damage = true;
2533 break;
2534
2535 case KILLED_BY_FALLING_THROUGH_GATE:
2536 desc += terse? "fell through a gate" : "Fell down through a gate";
2537 needs_damage = true;
2538 break;
2539
2540 case KILLED_BY_ACID:
2541 if (terse)
2542 desc += "acid";
2543 else if (!death_source_desc().empty())
2544 {
2545 desc += "Splashed by "
2546 + apostrophise(death_source_desc())
2547 + " acid";
2548 }
2549 else
2550 desc += "Splashed with acid";
2551 needs_damage = true;
2552 break;
2553
2554 case KILLED_BY_CURARE:
2555 desc += terse? "asphyx" : "Asphyxiated";
2556 break;
2557
2558 case KILLED_BY_DIVINE_WRATH:
2559 if (terse)
2560 desc += "divine wrath";
2561 else
2562 {
2563 desc += "Killed by ";
2564 if (auxkilldata.empty())
2565 desc += "divine wrath";
2566 else
2567 {
2568 // Lugonu's touch or "the <retribution> of <deity>";
2569 // otherwise it's a beam
2570 if (!isupper(auxkilldata[0])
2571 && !starts_with(auxkilldata, "the "))
2572 {
2573 desc += is_vowel(auxkilldata[0]) ? "an " : "a ";
2574 }
2575
2576 desc += auxkilldata;
2577 }
2578 }
2579 needs_damage = true;
2580 if (!death_source_name.empty())
2581 needs_called_by_monster_line = true;
2582 break;
2583
2584 case KILLED_BY_DISINT:
2585 if (terse)
2586 desc += "disintegration";
2587 else
2588 {
2589 if (death_source_name == "you")
2590 desc += "Blew themself up";
2591 else
2592 desc += "Blown up by " + death_source_desc();
2593 needs_beam_cause_line = true;
2594 }
2595
2596 needs_damage = true;
2597 break;
2598
2599 case KILLED_BY_MIRROR_DAMAGE:
2600 desc += terse ? "mirror damage" : "Killed by mirror damage";
2601 needs_damage = true;
2602 break;
2603
2604 case KILLED_BY_FRAILTY:
2605 desc += terse ? "frailty" : "Became unviable by " + auxkilldata;
2606 break;
2607
2608 case KILLED_BY_BARBS:
2609 desc += terse ? "barbs" : "Succumbed to barbed spike wounds";
2610 break;
2611
2612 case KILLED_BY_BEING_THROWN:
2613 if (terse)
2614 desc += apostrophise(death_source_desc()) + " throw";
2615 else
2616 desc += "Thrown by " + death_source_desc();
2617 needs_damage = true;
2618 break;
2619
2620 case KILLED_BY_COLLISION:
2621 if (terse)
2622 desc += auxkilldata + " collision";
2623 else
2624 {
2625 desc += "Collided with " + auxkilldata;
2626 needs_called_by_monster_line = true;
2627 }
2628 needs_damage = true;
2629 break;
2630
2631 case KILLED_BY_ZOT:
2632 desc += terse ? "Zot" : "Tarried too long and was consumed by Zot";
2633 break;
2634
2635 case KILLED_BY_CONSTRICTION:
2636 if (terse)
2637 desc += "constriction";
2638 else
2639 desc += "Constricted to death by " + death_source_desc();
2640 needs_damage = true;
2641 break;
2642
2643 default:
2644 desc += terse? "program bug" : "Nibbled to death by software bugs";
2645 break;
2646 } // end switch
2647
2648 switch (death_type)
2649 {
2650 case KILLED_BY_STUPIDITY:
2651 case KILLED_BY_WEAKNESS:
2652 case KILLED_BY_CLUMSINESS:
2653 if (terse || oneline)
2654 {
2655 desc += " (";
2656 desc += auxkilldata;
2657 desc += ")";
2658 }
2659 else
2660 {
2661 desc += "\n";
2662 desc += " ";
2663 desc += "... caused by ";
2664 desc += auxkilldata;
2665 }
2666 break;
2667
2668 default:
2669 break;
2670 }
2671
2672 if (oneline && desc.length() > 2)
2673 desc[1] = tolower_safe(desc[1]);
2674
2675 // TODO: Eventually, get rid of "..." for cases where the text fits.
2676 if (terse)
2677 {
2678 if (death_type == KILLED_BY_MONSTER && !auxkilldata.empty())
2679 {
2680 desc += "/";
2681 desc += strip_article_a(auxkilldata);
2682 needs_damage = true;
2683 }
2684 else if (needs_beam_cause_line)
2685 desc += "/" + terse_beam_cause();
2686 else if (needs_called_by_monster_line)
2687 desc += death_source_name;
2688
2689 if (!killerpath.empty())
2690 desc += "[" + indirectkiller + "]";
2691
2692 if (needs_damage && damage > 0)
2693 desc += " " + damage_string(true);
2694 }
2695 else if (verbose)
2696 {
2697 bool done_damage = false; // paranoia
2698
2699 if (!semiverbose && needs_damage && damage > 0)
2700 {
2701 desc += " " + damage_string();
2702 needs_damage = false;
2703 done_damage = true;
2704 }
2705
2706 if (death_type == KILLED_BY_LEAVING
2707 || death_type == KILLED_BY_WINNING)
2708 {
2709 if (num_runes > 0)
2710 {
2711 desc += _hiscore_newline_string();
2712
2713 desc += make_stringf("... %s %d rune%s",
2714 (death_type == KILLED_BY_WINNING) ? "and" : "with",
2715 num_runes, (num_runes > 1) ? "s" : "");
2716
2717 if (!semiverbose
2718 && death_time > 0
2719 && !_hiscore_same_day(birth_time, death_time))
2720 {
2721 desc += " on ";
2722 desc += _hiscore_date_string(death_time);
2723 }
2724
2725 desc = _append_sentence_delimiter(desc, "!");
2726 desc += _hiscore_newline_string();
2727 }
2728 else
2729 desc = _append_sentence_delimiter(desc, ".");
2730 }
2731 else if (death_type != KILLED_BY_QUITTING
2732 && death_type != KILLED_BY_WIZMODE)
2733 {
2734 desc += _hiscore_newline_string();
2735
2736 if (death_type == KILLED_BY_MONSTER && !auxkilldata.empty())
2737 {
2738 if (!semiverbose)
2739 {
2740 desc += make_stringf("... wielding %s",
2741 auxkilldata.c_str());
2742 needs_damage = true;
2743 desc += _hiscore_newline_string();
2744 }
2745 else
2746 desc += make_stringf(" (%s)", auxkilldata.c_str());
2747 }
2748 else if (needs_beam_cause_line)
2749 {
2750 if (!semiverbose)
2751 {
2752 desc += auxkilldata == "damnation" ? "... with " :
2753 auxkilldata == "creeping frost" ? "... by " :
2754 (is_vowel(auxkilldata[0])) ? "... with an "
2755 : "... with a ";
2756 desc += auxkilldata;
2757 desc += _hiscore_newline_string();
2758 needs_damage = true;
2759 }
2760 else if (death_type == KILLED_BY_DRAINING
2761 || death_type == KILLED_BY_BURNING)
2762 {
2763 desc += make_stringf(" (%s)", auxkilldata.c_str());
2764 }
2765 }
2766 else if (needs_called_by_monster_line)
2767 {
2768 desc += make_stringf("... %s by %s",
2769 death_type == KILLED_BY_COLLISION ? "caused" :
2770 auxkilldata == "by angry trees" ? "awakened" :
2771 auxkilldata == "by Freeze" ? "generated"
2772 : "invoked",
2773 death_source_name.c_str());
2774 desc += _hiscore_newline_string();
2775 needs_damage = true;
2776 }
2777
2778 if (!killerpath.empty())
2779 {
2780 vector<string> summoners = _xlog_split_fields(killerpath);
2781
2782 for (const auto &sumname : summoners)
2783 {
2784 if (!semiverbose)
2785 {
2786 desc += "... " + sumname;
2787 desc += _hiscore_newline_string();
2788 }
2789 else
2790 desc += " (" + sumname;
2791 }
2792
2793 if (semiverbose)
2794 desc += string(summoners.size(), ')');
2795 }
2796
2797 if (!semiverbose)
2798 {
2799 if (needs_damage && !done_damage && damage > 0)
2800 desc += " " + damage_string();
2801
2802 if (needs_damage && !done_damage)
2803 desc += _hiscore_newline_string();
2804
2805 if (you.duration[DUR_PARALYSIS])
2806 {
2807 desc += "... while paralysed";
2808 if (you.props.exists(PARALYSED_BY_KEY))
2809 {
2810 desc += " by "
2811 + you.props[PARALYSED_BY_KEY].get_string();
2812 }
2813 desc += _hiscore_newline_string();
2814 }
2815 else if (you.duration[DUR_PETRIFIED])
2816 {
2817 desc += "... while petrified";
2818 if (you.props.exists(PETRIFIED_BY_KEY))
2819 {
2820 desc += " by "
2821 + you.props[PETRIFIED_BY_KEY].get_string();
2822 }
2823 desc += _hiscore_newline_string();
2824 }
2825
2826 }
2827 }
2828 }
2829
2830 if (!oneline)
2831 {
2832 if (death_type == KILLED_BY_LEAVING
2833 || death_type == KILLED_BY_WINNING)
2834 {
2835 // TODO: strcat "after reaching level %d"; for LEAVING
2836 if (verbosity == DDV_NORMAL)
2837 {
2838 desc = _append_sentence_delimiter(desc,
2839 num_runes > 0? "!" : ".");
2840 }
2841 desc += _hiscore_newline_string();
2842 }
2843 }
2844
2845 if (death_type == KILLED_BY_DEATH_EXPLOSION && !terse && !auxkilldata.empty())
2846 {
2847 desc += "... ";
2848 desc += auxkilldata;
2849 desc += "\n";
2850 desc += " ";
2851 }
2852
2853 if (terse)
2854 {
2855 trim_string(desc);
2856 desc = strip_article_a(desc);
2857 }
2858
2859 return desc;
2860 }
2861
2862 //////////////////////////////////////////////////////////////////////////////
2863 // xlog_fields
2864
xlog_fields()2865 xlog_fields::xlog_fields() : fields(), fieldmap()
2866 {
2867 }
2868
xlog_fields(const string & line)2869 xlog_fields::xlog_fields(const string &line) : fields(), fieldmap()
2870 {
2871 init(line);
2872 }
2873
2874 // xlogfile escape: s/:/::/g
_xlog_escape(const string & s)2875 static string _xlog_escape(const string &s)
2876 {
2877 return replace_all(s, ":", "::");
2878 }
2879
2880 // xlogfile unescape: s/::/:/g
_xlog_unescape(const string & s)2881 static string _xlog_unescape(const string &s)
2882 {
2883 return replace_all(s, "::", ":");
2884 }
2885
_xlog_next_separator(const string & s,string::size_type start)2886 static string::size_type _xlog_next_separator(const string &s,
2887 string::size_type start)
2888 {
2889 string::size_type p = s.find(':', start);
2890 if (p != string::npos && p < s.length() - 1 && s[p + 1] == ':')
2891 return _xlog_next_separator(s, p + 2);
2892
2893 return p;
2894 }
2895
_xlog_split_fields(const string & s)2896 static vector<string> _xlog_split_fields(const string &s)
2897 {
2898 string::size_type start = 0, end = 0;
2899 vector<string> fs;
2900
2901 for (; (end = _xlog_next_separator(s, start)) != string::npos;
2902 start = end + 1)
2903 {
2904 fs.push_back(s.substr(start, end - start));
2905 }
2906
2907 if (start < s.length())
2908 fs.push_back(s.substr(start));
2909
2910 return fs;
2911 }
2912
init(const string & line)2913 void xlog_fields::init(const string &line)
2914 {
2915 for (const string &field : _xlog_split_fields(line))
2916 {
2917 string::size_type st = field.find('=');
2918 if (st == string::npos)
2919 continue;
2920
2921 fields.emplace_back(field.substr(0, st),
2922 _xlog_unescape(field.substr(st + 1)));
2923 }
2924
2925 map_fields();
2926 }
2927
add_field(const string & key,const char * format,...)2928 void xlog_fields::add_field(const string &key, const char *format, ...)
2929 {
2930 va_list args;
2931 va_start(args, format);
2932 string buf = vmake_stringf(format, args);
2933 va_end(args);
2934
2935 fields.emplace_back(key, buf);
2936 fieldmap[key] = buf;
2937 }
2938
str_field(const string & s) const2939 string xlog_fields::str_field(const string &s) const
2940 {
2941 return lookup(fieldmap, s, "");
2942 }
2943
int_field(const string & s) const2944 int xlog_fields::int_field(const string &s) const
2945 {
2946 string field = str_field(s);
2947 return atoi(field.c_str());
2948 }
2949
map_fields() const2950 void xlog_fields::map_fields() const
2951 {
2952 fieldmap.clear();
2953 for (const pair<string, string> &f : fields)
2954 fieldmap[f.first] = f.second;
2955 }
2956
xlog_line() const2957 string xlog_fields::xlog_line() const
2958 {
2959 string line;
2960 for (const pair<string, string> &f : fields)
2961 {
2962 // Don't write empty fields.
2963 if (f.second.empty())
2964 continue;
2965
2966 if (!line.empty())
2967 line += ":";
2968
2969 line += f.first;
2970 line += "=";
2971 line += _xlog_escape(f.second);
2972 }
2973
2974 return line;
2975 }
2976
2977 ///////////////////////////////////////////////////////////////////////////////
2978 // Milestones
2979
2980 /**
2981 * @brief Record the player reaching a milestone, if ::DGL_MILESTONES is defined.
2982 * @callergraph
2983 */
mark_milestone(const string & type,const string & milestone,const string & origin_level,time_t milestone_time)2984 void mark_milestone(const string &type, const string &milestone,
2985 const string &origin_level, time_t milestone_time)
2986 {
2987 #ifdef DGL_MILESTONES
2988 static string lasttype, lastmilestone;
2989 static long lastturn = -1;
2990
2991 if (crawl_state.game_is_arena()
2992 || !crawl_state.need_save
2993 // Suppress duplicate milestones on the same turn.
2994 || (lastturn == you.num_turns
2995 && lasttype == type
2996 && lastmilestone == milestone)
2997 #ifndef SCORE_WIZARD_CHARACTERS
2998 // Don't mark normal milestones in wizmode or explore mode
2999 || (type != "crash" && (you.wizard || you.suppress_wizard || you.explore))
3000 #endif
3001 )
3002 {
3003 return;
3004 }
3005
3006 lasttype = type;
3007 lastmilestone = milestone;
3008 lastturn = you.num_turns;
3009
3010 const string milestone_file = catpath(
3011 Options.save_dir, "milestones" + crawl_state.game_type_qualifier());
3012 const scorefile_entry se(0, MID_NOBODY, KILL_MISC, nullptr);
3013 se.set_base_xlog_fields();
3014 xlog_fields xl = se.get_fields();
3015 if (!origin_level.empty())
3016 {
3017 xl.add_field("oplace", "%s",
3018 ((origin_level == "parent") ?
3019 current_level_parent().describe() :
3020 origin_level).c_str());
3021 }
3022 xl.add_field("time", "%s",
3023 make_date_string(
3024 milestone_time ? milestone_time
3025 : se.get_death_time()).c_str());
3026 xl.add_field("type", "%s", type.c_str());
3027 xl.add_field("milestone", "%s", milestone.c_str());
3028 const string xlog_line = xl.xlog_line();
3029 if (FILE *fp = lk_open("a", milestone_file))
3030 {
3031 fprintf(fp, "%s\n", xlog_line.c_str());
3032 lk_close(fp);
3033 }
3034 #else
3035 UNUSED(type, milestone, origin_level, milestone_time);
3036 #endif // DGL_MILESTONES
3037 }
3038
3039 #ifdef DGL_WHEREIS
xlog_status_line()3040 string xlog_status_line()
3041 {
3042 const scorefile_entry se(0, MID_NOBODY, KILL_MISC, nullptr);
3043 se.set_base_xlog_fields();
3044 xlog_fields xl = se.get_fields();
3045 xl.add_field("time", "%s", make_date_string(time(nullptr)).c_str());
3046 return xl.xlog_line();
3047 }
3048 #endif // DGL_WHEREIS
3049