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