1 /*
2    Copyright (C) 2002/2006 Kai Sterker <kai.sterker@gmail.com>
3    Part of the Adonthell Project  <http://adonthell.nongnu.org>
4 
5    Dlgedit is free software; you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation; either version 2 of the License, or
8    (at your option) any later version.
9 
10    Dlgedit is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU General Public License for more details.
14 
15    You should have received a copy of the GNU General Public License
16    along with Dlgedit.  If not, see <http://www.gnu.org/licenses/>.
17 */
18 
19 /**
20  * @file dlg_compiler.cc
21  *
22  * @author Kai Sterker
23  * @brief The dialogue compiler.
24  */
25 
26 #include <iterator>
27 #include <iostream>
28 #include <string.h>
29 #include "dlg_cmdline.h"
30 #include "dlg_compiler.h"
31 #include "dlg_types.h"
32 #include "dlg_arrow.h"
33 #include "gui_error.h"
34 
35 // Operators that may appear in Python code
36 std::string DlgCompiler::operators[NUM_OPS] = { "==", "!=", "<", "<=", ">",
37     ">=", "=", ".", ":", "if", "elif", "else", "pass", "return", "and", "or",
38     "not", "+", "-", "*", "/", "\"", "'", "(", ")", "[", "]", ",", "#", "%",
39     "&", "|", "^"};
40 
41 // keywords that need no expansion
42 std::string DlgCompiler::fixed[NUM_FXD] = { "self", "quests", "the_npc",
43     "the_player", "characters"};
44 
45 
DlgCompiler(DlgModule * module)46 DlgCompiler::DlgCompiler (DlgModule *module)
47 {
48     start.type () = PLAYER;
49     dialogue = module;
50     errors = 0;
51 
52     int length = module->getNodes ().size () + 1;
53 
54     // create our lookup tables
55     codeTable = new int[length];
56     conditionTable = new int[length];
57     operationTable = new int[length];
58 
59     // initialize them with something sensible
60     memset (codeTable, 255, length * sizeof (int));
61     memset (conditionTable, 255, length * sizeof (int));
62     memset (operationTable, 0, length * sizeof (int));
63 }
64 
65 // dtor
~DlgCompiler()66 DlgCompiler::~DlgCompiler ()
67 {
68     delete[] codeTable;
69     delete[] conditionTable;
70     delete[] operationTable;
71 
72     for (DlgNode *next = start.next (FIRST); next != NULL; next = start.next (FIRST))
73         delete (DlgArrow *) next;
74 }
75 
76 // compile the dialogue into Python script
run()77 void DlgCompiler::run ()
78 {
79     if (DlgCmdline::compile == false)
80     {
81         // make sure the error console exists
82         if (GuiError::console == NULL)
83             GuiError::console = new GuiError ();
84         else
85             GuiError::console->clear ();
86     }
87 
88     // try to open the file
89     std::string fname = dialogue->fullName ();
90 
91     // remove the file extension
92     unsigned long pos = fname.rfind (FILE_EXT);
93     if (pos != fname.npos) fname.erase (pos);
94 
95     // try to open the file
96     file.open ((fname + ".py").c_str ());
97     if (!file.is_open())
98     {
99     	if (DlgCmdline::compile) std::cout << "*** error creating " << fname << std::endl;
100     	return;
101     }
102 
103     gchar *basename = g_path_get_basename(fname.c_str ());
104 
105     // write the script header
106     writeHeader (basename);
107 
108     // write the dialogue text
109     writeText ();
110 
111     // write loop table
112     writeLoop ();
113 
114     // write the conditions
115     writeConditions ();
116 
117     // write the code
118     writeCode ();
119 
120     // write the start of the dialoge
121     writeStart ();
122 
123     // write the rest of the dialogue
124     writeDialogue ();
125 
126     // write the custom code
127     writeCustomCode ();
128 
129     // display errors if there were any
130     if (DlgCmdline::compile == false && errors > 0)
131         GuiError::console->display ();
132 
133     file.flush();
134     file.close();
135 
136     g_free(basename);
137 }
138 
139 // write the topmost part of the dialogue
writeHeader(const std::string & theClass)140 void DlgCompiler::writeHeader (const std::string &theClass)
141 {
142     // imports
143     file << "import dialogue\n";
144 
145     // custom imports
146     if (dialogue->entry ()->imports () != "")
147         file << dialogue->entry ()->imports () << "\n\n";
148 
149     // stuff needed for i18n
150     file << "# -- pygettext support\n"
151          << "def _(message): return message\n\n"
152 
153     // the classname
154          << "class " << theClass << " (dialogue.base):\n";
155 }
156 
157 // write array with the dialogue text and initialize some stuff
writeText()158 void DlgCompiler::writeText ()
159 {
160     std::vector<DlgNode*> nodes = dialogue->getNodes ();
161     std::vector<DlgNode*>::iterator i;
162     DlgCircleEntry *entry;
163     DlgArrow *arrow;
164     unsigned int j = 0;
165 
166     // the array with the dialogue text
167     file << "\ttext = [None";
168 
169     for (i = nodes.begin (); i != nodes.end (); i++)
170     {
171         // nothing to be done for Arrows
172         if ((*i)->type () == LINK) continue;
173 
174         // get the entry of the current circle
175         entry = ((DlgCircle *) *i)->entry ();
176 
177         // see whether this is a start-node
178         if ((*i)->prev (FIRST) == NULL)
179             arrow = new DlgArrow (&start, *i);
180 
181         // set index of this node for later use
182         (*i)->setIndex (++j);
183 
184         // build condition vector
185         if (entry->condition () != "")
186             addCondition ((DlgCircle *) *i, j);
187 
188         // build code vector
189         if (entry->code () != "")
190             addCode (entry->code (), j);
191 
192         // build loop vector
193         if (entry->loop () != false)
194             loop.push_back (j);
195 
196         // write text of the node
197         file << ",\\\n\t\t_(\"" << entry->text () << "\")";
198     }
199 
200     // close text array
201     file << "]\n\n";
202 }
203 
204 // write loops
writeLoop()205 void DlgCompiler::writeLoop ()
206 {
207     // nothing to do if there are no loops
208     if (loop.empty ()) return;
209 
210     file << "\tloop = [";
211 
212     for (std::vector<int>::iterator i = loop.begin (); i != loop.end (); i++)
213     {
214         if (i != loop.begin ()) file << ", ";
215         file << (*i);
216     }
217 
218     // close array
219     file << "]\n\n";
220 }
221 
222 // write conditions of the dialogue
writeConditions()223 void DlgCompiler::writeConditions ()
224 {
225     // nothing to do if there are no conditions
226     if (conditions.empty ()) return;
227 
228     file << "\tcond = [\\";
229 
230     // write the individual conditions
231     for (std::vector<std::string>::iterator i = conditions.begin (); i != conditions.end (); i++)
232     {
233         if (i != conditions.begin ()) file << ",\\";
234         file << "\n\t\t\"" << escapeCode (splitCode (*i)) << "\"";
235     }
236 
237     // close array
238     file << "]\n\n";
239 }
240 
241 // write code of the dialogue
writeCode()242 void DlgCompiler::writeCode ()
243 {
244     // nothing to do if there are no conditions
245     if (code.empty ()) return;
246 
247     file << "\tcode = [\\";
248 
249     // write the individual conditions
250     for (std::vector<std::string>::iterator i = code.begin (); i != code.end (); i++)
251     {
252         if (i != code.begin ()) file << ",\\";
253         file << "\n\t\t\"" << escapeCode (splitCode (*i)) << "\"";
254     }
255 
256     // close array
257     file << "]\n\n";
258 }
259 
260 // write additional Python code
writeCustomCode()261 void DlgCompiler::writeCustomCode ()
262 {
263     // constructor
264     file << "\n\tdef __init__(self, p, n):"
265          << "\n\t\tself.namespace = globals ()"
266          << "\n\t\tself.the_player = p"
267          << "\n\t\tself.the_npc = n\n";
268 
269     // custom constructor code
270     if (dialogue->entry ()->ctor () != "")
271         file << "\n" << splitCode (dialogue->entry ()->ctor (), 2) << "\n";
272 
273     // custom destructor code
274     if (dialogue->entry ()->dtor () != "")
275         file << "\n\tdef __del__(self):\n"
276              << splitCode (dialogue->entry ()->dtor (), 2) << "\n";
277 
278     // custom methods
279     if (dialogue->entry ()->methods () != "")
280         file << "\n" << splitCode (dialogue->entry ()->methods (), 1) << "\n";
281 }
282 
283 // replace '\n', '\t', '"' and the like with '\\n' '\\t' and '\"'
escapeCode(std::string code)284 std::string DlgCompiler::escapeCode (std::string code)
285 {
286     char c;
287 
288     for (unsigned int i = 0; i < code.length (); i++)
289     {
290         c = code[i];
291 
292         if (c == '"') code.insert (i++, "\\");
293         else if (c == '\n') code.replace (i, 1, "\\n");
294         else if (c == '\t') code.replace (i, 1, "    ");
295     }
296 
297     return code;
298 }
299 
300 // splits python code into individual lines
splitCode(std::string code,int space)301 std::string DlgCompiler::splitCode (std::string code, int space)
302 {
303     std::string new_code = "";
304     unsigned int i = 0, j;
305 
306     code += '\n';
307 
308     while ((j = code.find ('\n', i)) < code.size ())
309     {
310         new_code.append (space, '\t');
311         new_code += inflateCode (code.substr (i, j-i));
312         new_code += "\n";
313         i = ++j;
314     }
315 
316     code.erase (code.end()-1);
317 
318     return new_code;
319 }
320 
inflateCode(std::string code)321 std::string DlgCompiler::inflateCode (std::string code)
322 {
323 	unsigned long pos;
324     unsigned int i, begin = 0, prefix;
325     int suffix;
326     std::string token, stripped, last_op = "";
327     bool is_local = true;
328 
329 #ifdef _DEBUG_
330     std::cout << ">>> " << code << std::endl;
331 #endif
332     // replace the_npc/the_player with self.the_npc/self.the_player
333     pos = code.find ("the_npc", 0);
334 
335     while (pos != code.npos)
336     {
337         if (pos < 5 || strncmp (code.substr (pos-5, pos).c_str(), "self.", 5))
338         {
339             code.insert (pos, "self.");
340             pos += 5;
341         }
342 
343         pos = code.find ("the_npc", pos+7);
344     }
345 
346     pos = code.find ("the_player", 0);
347 
348     while (pos != code.npos)
349     {
350         if (pos < 5 || strncmp (code.substr (pos-5, pos).c_str(), "self.", 5))
351         {
352             code.insert (pos, "self.");
353             pos += 5;
354         }
355 
356         pos = code.find ("the_player", pos+10);
357     }
358 
359     // scan the string from left to right
360     for (pos = 0; pos < code.length (); pos++)
361         for (i = 0; i < NUM_OPS; i++)
362             // search for the leftmost operator from the current position
363             if (!strncmp (code.substr (pos).c_str (), operators[i].c_str (),
364                 operators[i].length ()) || (i == NUM_OPS-1 && pos == code.length()-1))
365             {
366                 // takes care of the rare situation when the last token
367                 // of the string is a variable in need of expanding
368                 if (pos == code.length()-1 && i == NUM_OPS-1) pos++;
369 
370                 token = code.substr (begin, pos-begin);
371 
372                 // strip leading and trailing whitespace
373                 for (prefix = 0; prefix < token.length() && token[prefix] == ' '; prefix++);
374                 for (suffix = token.length()-1; suffix >= 0 && token[suffix] == ' '; suffix--);
375                 stripped = token.substr (prefix, suffix-prefix+1);
376 
377                 // have to be careful with textual operators and keywords
378                 if (i == BAND || i == BOR || i == NOT || i == RETURN ||
379                     i == PASS || i == IF || i == ELIF || i == ELSE)
380                 {
381                     if (pos > 0 && isalpha (code[pos-1]))
382                         break;
383                     if (pos < code.length()-operators[i].length()-1 &&
384                         isalpha (code[pos+operators[i].length()]))
385                         break;
386                 }
387 
388 #ifdef _DEBUG_
389                 std::cout << "token = '" << stripped << "', operator = '" <<
390                     operators[i] << "'\n" << std::flush;
391 #endif
392 
393                 // skip functions and arrays
394                 if (i == LBRACKET || i == LBRACE)
395                 {
396                     begin = pos + 1;
397                     break;
398                 }
399 
400                 // see whether we've got a variable and act accordingly
401                 if (getToken (stripped) == VARIABLE)
402                 {
403                     // make sure we don't have a local variable
404                     if (!is_local)
405                     {
406                         // assignment
407                         if (i == ASSIGN)
408                         {
409                             code[pos] = ',';
410                             code.insert (begin+suffix+1, "\"");
411                             code.insert (begin+prefix, "set_val (\"");
412                             code.append (")");
413                             pos += 11;
414                         }
415                         else
416                         {
417                             code.insert (begin+suffix+1, "\")");
418                             code.insert (begin+prefix, "get_val (\"");
419                             pos += 12;
420                         }
421                     }
422 
423                     // variable left of '.'
424                     if (i == ACCESS && last_op != ".")
425                     {
426                         // check whether we access the quest- or character array
427                         if (dialogue->entry ()->isQuest (stripped))
428                         {
429                             code.insert (begin+prefix+stripped.length(), "\")");
430                             code.insert (begin+prefix, "adonthell.gamedata_get_quest(\"");
431                             pos += 32;
432                             is_local = false;
433                         }
434 
435                         if (dialogue->entry ()->isCharacter (stripped))
436                         {
437                             code.insert (begin+prefix+stripped.length(), "\")");
438                             code.insert (begin+prefix, "adonthell.gamedata_get_character(\"");
439                             pos += 36;
440                             is_local = false;
441                         }
442                     }
443                     else is_local = true;
444                 }
445 
446                 // these are shortcuts for access to the character array, so
447                 // we handle them similar
448                 if (stripped == "the_npc" || stripped == "the_player")
449                     is_local = false;
450 
451                 // a trailing comma operator ends an expression
452                 if (i == COMMA)
453                     is_local = true;
454 
455                 // skip strings
456                 if (i == QUOT || i == SQUOT)
457                     pos = code.find (operators[i], pos+1) - 1;
458 
459                 // skip comments
460                 if (i == COMMENT)
461                     pos = code.length ();
462 
463                 last_op = operators[i];
464                 pos += operators[i].length ();
465                 begin = pos;
466 #ifdef _DEBUG_
467                 std::cout << code << std::endl;
468                 for (unsigned int j = 0; j < begin; j++) std::cout << " ";
469                 std::cout << "^\n";
470 #endif
471                 break;
472             }
473 
474 #ifdef _DEBUG_
475     std::cout << "<<< " << code << "\n\n";
476 #endif
477     return code;
478 }
479 
480 // write the start of the dialogue
writeStart()481 void DlgCompiler::writeStart ()
482 {
483     // check the conditions of the circle's children
484     checkConditions (&start);
485 
486     // begin dialogue array
487     file << "\t# -- (speaker, code, ((text, operation, condition), ...))"
488          << "\n\tdlg = [\\\n"
489          << "\t\t(None, -1, (";
490 
491     // write the "followers" (in this case the start nodes)
492     for (DlgCircle *child = start.child (FIRST); child != NULL; child = start.child (NEXT))
493         writeFollower (child);
494 }
495 
496 // write the actual dialogue data
writeDialogue()497 void DlgCompiler::writeDialogue ()
498 {
499     std::vector<DlgNode*> nodes = dialogue->getNodes ();
500     std::vector<DlgNode*>::iterator i;
501     DlgCircle *circle, *child;
502     DlgCircleEntry *entry;
503 
504     for (i = nodes.begin (); i != nodes.end (); i++)
505     {
506         // nothing to be done for Arrows
507         if ((*i)->type () == LINK) continue;
508 
509         circle = (DlgCircle *) (*i);
510 
511         // check the conditions of the circle's children
512         checkConditions (circle);
513 
514         // get the entry of the current circle
515         entry = circle->entry ();
516 
517         file << ")),\\\n\t\t(";
518 
519         // write Speaker
520         switch (circle->type ())
521         {
522             case NPC:
523                 file << "\"" << entry->npc () << "\"";
524                 break;
525 
526             case NARRATOR:
527                 file << "\"Narrator\"";
528                 break;
529 
530             default:
531                 file << "None";
532         }
533 
534         // write code
535         file << ", " << codeTable[circle->index ()] << ", (";
536 
537         // check whether the followers are valid
538         checkFollowers (circle);
539 
540         // write all followers
541         for (child = circle->child (FIRST); child != NULL; child = circle->child (NEXT))
542             writeFollower (child);
543     }
544 
545     // end of array
546     file << "))]\n\n";
547 }
548 
549 // write a single follower
writeFollower(DlgNode * node)550 void DlgCompiler::writeFollower (DlgNode *node)
551 {
552     file << "(" << node->index () << ", " << operationTable[node->index ()]
553          << ", " << conditionTable[node->index ()] << "), ";
554 }
555 
556 // add node to the dialogue's start nodes
addStart(DlgNode * node)557 void DlgCompiler::addStart (DlgNode *node)
558 {
559 /*
560     std::vector<DlgNode*>::iterator i = start.begin ();
561 
562     // search the proper place for insertion
563     while (i != start.end ())
564     {
565         if (*node < *(*i)) break;
566         i++;
567     }
568 
569     // insert
570     start.insert (i, node);
571 */
572 }
573 
574 // add a condition to the list of conditions
addCondition(DlgCircle * circle,int idx)575 bool DlgCompiler::addCondition (DlgCircle *circle, int idx)
576 {
577     std::string error, condition, cnd = circle->entry ()->condition ();
578     bool retval = true;
579 
580     // see what kind of statement the condition is and get rid of the keyword
581     switch (getKeyword (cnd))
582     {
583         case IF:
584         {
585             condition = cnd.substr (3);
586             break;
587         }
588 
589         case ELIF:
590         {
591             condition = cnd.substr (5);
592             operationTable[idx] = 1;
593             break;
594         }
595 
596         case ELSE:
597         {
598             operationTable[idx] = 1;
599             return true;
600         }
601 
602         default:
603         {
604             // a condition that doesn't start with any of the above is wrong
605             error = "*** Error: Faulty condition\n    \"" + cnd;
606             error += "\" of node\n    \"" + circle->entry ()->text ();
607             error += "\"\n ";
608 
609             // add error to list
610             if (DlgCmdline::compile) std::cout << error;
611             else GuiError::console->add (error, circle);
612 
613             errors++;
614             return false;
615         }
616     }
617 
618     // now get rid of the colon at the end of the condition
619     if (condition[condition.size () - 1] == ':')
620         condition.erase (condition.size () - 1);
621     // if there is none, that's not too tragical, but report it anyway
622     else
623     {
624         error = "*** Warning: Colon missing in condition\n    \"" + cnd;
625         error += "\" of node\n    \"" + circle->entry ()->text ();
626         error += "\"\n ";
627 
628         // add error to list
629         if (DlgCmdline::compile) std::cout << error;
630         else GuiError::console->add (error, circle);
631 
632         errors++;
633         retval = false;
634     }
635 
636     // now the condition is ready for addition to the condition vector
637     for (std::vector<std::string>::iterator i = conditions.begin (); i != conditions.end (); i++)
638         if (strcmp ((*i).c_str (), condition.c_str ()) == 0)
639         {
640             conditionTable[idx] = distance (conditions.begin (), i);
641             return retval;
642         }
643 
644     // the condition isn't in the table so far, so add it
645     conditionTable[idx] = conditions.size ();
646     conditions.push_back (condition);
647 
648     return retval;
649 }
650 
651 // add arbitrary code to the list of code
addCode(const std::string & cde,int idx)652 void DlgCompiler::addCode (const std::string &cde, int idx)
653 {
654     // see if code like this already exists
655     for (std::vector<std::string>::iterator i = code.begin (); i != code.end (); i++)
656         if (strcmp ((*i).c_str (), cde.c_str ()) == 0)
657         {
658             codeTable[idx] = distance (code.begin (), i);
659             return;
660         }
661 
662     // the code isn't in the table so far, so add it
663     codeTable[idx] = code.size ();
664     code.push_back (cde);
665 }
666 
667 // check whether PLAYER or NPC/NARRATOR nodes follow the given node
checkFollowers(DlgCircle * circle)668 int DlgCompiler::checkFollowers (DlgCircle *circle)
669 {
670     DlgNode *child = circle->child (FIRST);
671     if (child == NULL) return 0;
672 
673     node_type type = child->type ();
674     std::string error;
675 
676     // make sure that the followers are consistent
677     for (; child != NULL; child = circle->child (NEXT))
678     {
679         if (type == child->type ()) continue;
680 
681         // that's an error
682         if ((type == PLAYER && child->type () != PLAYER) ||
683             (type != PLAYER && child->type () == PLAYER))
684         {
685             // compose error text
686             error = "*** Error: Must not mix PLAYER and NPC nodes in";
687             error += " children of node\n    \"" + circle->entry ()->text ();
688             error += "\"\n ";
689 
690             // add error to the error console
691             if (DlgCmdline::compile) std::cout << error;
692             else GuiError::console->add (error, circle);
693 
694             errors++;
695             break;
696         }
697     }
698 
699     return 1;
700 }
701 
702 // check whether the children of given node have proper conditions
checkConditions(DlgCircle * circle)703 bool DlgCompiler::checkConditions (DlgCircle *circle)
704 {
705     DlgCircle *child = circle->child (FIRST);
706     if (child == NULL) return true;
707 
708     std::string error = "";
709     bool retval = true;
710 
711     // get keyword of first child
712     token k2, k1 = getKeyword (child->entry ()->condition ());
713 
714     for (; child != NULL; child = circle->child (NEXT), k1 = k2)
715     {
716         k2 = getKeyword (child->entry ()->condition ());
717         if (k2 == NONE) continue;
718 
719         // 'elif' may only follow 'if' or 'elif'
720         if (k2 == ELIF && (k1 != IF && k1 != ELIF))
721             error = "*** Error: 'elif' without preceeding 'if' in node\n    \"";
722 
723         // 'else' may only follow 'if' or 'elif'
724         else if (k2 == ELSE && (k1 != IF && k1 != ELIF))
725             error = "*** Error: 'else' without preceeding 'if' in node\n    \"";
726 
727         // display error if there is any
728         if (error != "")
729         {
730             error += child->entry ()->text ();
731             error += "\"\n ";
732 
733             if (DlgCmdline::compile) std::cout << error;
734             else GuiError::console->add (error, child);
735 
736             errors++;
737             error = "";
738             retval = false;
739         }
740     }
741 
742     return retval;
743 }
744 
745 // get the keyword the statement begins with
getKeyword(const std::string & statement)746 token DlgCompiler::getKeyword (const std::string &statement)
747 {
748     if (strncmp ("if ", statement.c_str (), 3) == 0)
749         return IF;
750     else if (strncmp ("elif ", statement.c_str (), 5) == 0)
751         return ELIF;
752     else if (strncmp ("else:", statement.c_str (), 5) == 0)
753         return ELSE;
754 
755     return NONE;
756 }
757 
758 // splits a string into tokens
getToken(const std::string & token)759 token DlgCompiler::getToken (const std::string &token)
760 {
761     static unsigned int i;
762 
763     // No valid token
764     if (token == "")
765         return NONE;
766 
767     // Operator
768     for (i = 0; i < NUM_OPS; i++)
769         if (token == operators[i])
770             return NONE;
771 
772     // Fixed: (i.e. something that never needs expanding)
773     for (i = 0; i < NUM_FXD; i++)
774         if (token == fixed[i])
775             return FIXED;
776 
777     // Constant:
778     if (isdigit (token[0]) || (token[0] == '-' && isdigit (token[token.length()-1])))
779         return CONSTANT;
780 
781     // Variable:
782     return VARIABLE;
783 }
784