1 /**
2  * @file
3  * @brief Dumps character info out to the morgue file.
4 **/
5 
6 #include "AppHdr.h"
7 
8 #include "chardump.h"
9 
10 #include <string>
11 #include <cctype>
12 #include <cinttypes>
13 #include <cstdio>
14 #include <cstdlib>
15 #include <cstring>
16 #include <fcntl.h>
17 #if defined(UNIX) || defined(TARGET_COMPILER_MINGW)
18 #include <unistd.h>
19 #endif
20 
21 #include "ability.h"
22 #include "artefact.h"
23 #include "art-enum.h"
24 #include "branch.h"
25 #include "describe.h"
26 #include "dgn-overview.h"
27 #include "dungeon.h"
28 #include "fight.h"
29 #include "files.h"
30 #include "god-prayer.h"
31 #include "hiscores.h"
32 #include "initfile.h"
33 #include "invent.h"
34 #include "item-prop.h"
35 #include "items.h"
36 #include "kills.h"
37 #include "libutil.h"
38 #include "melee-attack.h"
39 #include "message.h"
40 #include "mutation.h"
41 #include "notes.h"
42 #include "output.h"
43 #include "place.h"
44 #include "prompt.h"
45 #include "religion.h"
46 #include "scroller.h"
47 #include "showsymb.h"
48 #include "skills.h"
49 #include "spl-book.h"
50 #include "spl-util.h"
51 #include "state.h"
52 #include "stringutil.h"
53 #include "tag-version.h"
54 #include "transform.h"
55 #include "travel.h"
56 #include "unicode.h"
57 #include "version.h"
58 #include "viewchar.h"
59 #include "view.h"
60 #include "xom.h"
61 
62 struct dump_params;
63 
64 static void _sdump_header(dump_params &);
65 static void _sdump_stats(dump_params &);
66 static void _sdump_location(dump_params &);
67 static void _sdump_religion(dump_params &);
68 static void _sdump_transform(dump_params &);
69 static void _sdump_visits(dump_params &);
70 static void _sdump_gold(dump_params &);
71 static void _sdump_misc(dump_params &);
72 static void _sdump_turns_by_place(dump_params &);
73 static void _sdump_notes(dump_params &);
74 static void _sdump_screenshots(dump_params &);
75 static void _sdump_inventory(dump_params &);
76 static void _sdump_skills(dump_params &);
77 static void _sdump_spells(dump_params &);
78 static void _sdump_mutations(dump_params &);
79 static void _sdump_messages(dump_params &);
80 static void _sdump_screenshot(dump_params &);
81 static void _sdump_kills_by_place(dump_params &);
82 static void _sdump_kills(dump_params &);
83 static void _sdump_xp_by_level(dump_params &);
84 static void _sdump_newline(dump_params &);
85 static void _sdump_overview(dump_params &);
86 static void _sdump_hiscore(dump_params &);
87 static void _sdump_monster_list(dump_params &);
88 static void _sdump_vault_list(dump_params &);
89 static void _sdump_skill_gains(dump_params &);
90 static void _sdump_action_counts(dump_params &);
91 static void _sdump_separator(dump_params &);
92 static void _sdump_lua(dump_params &);
93 static bool _write_dump(const string &fname, const dump_params &,
94                         bool print_dump_path = false);
95 
96 struct dump_section_handler
97 {
98     const char *name;
99     void (*handler)(dump_params &);
100 };
101 
102 struct dump_params
103 {
104     string text;
105     string section;
106     bool full_id;
107     const scorefile_entry *se;
108 
dump_paramsdump_params109     dump_params(const string &sec = "", bool id = false,
110                 const scorefile_entry *s = nullptr)
111         : section(sec), full_id(id), se(s)
112     {
113         // Start with enough room for 100 80 character lines.
114         text.reserve(100 * 80);
115     }
116 };
117 
118 static dump_section_handler dump_handlers[] =
119 {
120     { "header",         _sdump_header        },
121     { "stats",          _sdump_stats         },
122     { "location",       _sdump_location      },
123     { "religion",       _sdump_religion      },
124     { "transform",      _sdump_transform     },
125     { "visits",         _sdump_visits        },
126     { "gold",           _sdump_gold          },
127     { "misc",           _sdump_misc          },
128     { "turns_by_place", _sdump_turns_by_place},
129     { "notes",          _sdump_notes         },
130     { "screenshots",    _sdump_screenshots   },
131     { "inventory",      _sdump_inventory     },
132     { "skills",         _sdump_skills        },
133     { "spells",         _sdump_spells        },
134     { "mutations",      _sdump_mutations     },
135     { "messages",       _sdump_messages      },
136     { "screenshot",     _sdump_screenshot    },
137     { "kills_by_place", _sdump_kills_by_place},
138     { "kills",          _sdump_kills         },
139     { "xp_by_level",    _sdump_xp_by_level   },
140     { "overview",       _sdump_overview      },
141     { "hiscore",        _sdump_hiscore       },
142     { "monlist",        _sdump_monster_list  },
143     { "vaults",         _sdump_vault_list    },
144     { "spell_usage",    _sdump_action_counts }, // compat
145     { "action_counts",  _sdump_action_counts },
146     { "skill_gains",    _sdump_skill_gains   },
147 
148     // Conveniences for the .crawlrc artist.
149     { "",               _sdump_newline       },
150     { "-",              _sdump_separator     },
151 
152     { nullptr,          _sdump_lua           }
153 };
154 
dump_section(dump_params & par)155 static void dump_section(dump_params &par)
156 {
157     for (int i = 0; ; ++i)
158     {
159         if (!dump_handlers[i].name || par.section == dump_handlers[i].name)
160         {
161             if (dump_handlers[i].handler)
162                 (*dump_handlers[i].handler)(par);
163             break;
164         }
165     }
166 }
167 
_get_dump(bool full_id=false,const scorefile_entry * se=nullptr)168 static dump_params _get_dump(bool full_id = false,
169                              const scorefile_entry *se = nullptr)
170 {
171     dump_params par("", full_id, se);
172 
173     for (const string &section : Options.dump_order)
174     {
175         par.section = section;
176         dump_section(par);
177     }
178 
179     // Hopefully we get RVO so we don't have to copy the text.
180     return par;
181 }
182 
dump_char(const string & fname,bool quiet,bool full_id,const scorefile_entry * se)183 bool dump_char(const string &fname, bool quiet, bool full_id,
184                const scorefile_entry *se)
185 {
186     return _write_dump(fname, _get_dump(full_id, se), quiet);
187 }
188 
seed_description()189 string seed_description()
190 {
191     return make_stringf(
192         "Game seed: %" PRIu64 "%s", crawl_state.seed,
193             crawl_state.type == GAME_TYPE_CUSTOM_SEED
194             ? " (custom seed)"
195             : you.deterministic_levelgen ? "" : " (classic levelgen)");
196 }
197 
_sdump_header(dump_params & par)198 static void _sdump_header(dump_params &par)
199 {
200     string type = crawl_state.game_type_name();
201     if (type.empty())
202         type = CRAWL;
203     else
204         type += " DCSS";
205 
206     par.text += " " + type + " version " + Version::Long;
207 #ifdef USE_TILE_LOCAL
208     par.text += " (tiles)";
209 #elif defined(USE_TILE_WEB)
210     if (::tiles.is_controlled_from_web())
211         par.text += " (webtiles)";
212     else
213         par.text += " (console)";
214 #else
215     par.text += " (console)";
216 #endif
217     par.text += " character file.\n\n";
218 
219     if (you.fully_seeded
220 #ifdef DGAMELAUNCH
221         && (par.se // for online games, show seed for a dead char
222             || you.wizard
223             || crawl_state.type == GAME_TYPE_CUSTOM_SEED)
224 #endif
225         )
226     {
227         par.text += seed_description() + "\n\n";
228     }
229 }
230 
_sdump_stats(dump_params & par)231 static void _sdump_stats(dump_params &par)
232 {
233     par.text += dump_overview_screen(par.full_id);
234     par.text += "\n\n";
235 }
236 
_sdump_transform(dump_params & par)237 static void _sdump_transform(dump_params &par)
238 {
239     string &text(par.text);
240     if (you.form != transformation::none)
241         text += get_form()->get_description(par.se) + "\n\n";}
242 
243 static branch_type single_portals[] =
244 {
245     BRANCH_TROVE,
246     BRANCH_SEWER,
247     BRANCH_OSSUARY,
248     BRANCH_BAILEY,
249     BRANCH_GAUNTLET,
250     BRANCH_ICE_CAVE,
251     BRANCH_VOLCANO,
252     BRANCH_WIZLAB,
253     BRANCH_DESOLATION,
254 #if TAG_MAJOR_VERSION == 34
255     BRANCH_LABYRINTH,
256 #endif
257 };
258 
_sdump_visits(dump_params & par)259 static void _sdump_visits(dump_params &par)
260 {
261     string &text(par.text);
262 
263     string have = "have ";
264     string seen = "seen";
265     if (par.se) // you died -> past tense
266     {
267         have = "";
268         seen = "saw";
269     }
270 
271     const vector<PlaceInfo> branches_visited = you.get_all_place_info(true, true);
272 
273     PlaceInfo branches_total;
274     for (const PlaceInfo &branch : branches_visited)
275         branches_total += branch;
276 
277     text += make_stringf("You %svisited %d branch",
278                          have.c_str(), (int)branches_visited.size());
279     if (branches_visited.size() != 1)
280         text += "es";
281     if (brdepth[root_branch] > 1 || branches_visited.size() != 1)
282     {
283         text += make_stringf(" of the dungeon, and %s %d of its levels.\n",
284                              seen.c_str(), branches_total.levels_seen);
285     }
286 
287     {
288         const PlaceInfo place_info = you.get_place_info(BRANCH_PANDEMONIUM);
289         if (place_info.num_visits > 0)
290         {
291             text += make_stringf("You %svisited Pandemonium %d time",
292                                  have.c_str(), place_info.num_visits);
293             if (place_info.num_visits > 1)
294                 text += "s";
295             text += make_stringf(", and %s %d of its levels.\n",
296                                  seen.c_str(), place_info.levels_seen);
297         }
298     }
299 
300     {
301         const PlaceInfo place_info = you.get_place_info(BRANCH_ABYSS);
302         if (place_info.num_visits > 0)
303         {
304             text += make_stringf("You %svisited the Abyss %d time",
305                                  have.c_str(), place_info.num_visits);
306             if (place_info.num_visits > 1)
307                 text += "s";
308             text += ".\n";
309         }
310     }
311 
312     {
313         const PlaceInfo place_info = you.get_place_info(BRANCH_BAZAAR);
314         if (place_info.num_visits > 0)
315         {
316             text += make_stringf("You %svisited %d bazaar",
317                                  have.c_str(), place_info.num_visits);
318             if (place_info.num_visits > 1)
319                 text += "s";
320             text += ".\n";
321         }
322     }
323 
324     {
325         const PlaceInfo place_info = you.get_place_info(BRANCH_ZIGGURAT);
326         if (place_info.num_visits > 0)
327         {
328             int num_zigs = place_info.num_visits;
329             text += make_stringf("You %s%s %d ziggurat",
330                                  have.c_str(),
331                                  (num_zigs == you.zigs_completed) ? "completed"
332                                                                   : "visited",
333                                  num_zigs);
334             if (num_zigs > 1)
335                 text += "s";
336             if (num_zigs != you.zigs_completed && you.zigs_completed)
337                 text += make_stringf(" (completing %d)", you.zigs_completed);
338             text += make_stringf(", and %s %d of %s levels",
339                                  seen.c_str(), place_info.levels_seen,
340                                  num_zigs > 1 ? "their" : "its");
341             if (num_zigs != 1 && !you.zigs_completed)
342                 text += make_stringf(" (deepest: %d)", you.zig_max);
343             text += ".\n";
344         }
345     }
346 
347     vector<string> misc_portals;
348     for (branch_type br : single_portals)
349     {
350         const PlaceInfo place_info = you.get_place_info(br);
351         if (!place_info.num_visits)
352             continue;
353         string name = branches[br].shortname;
354         if (place_info.num_visits > 1)
355             name += make_stringf(" (%d times)", place_info.num_visits);
356         misc_portals.push_back(name);
357     }
358     if (!misc_portals.empty())
359     {
360         text += "You " + have + "also visited: "
361                 + comma_separated_line(misc_portals.begin(),
362                                        misc_portals.end())
363                 + ".\n";
364     }
365 
366     text += "\n";
367 }
368 
_sdump_gold(dump_params & par)369 static void _sdump_gold(dump_params &par)
370 {
371     string &text(par.text);
372 
373     int lines = 0;
374 
375     const char* have = "have ";
376     if (par.se) // you died -> past tense
377         have = "";
378 
379     if (you.attribute[ATTR_GOLD_FOUND] > 0)
380     {
381         lines++;
382         text += make_stringf("You %scollected %d gold pieces.\n", have,
383                              you.attribute[ATTR_GOLD_FOUND]);
384     }
385 
386     if (you.attribute[ATTR_PURCHASES] > 0)
387     {
388         lines++;
389         text += make_stringf("You %sspent %d gold pieces at shops.\n", have,
390                              you.attribute[ATTR_PURCHASES]);
391     }
392 
393     if (you.attribute[ATTR_DONATIONS] > 0)
394     {
395         lines++;
396         text += make_stringf("You %sdonated %d gold pieces to Zin.\n", have,
397                              you.attribute[ATTR_DONATIONS]);
398     }
399 
400     if (you.attribute[ATTR_GOZAG_GOLD_USED] > 0)
401     {
402         lines++;
403         text += make_stringf("You %spaid %d gold pieces to Gozag.\n", have,
404                              you.attribute[ATTR_GOZAG_GOLD_USED]);
405     }
406 
407     if (you.attribute[ATTR_MISC_SPENDING] > 0)
408     {
409         lines++;
410         text += make_stringf("You %sused %d gold pieces for miscellaneous "
411                              "purposes.\n", have,
412                              you.attribute[ATTR_MISC_SPENDING]);
413     }
414 
415     if (lines > 0)
416         text += "\n";
417 }
418 
_sdump_misc(dump_params & par)419 static void _sdump_misc(dump_params &par)
420 {
421     _sdump_location(par);
422     _sdump_religion(par);
423     _sdump_transform(par);
424     _sdump_visits(par);
425     _sdump_gold(par);
426 }
427 
428 #define TO_PERCENT(x, y) (100.0f * (static_cast<float>(x)) / (static_cast<float>(y)))
429 
_denanify(const string & s)430 static string _denanify(const string &s)
431 {
432     string out = replace_all(s, " nan ", " N/A ");
433     out = replace_all(out, " -nan ", " N/A  ");
434     out = replace_all(out, " 1#IND ", "  N/A  ");
435     out = replace_all(out, " -1#IND ", "  N/A   ");
436     return out;
437 }
438 
_sdump_level_xp_info(LevelXPInfo xp_info,string name="")439 static string _sdump_level_xp_info(LevelXPInfo xp_info, string name = "")
440 {
441     string out;
442 
443     if (name.empty())
444         name = xp_info.level.describe();
445 
446     float c, f;
447     unsigned int total_xp = xp_info.vault_xp + xp_info.non_vault_xp;
448     unsigned int total_count = xp_info.vault_count + xp_info.non_vault_count;
449 
450     c = TO_PERCENT(xp_info.vault_xp, total_xp);
451     f = TO_PERCENT(xp_info.vault_count, total_count);
452 
453     out =
454         make_stringf("%11s | %7d | %7d | %5.1f | %7d | %7d | %5.1f\n",
455                      name.c_str(), xp_info.non_vault_xp, xp_info.vault_xp,
456                      c, xp_info.non_vault_count, xp_info.vault_count, f);
457 
458     return _denanify(out);
459 }
460 
_sdump_turns_place_info(const PlaceInfo place_info,string name="")461 static string _sdump_turns_place_info(const PlaceInfo place_info, string name = "")
462 {
463     string out;
464 
465     if (name.empty())
466         name = place_info.short_name();
467 
468     unsigned int non_interlevel =
469         place_info.elapsed_total / 10 - place_info.elapsed_interlevel / 10;
470 
471     const float g = static_cast<float>(place_info.elapsed_total / 10)
472                             / static_cast<float>(place_info.levels_seen);
473 
474     out =
475         make_stringf("%14s | %6d | %6d | %6d | %6d | %6d | %3d | %6.1f |\n",
476                      name.c_str(),
477                      place_info.elapsed_total / 10,
478                      non_interlevel,
479                      place_info.elapsed_interlevel / 10,
480                      place_info.elapsed_resting / 10,
481                      place_info.elapsed_explore / 10,
482                      place_info.levels_seen,
483                      g);
484 
485     return _denanify(out);
486 }
487 
_sdump_turns_by_place(dump_params & par)488 static void _sdump_turns_by_place(dump_params &par)
489 {
490     string &text(par.text);
491 
492     const vector<PlaceInfo> all_visited = you.get_all_place_info(true);
493 
494     text +=
495 "Table legend: (Time is in decaauts)\n"
496 " A = Elapsed time spent in this place.\n"
497 " B = Non-inter-level travel time spent in this place.\n"
498 " C = Inter-level travel time spent in this place.\n"
499 " D = Time resting spent in this place.\n"
500 " E = Time spent auto-exploring this place.\n"
501 " F = Levels seen in this place.\n"
502 " G = Mean time per level.\n";
503 
504     text += "               ";
505     text += "    A        B        C        D        E      F       G\n";
506     text += "               ";
507     text += "+--------+--------+--------+--------+--------+-----+--------+\n";
508 
509     text += _sdump_turns_place_info(you.global_info, "Total");
510 
511     for (const PlaceInfo &pi : all_visited)
512         text += _sdump_turns_place_info(pi);
513 
514     text += "               ";
515     text += "+--------+--------+--------+--------+--------+-----+--------+\n";
516 
517     text += "\n";
518 
519     CrawlHashTable &time_tracking = you.props[TIME_PER_LEVEL_KEY].get_table();
520     vector<pair<int, string>> to_sort;
521     for (auto &l : time_tracking)
522         if (l.first != "upgrade" && l.first != "Pan")
523             to_sort.emplace_back(l.second.get_int(), l.first);
524     if (to_sort.size() == 0)
525         return; // turn 0 game
526     sort(to_sort.begin(), to_sort.end());
527     reverse(to_sort.begin(), to_sort.end());
528 
529     text += "Top non-repeatable levels by time:\n";
530     for (unsigned int i = 0; i < 15 && i < to_sort.size(); i++)
531         text += make_stringf("%8s: %d daAuts\n", to_sort[i].second.c_str(), to_sort[i].first / 10);
532     if (time_tracking.exists("upgrade"))
533         text += "Note: time per level data comes from an upgraded game and may be incomplete.\n";
534     text += "\n";
535 }
536 
_sdump_xp_by_level(dump_params & par)537 static void _sdump_xp_by_level(dump_params &par)
538 {
539     string &text(par.text);
540 
541     vector<LevelXPInfo> all_info = you.get_all_xp_info(true);
542 
543     text +=
544 "Table legend:\n"
545 " A = Non-vault XP\n"
546 " B = Vault XP\n"
547 " C = Vault XP percentage of total XP\n"
548 " D = Non-vault monster count\n"
549 " E = Vault monster count\n"
550 " F = Vault count percentage of total count\n\n";
551 
552     text += "            ";
553     text += "     A         B        C        D         E        F   \n";
554     text += "            ";
555     text += "+---------+---------+-------+---------+---------+-------\n";
556 
557     text += _sdump_level_xp_info(you.global_xp_info, "Total");
558 
559     text += "            ";
560     text += "+---------+---------+-------+---------+---------+-------\n";
561 
562     for (const LevelXPInfo &mi : all_info)
563         text += _sdump_level_xp_info(mi);
564 
565     text += "            ";
566     text += "+---------+---------+-------+---------+---------+-------\n";
567 
568     text += "\n";
569 }
570 
_sdump_newline(dump_params & par)571 static void _sdump_newline(dump_params &par)
572 {
573     par.text += "\n";
574 }
575 
_sdump_separator(dump_params & par)576 static void _sdump_separator(dump_params &par)
577 {
578     par.text += string(79, '-') + "\n";
579 }
580 
581 // Assume this is an arbitrary Lua function name, call the function and
582 // dump whatever it returns.
_sdump_lua(dump_params & par)583 static void _sdump_lua(dump_params &par)
584 {
585     string luatext;
586     if (!clua.callfn(par.section.c_str(), ">s", &luatext)
587         && !clua.error.empty())
588     {
589         par.text += "Lua dump error: " + clua.error + "\n";
590     }
591     else
592         par.text += luatext;
593 }
594 
chardump_desc(const item_def & item)595 string chardump_desc(const item_def& item)
596 {
597     string desc = get_item_description(item, false, true);
598     string outs;
599 
600     outs.reserve(desc.length() + 32);
601 
602     const int indent = 3;
603 
604     if (desc.empty()) // always at least an empty line
605         return "\n";
606 
607     while (!desc.empty())
608     {
609         outs += string(indent, ' ')
610                   + wordwrap_line(desc, 79 - indent)
611                   + "\n";
612     }
613 
614     return outs;
615 }
616 
_sdump_messages(dump_params & par)617 static void _sdump_messages(dump_params &par)
618 {
619     // A little message history:
620     if (Options.dump_message_count > 0)
621     {
622         par.text += "Message History\n\n";
623         par.text += get_last_messages(Options.dump_message_count);
624     }
625 }
626 
_sdump_screenshot(dump_params & par)627 static void _sdump_screenshot(dump_params &par)
628 {
629     par.text += screenshot();
630     par.text += "\n\n";
631 }
632 
_sdump_screenshots(dump_params & par)633 static void _sdump_screenshots(dump_params &par)
634 {
635     string &text(par.text);
636     if (note_list.empty())
637         return;
638 
639     text += "Illustrated notes\n\n";
640 
641     for (const Note &note : note_list)
642     {
643         if (note.hidden() || note.type != NOTE_USER_NOTE || note.screen.length() == 0)
644             continue;
645 
646         text += note.screen;
647         text += "\n";
648         text += make_stringf("Turn %d on ", note.turn);
649         text += note.place.describe() + ": ";
650         text += note.name;
651         text += "\n\n";
652     }
653 }
654 
_sdump_notes(dump_params & par)655 static void _sdump_notes(dump_params &par)
656 {
657     string &text(par.text);
658     if (note_list.empty())
659         return;
660 
661     text += "Notes\n";
662     text += "Turn   | Place    | Note\n";
663     text += "-------+----------+-------------------------------------------\n";
664     for (const Note &note : note_list)
665     {
666         if (note.hidden())
667             continue;
668 
669         string prefix = note.describe(true, true, false);
670         string suffix = note.describe(false, false, true);
671         if (suffix.empty())
672             continue;
673         int spaceleft = 80 - prefix.length() - 1; // Use 100 cols
674         if (spaceleft <= 0)
675             return;
676 
677         linebreak_string(suffix, spaceleft);
678         vector<string> parts = split_string("\n", suffix);
679         if (parts.empty()) // Disregard pure-whitespace notes.
680             continue;
681 
682         text += prefix + parts[0] + "\n";
683         for (unsigned int j = 1; j < parts.size(); ++j)
684             text += string(prefix.length()-2, ' ') + string("| ") + parts[j] + "\n";
685     }
686     text += "\n";
687 }
688 
_sdump_location(dump_params & par)689 static void _sdump_location(dump_params &par)
690 {
691     if (you.depth == 0 && player_in_branch(BRANCH_DUNGEON))
692         par.text += "You escaped";
693     else if (par.se)
694         par.text += "You were " + prep_branch_level_name();
695     else
696         par.text += "You are " + prep_branch_level_name();
697 
698     par.text += ".";
699     par.text += "\n";
700 }
701 
_sdump_religion(dump_params & par)702 static void _sdump_religion(dump_params &par)
703 {
704     string &text(par.text);
705     if (!you_worship(GOD_NO_GOD))
706     {
707         if (par.se)
708             text += "You worshipped ";
709         else
710             text += "You worship ";
711         text += god_name(you.religion);
712         text += ".\n";
713 
714         if (!you_worship(GOD_XOM))
715         {
716             if (!player_under_penance())
717             {
718                 text += god_prayer_reaction();
719                 text += "\n";
720             }
721             else
722             {
723                 string verb = par.se ? "was" : "is";
724 
725                 text += uppercase_first(god_name(you.religion));
726                 text += " " + verb + " demanding penance.\n";
727             }
728         }
729         else
730         {
731             if (par.se)
732                 text += "You were ";
733             else
734                 text += "You are ";
735             text += describe_xom_favour();
736             text += "\n";
737         }
738     }
739 }
740 
_dump_item_origin(const item_def & item)741 static bool _dump_item_origin(const item_def &item)
742 {
743 #define fs(x) (flags & (x))
744     const int flags = Options.dump_item_origins;
745     if (flags == IODS_EVERYTHING)
746         return true;
747 
748     if (fs(IODS_ARTEFACTS)
749         && is_artefact(item) && item_ident(item, ISFLAG_KNOW_PROPERTIES))
750     {
751         return true;
752     }
753     if (fs(IODS_EGO_ARMOUR) && item.base_type == OBJ_ARMOUR
754         && item_type_known(item))
755     {
756         const int spec_ench = get_armour_ego_type(item);
757         return spec_ench != SPARM_NORMAL;
758     }
759 
760     if (fs(IODS_EGO_WEAPON) && item.base_type == OBJ_WEAPONS
761         && item_type_known(item))
762     {
763         return get_weapon_brand(item) != SPWPN_NORMAL;
764     }
765 
766     if (fs(IODS_JEWELLERY) && item.base_type == OBJ_JEWELLERY)
767         return true;
768 
769     if (fs(IODS_RUNES) && item.base_type == OBJ_RUNES)
770         return true;
771 
772     if (fs(IODS_STAVES) && item.base_type == OBJ_STAVES)
773         return true;
774 
775     if (fs(IODS_BOOKS) && item.base_type == OBJ_BOOKS)
776         return true;
777 
778     const int refpr = Options.dump_item_origin_price;
779     if (refpr == -1)
780         return false;
781     return (int)item_value(item, false) >= refpr;
782 #undef fs
783 }
784 
_sdump_inventory(dump_params & par)785 static void _sdump_inventory(dump_params &par)
786 {
787     int i;
788 
789     string &text(par.text);
790 
791     int inv_class2[NUM_OBJECT_CLASSES] = { 0, };
792     int inv_count = 0;
793 
794     for (const auto &item : you.inv)
795     {
796         if (item.defined())
797         {
798             // adds up number of each class in invent.
799             inv_class2[item.base_type]++;
800             inv_count++;
801         }
802     }
803 
804     if (!inv_count)
805     {
806         text += "You aren't carrying anything.";
807         text += "\n";
808     }
809     else
810     {
811         text += "Inventory:\n\n";
812 
813         for (int obj = 0; obj < NUM_OBJECT_CLASSES; obj++)
814         {
815             i = inv_order[obj];
816 
817             if (inv_class2[i] == 0)
818                 continue;
819 
820             text += item_class_name(i);
821             text += "\n";
822 
823             for (const auto &item : you.inv)
824             {
825                 if (!item.defined() || item.base_type != i)
826                     continue;
827 
828                 text += " ";
829                 text += item.name(DESC_INVENTORY_EQUIP);
830 
831                 inv_count--;
832 
833                 if (origin_describable(item) && _dump_item_origin(item))
834                     text += "\n" "   (" + origin_desc(item) + ")";
835 
836                 if (is_dumpable_artefact(item))
837                     text += chardump_desc(item);
838                 else
839                     text += "\n";
840             }
841         }
842     }
843     text += "\n\n";
844 }
845 
_sdump_skills(dump_params & par)846 static void _sdump_skills(dump_params &par)
847 {
848     string &text(par.text);
849 
850     text += "   Skills:\n";
851 
852     dump_skills(text);
853     text += "\n";
854     text += "\n";
855 }
856 
spell_type_shortname(spschool spell_class,bool slash)857 static string spell_type_shortname(spschool spell_class, bool slash)
858 {
859     string ret;
860 
861     if (slash)
862         ret = "/";
863 
864     ret += spelltype_short_name(spell_class);
865 
866     return ret;
867 }
868 
_sdump_spells(dump_params & par)869 static void _sdump_spells(dump_params &par)
870 {
871     string &text(par.text);
872 
873     int spell_levels = player_spell_levels();
874 
875     string verb = par.se? "had" : "have";
876 
877     if (spell_levels == 1)
878         text += "You " + verb + " one spell level left.";
879     else if (spell_levels == 0)
880     {
881         verb = par.se? "couldn't" : "cannot";
882 
883         text += "You " + verb + " memorise any spells.";
884     }
885     else
886     {
887         if (par.se)
888             text += "You had ";
889         else
890             text += "You have ";
891         text += make_stringf("%d spell levels left.", spell_levels);
892     }
893 
894     text += "\n";
895 
896     if (!you.spell_no)
897     {
898         verb = par.se? "didn't" : "don't";
899 
900         text += "You " + verb + " know any spells.\n";
901     }
902     else
903     {
904         verb = par.se? "knew" : "know";
905 
906         text += "You " + verb + " the following spells:\n\n";
907 
908         text += " Your Spells              Type           Power      Damage    Failure   Level" "\n";
909 
910         for (int j = 0; j < 52; j++)
911         {
912             const char letter = index_to_letter(j);
913             const spell_type spell  = get_spell_by_letter(letter);
914 
915             if (spell != SPELL_NO_SPELL)
916             {
917                 string spell_line;
918 
919                 spell_line += letter;
920                 spell_line += " - ";
921                 spell_line += spell_title(spell);
922 
923                 spell_line = chop_string(spell_line, 24);
924                 spell_line += "  ";
925 
926                 bool already = false;
927 
928                 for (const auto bit : spschools_type::range())
929                 {
930                     if (spell_typematch(spell, bit))
931                     {
932                         spell_line += spell_type_shortname(bit, already);
933                         already = true;
934                     }
935                 }
936 
937                 spell_line = chop_string(spell_line, 41);
938 
939                 spell_line += spell_power_string(spell);
940 
941                 spell_line = chop_string(spell_line, 52);
942 
943                 const string spell_damage = spell_damage_string(spell);
944                 spell_line += spell_damage.length() ? spell_damage : "N/A";
945 
946                 spell_line = chop_string(spell_line, 62);
947 
948                 spell_line += failure_rate_to_string(raw_spell_fail(spell));
949 
950                 spell_line = chop_string(spell_line, 74);
951 
952                 spell_line += make_stringf("%d", spell_difficulty(spell));
953 
954                 spell_line += "\n";
955 
956                 text += spell_line;
957             }
958         }
959         text += "\n";
960     }
961 
962     if (!you.spell_library.count())
963     {
964         verb = par.se ? "was" : "is";
965         text += "Your spell library " + verb + " empty.\n\n";
966     }
967     else
968     {
969         verb = par.se? "contained" : "contains";
970         text += "Your spell library " + verb + " the following spells:\n\n";
971         text += " Spells                   Type           Power      Damage    Failure   Level" "\n";
972 
973         auto const library = get_sorted_spell_list(true, false);
974 
975         for (const spell_type spell : library)
976         {
977             const bool memorisable = you_can_memorise(spell);
978 
979             string spell_line;
980 
981             spell_line += ' ';
982             spell_line += spell_title(spell);
983 
984             spell_line = chop_string(spell_line, 24);
985             spell_line += "  ";
986 
987             bool already = false;
988 
989             for (const auto bit : spschools_type::range())
990             {
991                 if (spell_typematch(spell, bit))
992                 {
993                     spell_line += spell_type_shortname(bit, already);
994                     already = true;
995                 }
996             }
997 
998             spell_line = chop_string(spell_line, 41);
999 
1000             if (memorisable)
1001                 spell_line += spell_power_string(spell);
1002             else
1003                 spell_line += "Unusable";
1004 
1005             spell_line = chop_string(spell_line, 52);
1006 
1007             const string spell_damage = spell_damage_string(spell);
1008             spell_line += spell_damage.length() ? spell_damage : "N/A";
1009 
1010             spell_line = chop_string(spell_line, 62);
1011 
1012             if (memorisable)
1013                 spell_line += failure_rate_to_string(raw_spell_fail(spell));
1014             else
1015                 spell_line += "N/A";
1016 
1017             spell_line = chop_string(spell_line, 74);
1018 
1019             spell_line += make_stringf("%d", spell_difficulty(spell));
1020 
1021             spell_line += "\n";
1022 
1023             text += spell_line;
1024         }
1025         text += "\n\n";
1026     }
1027 }
1028 
_sdump_kills(dump_params & par)1029 static void _sdump_kills(dump_params &par)
1030 {
1031     par.text += you.kills.kill_info();
1032     par.text += "\n";
1033 }
1034 
_sdump_kills_place_info(const PlaceInfo place_info,string name="")1035 static string _sdump_kills_place_info(const PlaceInfo place_info, string name = "")
1036 {
1037     string out;
1038 
1039     if (name.empty())
1040         name = place_info.short_name();
1041 
1042     unsigned int global_total_kills = 0;
1043     for (int i = 0; i < KC_NCATEGORIES; i++)
1044         global_total_kills += you.global_info.mon_kill_num[i];
1045 
1046     unsigned int total_kills = 0;
1047     for (int i = 0; i < KC_NCATEGORIES; i++)
1048         total_kills += place_info.mon_kill_num[i];
1049 
1050     // Skip places where nothing was killed.
1051     if (total_kills == 0)
1052         return "";
1053 
1054     float a, b, c, d, e, f;
1055 
1056     a = TO_PERCENT(total_kills, global_total_kills);
1057     b = TO_PERCENT(place_info.mon_kill_num[KC_YOU],
1058                    you.global_info.mon_kill_num[KC_YOU]);
1059     c = TO_PERCENT(place_info.mon_kill_num[KC_FRIENDLY],
1060                    you.global_info.mon_kill_num[KC_FRIENDLY]);
1061     d = TO_PERCENT(place_info.mon_kill_num[KC_OTHER],
1062                    you.global_info.mon_kill_num[KC_OTHER]);
1063     e = TO_PERCENT(place_info.mon_kill_exp,
1064                    you.global_info.mon_kill_exp);
1065 
1066     f = float(place_info.mon_kill_exp) / place_info.levels_seen;
1067 
1068     out =
1069         make_stringf("%14s | %5.1f | %5.1f | %5.1f | %5.1f | %5.1f |"
1070                      " %13.1f\n",
1071                      name.c_str(), a, b, c , d, e, f);
1072 
1073     return _denanify(out);
1074 }
1075 
_sdump_kills_by_place(dump_params & par)1076 static void _sdump_kills_by_place(dump_params &par)
1077 {
1078     string &text(par.text);
1079 
1080     const vector<PlaceInfo> all_visited = you.get_all_place_info(true);
1081 
1082     string result = "";
1083 
1084     string header =
1085     "Table legend:\n"
1086     " A = Kills in this place as a percentage of kills in entire the game.\n"
1087     " B = Kills by you in this place as a percentage of kills by you in\n"
1088     "     the entire game.\n"
1089     " C = Kills by friends in this place as a percentage of kills by\n"
1090     "     friends in the entire game.\n"
1091     " D = Other kills in this place as a percentage of other kills in the\n"
1092     "     entire game.\n"
1093     " E = Experience gained in this place as a percentage of experience\n"
1094     "     gained in the entire game.\n"
1095     " F = Experience gained in this place divided by the number of levels of\n"
1096     "     this place that you have seen.\n\n";
1097 
1098     header += "               ";
1099     header += "    A       B       C       D       E               F\n";
1100     header += "               ";
1101     header += "+-------+-------+-------+-------+-------+----------------------\n";
1102 
1103     string footer = "               ";
1104     footer += "+-------+-------+-------+-------+-------+----------------------\n";
1105 
1106     result += _sdump_kills_place_info(you.global_info, "Total");
1107 
1108     for (const PlaceInfo &pi : all_visited)
1109         result += _sdump_kills_place_info(pi);
1110 
1111     if (!result.empty())
1112         text += header + result + footer + "\n";
1113 }
1114 
_sdump_overview(dump_params & par)1115 static void _sdump_overview(dump_params &par)
1116 {
1117     string overview =
1118         formatted_string::parse_string(overview_description_string(false));
1119     trim_string(overview);
1120     linebreak_string(overview, 80);
1121     par.text += overview;
1122     par.text += "\n\n";
1123 }
1124 
_sdump_hiscore(dump_params & par)1125 static void _sdump_hiscore(dump_params &par)
1126 {
1127     if (!par.se)
1128         return;
1129 
1130     string hiscore = hiscores_format_single_long(*(par.se), true);
1131     trim_string(hiscore);
1132     par.text += hiscore;
1133     par.text += "\n\n";
1134 }
1135 
_sdump_monster_list(dump_params & par)1136 static void _sdump_monster_list(dump_params &par)
1137 {
1138     string monlist = mpr_monster_list(par.se);
1139     trim_string(monlist);
1140     while (!monlist.empty())
1141         par.text += wordwrap_line(monlist, 80) + "\n";
1142     par.text += "\n";
1143 }
1144 
_sdump_vault_list(dump_params & par)1145 static void _sdump_vault_list(dump_params &par)
1146 {
1147     if (par.full_id || par.se
1148 #ifdef WIZARD
1149         || you.wizard || you.suppress_wizard
1150 #endif
1151      )
1152     {
1153         par.text += "Levels and vault maps discovered:\n";
1154         par.text += dump_vault_maps();
1155         par.text += "\n";
1156     }
1157 }
1158 
_sort_by_first(pair<int,FixedVector<int,28>> a,pair<int,FixedVector<int,28>> b)1159 static bool _sort_by_first(pair<int, FixedVector<int, 28> > a,
1160                            pair<int, FixedVector<int, 28> > b)
1161 {
1162     for (int i = 0; i < 27; i++)
1163     {
1164         if (a.second[i] > b.second[i])
1165             return true;
1166         else if (a.second[i] < b.second[i])
1167             return false;
1168     }
1169     return false;
1170 }
1171 
_count_action(caction_type type,int subtype)1172 static void _count_action(caction_type type, int subtype)
1173 {
1174     pair<caction_type, int> pair(type, subtype);
1175     if (!you.action_count.count(pair))
1176         you.action_count[pair].init(0);
1177     you.action_count[pair][you.experience_level - 1]++;
1178 }
1179 
1180 /**
1181  * The alternate type is stored in the higher bytes.
1182  **/
count_action(caction_type type,int subtype,int auxtype)1183 void count_action(caction_type type, int subtype, int auxtype)
1184 {
1185     ASSERT_RANGE(subtype, -32768, 32768);
1186     ASSERT_RANGE(auxtype, -32768, 32768);
1187     _count_action(type, caction_compound(subtype, auxtype));
1188 }
1189 
caction_compound(int subtype,int auxtype)1190 int caction_compound(int subtype, int auxtype)
1191 {
1192     ASSERT_RANGE(subtype, -32768, 32768);
1193     ASSERT_RANGE(auxtype, -32768, 32768);
1194     return (auxtype << 16) | (subtype & 0xFFFF);
1195 }
1196 
1197 /**
1198  * .first is the subtype; .second is the auxtype (-1 if none).
1199  **/
caction_extract_types(int compound_subtype)1200 pair<int, int> caction_extract_types(int compound_subtype)
1201 {
1202     return make_pair(int16_t(compound_subtype),
1203                      int16_t(compound_subtype >> 16));
1204 }
1205 
_describe_action(caction_type type)1206 static string _describe_action(caction_type type)
1207 {
1208     switch (type)
1209     {
1210     case CACT_MELEE:
1211         return "Melee";
1212     case CACT_FIRE:
1213         return " Fire";
1214     case CACT_THROW:
1215         return "Throw";
1216     case CACT_ARMOUR:
1217         return "Armor"; // "Armour" is too long
1218     case CACT_BLOCK:
1219         return "Block";
1220     case CACT_DODGE:
1221         return "Dodge";
1222     case CACT_CAST:
1223         return " Cast";
1224     case CACT_INVOKE:
1225         return "Invok";
1226     case CACT_ABIL:
1227         return " Abil";
1228     case CACT_EVOKE:
1229         return "Evoke";
1230     case CACT_USE:
1231         return "  Use";
1232     case CACT_STAB:
1233         return " Stab";
1234 #if TAG_MAJOR_VERSION == 34
1235     case CACT_EAT:
1236         return "  Eat";
1237 #endif
1238     case CACT_RIPOSTE:
1239         return "Rpst.";
1240     default:
1241         return "Error";
1242     }
1243 }
1244 
1245 static const char* _stab_names[] =
1246 {
1247     "Normal",
1248     "Distracted",
1249     "Confused",
1250     "Fleeing",
1251     "Invisible",
1252     "Held in net/web",
1253     "Petrifying", // could be nice to combine the two
1254     "Petrified",
1255     "Paralysed",
1256     "Sleeping",
1257     "Betrayed ally",
1258 };
1259 
1260 static const char* _aux_attack_names[1 + UNAT_LAST_ATTACK] =
1261 {
1262     "No attack",
1263     "Constrict",
1264     "Kick",
1265     "Headbutt",
1266     "Peck",
1267     "Tailslap",
1268     "Punch",
1269     "Bite",
1270     "Pseudopods",
1271     "Tentacles",
1272 };
1273 
_describe_action_subtype(caction_type type,int compound_subtype)1274 static string _describe_action_subtype(caction_type type, int compound_subtype)
1275 {
1276     pair<int, int> types = caction_extract_types(compound_subtype);
1277     int subtype = types.first;
1278     int auxtype = types.second;
1279 
1280     switch (type)
1281     {
1282     case CACT_THROW:
1283     {
1284         if (auxtype == OBJ_MISSILES)
1285             return uppercase_first(item_base_name(OBJ_MISSILES, subtype));
1286         else
1287             return "Other";
1288     }
1289     case CACT_MELEE:
1290     case CACT_FIRE:
1291     case CACT_RIPOSTE:
1292         if (subtype == -1)
1293         {
1294             if (auxtype == -1)
1295                 return "Unarmed";
1296             else
1297             {
1298                 ASSERT_RANGE(auxtype, 0, NUM_UNARMED_ATTACKS);
1299                 return _aux_attack_names[auxtype];
1300             }
1301         }
1302         else if (subtype >= UNRAND_START)
1303         {
1304             // Paranoia: an artefact may lose its specialness.
1305             const char *tn = get_unrand_entry(subtype)->type_name;
1306             if (tn)
1307                 return uppercase_first(tn);
1308             subtype = get_unrand_entry(subtype)->sub_type;
1309         }
1310         return uppercase_first(item_base_name(OBJ_WEAPONS, subtype));
1311     case CACT_ARMOUR:
1312         return (subtype == -1) ? "Skin"
1313                : uppercase_first(item_base_name(OBJ_ARMOUR, subtype));
1314     case CACT_BLOCK:
1315     {
1316         if (subtype > -1)
1317             return uppercase_first(item_base_name(OBJ_ARMOUR, subtype));
1318         switch (auxtype)
1319         {
1320         case BLOCK_OTHER:
1321             return "Other"; // non-shield block
1322         case BLOCK_REFLECT:
1323             return "Reflection";
1324         default:
1325             return "Error";
1326         }
1327     }
1328     case CACT_DODGE:
1329     {
1330         switch ((dodge_type)subtype)
1331         {
1332         case DODGE_EVASION:
1333             return "Dodged";
1334         case DODGE_REPEL:
1335             return "Repelled";
1336         default:
1337             return "Error";
1338         }
1339     }
1340     case CACT_CAST:
1341         return spell_title((spell_type)subtype);
1342     case CACT_INVOKE:
1343     case CACT_ABIL:
1344         return ability_name((ability_type)subtype);
1345     case CACT_EVOKE:
1346         if (subtype >= UNRAND_START && subtype <= UNRAND_LAST)
1347             return uppercase_first(get_unrand_entry(subtype)->name);
1348 
1349         if (auxtype > -1)
1350         {
1351             item_def dummy;
1352             dummy.base_type = (object_class_type)(auxtype);
1353             dummy.sub_type  = subtype;
1354             dummy.quantity  = 1;
1355             return uppercase_first(dummy.name(DESC_DBNAME, true));
1356         }
1357 
1358         switch ((evoc_type)subtype)
1359         {
1360         case EVOC_WAND:
1361             return "Wand";
1362 #if TAG_MAJOR_VERSION == 34
1363         case EVOC_ROD:
1364             return "Rod";
1365         case EVOC_DECK:
1366             return "Deck";
1367         case EVOC_MISC:
1368             return "Miscellaneous";
1369         case EVOC_BUGGY_TOME:
1370             return "tome";
1371 #endif
1372         default:
1373             return "Error";
1374         }
1375     case CACT_USE:
1376         return uppercase_first(base_type_string((object_class_type)subtype));
1377     case CACT_STAB:
1378         COMPILE_CHECK(ARRAYSZ(_stab_names) == NUM_STABS);
1379         ASSERT_RANGE(subtype, 1, NUM_STABS);
1380         return _stab_names[subtype];
1381 #if TAG_MAJOR_VERSION == 34
1382     case CACT_EAT:
1383         return "Removed food";
1384 #endif
1385     default:
1386         return "Error";
1387     }
1388 }
1389 
_sdump_action_counts(dump_params & par)1390 static void _sdump_action_counts(dump_params &par)
1391 {
1392     if (you.action_count.empty())
1393         return;
1394     int max_lt = (min<int>(you.max_level, 27) - 1) / 3;
1395 
1396     // Don't show both a total and 1..3 when there's only one tier.
1397     if (max_lt)
1398         max_lt++;
1399 
1400     par.text += make_stringf("%-24s", "Action");
1401     for (int lt = 0; lt < max_lt; lt++)
1402         par.text += make_stringf(" | %2d-%2d", lt * 3 + 1, lt * 3 + 3);
1403     par.text += make_stringf(" || %5s", "total");
1404     par.text += "\n-------------------------";
1405     for (int lt = 0; lt < max_lt; lt++)
1406         par.text += "+-------";
1407     par.text += "++-------\n";
1408 
1409     for (int cact = 0; cact < NUM_CACTIONS; cact++)
1410     {
1411         vector<pair<int, FixedVector<int, 28> > > action_vec;
1412         for (const auto &entry : you.action_count)
1413         {
1414             if (entry.first.first != cact)
1415                 continue;
1416             FixedVector<int, 28> v;
1417             v[27] = 0;
1418             for (int i = 0; i < 27; i++)
1419             {
1420                 v[i] = entry.second[i];
1421                 v[27] += v[i];
1422             }
1423             action_vec.emplace_back(entry.first.second, v);
1424         }
1425         sort(action_vec.begin(), action_vec.end(), _sort_by_first);
1426 
1427         for (auto ac = action_vec.begin(); ac != action_vec.end(); ++ac)
1428         {
1429             if (ac == action_vec.begin())
1430             {
1431                 par.text += _describe_action(caction_type(cact));
1432                 par.text += ": ";
1433             }
1434             else
1435                 par.text += "       ";
1436             par.text += chop_string(_describe_action_subtype(caction_type(cact), ac->first), 17);
1437             for (int lt = 0; lt < max_lt; lt++)
1438             {
1439                 int ltotal = 0;
1440                 for (int i = lt * 3; i < lt * 3 + 3; i++)
1441                     ltotal += ac->second[i];
1442                 if (ltotal)
1443                     par.text += make_stringf(" |%6d", ltotal);
1444                 else
1445                     par.text += " |      ";
1446             }
1447             par.text += make_stringf(" ||%6d", ac->second[27]);
1448             par.text += "\n";
1449         }
1450     }
1451     par.text += "\n";
1452 }
1453 
_sdump_skill_gains(dump_params & par)1454 static void _sdump_skill_gains(dump_params &par)
1455 {
1456     typedef map<int, int> XlToSkillLevelMap;
1457     map<skill_type, XlToSkillLevelMap> skill_gains;
1458     vector<skill_type> skill_order;
1459     int xl = 0;
1460     int max_xl = 0;
1461     for (const Note &note : note_list)
1462     {
1463         if (note.type == NOTE_XP_LEVEL_CHANGE)
1464             xl = note.first;
1465         else if (note.type == NOTE_GAIN_SKILL || note.type == NOTE_LOSE_SKILL)
1466         {
1467             skill_type skill = static_cast<skill_type>(note.first);
1468             int skill_level = note.second;
1469             if (skill_gains.find(skill) == skill_gains.end())
1470                 skill_order.push_back(skill);
1471             skill_gains[skill][xl] = skill_level;
1472             max_xl = max(max_xl, xl);
1473         }
1474     }
1475 
1476     if (skill_order.empty())
1477         return;
1478 
1479     for (int i = 0; i < NUM_SKILLS; i++)
1480     {
1481         skill_type skill = static_cast<skill_type>(i);
1482         if (you.skill(skill, 10, true) > 0
1483             && skill_gains.find(skill) == skill_gains.end())
1484         {
1485             skill_order.push_back(skill);
1486         }
1487     }
1488 
1489     par.text += "Skill      XL: |";
1490     for (xl = 1; xl <= max_xl; xl++)
1491         par.text += make_stringf(" %2d", xl);
1492     par.text += " |\n";
1493     par.text += "---------------+";
1494     for (xl = 1; xl <= max_xl; xl++)
1495         par.text += "---";
1496     par.text += "-+-----\n";
1497 
1498     for (skill_type skill : skill_order)
1499     {
1500         par.text += make_stringf("%-14s |", skill_name(skill));
1501         const XlToSkillLevelMap &gains = skill_gains[skill];
1502         for (xl = 1; xl <= max_xl; xl++)
1503         {
1504             auto it = gains.find(xl);
1505             if (it != gains.end())
1506                 par.text += make_stringf(" %2d", it->second);
1507             else
1508                 par.text += "   ";
1509         }
1510         par.text += make_stringf(" | %4.1f\n",
1511                                  you.skill(skill, 10, true) * 0.1);
1512     }
1513     par.text += "\n";
1514 }
1515 
_sdump_mutations(dump_params & par)1516 static void _sdump_mutations(dump_params &par)
1517 {
1518     string &text(par.text);
1519 
1520     if (you.how_mutated(true, false))
1521     {
1522         text += "\n";
1523         text += (formatted_string::parse_string(describe_mutations(false)));
1524         text += "\n\n";
1525     }
1526 }
1527 
morgue_directory()1528 string morgue_directory()
1529 {
1530     string dir = (!Options.morgue_dir.empty() ? Options.morgue_dir :
1531                   !SysEnv.crawl_dir.empty()   ? SysEnv.crawl_dir
1532                                               : "");
1533 
1534     if (!dir.empty() && dir[dir.length() - 1] != FILE_SEPARATOR)
1535         dir += FILE_SEPARATOR;
1536 
1537     return dir;
1538 }
1539 
dump_map(FILE * fp,bool debug,bool dist,bool log)1540 void dump_map(FILE *fp, bool debug, bool dist, bool log)
1541 {
1542     if (debug)
1543     {
1544 #ifdef COLOURED_DUMPS
1545         // Usage: make EXTERNAL_DEFINES="-DCOLOURED_DUMPS"
1546         // To read the dumps, cat them or use less -R.
1547         // ansi2html can be used to make html.
1548 
1549         fprintf(fp, "Vaults used:\n");
1550         for (size_t i = 0; i < env.level_vaults.size(); ++i)
1551         {
1552             const vault_placement &vp(*env.level_vaults[i]);
1553             fprintf(fp, "  \e[3%dm%s\e[0m at (%d,%d) size (%d,%d)\n",
1554                     6 - (int)i % 6, vp.map.name.c_str(),
1555                     vp.pos.x, vp.pos.y, vp.size.x, vp.size.y);
1556         }
1557         fprintf(fp, "  (bright = stacked, \e[37;1mwhite\e[0m = not in level_map_ids)\n");
1558         size_t last_nv = 0;
1559         int    last_v = 0;
1560 #endif
1561         // Write the whole map out without checking for mappedness. Handy
1562         // for debugging level-generation issues.
1563         for (int y = 0; y < GYM; ++y)
1564         {
1565             for (int x = 0; x < GXM; ++x)
1566             {
1567 #ifdef COLOURED_DUMPS
1568                 size_t nv = 0;
1569                 for (auto &vault : env.level_vaults)
1570                     if (vault->map.in_map(coord_def(x, y) - vault->pos))
1571                         nv++;
1572 
1573                 int v = env.level_map_ids[x][y];
1574                 if (v == INVALID_MAP_INDEX)
1575                     v = -1;
1576                 if (nv != last_nv || v != last_v)
1577                 {
1578                     if (nv)
1579                         fprintf(fp, "\e[%d;3%dm", nv != 1, 6 - v % 6);
1580                     else
1581                         fprintf(fp, "\e[0m");
1582                     last_nv = nv;
1583                     last_v = v;
1584                 }
1585 #endif
1586                 if (dist && you.pos() == coord_def(x, y))
1587                     fputc('@', fp);
1588                 else if (testbits(env.pgrid[x][y], FPROP_HIGHLIGHT))
1589                     fputc('?', fp);
1590                 else if (dist && env.grid[x][y] == DNGN_FLOOR
1591                          && travel_point_distance[x][y] > 0
1592                          && travel_point_distance[x][y] < 10)
1593                 {
1594                     fputc('0' + travel_point_distance[x][y], fp);
1595                 }
1596                 else if (env.grid[x][y] >= NUM_FEATURES)
1597                     fputc('!', fp);
1598                 else
1599                 {
1600                     fputs(OUTS(stringize_glyph(
1601                                get_feature_def(env.grid[x][y]).symbol())), fp);
1602                 }
1603             }
1604             fputc('\n', fp);
1605 #ifdef COLOURED_DUMPS
1606             last_v = 0; // force a colour code, because of less+libvte
1607 #endif
1608         }
1609 #ifdef COLOURED_DUMPS
1610         fprintf(fp, "\e[0m");
1611 #endif
1612     }
1613     else
1614     {
1615         int min_x = GXM-1, max_x = 0, min_y = GYM-1, max_y = 0;
1616 
1617         for (int i = X_BOUND_1; i <= X_BOUND_2; i++)
1618             for (int j = Y_BOUND_1; j <= Y_BOUND_2; j++)
1619                 if (env.map_knowledge[i][j].known())
1620                 {
1621                     if (i > max_x) max_x = i;
1622                     if (i < min_x) min_x = i;
1623                     if (j > max_y) max_y = j;
1624                     if (j < min_y) min_y = j;
1625                 }
1626 
1627         for (int y = min_y; y <= max_y; ++y)
1628         {
1629             for (int x = min_x; x <= max_x; ++x)
1630             {
1631                 fputs(OUTS(stringize_glyph(
1632                            get_cell_glyph(coord_def(x, y)).ch)), fp);
1633             }
1634 
1635             fputc('\n', fp);
1636         }
1637     }
1638 
1639     // for debug use in scripts, e.g. placement.lua
1640     if (log)
1641     {
1642         string the_log = get_last_messages(NUM_STORED_MESSAGES, true);
1643         fprintf(fp, "\n%s", the_log.c_str());
1644     }
1645 }
1646 
dump_map(const char * fname,bool debug,bool dist,bool log)1647 void dump_map(const char* fname, bool debug, bool dist, bool log)
1648 {
1649     FILE* fp = fopen_replace(fname);
1650     if (!fp)
1651         return;
1652 
1653     dump_map(fp, debug, dist, log);
1654 
1655     fclose(fp);
1656 }
1657 
_write_dump(const string & fname,const dump_params & par,bool quiet)1658 static bool _write_dump(const string &fname, const dump_params &par, bool quiet)
1659 {
1660     bool succeeded = false;
1661 
1662     string file_name = morgue_directory();
1663 
1664     file_name += strip_filename_unsafe_chars(fname);
1665 
1666     StashTrack.update_corpses();
1667 
1668     string stash_file_name;
1669     stash_file_name = file_name;
1670     stash_file_name += ".lst";
1671     StashTrack.dump(stash_file_name.c_str(), par.full_id);
1672 
1673     file_name += ".txt";
1674     FILE *handle = fopen_replace(file_name.c_str());
1675 
1676     dprf("File name: %s", file_name.c_str());
1677 
1678     if (handle != nullptr)
1679     {
1680         fputs(OUTS(par.text), handle);
1681         fclose(handle);
1682         succeeded = true;
1683         if (!quiet)
1684 #ifdef DGAMELAUNCH
1685             mpr("Char dumped successfully.");
1686 #else
1687             mprf("Char dumped to '%s'.", file_name.c_str());
1688 #endif
1689     }
1690     else
1691         mprf(MSGCH_ERROR, "Error opening file '%s'", file_name.c_str());
1692 
1693     return succeeded;
1694 }
1695 
display_notes()1696 void display_notes()
1697 {
1698     formatted_scroller scr(FS_START_AT_END | FS_PREWRAPPED_TEXT);
1699     scr.set_more();
1700     scr.set_tag("notes");
1701     scr.add_raw_text("Turn   | Place    | Note\n");
1702     for (const Note &note : note_list)
1703     {
1704         if (note.hidden())
1705             continue;
1706         string prefix = note.describe(true, true, false);
1707         string suffix = note.describe(false, false, true);
1708         if (suffix.empty())
1709             continue;
1710         int spaceleft = 80 - prefix.length() - 1; // Use 100 cols
1711         if (spaceleft <= 0)
1712             return;
1713 
1714         linebreak_string(suffix, spaceleft);
1715         vector<string> parts = split_string("\n", suffix);
1716         if (parts.empty()) // Disregard pure-whitespace notes.
1717             continue;
1718 
1719         scr.add_raw_text(prefix + parts[0] + "\n");
1720         for (unsigned int j = 1; j < parts.size(); ++j)
1721             scr.add_raw_text(string(prefix.length()-2, ' ') + string("| ") + parts[j] + "\n");
1722     }
1723     scr.show();
1724 }
1725 
display_char_dump()1726 void display_char_dump()
1727 {
1728     formatted_scroller scr(FS_PREWRAPPED_TEXT);
1729     scr.add_raw_text(_get_dump().text, false);
1730     scr.set_more();
1731     scr.set_tag("dump");
1732     scr.show();
1733 }
1734 
1735 #ifdef DGL_WHEREIS
1736 ///////////////////////////////////////////////////////////////////////////
1737 // whereis player
whereis_record(const char * status)1738 void whereis_record(const char *status)
1739 {
1740     const string file_name = morgue_directory()
1741                              + strip_filename_unsafe_chars(you.your_name)
1742                              + string(".where");
1743 
1744     if (FILE *handle = fopen_replace(file_name.c_str()))
1745     {
1746         // no need to bother with supporting ancient charsets for DGL
1747         fprintf(handle, "%s:status=%s\n",
1748                 xlog_status_line().c_str(),
1749                 status? status : "");
1750         fclose(handle);
1751     }
1752 }
1753 #endif
1754 
1755 ///////////////////////////////////////////////////////////////////////////////
1756 // Turn timestamps
1757 //
1758 // For DGL installs, write a timestamp at regular intervals into a file in
1759 // the morgue directory. The timestamp file is named
1760 // "timestamp-<player>-<starttime>.ts". All timestamps are standard Unix
1761 // time_t, but currently only the low 4 bytes are saved even on systems
1762 // with 64-bit time_t.
1763 //
1764 // Timestamp files are append only, and Crawl will check and handle cases
1765 // where a previous Crawl process crashed at a higher turn count on the same
1766 // game.
1767 //
1768 // Having timestamps associated with the game allows for much easier seeking
1769 // within Crawl ttyrecs by external tools such as FooTV.
1770 
1771 #ifdef DGL_TURN_TIMESTAMPS
1772 
1773 #include "syscalls.h"
1774 #include <sys/stat.h>
1775 
1776 // File-format version for timestamp files. Crawl will never append to a
1777 const uint32_t DGL_TIMESTAMP_VERSION = 1;
1778 const int VERSION_SIZE = sizeof(DGL_TIMESTAMP_VERSION);
1779 const int TIMESTAMP_SIZE = sizeof(uint32_t);
1780 
1781 // Returns the name of the timestamp file based on the morgue_dir,
1782 // character name and the game start time.
_dgl_timestamp_filename()1783 static string _dgl_timestamp_filename()
1784 {
1785     const string filename = "timestamp-" + you.your_name + "-"
1786                             + make_file_time(you.birth_time);
1787     return morgue_directory() + strip_filename_unsafe_chars(filename) + ".ts";
1788 }
1789 
1790 // Returns true if the given file exists and is not a timestamp file
1791 // of a known version or truncated timestamp file.
_dgl_unknown_timestamp_file(const string & filename)1792 static bool _dgl_unknown_timestamp_file(const string &filename)
1793 {
1794     if (FILE *inh = fopen_u(filename.c_str(), "rb"))
1795     {
1796         reader r(inh);
1797         r.set_safe_read(true);
1798         try
1799         {
1800             const uint32_t file_version = unmarshallInt(r);
1801             fclose(inh);
1802             return file_version != DGL_TIMESTAMP_VERSION;
1803         }
1804         catch (short_read_exception &e)
1805         {
1806             // Empty file, or <4 bytes: remove file and use it.
1807             fclose(inh);
1808             // True (don't use) if we couldn't remove it, false if we could.
1809             return unlink_u(filename.c_str()) != 0;
1810         }
1811     }
1812     return false;
1813 }
1814 
1815 // Returns a filehandle to use to write turn timestamps, nullptr if
1816 // timestamps should not be written.
_dgl_timestamp_filehandle()1817 static FILE *_dgl_timestamp_filehandle()
1818 {
1819     static FILE *timestamp_file = nullptr;
1820     static bool opened_file = false;
1821     if (!opened_file)
1822     {
1823         opened_file = true;
1824 
1825         const string filename = _dgl_timestamp_filename();
1826         // First check if there's already a timestamp file. If it exists
1827         // but has a different version, we cannot safely modify it, so bail.
1828         if (!_dgl_unknown_timestamp_file(filename))
1829             timestamp_file = fopen_u(filename.c_str(), "ab");
1830     }
1831     return timestamp_file;
1832 }
1833 
1834 // Records a timestamp in the .ts file at the given offset. If no timestamp
1835 // file exists, a new file will be created.
_dgl_record_timestamp(unsigned long file_offset,time_t time)1836 static void _dgl_record_timestamp(unsigned long file_offset, time_t time)
1837 {
1838     static bool timestamp_first_write = true;
1839     if (FILE *ftimestamp = _dgl_timestamp_filehandle())
1840     {
1841         writer w(_dgl_timestamp_filename(), ftimestamp, true);
1842         if (timestamp_first_write)
1843         {
1844             unsigned long ts_size = file_size(ftimestamp);
1845             if (!ts_size)
1846             {
1847                 marshallInt(w, DGL_TIMESTAMP_VERSION);
1848                 ts_size += sizeof(DGL_TIMESTAMP_VERSION);
1849             }
1850 
1851             // It's possible that the file we want to write is already
1852             // larger than the offset we expect if the game previously
1853             // crashed. When the game crashes, turn count is
1854             // effectively rewound to the point of the last save. In
1855             // such cases, we should not add timestamps until we reach
1856             // the correct turn count again.
1857             if (ts_size && ts_size > file_offset)
1858                 return;
1859 
1860             if (file_offset > ts_size)
1861             {
1862                 const int backlog =
1863                     (file_offset - ts_size) / TIMESTAMP_SIZE;
1864                 for (int i = 0; i < backlog; ++i)
1865                     marshallInt(w, 0);
1866             }
1867 
1868             timestamp_first_write = false;
1869         }
1870         fseek(ftimestamp, 0, SEEK_END);
1871         // [ds] FIXME: Eventually switch to 8 byte timestamps.
1872         marshallInt(w, static_cast<uint32_t>(time));
1873         fflush(ftimestamp);
1874     }
1875 }
1876 
1877 // Record timestamps every so many turns:
1878 const int TIMESTAMP_TURN_INTERVAL = 100;
1879 // Stop recording timestamps after this turncount.
1880 const int TIMESTAMP_TURN_MAX = 500000;
_dgl_record_timestamp(int turn)1881 static void _dgl_record_timestamp(int turn)
1882 {
1883     if (turn && turn < TIMESTAMP_TURN_MAX && !(turn % TIMESTAMP_TURN_INTERVAL))
1884     {
1885         const time_t now = time(nullptr);
1886         const unsigned long offset =
1887             (VERSION_SIZE +
1888              (turn / TIMESTAMP_TURN_INTERVAL - 1) * TIMESTAMP_SIZE);
1889         _dgl_record_timestamp(offset, now);
1890     }
1891 }
1892 
1893 #endif
1894 
1895 // Records a timestamp for the current player turn if appropriate.
record_turn_timestamp()1896 void record_turn_timestamp()
1897 {
1898 #ifdef DGL_TURN_TIMESTAMPS
1899     if (crawl_state.need_save)
1900         _dgl_record_timestamp(you.num_turns);
1901 #endif
1902 }
1903