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