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 &regex,
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 &regex,
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 &regex,
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 &regex,
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