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 }