1 /**
2 * @file
3 * database.cc
4 **/
5
6 #include "AppHdr.h"
7
8 #include "database.h"
9
10 #include <cstdlib>
11 #include <fcntl.h>
12 #include <sys/stat.h>
13 #include <sys/types.h>
14 #if defined(UNIX) || defined(TARGET_COMPILER_MINGW)
15 #include <unistd.h>
16 #endif
17
18 #include "clua.h"
19 #include "end.h"
20 #include "files.h"
21 #include "libutil.h"
22 #include "options.h"
23 #include "random.h"
24 #include "stringutil.h"
25 #include "syscalls.h"
26 #include "unicode.h"
27
28 // TextDB handles dependency checking the db vs text files, creating the
29 // db, loading, and destroying the DB.
30 class TextDB
31 {
32 public:
33 // db_name is the savedir-relative name of the db file,
34 // minus the "db" extension.
35 TextDB(const char* db_name, const char* dir, vector<string> files);
36 TextDB(TextDB *parent);
~TextDB()37 ~TextDB() { shutdown(true); delete translation; }
38 void init();
39 void shutdown(bool recursive = false);
get()40 DBM* get() { return _db; }
41
42 // Make it easier to migrate from raw DBM* to TextDB
operator bool() const43 operator bool() const { return _db != 0; }
operator DBM*() const44 operator DBM*() const { return _db; }
45
46 private:
47 bool _needs_update() const;
48 void _regenerate_db();
49
50 private:
51 bool open_db();
52 const char* const _db_name;
53 string _directory;
54 vector<string> _input_files;
55 DBM* _db;
56 string timestamp;
57 TextDB *_parent;
lang()58 const char* lang() { return _parent ? Options.lang_name : 0; }
59 public:
60 TextDB *translation;
61 };
62
63 // Convenience functions for (read-only) access to generic
64 // berkeley DB databases.
65 static void _store_text_db(const string &in, DBM *db);
66
67 static string _query_database(TextDB &db, string key, bool canonicalise_key,
68 bool run_lua, bool untranslated = false);
69 static void _add_entry(DBM *db, const string &k, string &v);
70
71 static TextDB AllDBs[] =
72 {
73 TextDB("descriptions", "descript/",
74 { "features.txt",
75 "items.txt",
76 "unident.txt",
77 "unrand.txt",
78 "monsters.txt",
79 "spells.txt",
80 "gods.txt",
81 "branches.txt",
82 "skills.txt",
83 "ability.txt",
84 "cards.txt",
85 "commands.txt",
86 "clouds.txt",
87 "status.txt" }),
88
89 TextDB("gamestart", "descript/",
90 { "species.txt",
91 "backgrounds.txt" }),
92
93 TextDB("randart", "database/",
94 { "randname.txt",
95 "rand_wpn.txt", // mostly weapons
96 "rand_arm.txt", // mostly armour
97 "rand_all.txt", // jewellery and general
98 "randbook.txt", // artefact books
99 // This doesn't really belong here, but they *are* god gifts...
100 "monname.txt" // orcish names for Beogh to choose from
101 }),
102
103 TextDB("speak", "database/",
104 { "monspeak.txt", // monster speech
105 "monspell.txt", // monster spellcasting speech
106 "monflee.txt", // monster fleeing speech
107 "wpnnoise.txt", // noisy weapon speech
108 "insult.txt", // imp/demon taunts
109 "godspeak.txt" // god speech
110 }),
111
112 TextDB("shout", "database/",
113 { "shout.txt",
114 "insult.txt" // imp/demon taunts, again
115 }),
116
117 TextDB("misc", "database/",
118 { "miscname.txt", // names for miscellaneous things
119 "godname.txt", // god-related names (mostly His Xomminess)
120 "montitle.txt", // titles for monsters (i.e. uniques)
121 }),
122
123 TextDB("quotes", "descript/",
124 { "quotes.txt" // quotes for items and monsters
125 }),
126
127 TextDB("help", "database/",
128 { "help.txt" // database for outsourced help texts
129 }),
130
131 TextDB("FAQ", "database/",
132 { "FAQ.txt", // database for Frequently Asked Questions
133 }),
134
135 TextDB("hints", "descript/",
136 { "hints.txt", // hints mode
137 "tutorial.txt", // tutorial mode
138 }),
139 };
140
141 static TextDB& DescriptionDB = AllDBs[0];
142 static TextDB& GameStartDB = AllDBs[1];
143 static TextDB& RandartDB = AllDBs[2];
144 static TextDB& SpeakDB = AllDBs[3];
145 static TextDB& ShoutDB = AllDBs[4];
146 static TextDB& MiscDB = AllDBs[5];
147 static TextDB& QuotesDB = AllDBs[6];
148 static TextDB& HelpDB = AllDBs[7];
149 static TextDB& FAQDB = AllDBs[8];
150 static TextDB& HintsDB = AllDBs[9];
151
_db_cache_path(string db,const char * lang)152 static string _db_cache_path(string db, const char *lang)
153 {
154 if (lang)
155 db = db + "." + lang;
156 return savedir_versioned_path("db/" + db);
157 }
158
159 // ----------------------------------------------------------------------
160 // TextDB
161 // ----------------------------------------------------------------------
162
TextDB(const char * db_name,const char * dir,vector<string> files)163 TextDB::TextDB(const char* db_name, const char* dir, vector<string> files)
164 : _db_name(db_name), _directory(dir), _input_files(files),
165 _db(nullptr), timestamp(""), _parent(0), translation(0)
166 {
167 }
168
TextDB(TextDB * parent)169 TextDB::TextDB(TextDB *parent)
170 : _db_name(parent->_db_name),
171 _directory(parent->_directory + Options.lang_name + "/"),
172 _input_files(parent->_input_files), // FIXME: pointless copy
173 _db(nullptr), timestamp(""), _parent(parent), translation(nullptr)
174 {
175 }
176
open_db()177 bool TextDB::open_db()
178 {
179 if (_db)
180 return true;
181
182 const string full_db_path = _db_cache_path(_db_name, lang());
183 _db = dbm_open(full_db_path.c_str(), O_RDONLY, 0660);
184 if (!_db)
185 return false;
186
187 timestamp = _query_database(*this, "TIMESTAMP", false, false, true);
188 if (timestamp.empty())
189 return false;
190
191 return true;
192 }
193
init()194 void TextDB::init()
195 {
196 if (Options.lang_name && !_parent)
197 {
198 translation = new TextDB(this);
199 translation->init();
200 }
201
202 open_db();
203
204 if (!_needs_update())
205 return;
206 _regenerate_db();
207
208 if (!open_db())
209 {
210 end(1, true, "Failed to open DB: %s",
211 _db_cache_path(_db_name, lang()).c_str());
212 }
213 }
214
shutdown(bool recursive)215 void TextDB::shutdown(bool recursive)
216 {
217 if (_db)
218 {
219 dbm_close(_db);
220 _db = nullptr;
221 }
222 if (recursive && translation)
223 translation->shutdown(recursive);
224 }
225
_needs_update() const226 bool TextDB::_needs_update() const
227 {
228 string ts;
229 bool no_files = true;
230
231 for (const string &file : _input_files)
232 {
233 string full_input_path = _directory + file;
234 full_input_path = datafile_path(full_input_path, !_parent);
235 time_t mtime = file_modtime(full_input_path);
236 #ifdef __ANDROID__
237 if (file_exists(full_input_path))
238 #else
239 if (mtime)
240 #endif
241 no_files = false;
242 char buf[20];
243 snprintf(buf, sizeof(buf), ":%" PRId64, (int64_t)mtime);
244 ts += buf;
245 }
246
247 if (no_files && timestamp.empty())
248 {
249 // No point in empty databases, although for simplicity keep ones
250 // for disappeared translations for now.
251 ASSERT(_parent);
252 TextDB *en = _parent;
253 delete en->translation; // ie, ourself
254 en->translation = 0;
255 return false;
256 }
257
258 return ts != timestamp;
259 }
260
_regenerate_db()261 void TextDB::_regenerate_db()
262 {
263 shutdown();
264 if (_parent)
265 {
266 #ifdef DEBUG_DIAGNOSTICS
267 printf("Regenerating db: %s [%s]\n", _db_name, Options.lang_name);
268 #endif
269 mprf(MSGCH_PLAIN, "Regenerating db: %s [%s]", _db_name, Options.lang_name);
270 }
271 else
272 {
273 #ifdef DEBUG_DIAGNOSTICS
274 printf("Regenerating db: %s\n", _db_name);
275 #endif
276 mprf(MSGCH_PLAIN, "Regenerating db: %s", _db_name);
277 }
278
279 string db_path = _db_cache_path(_db_name, lang());
280 string full_db_path = db_path + ".db";
281
282 {
283 string output_dir = get_parent_directory(db_path);
284 if (!check_mkdir("DB directory", &output_dir))
285 end(1, false, "Cannot create db directory '%s'.", output_dir.c_str());
286 }
287
288 file_lock lock(db_path + ".lk", "wb");
289 #ifndef DGL_REWRITE_PROTECT_DB_FILES
290 unlink_u(full_db_path.c_str());
291 #endif
292
293 string ts;
294 if (!(_db = dbm_open(db_path.c_str(), O_RDWR | O_CREAT, 0660)))
295 end(1, true, "Unable to open DB: %s", db_path.c_str());
296 for (const string &file : _input_files)
297 {
298 string full_input_path = _directory + file;
299 full_input_path = datafile_path(full_input_path, !_parent);
300 char buf[20];
301 time_t mtime = file_modtime(full_input_path);
302 snprintf(buf, sizeof(buf), ":%" PRId64, (int64_t)mtime);
303 ts += buf;
304 if (
305 #ifdef __ANDROID__
306 file_exists(full_input_path)
307 #else
308 mtime
309 #endif
310 || !_parent) // english is mandatory
311 {
312 _store_text_db(full_input_path, _db);
313 }
314 }
315 _add_entry(_db, "TIMESTAMP", ts);
316
317 dbm_close(_db);
318 _db = 0;
319 }
320
321 // ----------------------------------------------------------------------
322 // DB system
323 // ----------------------------------------------------------------------
324
325 #define NUM_DB ARRAYSZ(AllDBs)
326
databaseSystemInit()327 void databaseSystemInit()
328 {
329 for (unsigned int i = 0; i < NUM_DB; i++)
330 AllDBs[i].init();
331 }
332
databaseSystemShutdown()333 void databaseSystemShutdown()
334 {
335 for (unsigned int i = 0; i < NUM_DB; i++)
336 AllDBs[i].shutdown(true);
337 }
338
339 ////////////////////////////////////////////////////////////////////////////
340 // Main DB functions
341
_database_fetch(DBM * database,const string & key)342 static datum _database_fetch(DBM *database, const string &key)
343 {
344 datum result;
345 result.dptr = nullptr;
346 result.dsize = 0;
347 datum dbKey;
348
349 dbKey.dptr = (DPTR_COERCE) key.c_str();
350 dbKey.dsize = key.length();
351
352 // Don't use the database if called from "monster".
353 if (database)
354 result = dbm_fetch(database, dbKey);
355
356 return result;
357 }
358
_database_find_keys(DBM * database,const string & regex,bool ignore_case,db_find_filter filter=nullptr)359 static vector<string> _database_find_keys(DBM *database,
360 const string ®ex,
361 bool ignore_case,
362 db_find_filter filter = nullptr)
363 {
364 text_pattern tpat(regex, ignore_case);
365 vector<string> matches;
366
367 datum dbKey = dbm_firstkey(database);
368
369 while (dbKey.dptr != nullptr)
370 {
371 string key((const char *)dbKey.dptr, dbKey.dsize);
372
373 if (tpat.matches(key)
374 && key.find("__") == string::npos
375 && (filter == nullptr || !(*filter)(key, "")))
376 {
377 matches.push_back(key);
378 }
379
380 dbKey = dbm_nextkey(database);
381 }
382
383 return matches;
384 }
385
_database_find_bodies(DBM * database,const string & regex,bool ignore_case,db_find_filter filter=nullptr)386 static vector<string> _database_find_bodies(DBM *database,
387 const string ®ex,
388 bool ignore_case,
389 db_find_filter filter = nullptr)
390 {
391 text_pattern tpat(regex, ignore_case);
392 vector<string> matches;
393
394 datum dbKey = dbm_firstkey(database);
395
396 while (dbKey.dptr != nullptr)
397 {
398 string key((const char *)dbKey.dptr, dbKey.dsize);
399
400 datum dbBody = dbm_fetch(database, dbKey);
401 string body((const char *)dbBody.dptr, dbBody.dsize);
402
403 if (tpat.matches(body)
404 && key.find("__") == string::npos
405 && (filter == nullptr || !(*filter)(key, body)))
406 {
407 matches.push_back(key);
408 }
409
410 dbKey = dbm_nextkey(database);
411 }
412
413 return matches;
414 }
415
416 ///////////////////////////////////////////////////////////////////////////
417 // Internal DB utility functions
_execute_embedded_lua(string & str)418 static void _execute_embedded_lua(string &str)
419 {
420 // Execute any lua code found between "{{" and "}}". The lua code
421 // is expected to return a string, with which the lua code and
422 // braces will be replaced.
423 string::size_type pos = str.find("{{");
424 while (pos != string::npos)
425 {
426 string::size_type end = str.find("}}", pos + 2);
427 if (end == string::npos)
428 {
429 mprf(MSGCH_DIAGNOSTICS, "Unbalanced {{, bailing.");
430 break;
431 }
432
433 string lua_full = str.substr(pos, end - pos + 2);
434 string lua = str.substr(pos + 2, end - pos - 2);
435
436 if (clua.execstring(lua.c_str(), "db_embedded_lua", 1))
437 {
438 string err = "{{" + clua.error + "}}";
439 str.replace(pos, lua_full.length(), err);
440 return;
441 }
442
443 string result;
444 clua.fnreturns(">s", &result);
445
446 str.replace(pos, lua_full.length(), result);
447
448 pos = str.find("{{", pos + result.length());
449 }
450 }
451
_substitute_descriptions(TextDB & db,string & str,bool canonicalise_key,bool run_lua,bool untranslated)452 static void _substitute_descriptions(TextDB &db, string &str,
453 bool canonicalise_key, bool run_lua,
454 bool untranslated)
455 {
456 // Replace all keys found between "[[" and "]]" with corresponding
457 // descriptions from the database.
458 string::size_type pos = str.find("[[");
459 while (pos != string::npos)
460 {
461 string::size_type end = str.find("]]", pos + 2);
462 if (end == string::npos)
463 {
464 mprf(MSGCH_DIAGNOSTICS, "Unbalanced [[, bailing.");
465 break;
466 }
467
468 string key = str.substr(pos + 2, end - pos - 2);
469 string result = _query_database(db, key, canonicalise_key,
470 run_lua, untranslated);
471 str.replace(pos, key.length() + 4, trim_string_right(result));
472
473 pos = str.find("[[", pos + result.length());
474 }
475 }
476
_trim_leading_newlines(string & s)477 static void _trim_leading_newlines(string &s)
478 {
479 s.erase(0, s.find_first_not_of("\n"));
480 }
481
_add_entry(DBM * db,const string & k,string & v)482 static void _add_entry(DBM *db, const string &k, string &v)
483 {
484 _trim_leading_newlines(v);
485 datum key, value;
486 key.dptr = (char *) k.c_str();
487 key.dsize = k.length();
488
489 value.dptr = (char *) v.c_str();
490 value.dsize = v.length();
491
492 if (dbm_store(db, key, value, DBM_REPLACE))
493 end(1, true, "Error storing %s", k.c_str());
494 }
495
_parse_text_db(LineInput & inf,DBM * db)496 static void _parse_text_db(LineInput &inf, DBM *db)
497 {
498 string key;
499 string value;
500
501 bool in_entry = false;
502 while (!inf.eof())
503 {
504 string line = inf.get_line();
505
506 if (!line.empty() && line[0] == '#')
507 continue;
508
509 if (!line.compare(0, 4, "%%%%"))
510 {
511 if (!key.empty())
512 _add_entry(db, key, value);
513 key.clear();
514 value.clear();
515 in_entry = true;
516 continue;
517 }
518
519 if (!in_entry)
520 continue;
521
522 if (key.empty())
523 {
524 key = line;
525 trim_string(key);
526 lowercase(key);
527 }
528 else
529 {
530 trim_string_right(line);
531 value += line + "\n";
532 }
533 }
534
535 if (!key.empty())
536 _add_entry(db, key, value);
537 }
538
_store_text_db(const string & in,DBM * db)539 static void _store_text_db(const string &in, DBM *db)
540 {
541 UTF8FileLineInput inf(in.c_str());
542 if (inf.error())
543 end(1, true, "Unable to open input file: %s", in.c_str());
544
545 _parse_text_db(inf, db);
546 }
547
_chooseStrByWeight(const string & entry,int fixed_weight=-1)548 static string _chooseStrByWeight(const string &entry, int fixed_weight = -1)
549 {
550 vector<string> parts;
551 vector<int> weights;
552
553 vector<string> lines = split_string("\n", entry, false, true);
554
555 int total_weight = 0;
556 for (int i = 0, size = lines.size(); i < size; i++)
557 {
558 // Skip over multiple blank lines, and leading and trailing
559 // blank lines.
560 while (i < size && lines[i].empty())
561 i++;
562
563 if (i == size)
564 break;
565
566 int weight;
567 string part = "";
568
569 if (sscanf(lines[i].c_str(), "w:%d", &weight))
570 {
571 i++;
572 if (i == size)
573 return "BUG, WEIGHT AT END OF ENTRY";
574 }
575 else
576 weight = 10;
577
578 total_weight += weight;
579
580 while (i < size && !lines[i].empty())
581 {
582 part += lines[i++];
583 part += "\n";
584 }
585 trim_string(part);
586
587 parts.push_back(part);
588 weights.push_back(total_weight);
589 }
590
591 if (parts.empty())
592 return "BUG, EMPTY ENTRY";
593
594 int choice = 0;
595 if (fixed_weight != -1)
596 choice = fixed_weight % total_weight;
597 else
598 choice = random2(total_weight);
599
600 for (int i = 0, size = parts.size(); i < size; i++)
601 if (choice < weights[i])
602 return parts[i];
603
604 return "BUG, NO STRING CHOSEN";
605 }
606
607 #define MAX_RECURSION_DEPTH 10
608 #define MAX_REPLACEMENTS 100
609
_getWeightedString(TextDB & db,const string & key,const string & suffix,int fixed_weight=-1)610 static string _getWeightedString(TextDB &db, const string &key,
611 const string &suffix, int fixed_weight = -1)
612 {
613 // We have to canonicalise the key (in case the user typed it
614 // in and got the case wrong.)
615 string canonical_key = key + suffix;
616 lowercase(canonical_key);
617
618 // Query the DB.
619 datum result;
620
621 if (db.translation)
622 result = _database_fetch(db.translation->get(), canonical_key);
623 if (result.dsize <= 0)
624 result = _database_fetch(db.get(), canonical_key);
625
626 if (result.dsize <= 0)
627 {
628 // Try ignoring the suffix.
629 canonical_key = key;
630 lowercase(canonical_key);
631
632 // Query the DB.
633 if (db.translation)
634 result = _database_fetch(db.translation->get(), canonical_key);
635 if (result.dsize <= 0)
636 result = _database_fetch(db.get(), canonical_key);
637
638 if (result.dsize <= 0)
639 return "";
640 }
641
642 // Cons up a (C++) string to return. The caller must release it.
643 string str = string((const char *)result.dptr, result.dsize);
644
645 return _chooseStrByWeight(str, fixed_weight);
646 }
647
648 static void _call_recursive_replacement(string &str, TextDB &db,
649 const string &suffix,
650 int &num_replacements,
651 int recursion_depth = 0);
652
_getRandomisedStr(TextDB & db,const string & key,const string & suffix,int & num_replacements,int recursion_depth=0)653 static string _getRandomisedStr(TextDB &db, const string &key,
654 const string &suffix,
655 int &num_replacements,
656 int recursion_depth = 0)
657 {
658 recursion_depth++;
659 if (recursion_depth > MAX_RECURSION_DEPTH)
660 {
661 mprf(MSGCH_DIAGNOSTICS, "Too many nested replacements, bailing.");
662
663 return "TOO MUCH RECURSION";
664 }
665
666 string str = _getWeightedString(db, key, suffix);
667
668 _call_recursive_replacement(str, db, suffix, num_replacements,
669 recursion_depth);
670
671 return str;
672 }
673
674 // Replace any "@foo@" markers that can be found in this database.
675 // Those that can't be found are left alone for the caller to deal with.
_call_recursive_replacement(string & str,TextDB & db,const string & suffix,int & num_replacements,int recursion_depth)676 static void _call_recursive_replacement(string &str, TextDB &db,
677 const string &suffix,
678 int &num_replacements,
679 int recursion_depth)
680 {
681 string::size_type pos = str.find("@");
682 while (pos != string::npos)
683 {
684 num_replacements++;
685 if (num_replacements > MAX_REPLACEMENTS)
686 {
687 mprf(MSGCH_DIAGNOSTICS, "Too many string replacements, bailing.");
688 return;
689 }
690
691 string::size_type end = str.find("@", pos + 1);
692 if (end == string::npos)
693 {
694 mprf(MSGCH_DIAGNOSTICS, "Unbalanced @, bailing.");
695 break;
696 }
697
698 string marker_full = str.substr(pos, end - pos + 1);
699 string marker = str.substr(pos + 1, end - pos - 1);
700
701 string replacement =
702 _getRandomisedStr(db, marker, suffix, num_replacements,
703 recursion_depth);
704
705 if (replacement.empty())
706 {
707 // Nothing in database, leave it alone and go onto next @foo@
708 pos = str.find("@", end + 1);
709 }
710 else
711 {
712 str.replace(pos, marker_full.length(), replacement);
713
714 // Start search from pos rather than end + 1, so that if
715 // the replacement contains its own @foo@, we can replace
716 // that too.
717 pos = str.find("@", pos);
718 }
719 } // while (pos != string::npos)
720 }
721
_query_database(TextDB & db,string key,bool canonicalise_key,bool run_lua,bool untranslated)722 static string _query_database(TextDB &db, string key, bool canonicalise_key,
723 bool run_lua, bool untranslated)
724 {
725 if (canonicalise_key)
726 {
727 // We have to canonicalise the key (in case the user typed it
728 // in and got the case wrong.)
729 lowercase(key);
730 }
731
732 // Query the DB.
733 datum result;
734
735 if (db.translation && !untranslated)
736 result = _database_fetch(db.translation->get(), key);
737 if (result.dsize <= 0)
738 result = _database_fetch(db.get(), key);
739
740 if (result.dsize <= 0)
741 return "";
742
743 string str((const char *)result.dptr, result.dsize);
744
745 // <foo> is an alias to key foo
746 if (str[0] == '<' && str[str.size() - 2] == '>'
747 && str.find('<', 1) == str.npos
748 && str.find('\n') == str.size() - 1)
749 {
750 return _query_database(db, str.substr(1, str.size() - 3),
751 canonicalise_key, run_lua, untranslated);
752 }
753
754 _substitute_descriptions(db, str, canonicalise_key, run_lua, untranslated);
755
756 if (run_lua)
757 _execute_embedded_lua(str);
758
759 return str;
760 }
761
762 /////////////////////////////////////////////////////////////////////////////
763 // Quote DB specific functions.
764
getQuoteString(const string & key)765 string getQuoteString(const string &key)
766 {
767 return unwrap_desc(_query_database(QuotesDB, key, true, true));
768 }
769
770 /////////////////////////////////////////////////////////////////////////////
771 // Description DB specific functions.
772
getLongDescription(const string & key)773 string getLongDescription(const string &key)
774 {
775 return unwrap_desc(_query_database(DescriptionDB, key, true, true));
776 }
777
getLongDescKeysByRegex(const string & regex,db_find_filter filter)778 vector<string> getLongDescKeysByRegex(const string ®ex,
779 db_find_filter filter)
780 {
781 if (!DescriptionDB.get())
782 {
783 vector<string> empty;
784 return empty;
785 }
786
787 // FIXME: need to match regex against translated keys, which can't
788 // be done by db only.
789 return _database_find_keys(DescriptionDB.get(), regex, true, filter);
790 }
791
getLongDescBodiesByRegex(const string & regex,db_find_filter filter)792 vector<string> getLongDescBodiesByRegex(const string ®ex,
793 db_find_filter filter)
794 {
795 if (!DescriptionDB.get())
796 {
797 vector<string> empty;
798 return empty;
799 }
800
801 // On partial translations, this will match only translated descriptions.
802 // Not good, but otherwise we'd have to check hundreds of keys, with
803 // two queries for each.
804 // SQL can do this in one go, DBM can't.
805 DBM *database = DescriptionDB.translation ?
806 DescriptionDB.translation->get() : DescriptionDB.get();
807 return _database_find_bodies(database, regex, true, filter);
808 }
809
810 /////////////////////////////////////////////////////////////////////////////
811 // GameStart DB specific functions.
getGameStartDescription(const string & key)812 string getGameStartDescription(const string &key)
813 {
814 return _query_database(GameStartDB, key, true, true);
815 }
816
817 /////////////////////////////////////////////////////////////////////////////
818 // Shout DB specific functions.
getShoutString(const string & monst,const string & suffix)819 string getShoutString(const string &monst, const string &suffix)
820 {
821 int num_replacements = 0;
822
823 return _getRandomisedStr(ShoutDB, monst, suffix, num_replacements);
824 }
825
826 /////////////////////////////////////////////////////////////////////////////
827 // Speak DB specific functions.
getSpeakString(const string & key)828 string getSpeakString(const string &key)
829 {
830 int num_replacements = 0;
831
832 #ifdef DEBUG_MONSPEAK
833 dprf(DIAG_SPEECH, "monster speech lookup for %s", key.c_str());
834 #endif
835 string txt = _getRandomisedStr(SpeakDB, key, "", num_replacements);
836 _execute_embedded_lua(txt);
837
838 return txt;
839 }
840
841 /////////////////////////////////////////////////////////////////////////////
842 // Randname DB specific functions.
getRandNameString(const string & itemtype,const string & suffix)843 string getRandNameString(const string &itemtype, const string &suffix)
844 {
845 int num_replacements = 0;
846
847 return _getRandomisedStr(RandartDB, itemtype, suffix, num_replacements);
848 }
849
850 /////////////////////////////////////////////////////////////////////////////
851 // Help DB specific functions.
852
getHelpString(const string & topic)853 string getHelpString(const string &topic)
854 {
855 string help = _query_database(HelpDB, topic, false, true);
856 if (help.empty())
857 help = "Error! The help for \"" + topic + "\" is missing!";
858 return help;
859 }
860
861 /////////////////////////////////////////////////////////////////////////////
862 // FAQ DB specific functions.
getAllFAQKeys()863 vector<string> getAllFAQKeys()
864 {
865 if (!FAQDB.get())
866 {
867 vector<string> empty;
868 return empty;
869 }
870
871 return _database_find_keys(FAQDB.get(), "^q.+", false);
872 }
873
getFAQ_Question(const string & key)874 string getFAQ_Question(const string &key)
875 {
876 return _query_database(FAQDB, key, false, true);
877 }
878
getFAQ_Answer(const string & question)879 string getFAQ_Answer(const string &question)
880 {
881 string key = "a" + question.substr(1, question.length()-1);
882 string val = unwrap_desc(_query_database(FAQDB, key, false, true));
883
884 // Remove blank lines between items on a bulleted list, for small
885 // terminals' sake. Far easier to store them as separated paragraphs
886 // in the source.
887 // Also, use a nicer bullet as we're already here.
888 val = replace_all(val, "\n\n*", "\n•");
889
890 return val;
891 }
892
893 /////////////////////////////////////////////////////////////////////////////
894 // Miscellaneous DB specific functions.
895
getMiscString(const string & misc,const string & suffix)896 string getMiscString(const string &misc, const string &suffix)
897
898 {
899 int num_replacements = 0;
900
901 string txt = _getRandomisedStr(MiscDB, misc, suffix, num_replacements);
902 _execute_embedded_lua(txt);
903
904 return txt;
905 }
906
907 /////////////////////////////////////////////////////////////////////////////
908 // Hints DB specific functions.
909
getHintString(const string & key)910 string getHintString(const string &key)
911 {
912 return unwrap_desc(_query_database(HintsDB, key, true, true));
913 }
914