1 /*
2   Paradox Script Parser
3   Copyright (C) 2022 TheZipCreator
4 
5   This program 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 3 of the License, or
8   (at your option) any later version.
9 
10   This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18 
19 module pdxparser;
20 
21 import std.variant, std.conv, std.array;
22 
23 /// Thrown whenever a function is called on a node when it's not appropriate
24 class PDXInvalidTypeException : Exception {
25   this() {
26     super("Invalid type for operation");
27   }
28 }
29 
30 /// Thrown whenever parsing fails
31 class PDXParsingException : Exception {
32   this(string msg) {
33     super(msg);
34   }
35 }
36 
37 /// Type of a node
38 enum NodeType {
39   ASSIGNMENT, ///
40   BLOCK, ///
41   VALUE ///
42 }
43 
44 /// Represents a single usable thing in the given script
45 interface Node {
46   /// Gets all children of a block
47   Node[] children();
48   /// Returns a `NodeType` that corresponds to the type
49   NodeType type(); 
50   /// Should be equivalent to `node.children[i]`
51   Node opIndex(size_t i);
52   /// Returns the first assignment with key `s`
53   Node opIndex(string s);
54   /// equivalent to looping over `.children``
55   /// Bugs: doesn't play nice with continue, break, etc. If you need to use those, just `foreach` over `children`
56   int opApply(int delegate(Node) dg);
57   /// Gets the key of an assignment
58   string key(); 
59   Variant value_(); // for some reason (seriously why the f**k did they do this) template functions in interfaces are final by default. So I have to do this instead
60   /// Gets the value of an assignment or the value of a value
61   T value(T)() {
62     return value_().get!T();
63   }
64   /// Equivalent to .value!Block
65   final Block block() {
66     return value!Block;
67   }
68   /// Adds a node to a block
69   void add(Node b); // TODO: this *maybe* could be operator overloaded
70   /// Tests both the key and value of an assignment
71   bool equals(T)(string k, T v) {
72     if(k != key)
73       return false;
74     if(value_.peek!T is null)
75       return false;
76     if(value!T != v)
77       return false;
78     return true;
79   }
80   /// Converts a node to a string that should be readable by the game
81   string toString();
82   string toString(string indent);
83 }
84 
85 /// Detects whether a variant is a Node or a descendant type
86 bool isNode(Variant v) {
87   return v.peek!Node !is null || v.peek!Assignment !is null || v.peek!Block !is null || v.peek!Value !is null;
88 }
89 
90 /// Two values seperated by an equals. Usually an effect or condition
91 /// ex:
92 /// `culture = albanian`
93 class Assignment : Node {
94   string _key;
95   Variant _value;
96   this(string k, Variant v) {
97     this._key = k;
98     this._value = v;
99   }
100   static Assignment opCall(T)(string k, T v) {
101     return new Assignment(k, cast(Variant)v);
102   }
103 
104   Node[] children() {
105     throw new PDXInvalidTypeException;
106   }
107   NodeType type() {
108     return NodeType.ASSIGNMENT;
109   }
110   Node opIndex(size_t i) {
111     return block[i];
112   }
113   Node opIndex(string s) {
114     return block[s];
115   }
116   int opApply(int delegate(Node) dg) {
117     throw new PDXInvalidTypeException;
118   }
119   string key() {
120     return _key;
121   }
122   Variant value_() {
123     return _value;
124   }
125   void add(Node b) {
126     throw new PDXInvalidTypeException;
127   }
128   override string toString() {
129     return toString("");
130   }
131   string toString(string indent) {
132     string v;
133     if(_value.isNode)
134       v = _value.get!Node.toString(indent);
135     else if(_value.peek!bool !is null)
136       v = _value.get!bool ? "yes" : "no";
137     else
138       v = _value.toString();
139     return indent~_key~" = "~v;
140   }
141 }
142 
143 /// Represents a series of ret surrounded by curly braces (typically the value of an assignment)
144 class Block : Node {
145   Node[] _children;
146   this(Node[] children) {
147     _children = children;
148   }
149   static Block opCall(Node[] children) {
150     return new Block(children);
151   }
152 
153   Node[] children() {
154     return _children;
155   }
156   NodeType type() {
157     return NodeType.BLOCK;
158   }
159   Node opIndex(size_t i) {
160     return _children[i];
161   }
162   Node opIndex(string s) {
163     foreach(child; _children) {
164       if(child.type == NodeType.ASSIGNMENT && child.key == s)
165         return child;
166     }
167     import core.exception : RangeError;
168     throw new RangeError("Cannot find key \""~s~"\"");
169   }
170   int opApply(int delegate(Node) dg) {
171     // TODO: this is broken with returns
172     foreach(ref Node child; _children) {
173       if(dg(child))
174         return 1;
175     }
176     return 0;
177   }
178   string key() {
179     throw new PDXInvalidTypeException;
180   }
181   Variant value_() {
182     throw new PDXInvalidTypeException;
183   }
184   void add(Node b) {
185     _children ~= b;
186   }
187   override string toString() {
188     return toString("");
189   }
190   string toString(string indent) {
191     auto ap = appender!string;
192     ap.put("{\n");
193     foreach(child; _children) {
194       ap.put(child.toString(indent~"  ")~"\n");
195     }
196     ap.put(indent~"}");
197     return ap[];
198   }
199 }
200 
201 /// Represents a single value with no = after it.
202 class Value : Node {
203   Variant _value;
204   this(Variant v) {
205     _value = v;
206   }
207   static Value opCall(T)(T v) {
208     return new Value(cast(Variant)v);
209   }
210 
211   Node[] children() {
212     throw new PDXInvalidTypeException;
213   }
214   NodeType type() {
215     return NodeType.VALUE;
216   }
217   Node opIndex(size_t i) {
218     throw new PDXInvalidTypeException;
219   }
220   int opApply(int delegate(Node) dg) {
221     throw new PDXInvalidTypeException;
222   }
223   Node opIndex(string s) {
224     throw new PDXInvalidTypeException;
225   }
226   string key() {
227     throw new PDXInvalidTypeException;
228   }
229   void add(Node b) {
230     throw new PDXInvalidTypeException;
231   }
232   Variant value_() {
233     return _value;
234   }
235   override string toString() {
236     return toString("");
237   }
238   string toString(string indent) {
239     if(_value.isNode)
240       return (_value.get!Node).toString(indent);
241     else if(_value.peek!bool !is null)
242       return indent~(_value.get!bool ? "yes" : "no");
243     return indent~_value.toString();
244   }
245 }
246 
247 /// Takes a paradox script and returns a Block representing all nodes within it
248 Node parse(string script) {
249   int loc = 0;
250   return parse(script~"\n}", &loc);
251 }
252 
253 Block parse(string script, int* l) {
254   Node[] ret;
255   string value = ""; // stores value currently working on
256   string buf = ""; // stores previous value when parsing assignment (buf != "" means it's currently in an assignment)
257   bool seenSpace = false;
258   Variant get() {
259     // test whether it's a number
260     bool number = true;
261     bool dot = false;
262     if(value[0] == '-') value = value[1..$];
263     foreach(char c; value) {
264       if(c == '.') {
265         if(dot) {
266           number = false;
267           break;
268         }
269         else dot = true;
270       } else if(c < '0' || c > '9') {
271         number = false;
272         break;
273       }
274     }
275     // TODO: maybe add date as a type? (e.g. 1444.4.4 would return a custom struct Date or something)
276     // TODO: "yes" and "no" still get converted to bools when surrounded by quotes, fix
277     if(number && dot)
278       return cast(Variant)(value.to!float);
279     else if(number)
280       return cast(Variant)(value.to!int);
281     else if(value == "yes")
282       return cast(Variant)true;
283     else if(value == "no")
284       return cast(Variant)false;
285     else
286       return cast(Variant)(value);
287   }
288   while(true) {
289     char c = script[*l];
290     *l += 1;
291     if(*l > script.length)
292       throw new PDXParsingException("Unbalanced braces");
293     switch(c) {
294       case ' ':
295       case '\t':
296       case '\n':
297         if(value != "") {
298           if(buf != "") {
299             // finish assignment
300             ret ~= new Assignment(buf, get);
301             buf = "";
302             value = "";
303             break;
304           }
305           seenSpace = true;
306         }
307         break;
308       case '{':
309         if(buf != "") {
310           *l += 1;
311           Block b = parse(script, l);
312           ret ~= new Assignment(buf, cast(Variant)b);
313           buf = "";
314           value = "";
315           break;
316         }
317         ret ~= parse(script, l);
318         break;
319       case '}':
320         if(seenSpace) {
321           ret ~= new Value(get);
322           value = c.to!string;
323           seenSpace = false;
324         }
325         return new Block(ret);
326       case '#':
327         while(script[*l] != '\n')
328           *l += 1;
329         break;
330       case '\r':
331         break; // windows
332       case '=':
333         buf = value;
334         value = "";
335         seenSpace = false;
336         break;
337       case '"':
338         while(script[*l] != '"') {
339           value ~= script[*l];
340           *l += 1;
341         }
342         *l += 1;
343         break;
344       default:
345         if(seenSpace) {
346           ret ~= new Value(get);
347           value = c.to!string;
348           seenSpace = false;
349           break;
350         }
351         value ~= c;
352         break;
353     }
354   }
355 }
356 
357 /// Takes a filename, and parses a script from that file. Supported encodings are UTF-8 and ANSI
358 Node parseFromFile(string filename) {
359   import std.file, std.encoding;
360   import core.exception : UnicodeException;
361   // Paradox script files are sometimes UTF-8 and sometimes ANSI, so I have to handle both here
362   try {
363     return parse(readText(filename));
364   } catch(UnicodeException e) {
365     string s;
366     transcode(cast(Latin1String)read(filename), s);
367     return parse(s);
368   }
369 }
370 
371 /// Saves a node to a string. Different from `Node.toString()`` because it cuts off leading and trailing {}. Use this instead of `Node.toString()` if you want to save a modified node
372 string saveNode(Node n) {
373   if(Block b = cast(Block)n)
374     return b.toString()[2..$-1];
375   return n.toString(); 
376 }
377 
378 /// Same as `saveNode()` except it saves it to a file
379 void saveNodeToFile(string filename, Node n) {
380   import std.file;
381   write(filename, n.saveNode);
382 }
383 
384 unittest {
385   string albania = `government = monarchy
386 add_government_reform = autocracy_reform
387 government_rank = 1
388 primary_culture = albanian
389 religion = catholic
390 technology_group = eastern
391 capital = 4175 # Lezhe
392 
393 # The League of Lezhe
394 1443.3.4 = {
395 	monarch = {
396 		name = "Gjergj Skanderbeg"
397 		dynasty = "Kastrioti"
398 		birth_date = 1405.1.1		
399 		adm = 6
400 		dip = 5
401 		mil = 6
402 		leader = {	name = "Skanderbeg"            	type = general	fire = 5	shock = 5	manuever = 5	siege = 0}
403 	}
404 	clear_scripted_personalities = yes
405 	add_ruler_personality = inspiring_leader_personality
406 	add_ruler_personality = silver_tongue_personality
407 }`; // excerpt of "history/countries/ALB - Albania.txt" from EU4
408   auto res = parse(albania);
409   assert(res[0].key == "government");
410   assert(res[0].value!string == "monarchy");
411   assert(res[2].value!int == 1);
412   assert(res[7].key == "1443.3.4");
413   assert(res[7][1].key == "clear_scripted_personalities");
414   assert(res[7][0][1].value!string == "Kastrioti");
415   assert(res[7][0][2].value!string != "aaa");
416 }
417 
418 unittest {
419   string valueTest = `values = { 1 1 1 }`;
420   auto res = parse(valueTest);
421   assert(res[0].block.children.length == 3);
422   foreach(v; res[0].block) {
423     assert(v.value!int == 1);
424   }
425 }
426 
427 unittest {
428   string utf8file = `a = b`;
429   import std.file, std.encoding;
430   write("tmp.txt", utf8file);
431   auto res = parseFromFile("tmp.txt");
432   assert(res[0].key == "a");
433   assert(res[0].value!string == "b");
434   Latin1String ansifile;
435   transcode(utf8file, ansifile);
436   write("tmp.txt", ansifile);
437   res = parseFromFile("tmp.txt");
438   assert(res[0].key == "a");
439   assert(res[0].value!string == "b");
440   remove("tmp.txt");
441 }
442 
443 unittest {
444   string file = `# The Kingdom of God on Earth
445 country_event = {
446 	id = catholic_flavor.2
447 	title = catholic_flavor.2.t
448 	desc = catholic_flavor.2.d
449 	picture = POPE_PREACHING_eventPicture
450 	
451 	major = yes
452 	is_triggered_only = yes
453 
454 	option = {
455 		name = catholic_flavor.2.a
456 		add_government_reform = kingdom_of_god_reform
457 		#set_government_rank = 3
458 	}
459 
460 	option = {
461 		name = catholic_flavor.2.b
462 		add_prestige = 10
463 	}
464 }`; // From EU4: events/Catholic.txt
465   auto tree = parse(file);
466   assert(tree.children.length == 1);
467   assert(tree[0]["id"].value!string == "catholic_flavor.2");
468 }
469 
470 unittest {
471   import std.file;
472   auto n = Block([
473     Assignment("hello", "world"),
474     Assignment("block", Block([
475       Assignment("x", 1),
476       Assignment("y", 2)
477     ]))
478   ]);
479   saveNodeToFile("tmp.txt", n);
480   auto r = parseFromFile("tmp.txt");
481   assert(r[0].key == "hello");
482   assert(r[0].value!string == "world");
483   assert(r[1].key == "block");
484   assert(r[1]["x"].value!int == 1);
485   assert(r[1]["y"].value!int == 2);
486   remove("tmp.txt");
487 }
488 
489 unittest {
490   string pair = `abc = 123`;
491   auto tree = parse(pair);
492   assert(tree[0].equals("abc", 123));
493 }