├── .gitignore ├── LICENSE ├── README.md ├── examples ├── output │ └── sample.rpy └── sample.rps ├── rpsb.py └── tests ├── output ├── audio.rpy ├── character_test.rpy ├── control.rpy ├── files.rpy ├── flow_control.rpy ├── foobar.rpy ├── line_test.rpy ├── other.rpy └── prefix_test.rpy ├── test1.rps └── test2.rps /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.bak 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Nathan Sullivan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ren'Py script builder 2 | ===================== 3 | 4 | Introduction 5 | ------------ 6 | 7 | The Ren'Py script builder was formed out a desire almost entirely to not have to type "" on every damn line! 8 | Most of the ideas pretty much grew from that and having to work around writing scripts without quotation marks while still maintaining the basic level of flexibility that Ren'Py offers. 9 | 10 | The builder will take more manageable, human readable/writeable script file(s) and turn them into something Ren'Py can understand. Not that Ren'Py scripts aren't human readable, but the goal is simply to cut down on some of the tediousness of writing dialogue and narration in the format that Ren'Py requires. 11 | 12 | Please keep in mind that this tool is only intended to help speed up the actual _script_ part of your game (the dialogue and narration and stuff). You should still do the more programming intensive stuff, such as init and gui programming in pure rpy. 13 | 14 | Table of Contents 15 | ----------------- 16 | 17 | - [Ren'Py script builder](#renpy-script-builder) 18 | - [Introduction](#introduction) 19 | - [Table of Contents](#table-of-contents) 20 | - [Usage](#usage) 21 | - [What it Does](#what-it-does) 22 | - [Comments](#comments) 23 | - [Blocks](#blocks) 24 | - [Commands](#commands) 25 | - [Line and Character Replacements](#line-and-character-replacements) 26 | - [Wild card Matches and Regex](#wild-card-matches-and-regex) 27 | - [Labels](#labels) 28 | - [Scene, Show and With](#scene-show-and-with) 29 | - [Music and sound](#music-and-sound) 30 | - [Flow control](#flow-control) 31 | - [Choices](#choices) 32 | - [if, elif and else](#if-elif-and-else) 33 | - [NVL](#nvl) 34 | - [Import](#import) 35 | - [File](#file) 36 | - [Logging](#logging) 37 | - [Log Levels](#log-levels) 38 | - [Special Characters](#special-characters) 39 | - [Configuration](#configuration) 40 | - [List of Config Options](#list-of-config-options) 41 | - [Syntax Reference](#syntax-reference) 42 | - [Comments](#comments-1) 43 | - [Commands](#commands-1) 44 | - [Wildcards](#wildcards) 45 | - [Config Options](#config-options) 46 | - [Log Levels](#log-levels-1) 47 | 48 | Usage 49 | ----- 50 | To convert a .rps script file into a Ren'Py v6.99.11 compatible .rpy file(s), download the ***rpsb.py*** file to the location of your choice then start up a cmd or bash shell in that directory then run `python rpsb.py input-file` where `input-file` is the path to the file that you wish to convert. 51 | 52 | The file(s) will be output in a location relative to the source document (by default, this will be in the same directory as the input file's path). This can be changed by setting `output_path` (see [Configuration](#configuration)). 53 | 54 | What it Does 55 | ------------ 56 | 57 | the builder takes the input file and goes over it, reading each line and deciding what to do with it: 58 | 59 | + Empty lines are ignored. 60 | + Any line starting with a `#` is a [comment](#comments) and is ignored. 61 | + If the line starts with a `:` it is interpreted as a [command](#commands). 62 | + If the command is a known command or matches a [line replacement](#line-and-prefix-replacement) then it is interpreted and output. 63 | + Otherwise, the command (and any block it opens) is copied verbatim, sans the leading `:`. 64 | + A line starting with any of the [special characters](#special) are treated accordingly. 65 | + Finally, if a line doesn't match any of the above requirements, it is first checked for any [prefix replacements](#line-and-prefix-replacement). Any portion of the line that isn't prefix replaced is wrapped in quotes (first escaping any pre-exiting quotes to preserve them) and output. 66 | 67 | Comments 68 | -------- 69 | 70 | Any line beginning with a `#` is treated as a comment and is skipped over during parsing. 71 | **Note:** Unlike python, the `#` is not treated as a comment when it appears in the middle of a line. It must be at the beginning of the line to be treated as such. 72 | 73 | By default, only lines starting with `##` are copied to the output and all other comments are simply ignored. 74 | See [Configuration](#configuration) 75 | 76 | Blocks 77 | ------ 78 | 79 | A `:` at the end of a line, opens a new block. 80 | Blocks are indented in the same way as python and are closed in the same way. 81 | 82 | Commands 83 | -------- 84 | 85 | Any line starting with a `:` is a command. 86 | The command is between the the `:` and the first space or trailing `:` 87 | 88 | Any command not recognized by the interpreter is simply transposed as is into the output file, sans the leading `:`. Any block that is opened by this unknown command is also transposed as is. 89 | You can use this output pure code into the output file. 90 | 91 | ```renpy 92 | :init python: 93 | chars = ["Sarah", "George"] 94 | new_chars = [] 95 | for char in chars: 96 | new_chars.append(char + "_happy") 97 | ``` 98 | 99 | ### Line and Character Replacement 100 | 101 | ```html 102 | :line find = replace 103 | :character find_character = replace 104 | 105 | :line: 106 | find = replace 107 | ... 108 | :character: 109 | find_character = replace 110 | ... 111 | ``` 112 | 113 | These commands focus on replacing certain elements in each line. 114 | 115 | - `:line find = replace` defines a line replacement; if an entire line comprises of `find` it will be replaced by `replace` 116 | 117 | Example: 118 | `:line tr_fade = with Fade(0.33)` will look for lines that consist only of `tr_fade` and replace that line with `with Fade(0.33)` in the output. 119 | - `:character find = replace` defines a character replacement. The interpreter scans the beginning of the line, looking for `find` followed by a space and replaces it with `replace`. 120 | Use this for character prefixes on spoken script lines. 121 | 122 | Example: 123 | `:character m: = Mick` will look at the beginning of each line and replace `m:` with `Mick` before quoting the rest of the string so that `m: Hello, World!` becomes `Mick "Hello, World!"` in the output. 124 | 125 | To define multiple replacements in a single go, you can instead place a `:` at the end of the command and list your replacements in a block following it. Example: 126 | 127 | ```html 128 | :line: 129 | tr_dis = with dissolve 130 | tr_fade = with Fade(0.75) 131 | park = scene bg park 132 | 133 | :character: 134 | d: = David 135 | a: = Annie 136 | ``` 137 | 138 | You can specify multi-line replacements using `\n` 139 | ```html 140 | :line dorm = scene bg dorm\nwith dissolve 141 | ``` 142 | becomes 143 | ```renpy 144 | scene bg dorm 145 | with dissolve 146 | ``` 147 | 148 | Substitutions are quite flexible and can even be used to create new commands and/or replace otherwise odd strings. 149 | ```html 150 | :line: 151 | :dance = $ dance_func() 152 | >> {+} = scene {}\nwith time_skip_short 153 | >>> {+} = scene {}\nwith time_skip_long 154 | ``` 155 | 156 | #### Wild card Matches and Regex 157 | 158 | Wild cards can be used to match slightly varying strings. 159 | 160 | - `?` will match 1 or 0 characters 161 | `:line some?thing` will match `something` and `some_thing` but not `some other thing` 162 | - `*` will match any number of character, including 0 163 | `:line some*thing` will match each of `something`, `some_thing` and `some other thing` 164 | - `+` will match any number of character but will always match at least 1. 165 | `:line some+thing` will match `some_thing` and `some other thing` but not `something` 166 | 167 | You can use `{}` to capture a wild card match and substitute it into the output using `{}` where n is the matched group. **Note:** This is different than in python, where `()` are normally used to capture matches, instead here, `{}` are used. 168 | ```html 169 | :line dis({+}) = with Dissolve({}) 170 | dis(0.75) 171 | ``` 172 | becomes 173 | ```renpy 174 | with Dissolve(0.75) 175 | ``` 176 | 177 | Substitutions will match in order, so `:line foo{*}bar{*} = $ some_func({},{})` will substitute the first parameter with the match following `foo` and the second parameter will be substituted with the match following `bar` 178 | 179 | You can control this behaviour by using numbered substitutions in the replacement string `{n}` where `n` is the match group. 180 | Using the above example `:line foo{*}bar{*} = $ some_func({0},{1})` now substitutes the first parameter with the match following bar and the second with the match following foo. 181 | **Note:** Substitutions are zero indexed as in python, so `{0}` is the first match, `{1]` is the second and so on. 182 | 183 | If you need to have any of `?*+{}` or `\` in your match string, you must prefix it with a `\`. The same goes for if you wish to have `{}` or `\` in your replacement string. 184 | 185 | ### Labels 186 | 187 | ```html 188 | ::label_name 189 | ::label_name: 190 | ... 191 | ``` 192 | 193 | The label command can be used to add labels to each section of your script and is used as such: 194 | `::label` 195 | 196 | you can also specify sub labels using standard dot notation: `::.sub` 197 | This will group the label under the most recent parent label. 198 | 199 | Sub labels can be grouped under a specified parent label however by using `::parent.sub` 200 | 201 | ```html 202 | ::parent1 203 | ::.sub1 204 | This sub1 label is grouped under parent1 205 | ::parent2.sub1 206 | This sub1 label is instead grouped under parent2 207 | ::.sub2 208 | This is part of parent2 209 | ::parent1.sub2 210 | This label is listed under parent1 again 211 | ``` 212 | becomes 213 | ```renpy 214 | label parent1 215 | label .sub1 216 | "This sub1 label is grouped under parent1" 217 | 218 | label parent2 219 | label .sub1 220 | "This sub1 label is instead grouped under parent2" 221 | 222 | label .sub2 223 | "This is part of parent2 too" 224 | 225 | label parent1.sub2 226 | "This label is listed under parent1 again" 227 | ``` 228 | 229 | **Note:** The label command doesn't need to open a new block with a trailing `:` and it is optional to include it or not if you find the layout better. If you do include the trailing `:` then the following lines must be indented as a block. The output will be the same regardless if you use labels as block openers or not and you can mix and match. 230 | 231 | ```html 232 | ::non-block 233 | The label here doesn't open a block and so this line shouldn't be indented. 234 | 235 | ::block: 236 | The label here does open a block and so this line must be indented. 237 | ::.mix 238 | You can also mix block/non-block types just fine. 239 | Just as long as you maintain correct indenting. 240 | ``` 241 | 242 | ### Scene, Show and With 243 | 244 | ```html 245 | :sc scene_name 246 | :s image_name 247 | :w transition 248 | ``` 249 | 250 | To show a new scene, simply use `:sc scene_name` where `scene_name` is the name of the new scene to show. 251 | Likewise, use `:s image_name` to show an image (typically a character) on screen. You can even use `at location` as you would normally like `:s emily annoyed at right` 252 | 253 | Transitions can easily be done also using `:w transition` where `transition` is the transition to perform 254 | ```html 255 | :sc town night 256 | :w Dissolve(1.25) 257 | ``` 258 | 259 | ### Music and sound 260 | 261 | ```html 262 | :p channel audio [clause] 263 | :pm music [clause] 264 | :ps sound [clause] 265 | :pa audio [clause] 266 | :v voice 267 | :stop channel [clause] 268 | ``` 269 | 270 | To play music and sounds use the `:p channel audio` where `channel` is the channel to play the `audio` on. You can optionally specify a clause to add, such as `fadein` or `loop` to the end of the command. 271 | 272 | `:pm` `:ps` and `:pa` are simply syntactic sugar for playing audio on the respective channel. 273 | For example the following are equivalent: 274 | - `:pm some_music fadein 1.2` <-> `:p music some_music fadein 1.5` 275 | - `:ps a_sound` <-> `:p sound a_sound` 276 | - `:pa "bang.ogg"` <-> `:p audio "bang.ogg"` 277 | 278 | To stop a channel from playing audio, simply use `:stop channel_to_stop` 279 | 280 | The `:v voice` command can be used to play a voice along with a line of dialogue. 281 | 282 | **Note:** when specifying an audio file, you must enclose the file name in quotes as the parser won't do this for you. 283 | 284 | ### Flow control 285 | 286 | ```html 287 | :c label_name 288 | :j label_name 289 | :r 290 | ``` 291 | 292 | Having labels is all well and good, but a large part of Ren'Py is being able to move control from one place to the next. 293 | This is typically done in two ways: the `jump` keyword and the `call` keyword. 294 | 295 | In your script, these are replaced by `:j` and `:c` respectfully and otherwise operate the same way. 296 | 297 | When calling another label though, it is expected to `return` at the end of the called label. To do this in your script, just use `:r` 298 | 299 | ### Choices 300 | 301 | ```html 302 | :choice: 303 | ``` 304 | 305 | Choices are an important part of almost any visual novel and in Ren'Py this is accomplished through the `menu:` keyword. 306 | 307 | The choice dialogue is crafted in the same way as in Ren'Py using `:choice` 308 | ```html 309 | :choice: 310 | d: This is a bit of dialogue accompanying the choice 311 | This is the first choice: 312 | :c choice1 313 | This is the second choice: 314 | :c choice2 315 | ``` 316 | becomes 317 | ```renpy 318 | menu: 319 | DAVID "This is a bit of dialogue accompanying the choice" 320 | "This is the first choice": 321 | call choice1 322 | "This is the second choice": 323 | call choice2 324 | ``` 325 | 326 | ### if, elif and else 327 | 328 | ```html 329 | :if condition: 330 | :elif condition: 331 | :else: 332 | ``` 333 | 334 | If, elif and else are used in exactly the same way as in Ren'Py but must be prefixed with a `:`. 335 | 336 | ### NVL 337 | 338 | ```html 339 | :nvl: 340 | :clear 341 | :close 342 | ``` 343 | 344 | `:nvl` begins an nvl block. Each script line has `NVL` as the character and each prefixed script line has `_NVL` appended to the replaced prefix. 345 | 346 | ```html 347 | :nvl: 348 | This is an NVL block of text. 349 | As opposed to the typical ADV style that most script lines are read as. 350 | a: It sure is! 351 | ``` 352 | will become: 353 | ```renpy 354 | NVL "This is an NVL block of text." 355 | NVL "As opposed to the typical ADV style that most script lines are read as." 356 | Annie_NVL "It sure is!" 357 | ``` 358 | 359 | To clear the NVL dialogue box, simply use `:clear` inside the NVL block. `:clear` will have no effect when used outside of an NVL block. 360 | 361 | You can change the NVL character, NVL suffix and/or prefix using the [`:config` command](#configuration) 362 | 363 | ### Import 364 | 365 | ```html 366 | :import file_name 367 | ``` 368 | 369 | For large games, you want to keep your source script separated into different files, perhaps to keep different arcs or paths in your story separate, or this might be useful when you have multiple writers, each working on a different portion of the script. 370 | Whatever the case, the `:import file_name` command (where `file_name` is the relative path to the file to be imported) will pull all these files together. 371 | 372 | When called, the `:import` statement will stop parsing the current file at that point, open the new file, parse all of its contents (respecting any further imports) and then return to the importing file, continuing where it left off. 373 | Thus the position and order of your import commands are important. 374 | 375 | If you wish to import multiple files in a row, you must specify an `:import` statement for each one. 376 | 377 | ### File 378 | 379 | ```html 380 | :file file_name 381 | ``` 382 | 383 | You can specify that a new file be created at any point you like, provided you aren't inside of a block. 384 | To do this use the `:file file_name` command where `file_name` is the file name to use. If the file extension if left off then `.rpy` is appended before creating the file. 385 | 386 | When you create a new file, all output from that point onward, until the end of the script, the next `:file` command is encountered or the next parent label is encountered (providing `create_parent_files` is set to `True`). 387 | 388 | ### Logging 389 | 390 | ```html 391 | :log level message 392 | :break 393 | ``` 394 | 395 | You can log an output to the log file and/or console window by using the `:log level message` command, where `level` is an integer from the table below and `message` is the message to log. 396 | 397 | The `:break` command can be used to cease execution of the script builder at that point. 398 | 399 | #### Log Levels 400 | | int | Logging Level | 401 | |:---:|:------------- | 402 | | `0` | VERBOSE | 403 | | `1` | DEBUG | 404 | | `2` | INFO | 405 | | `3` | WARNING | 406 | | `4` | ERROR | 407 | 408 | Special Characters 409 | ------------------ 410 | 411 | The interpreter respects a few special characters from Ren'Py: 412 | 413 | + `$` placed at the beginning of a line, treats that line a python line just as Ren'Py does and simply copies the line directly to the output. 414 | + `#` begins a comment and is ignored during parsing. 415 | You can force the interpreter to copy comments to the output by setting `copy_comments` to `True` 416 | By default, a line starting with `##` _will_ be copied over, regardless of the `copy_comments` setting. 417 | 418 | Configuration 419 | ------------- 420 | 421 | ```html 422 | :config option = value 423 | :config: 424 | option = value 425 | ... 426 | ``` 427 | 428 | Some things can be configured about the script interpreter itself. This is done through the use of the `:config opt = val` command where `opt` is the configuration option to change and `val` is the new value. 429 | You can also use `:config:` as a block to set multiple configuration options at once. 430 | 431 | Although you can change configuration option at any point in the source script, and may be occasionally desirable to do so, it is often wisest to do all configuration at the very beginning of the script to avoid unexpected behaviour. 432 | 433 | ### List of Config Options 434 | 435 | The following are the available configuration options and their associated default values. 436 | 437 | + `create_parent_files = False` 438 | If set to `True` then the interpreter will create new files based on each parent label (labels without at least one leading `.`) and will place all sub labels under a parent label in that parent label's file. 439 | It will be likely that, unless you are making a kinetic novel, you will need to edit this file after the interpreter has run. 440 | + `create_flow_control_file = True` 441 | When set to `True` a master flow control file will be created, which will call each label in the order that they appear, respecting the `flow_control_ignore` list. Set this to `False` if you want to do this manually. 442 | + `flow_control_ignore = ["*.choice*", "*_ignore*"]` 443 | A list of label names to ignore when generating the master flow control file. Regex like matches can be used as defined in the section [Wild card Matches](#wild-card-matches-and-regex). 444 | + `copy_comments = False` 445 | When set to `True` comments in the source document will be copied as is to the output. 446 | + `copy_special_comments = "#"` 447 | When set to a string, comments beginning with that string will be copied as is to the output, regardless of the `copy_comments` setting. 448 | By default, any line begging with `##` will be copied to the output, and other comments will simply be ignored. 449 | + `nvl_character = "NVL"` 450 | Sets the character used for the NVL narrator. 451 | + `nvl_prefix = ""` 452 | Set the prefix applied to character names during NVL blocks. 453 | + `nvl_suffix = "_NVL"` 454 | Set the suffix applied to character names during NVL blocks. 455 | + `output_path = "."` 456 | Set the relative or absolute output path for the generated script files. The default `"."` will create the files in the same location as your script file. 457 | + `auto_return = True` 458 | When set to `True`, the script will automatically insert `return` statements at the end of each label block, mostly eliminating the need for the `:r` command. 459 | + `abort_on_error = True` 460 | If `True`, when an error is encountered, script execution will abort at that point. Setting this to `False` will force the script ignore the error and continue parsing. The `:break` command will still break processing, even if this is set to `False`. 461 | 462 | Syntax Reference 463 | ---------------- 464 | 465 | ### Comments 466 | 467 | | key | Definition | 468 | |:--- |:---------- | 469 | | `#` | Comment | 470 | | `##` | Comment (copied to output) | 471 | 472 | ### Commands 473 | 474 | | key | Definition | 475 | |:--- |:---------- | 476 | | `:` | Start command | 477 | | `:line find = replace` | Entire line find and replace | 478 | | `:line:` | Entire line find and replace (multiple) | 479 | | `:character find = replace` | Character (line prefix) find and replace | 480 | | `:character:` | Character (line prefix) find and replace (multiple) | 481 | | `::label_name` | Label | 482 | | `:sc scene` | Show scene | 483 | | `:s image` | Show image | 484 | | `:w transition` | With transition | 485 | | `:p channel sound` | Plays sound on channel | 486 | | `:pm music` | Short cut for playing music on the `music` channel | 487 | | `:ps sound` | Short cut for playing sound on the `sound` channel | 488 | | `:pa audio` | Short cut for playing audio on the `audio` channel | 489 | | `:v voice` | Plays a voice | 490 | | `:q channel sound` | Queues sound up on the named channel | 491 | | `:stop channel` | Stops playing audio on the named channel | 492 | | `:c label` | Call label | 493 | | `:j label` | Jump to label | 494 | | `:r` | Return | 495 | | `:choice:` | Create menu dialogue | 496 | | `:if condition:` | if statement | 497 | | `:elif condition:` | elif statement | 498 | | `:else:` | else statement | 499 | | `:nvl:` | Opens and NVL block | 500 | | `:clear` | outputs an `nvl clear` statement | 501 | | `:import file_name` | Import and parse another file | 502 | | `:file file_name` | Set a new output file | 503 | | `:log level message` | Log a message at a given level to the console and/or log file | 504 | | `:break` | Stops execution of the script builder at that point | 505 | | `:config option = value` | Change the value of a config option | 506 | | `:config:` | Set multiple config options at once | 507 | | `:UNKNOWN` | Unknown commands (and any blocks they open) are output as is | 508 | 509 | ### Wildcards 510 | 511 | | key | Definition | 512 | |:---:|:---------- | 513 | | `?` | Matches 1 or 0 characters | 514 | | `*` | Matches 0 or more characters | 515 | | `+` | Matches 1 or more characters | 516 | | `{}` | Captures match for replacement in the output string | 517 | | `\` | Escape wildcard characters | 518 | 519 | ### Config Options 520 | 521 | | key | Default | Definition | 522 | |:--- |:------- |:---------- | 523 | |`create_parent_files`|`False`|If `True`, create files based on parent labels| 524 | |`create_flow_control_file`|`True`|If `True`, create a master flow control file| 525 | |`flow_control_ignore`|`["*_choice*", "*_ignore*"]`|A list of label names to ignore when generating the master flow control file.| 526 | |`copy_comments`|`False`|If `True`, copy all comments to the output| 527 | |`copy_special_comments`|`"#"`|Comments starting with this string, will always be copied to the output. Disabled if set to `None`| 528 | |`nvl_character`|`"NVL"`|The character used for the NVL narrator| 529 | |`nvl_prefix`|`""`|The prefix applied to character names during NVL blocks| 530 | |`nvl_suffix`|`"_NVL"`|The suffix applied to character names during NVL blocks| 531 | |`output_path`|`"."`|The output path for generated files| 532 | |`auto_return`|`True`|If `True`, automatically insert `return` statements at the end of each label block| 533 | |`abort_on_error`|`True`|If `True`, ignore any errors encountered| 534 | 535 | ### Log Levels 536 | 537 | | int | Logging Level | 538 | |:---:|:------------- | 539 | | `0` | VERBOSE | 540 | | `1` | DEBUG | 541 | | `2` | INFO | 542 | | `3` | WARNING | 543 | | `4` | ERROR | 544 | -------------------------------------------------------------------------------- /examples/output/sample.rpy: -------------------------------------------------------------------------------- 1 | ## This comment will though. 2 | 3 | label Act_1: 4 | label .scene_01: 5 | scene tiny_office 6 | with Fade(1.25) 7 | "The rain fell heavy on the window as I worked." 8 | ADAM "I think I need a break." 9 | "I leaned back in my chair, the monitor glowing softly in font of me." 10 | "Suddenly I heard a knock at the door to my tiny office." 11 | ADAM "Come in. The door's unlocked." 12 | "I turned to face the wooden door as it opened, revealing a familiar face." 13 | show beth normal at center 14 | BETH "Hi Adam, how's the script going?" 15 | ADAM "It's going well, I've made some great progress!" 16 | ADAM "I'm just taking a bit of break at the moment." 17 | show beth cheeky at center 18 | "Beth smiled cheekily. Something was up." 19 | BETH "Well if you're not busy, perhaps you'd like to meet someone?" 20 | BETH "You'll like him, I'm sure!" 21 | ADAM "*sigh* Sure, I don't see why not." 22 | "I got up and grabbed my jacket and headed out the door with the waiting Beth." 23 | 24 | label .scene_02: 25 | scene office_reception 26 | with time_skip_transition 27 | show charles normal at center 28 | "Standing before me is man in a sharp suit." 29 | "He almost looks out of place amongst the small, casually dressed team we have." 30 | show charles handshake at center 31 | "Upon seeing me, he extends a hand which I take." 32 | "His grasp is firm as he give my had a shake." 33 | show charles normal at center 34 | CHARLES "The name's Charles!" 35 | CHARLES "I work for Big Name Publishings and we're very interested in the work you and your team have done making visual novels." 36 | 37 | menu: 38 | CHARLES "We'd like to offer you a lucrative publishing deal for your upcoming games." 39 | "Accept the offer": 40 | jump .accept 41 | "Reject him": 42 | jump .reject 43 | 44 | label .accept: 45 | "An opportunity like this only comes around once in a life time!" 46 | ADAM "I'm honoured, of course we'd all be happy to!" 47 | show charles happy at center 48 | CHARLES "Fantastic!" 49 | CHARLES "We'll get the paper work sorted out later." 50 | show beth smile at right 51 | BETH "See, I told you you'd like him!" 52 | scene success 53 | with time_skip_transition 54 | "We accepted their deal and became a big name in the industry." 55 | "We decided to all take a big holiday in Japan to celebrate." 56 | "THE END" 57 | return 58 | 59 | label .reject: 60 | "This deal would just mean loss of our creative freedom that we've become known for." 61 | ADAM "Thank you Charles for the offer..." 62 | ADAM "But we pride ourselves on our independence and creative freedom." 63 | ADAM "So I'm going to have to turn you down, sorry." 64 | show charles mopey at center 65 | CHARLES "I completely understand, thank you for your time." 66 | show beth sad at right 67 | BETH "Aw, I really though this would work." 68 | scene great_games 69 | with time_skip_transition 70 | "Despite not having a lot of budget for our small studio, by sticking to our values we produced many fine games!" 71 | "THE END" 72 | return 73 | -------------------------------------------------------------------------------- /examples/sample.rps: -------------------------------------------------------------------------------- 1 | :config: 2 | output_path = ./output 3 | create_flow_control_file = False 4 | 5 | :line >> {+} = scene {}\nwith time_skip_transition 6 | 7 | :character: 8 | a: = ADAM 9 | b: = BETH 10 | c: = CHARLES 11 | 12 | # This is a comment, and won't be written to the output file. 13 | ## This comment will though. 14 | 15 | ::Act_1 16 | ::.scene_01: 17 | :sc tiny_office 18 | :w Fade(1.25) 19 | The rain fell heavy on the window as I worked. 20 | a: I think I need a break. 21 | I leaned back in my chair, the monitor glowing softly in font of me. 22 | Suddenly I heard a knock at the door to my tiny office. 23 | a: Come in. The door's unlocked. 24 | I turned to face the wooden door as it opened, revealing a familiar face. 25 | :s beth normal at center 26 | b: Hi Adam, how's the script going? 27 | a: It's going well, I've made some great progress! 28 | a: I'm just taking a bit of break at the moment. 29 | :s beth cheeky at center 30 | Beth smiled cheekily. Something was up. 31 | b: Well if you're not busy, perhaps you'd like to meet someone? 32 | b: You'll like him, I'm sure! 33 | a: *sigh* Sure, I don't see why not. 34 | I got up and grabbed my jacket and headed out the door with the waiting Beth. 35 | 36 | ::.scene_02: 37 | >> office_reception 38 | :s charles normal at center 39 | Standing before me is man in a sharp suit. 40 | He almost looks out of place amongst the small, casually dressed team we have. 41 | :s charles handshake at center 42 | Upon seeing me, he extends a hand which I take. 43 | His grasp is firm as he give my had a shake. 44 | :s charles normal at center 45 | c: The name's Charles! 46 | c: I work for Big Name Publishings and we're very interested in the work you and your team have done making visual novels. 47 | 48 | :choice: 49 | c: We'd like to offer you a lucrative publishing deal for your upcoming games. 50 | Accept the offer: 51 | :j .accept 52 | Reject him: 53 | :j .reject 54 | 55 | ::.accept: 56 | An opportunity like this only comes around once in a life time! 57 | a: I'm honoured, of course we'd all be happy to! 58 | :s charles happy at center 59 | c: Fantastic! 60 | c: We'll get the paper work sorted out later. 61 | :s beth smile at right 62 | b: See, I told you you'd like him! 63 | >> success 64 | We accepted their deal and became a big name in the industry. 65 | We decided to all take a big holiday in Japan to celebrate. 66 | THE END 67 | :r 68 | 69 | ::.reject: 70 | This deal would just mean loss of our creative freedom that we've become known for. 71 | a: Thank you Charles for the offer... 72 | a: But we pride ourselves on our independence and creative freedom. 73 | a: So I'm going to have to turn you down, sorry. 74 | :s charles mopey at center 75 | c: I completely understand, thank you for your time. 76 | :s beth sad at right 77 | b: Aw, I really though this would work. 78 | >> great_games 79 | Despite not having a lot of budget for our small studio, by sticking to our values we produced many fine games! 80 | THE END 81 | :r 82 | -------------------------------------------------------------------------------- /rpsb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Ren'Py Script Builder takes an input script file and generates 6 | a Ren'Py v6.99.11 compatible .rpy file(s). 7 | 8 | Usage: "python rpsb.py input-file [-o output-dir]" 9 | Use "--help" for more info. 10 | """ 11 | from __future__ import print_function, unicode_literals 12 | from builtins import str, range, object 13 | 14 | import os 15 | import sys 16 | import getopt 17 | import re 18 | import time 19 | import types 20 | import traceback 21 | import codecs 22 | from os import path 23 | from functools import reduce 24 | 25 | __version__ = "0.6.2" 26 | __author__ = "Nathan Sullivan" 27 | __email__ = "contact@torrentails.com" 28 | __license__ = "MIT" 29 | 30 | ##----------------------------------------------------------------------------- 31 | ## Globals 32 | ##----------------------------------------------------------------------------- 33 | 34 | state = { 35 | "master_in_file": None, 36 | "cur_in_file": None, 37 | "cur_out_file": None, 38 | "next_out_file": None, 39 | "control_file": None, 40 | "open_files": set(), 41 | "known_line_rep": {}, 42 | "known_character_rep": {}, 43 | "file_chain": [], 44 | "parent_labels": set(), 45 | "is_nvl_mode": False, 46 | } 47 | 48 | config = { 49 | "create_parent_files": False, 50 | "create_flow_control_file": True, 51 | "flow_control_ignore": None, 52 | "copy_comments": False, 53 | "copy_special_comments": "#", 54 | "nvl_character": "NVL", 55 | "nvl_prefix": "", 56 | "nvl_suffix": "_NVL", 57 | "output_path": ".", 58 | "auto_return": True, 59 | "abort_on_error": True, 60 | } 61 | 62 | stats = { 63 | "in_files": 0, 64 | "out_files": 0, 65 | "in_lines": 0, 66 | "out_lines": 0, 67 | "commands_processed": 0, 68 | "line_replacements": 0, 69 | "character_replacements": 0, 70 | "narration_lines": 0, 71 | "dialogue_lines": 0, 72 | "start_time": time.time() 73 | } 74 | 75 | rep_dict1 = { 76 | r'\{': '\xc0', 77 | r'\}': '\xc1', 78 | r'\*': '\xc2', 79 | r'\+': '\xc3', 80 | r'\?': '\xc4', 81 | r'\\': '\xc5', 82 | '.': r'\.', 83 | '(': r'\(', 84 | ')': r'\)', 85 | '[': r'\[', 86 | ']': r'\]' 87 | } 88 | 89 | rep_dict2 = { 90 | '{': '(', 91 | '}': ')', 92 | '*': '.*?', 93 | '+': '.+?', 94 | '?': '.?', 95 | '^': r'\^', 96 | '$': r'\$', 97 | '|': r'\|', 98 | '\xc0': r'\{', 99 | '\xc1': r'\}', 100 | '\xc2': r'\*', 101 | '\xc3': r'\+', 102 | '\xc4': r'\?', 103 | '\xc5': r'\\' 104 | } 105 | 106 | rep_dict3 = {r'\{': '\xc0', r'\}': '\xc1', '\xc0': '{', '\xc1': '}'} 107 | 108 | command_list = [ 109 | re.compile("^:(line)\s+(.*?)\s*=\s*(.*)$"), 110 | re.compile("^:(line:)$"), 111 | re.compile("^:(character)\s+(.*?)\s*=\s*(.*)$"), 112 | re.compile("^:(character:)$"), 113 | re.compile("^:(:)\s*(\.?.*?):?$"), 114 | re.compile("^:(sc)\s+(.*)$"), 115 | re.compile("^:(s)\s+(.*)$"), 116 | re.compile("^:(w)\s+(.*)$"), 117 | re.compile("^:(p)\s+(.*?)\s+(.*)$"), 118 | re.compile("^:(pm)\s+(.*)$"), 119 | re.compile("^:(ps)\s+(.*)$"), 120 | re.compile("^:(pa)\s+(.*)$"), 121 | re.compile("^:(v)\s+(.*)$"), 122 | re.compile("^:(q)\s+(.*?)\s+(.*)$"), 123 | re.compile("^:(stop)\s*(.*)?$"), 124 | re.compile("^:(c)\s+(.*)$"), 125 | re.compile("^:(j)\s+(.*)$"), 126 | re.compile("^:(r)$"), 127 | re.compile("^:(r)\s+(.*)$"), 128 | re.compile("^:(choice):$"), 129 | re.compile("^:(if)\s+(.*?):$"), 130 | re.compile("^:(elif)\s+(.*?):$"), 131 | re.compile("^:(else):$"), 132 | re.compile("^:(nvl):$"), 133 | re.compile('^:(clear)$'), 134 | re.compile("^:(import)\s+(.*)$"), 135 | re.compile("^:(file)\s+(.*)$"), 136 | re.compile("^:(log)\s+(0|1|2|3|4|" \ 137 | "VERBOSE|DEBUG|INFO|WARN|WARNING|ERROR)\s+(.*)$"), 138 | re.compile("^:(config)\s+(.*?)\s*=\s*(.*)$"), 139 | re.compile("^:(config:)$"), 140 | re.compile("^:(break)$") 141 | ] 142 | 143 | parent_label_re = re.compile("^(\w*)\.?.*$") 144 | empty_line_re = re.compile("^\s*$") 145 | comment_re = re.compile("^(\s*)#(.*)$") 146 | python_re = re.compile("^(\$.*)$") 147 | command_re = re.compile("^:(.*)$") 148 | 149 | ##----------------------------------------------------------------------------- 150 | ## Helper Classes 151 | ##----------------------------------------------------------------------------- 152 | 153 | try: 154 | import colorama 155 | colorama.init() 156 | Fore = colorama.Fore 157 | Back = colorama.Back 158 | Style = colorama.Style 159 | except ImportError: 160 | class __fake_col__(object): 161 | def __getattr__(*a, **kw): 162 | return '' 163 | Fore = __fake_col__() 164 | Back = Fore 165 | Style = Fore 166 | 167 | 168 | class Colorama_Helper(object): 169 | r = Style.RESET_ALL+Fore.WHITE 170 | l = ( 171 | Fore.LIGHTBLACK_EX, 172 | Fore.LIGHTBLACK_EX, 173 | Fore.WHITE, 174 | Fore.YELLOW, 175 | Fore.RED 176 | ) 177 | 178 | def __getitem__(self, i): 179 | return self.l[i] 180 | _c = Colorama_Helper() 181 | 182 | 183 | class Current_Line_Str(object): 184 | def __str__(self): 185 | if len(state["file_chain"]): 186 | _f = state["file_chain"][-1] 187 | return "{}|{} ".format(_f["file_name"], _f["cur_line"]) 188 | return ' ' 189 | 190 | def __add__(self, other): 191 | if issubclass(type(other), (str, str)): 192 | return str(self)+other 193 | raise TypeError("unsupported operand type(s) for +:" 194 | " 'str' and '{}'".format(type(other).__name__)) 195 | 196 | def __radd__(self, other): 197 | if issubclass(type(other), (str, str)): 198 | return other+str(self) 199 | raise TypeError("unsupported operand type(s) for +:" 200 | " '{}' and 'str'".format(type(other).__name__)) 201 | _ln = Current_Line_Str() 202 | 203 | 204 | class Indent_Level_Str(object): 205 | def __str__(self): 206 | if state["is_nvl_mode"] is False: 207 | return ' '*state["file_chain"][-1]["cur_indent"] 208 | return ' '*(state["file_chain"][-1]["cur_indent"]-1) 209 | 210 | def __add__(self, other): 211 | if issubclass(type(other), (str, str)): 212 | return str(self)+other 213 | raise TypeError("unsupported operand type(s) for +:" 214 | " 'str' and '{}'".format(type(other).__name__)) 215 | 216 | def __radd__(self, other): 217 | if issubclass(type(other), (str, str)): 218 | return other+str(self) 219 | raise TypeError("unsupported operand type(s) for +:" 220 | " '{}' and 'str'".format(type(other).__name__)) 221 | _il = Indent_Level_Str() 222 | 223 | 224 | class LOGLEVEL(object): 225 | ERROR = 4 226 | WARN = 3 227 | WARNING = 3 228 | INFO = 2 229 | DEBUG = 1 230 | VERB = 0 231 | VERBOSE = 0 232 | 233 | def __getitem__(self, i): 234 | return ('VERB', 'DEBUG', 'INFO', 'WARN', 'ERROR')[i] 235 | LOGLEVEL = LOGLEVEL() 236 | 237 | ##----------------------------------------------------------------------------- 238 | ## Logger 239 | ##----------------------------------------------------------------------------- 240 | 241 | _tmp_log = [] 242 | 243 | class _Logger(object): 244 | 245 | def __init__(self, tmp_log, flush_number=10): 246 | if _debug == 2: 247 | self.log_save_level = LOGLEVEL.VERB 248 | self.log_display_level = LOGLEVEL.DEBUG 249 | elif _debug == 1: 250 | self.log_save_level = LOGLEVEL.DEBUG 251 | self.log_display_level = LOGLEVEL.DEBUG 252 | else: 253 | self.log_save_level = LOGLEVEL.INFO 254 | self.log_display_level = LOGLEVEL.INFO 255 | 256 | self.__errors = 0 257 | self.__warnings = 0 258 | 259 | self.__log = [] 260 | self.__log_flush_number = flush_number 261 | self.__log_count = 0 262 | _file, _ = path.splitext(path.basename(__file__)) 263 | self.__log_file = _file+'.log' 264 | 265 | _log = [] 266 | for val in tmp_log: 267 | if val['level'] >= self.log_save_level: 268 | log_string = ("<{0[3]:0>2n}:{0[4]:0>2n}:{0[5]:0>2n}>" 269 | " [{1:<6} {2}\n".format(time.localtime(val['time']), 270 | LOGLEVEL[val['level']]+']', val['message'])) 271 | _log.append(log_string) 272 | 273 | if LOGLEVEL.ERROR > val['level'] >= self.log_display_level: 274 | print(_c[val['level']]+"[{:<6} {}".format( 275 | LOGLEVEL[val['level']]+']', val['message'])+_c.r) 276 | 277 | if val['level'] == LOGLEVEL.WARN: 278 | self.__warnings += 1 279 | elif val['level'] >= LOGLEVEL.ERROR: 280 | self.__errors += 1 281 | 282 | if _log: 283 | try: 284 | with open(self.__log_file, 'w') as f: 285 | f.writelines(_log) 286 | except IOError: 287 | raise 288 | # log("Unable to open log file for writing.", LOGLEVEL.ERROR) 289 | 290 | def __call__(self, msg, level=LOGLEVEL.INFO, exit=1): 291 | if level >= self.log_save_level: 292 | self.__log_count += 1 293 | else: 294 | return 295 | 296 | cur_time = time.time() 297 | msg = _ln+str(msg) 298 | self.__log.append({'time': cur_time, 'level': level, 'message': msg}) 299 | 300 | if level == LOGLEVEL.WARN: 301 | self.__warnings += 1 302 | elif level >= LOGLEVEL.ERROR: 303 | self.__errors += 1 304 | 305 | if LOGLEVEL.ERROR > level >= self.log_display_level: 306 | print(_c[level]+"[{:<6} {}".format(LOGLEVEL[level]+']', msg)+_c.r) 307 | 308 | elif level >= LOGLEVEL.ERROR: 309 | print(_c[level]+"[{:<6} {}".format(LOGLEVEL[level]+']', msg)+_c.r) 310 | if exit and config["abort_on_error"]: 311 | sys.exit(exit) 312 | 313 | if self.__log_count >= self.__log_flush_number: 314 | self.flush() 315 | 316 | def flush(self): 317 | _log = [] 318 | for l in self.__log: 319 | if l['level'] >= self.log_save_level: 320 | log_string = ("<{0[3]:0>2n}:{0[4]:0>2n}:{0[5]:0>2n}>" 321 | " [{1:<6} {2}\n".format(time.localtime(l['time']), 322 | LOGLEVEL[l['level']]+']', l['message'])) 323 | _log.append(log_string) 324 | 325 | if _log: 326 | try: 327 | with open(self.__log_file, 'a') as f: 328 | f.writelines(_log) 329 | self.__log = [] 330 | except IOError: 331 | raise 332 | # log("Unable to open log file for writing.", LOGLEVEL.ERROR) 333 | 334 | def log_traceback(self, exit_code=1): 335 | _tb = '\n>>> '.join(traceback.format_exc().split('\n')[:-1]) 336 | log("Traceback:\n>>> {}".format(_tb), 337 | LOGLEVEL.ERROR, exit = False) 338 | sys.exit(exit_code) 339 | 340 | def stats(self, cur_time=None): 341 | log("Logging statistics", LOGLEVEL.VERB) 342 | _log = ["Statistics:"] 343 | 344 | def _s(t): 345 | _t = reduce(lambda ll, b: divmod(ll[0], b) + \ 346 | ll[1:], [(t*1000,), 1000, 60, 60]) 347 | return "{}:{:0>2}:{:0>2}.{:0>3}".format(*[int(v) for v in _t]) 348 | 349 | def _pretty_log(key): 350 | _log.append(" {:>19} : {}".format(key, stats[key])) 351 | 352 | _stats_list = ( 353 | "in_files", 354 | "out_files", 355 | "in_lines", 356 | "out_lines", 357 | "commands_processed", 358 | "line_replacements", 359 | "character_replacements", 360 | "narration_lines", 361 | "dialogue_lines" 362 | ) 363 | for k in _stats_list: 364 | _pretty_log(k) 365 | 366 | _log.append(" {1:>19} : {0[3]:0>2n}:{0[4]:0>2n}:{0[5]:0>2n}".format( 367 | time.localtime(stats["start_time"]), "start_time")) 368 | 369 | cur_time = cur_time or time.time() 370 | _run_time = cur_time - stats["start_time"] 371 | _log.append(" {:>19} : {}".format("total_run_time", _s(_run_time))) 372 | 373 | log('\n'.join(_log)) 374 | 375 | def close(self): 376 | log("Closing logger", LOGLEVEL.DEBUG) 377 | 378 | if self.__errors: 379 | log("Build failed with {} WARNINGS and {} ERRORS".format( 380 | self.__warnings, self.__errors), LOGLEVEL.WARN) 381 | else: 382 | log("Build completed sucessfully with {} WARNINGS " 383 | "and {} ERRORS".format(self.__warnings, self.__errors), 384 | LOGLEVEL.INFO) 385 | 386 | self.stats(time.time()) 387 | self.flush() 388 | 389 | 390 | def log(msg, level=LOGLEVEL.INFO, exit=True): 391 | msg = _ln+msg 392 | _tmp_log.append({'time': time.time(), 'level': level, 'message': msg}) 393 | if level >= LOGLEVEL.ERROR: 394 | if exit and config["abort_on_error"]: 395 | sys.exit(_c[level]+"[{:<6} {}".format(LOGLEVEL[level]+']', msg)) 396 | else: 397 | print(_c[level]+"[{:<6} {}".format(LOGLEVEL[level]+']', msg)+_c.r) 398 | 399 | 400 | def _log_close(*args, **kwargs): 401 | pass 402 | log.close = _log_close 403 | 404 | 405 | def _log_traceback(exit_code=1): 406 | _tb = "\n ".join(traceback.format_exc().split('\n')) 407 | log("Traceback:\n {}".format(_tb), 408 | LOGLEVEL.ERROR, exit = exit_code) 409 | log.log_traceback = _log_traceback 410 | 411 | ##----------------------------------------------------------------------------- 412 | ## Misc Functions 413 | ##----------------------------------------------------------------------------- 414 | 415 | def setup_globals(output_path=None, flush_number=10): 416 | global log, _tmp_log 417 | log("Initializing Globals", LOGLEVEL.DEBUG) 418 | 419 | output_path = output_path or '.' 420 | _old_output_path = output_path 421 | output_path = path.expandvars(path.expanduser(output_path)) 422 | if not path.isdir(output_path): 423 | usage("Invalid output path: {}".format(_old_output_path)) 424 | config["output_path"] = output_path 425 | 426 | log("initializing logger", LOGLEVEL.DEBUG) 427 | log = _Logger(_tmp_log, flush_number) 428 | del _tmp_log 429 | 430 | config["flow_control_ignore"] = [ 431 | re.compile('^'+regex_prep("*_choice*")+'$'), 432 | re.compile('^'+regex_prep("*_ignore*")+'$') 433 | ] 434 | 435 | 436 | def total(itter): 437 | _sum = 0 438 | for i in itter: 439 | _sum += len(i) 440 | return _sum 441 | 442 | 443 | def fix_brace(matchobj): 444 | _m = matchobj.group(0) 445 | if _m in rep_dict3: 446 | return rep_dict3[_m] 447 | else: 448 | log("Failed to match {}".format(_m), LOGLEVEL.WARN) 449 | return _m 450 | 451 | 452 | def usage(error=None, exit_code=None): 453 | log("Printing usage", LOGLEVEL.VERB) 454 | if error: 455 | exit_code = exit_code or 2 456 | log(str(error), LOGLEVEL.ERROR, exit=False) 457 | print('') 458 | print('::'+("-"*77)) 459 | print(":: Ren'Py Script Builder") 460 | print('::'+("-"*77)) 461 | print('\n Usage:') 462 | print(' {} -h|source [-o:dir] [--flush=value]' \ 463 | ' [--debug|--verbose]\n\n'.format(path.basename(__file__))) 464 | print(" {:>16} :: Print this help message\n".format('[-h|--help]')) 465 | print(" {:>16} :: Set the output directory".format('[-o:]')) 466 | print(" {:>16} :: NOTE: Output directory may be overwritten by config" \ 467 | .format('[--output=]')) 468 | print((" "*20)+":: options set in the source file.\n") 469 | print(" {:>16} :: Print this help message\n".format('[-h|--help]')) 470 | print(" {:>16} :: Set logging level to debug".format('[--debug]')) 471 | print(" {:>16} :: Set logging level to verbose.".format('[--verbose]')) 472 | print((" "*20)+":: " 473 | "WARNING: Verbose logging will print a lot of garbage to") 474 | print((" "*20)+":: " 475 | " the console and create a very large log file. Only use") 476 | print((" "*20)+":: " 477 | " this option if asked to to do so by the developer.") 478 | sys.exit(exit_code) 479 | 480 | ##----------------------------------------------------------------------------- 481 | ## Regex manager 482 | ##----------------------------------------------------------------------------- 483 | 484 | def regex_prep(string): 485 | log("Preping string for regex: {}".format(string), LOGLEVEL.VERB) 486 | 487 | def _re1(matchobj): 488 | _m = matchobj.group(0) 489 | if _m in rep_dict1: 490 | return rep_dict1[_m] 491 | else: 492 | log("Failed to match {}".format(_m), LOGLEVEL.WARN) 493 | return _m 494 | 495 | def _re2(matchobj): 496 | _m = matchobj.group(0) 497 | if _m in rep_dict2: 498 | return rep_dict2[_m] 499 | else: 500 | log("Failed to match {}".format(_m), LOGLEVEL.WARN) 501 | return _m 502 | 503 | string = string.strip() 504 | _s = re.sub('\\\\{|\\\\}|\\\\\*|\\\\\+|' 505 | '\\\\\?|\.|\(|\)|\[|\]', _re1, string) 506 | return re.sub('\{|\}|\*|\+|\?|\^|\$|\||' 507 | '\xc0|\xc1|\xc2|\xc3|\xc4|\xc5', _re2, _s) 508 | 509 | 510 | def line_regex(match, replace): 511 | log("Building line replacement regex: {} = {}".format(match, 512 | replace), LOGLEVEL.DEBUG) 513 | _rep = regex_prep(match) 514 | log("Regex result: {}".format(_rep), LOGLEVEL.DEBUG) 515 | _m = re.compile('^'+_rep+'$') 516 | state["known_line_rep"][_m] = replace 517 | 518 | 519 | def character_regex(match, replace): 520 | log("Building character replacement regex: {} = {}".format(match, 521 | replace), LOGLEVEL.DEBUG) 522 | _rep = regex_prep(match) 523 | log("Regex result: {}".format(_rep), LOGLEVEL.DEBUG) 524 | _m = re.compile('^'+_rep+'\s(.*)') 525 | state["known_character_rep"][_m] = (replace, ' "{}"') 526 | 527 | ##----------------------------------------------------------------------------- 528 | ## File manager 529 | ##----------------------------------------------------------------------------- 530 | 531 | def loop_file(in_file): 532 | file = open_file(in_file, "r") 533 | log("Parsing file {}".format(in_file), LOGLEVEL.DEBUG) 534 | state['cur_in_file'] = file 535 | if stats["in_files"] == 1: 536 | state["master_in_file"] = file 537 | 538 | for _lineno, line in enumerate(file, start=1): 539 | state["file_chain"][-1]["cur_line"] = _lineno 540 | parse_line(line) 541 | 542 | state["file_chain"].pop() 543 | 544 | 545 | def open_file(file_path, mode='w'): 546 | if mode == 'w': 547 | if not path.isabs(file_path): 548 | _, tail = path.split(file_path) 549 | if tail == file_path: 550 | file_path = path.join(config["output_path"], file_path) 551 | 552 | head, tail = path.split(file_path) 553 | try: 554 | os.makedirs(head) 555 | log("Creating directory {}".format(head), LOGLEVEL.DEBUG) 556 | except OSError: 557 | pass 558 | 559 | _path = path.abspath(path.expanduser(path.expandvars(file_path))) 560 | for f in state["open_files"]: 561 | if _path == f.name: 562 | return f 563 | 564 | _path_for_log = _path.replace(os.getcwd(), '.') 565 | _mode = {'r': 'READ', 'w': 'WRITE', 'a': 'APPEND'} 566 | log("Opening new file {} in {} mode".format(_path_for_log, _mode[mode]), 567 | LOGLEVEL.INFO) 568 | try: 569 | file = codecs.open(_path, mode, "utf-8") 570 | except IOError: 571 | log("Unable to open the file at {}".format(_path_for_log), 572 | LOGLEVEL.ERROR) 573 | 574 | state["open_files"].add(file) 575 | 576 | if mode == 'r': 577 | stats["in_files"] += 1 578 | dir_name, file_name = path.split(_path) 579 | if state["next_out_file"] is None: 580 | root, _ = path.splitext(file_name) 581 | next_out_file(root+'.rpy') 582 | state["file_chain"].append({ 583 | "file": file, 584 | "file_path": _path, 585 | "file_dir": dir_name, 586 | "file_name": file_name, 587 | "cur_line": 0, 588 | "cur_indent": 0, 589 | "prev_indent": 0, 590 | "new_indent": False, 591 | "prev_whitespace": [], 592 | # "temp_dedent": [], 593 | "command_block": False, 594 | "command": (None, None), 595 | "blank_line": True, 596 | "label_chain": [], 597 | "next_label_call": None 598 | }) 599 | else: 600 | stats["out_files"] += 1 601 | 602 | return file 603 | 604 | 605 | def get_out_file(): 606 | if not state["cur_out_file"]: 607 | state["cur_out_file"] = open_file(state["next_out_file"]) 608 | 609 | return state["cur_out_file"] 610 | 611 | 612 | def next_out_file(file): 613 | log("Seting next output file to {}".format(file), LOGLEVEL.DEBUG) 614 | state["next_out_file"] = file 615 | state["cur_out_file"] = None 616 | 617 | 618 | def write_line(line=None, indent=True, file=None): 619 | _f = state["file_chain"][-1] 620 | if line is None: 621 | if not _f["blank_line"]: 622 | line = '' 623 | _f["blank_line"] = True 624 | else: 625 | return 626 | else: 627 | _f["blank_line"] = False 628 | 629 | log("Writing line to output", LOGLEVEL.VERB) 630 | file = file or get_out_file() 631 | 632 | if line != '' and line[-1] != '"': 633 | line = line.replace('\\n', '\n') 634 | 635 | if indent: 636 | _i = str(_il) 637 | file.write('\n'.join([_i+l.strip() for l in line.split('\n')])+'\n') 638 | else: 639 | file.write(line+'\n') 640 | 641 | if config["create_flow_control_file"]: 642 | _label = _f["next_label_call"] 643 | if _label: 644 | log("Adding label call to control file", LOGLEVEL.DEBUG) 645 | _f["next_label_call"] = None 646 | if not state["control_file"]: 647 | state["control_file"] = open_file("control.rpy", 'w') 648 | write_line("label _control:", False, state["control_file"]) 649 | if _label[0] == '.': 650 | write_line(" call "+_f["label_chain"][-1]+_label, 651 | False, state["control_file"]) 652 | else: 653 | write_line(" call "+_label, False, state["control_file"]) 654 | _f["next_label_call"] = None 655 | 656 | stats["out_lines"] += len(line.split('\n')) 657 | 658 | ##----------------------------------------------------------------------------- 659 | ## Commands 660 | ##----------------------------------------------------------------------------- 661 | 662 | def parse_command_block(line): 663 | log("Parsing next line in command block", LOGLEVEL.VERB) 664 | _f = state['file_chain'][-1] 665 | _c = _f["command"] 666 | 667 | _m = _c[1].match(':'+_c[0]+' '+line) 668 | 669 | parse_command(_m.group(1), _m.groups()[0:], _c[1]) 670 | 671 | 672 | def parse_command(command, matches, _re): 673 | matches = [m.strip() for m in matches[1:]] 674 | log("Parsing command '{}' {}".format(command, matches), LOGLEVEL.DEBUG) 675 | stats["commands_processed"] += 1 676 | _f = state['file_chain'][-1] 677 | 678 | def _write_play(channel, sound): 679 | write_line("play "+channel+' '+ \ 680 | sound.replace(r'\"', '"').replace(r"\'", "'")) 681 | 682 | # ^:(line)\s*(.*?)=\s?(.*)$ 683 | if command == "line": 684 | log("command: New line replacement", LOGLEVEL.DEBUG) 685 | line_regex(matches[0], matches[1]) 686 | 687 | # ^:(line:)$ 688 | elif command == "line:": 689 | log("command: Line replacement block", LOGLEVEL.DEBUG) 690 | log("New indent is now expected", LOGLEVEL.VERB) 691 | _f["new_indent"] = 1 692 | _f["command_block"] = True 693 | _f["command"] = ('line', _re) 694 | 695 | # ^:(character)\s*(.*?)=\s?(.*)$ 696 | if command == "character": 697 | log("command: New character replacement", LOGLEVEL.DEBUG) 698 | character_regex(matches[0], matches[1]) 699 | 700 | # ^:(character:)$ 701 | elif command == "character:": 702 | log("command: Character replacement block", LOGLEVEL.DEBUG) 703 | log("New indent is now expected", LOGLEVEL.VERB) 704 | _f["new_indent"] = 1 705 | _f["command_block"] = True 706 | _f["command"] = ('character', _re) 707 | 708 | # ^:(:)\s+(\.?[\w\.]*)$ 709 | # TODO: Move all of this to another function and fix it up 710 | # TODO: Integrate auto_return for labels with content. 711 | elif command == ":": 712 | log("command: Label", LOGLEVEL.DEBUG) 713 | _m = parent_label_re.match(matches[0]) 714 | if _m and _m.groups()[0]: 715 | log("Parent label: {}".format(_m.group(1)), LOGLEVEL.DEBUG) 716 | # TODO: fix this so we write to the correct parent file if it is 717 | # already open 718 | if config["create_parent_files"]: 719 | if _m.group(1) not in state["parent_labels"]: 720 | log("New parent file: {}".format(_m.group(1))) 721 | next_out_file(_m.group(1)+'.rpy') 722 | state["parent_labels"].add(_m.group(1)) 723 | 724 | _f["next_label_call"] = None 725 | 726 | write_line('label '+matches[0]+':') 727 | 728 | if config["create_flow_control_file"]: 729 | ignore = False 730 | for _re in config["flow_control_ignore"]: 731 | if _re.match(matches[0]): 732 | ignore = True 733 | break 734 | if not ignore: 735 | _f["next_label_call"] = matches[0] 736 | 737 | # Build label chain links 738 | # TODO: Fix this mess up 739 | if matches[0][0] != '.': 740 | _parent = matches[0].split('.')[0] 741 | if _parent not in _f["label_chain"]: 742 | _f["label_chain"].append(_parent) 743 | # log(_f["label_chain"]) 744 | 745 | # ^:(sc)\s*(\w*)$ 746 | elif command == "sc": 747 | log("command: Scene", LOGLEVEL.DEBUG) 748 | write_line('scene '+matches[0]) 749 | 750 | # ^:(s)\s*(\w*)$ 751 | elif command == "s": 752 | log("command: Show", LOGLEVEL.DEBUG) 753 | write_line('show '+matches[0]) 754 | 755 | # ^:(w)\s*(\w*)$ 756 | elif command == "w": 757 | log("command: With", LOGLEVEL.DEBUG) 758 | write_line('with '+matches[0]) 759 | 760 | # ^:(p)\s+(.*?)\s+(.*)$ 761 | elif command == "p": 762 | log("command: Play", LOGLEVEL.DEBUG) 763 | _write_play(matches[0], matches[1]) 764 | 765 | # ^:(pm)\s+(.*)$ 766 | elif command == "pm": 767 | log("command: Play music", LOGLEVEL.DEBUG) 768 | _write_play("music", matches[0]) 769 | 770 | # ^:(ps)\s+(.*)$ 771 | elif command == "ps": 772 | log("command: Play sound", LOGLEVEL.DEBUG) 773 | _write_play("sound", matches[0]) 774 | 775 | # ^:(pa)\s+(.*)$ 776 | elif command == "pa": 777 | log("command: Play audio", LOGLEVEL.DEBUG) 778 | _write_play("audio", matches[0]) 779 | 780 | # ^:(v)\s+(.*)$ 781 | elif command == "v": 782 | log("command: Voice", LOGLEVEL.DEBUG) 783 | write_line("voice "+matches[0].replace(r'\"', '"').replace(r"\'", "'")) 784 | 785 | # ^:(q)\s+(.*?)\s+(.*)$ 786 | elif command == "q": 787 | log("command: Queue", LOGLEVEL.DEBUG) 788 | write_line("queue "+matches[0]+' '+ \ 789 | matches[1].replace(r'\"', '"').replace(r"\'", "'")) 790 | 791 | # ^:(stop)\s*(.*)?$ 792 | elif command == "stop": 793 | log("command: Stop", LOGLEVEL.DEBUG) 794 | write_line("stop "+matches[0]) 795 | 796 | # ^:(c)\s*(\w*)$ 797 | elif command == "c": 798 | log("command: Call", LOGLEVEL.DEBUG) 799 | write_line('call '+matches[0]) 800 | 801 | # ^:(j)\s*(\w*)$ 802 | elif command == "j": 803 | log("command: Jump", LOGLEVEL.DEBUG) 804 | # TODO: Work propper label chain into this 805 | write_line('jump '+matches[0]) 806 | 807 | # ^:(r)$ 808 | # ^:(r)\s+(.*)$ 809 | elif command == "r": 810 | log("command: Return", LOGLEVEL.DEBUG) 811 | if len(matches) >= 1: 812 | write_line("return {}".format(matches[0])) 813 | else: 814 | write_line('return') 815 | 816 | # ^:(choice):$ 817 | elif command == "choice": 818 | log("command: New menu block", LOGLEVEL.DEBUG) 819 | log("New indent is now expected", LOGLEVEL.VERB) 820 | _f["new_indent"] = 1 821 | write_line('menu:') 822 | 823 | # ^:(if)\s*(.*?):$ 824 | elif command == "if": 825 | log("command: if statement", LOGLEVEL.DEBUG) 826 | log("New indent is now expected", LOGLEVEL.VERB) 827 | _f["new_indent"] = 1 828 | write_line('if '+matches[0]+':') 829 | 830 | # ^:(elif)\s*(.*?):$ 831 | elif command == "elif": 832 | log("command: elif statement", LOGLEVEL.DEBUG) 833 | log("New indent is now expected", LOGLEVEL.VERB) 834 | _f["new_indent"] = 1 835 | write_line('elif '+matches[0]+':') 836 | 837 | # ^:(else):$ 838 | elif command == "else": 839 | log("command: else statement", LOGLEVEL.DEBUG) 840 | log("New indent is now expected", LOGLEVEL.VERB) 841 | _f["new_indent"] = 1 842 | write_line('else:') 843 | 844 | # ^:(nvl):$ 845 | elif command == "nvl": 846 | log("command: New NVL block", LOGLEVEL.DEBUG) 847 | log("New indent is now expected", LOGLEVEL.VERB) 848 | _f["new_indent"] = 1 849 | state["is_nvl_mode"] = True 850 | 851 | # ^:(clear)$ 852 | elif command == "clear": 853 | log("command: NVL clear", LOGLEVEL.DEBUG) 854 | if state["is_nvl_mode"]: 855 | write_line("nvl clear") 856 | else: 857 | write_line() 858 | 859 | # ^:(import)\s*(.*)$ 860 | elif command == "import": 861 | log("command: Import new file for reading", LOGLEVEL.DEBUG) 862 | _f = path.abspath(path.expanduser(path.expandvars(matches[0]))) 863 | if path.isfile(_f) is False: 864 | log("{} is not an accessible file".format( 865 | matches[0]), LOGLEVEL.ERROR) 866 | log("Importing file {}".format(matches[0]), LOGLEVEL.INFO) 867 | loop_file(_f) 868 | 869 | # ^:(file)\s*(.*)$ 870 | elif command == "file": 871 | log("command: New output file", LOGLEVEL.DEBUG) 872 | next_out_file(matches[0]) 873 | 874 | # ^:(log)\s*(0|1|2|3|4|VERBOSE|DEBUG|INFO|WARN|WARNING|ERROR)\s*(.*)$ 875 | elif command == "log": 876 | log("command: write to log", LOGLEVEL.VERB) 877 | try: 878 | log(matches[1], int(matches[0])) 879 | except ValueError: 880 | log(matches[1], eval("LOGLEVEL."+matches[0])) 881 | 882 | # ^:(config)\s*(.*?)=\s?(.*)$ 883 | elif command == "config": 884 | log("command: Congiguration setting", LOGLEVEL.DEBUG) 885 | if matches[0] not in config: 886 | log("Unknown config option {}".format(matches[0]), LOGLEVEL.ERROR) 887 | 888 | if matches[0] == "flow_control_ignore": 889 | _l = [] 890 | for v in eval(matches[1].replace(r'\"', '"')): 891 | _l.append(re.compile('^'+regex_prep(v)+'$')) 892 | config["flow_control_ignore"] = _l 893 | 894 | else: 895 | try: 896 | config[matches[0]] = eval(matches[1]) 897 | except (SyntaxError, NameError): 898 | config[matches[0]] = matches[1] 899 | 900 | # ^:(config:)$ 901 | elif command == "config:": 902 | log("command: Config block", LOGLEVEL.DEBUG) 903 | log("New indent is now expected", LOGLEVEL.VERB) 904 | _f["new_indent"] = 1 905 | _f["command_block"] = True 906 | _f["command"] = ('config', _re) 907 | 908 | # ^:(break)$ 909 | elif command == "break": 910 | log("Break command encountered", LOGLEVEL.INFO) 911 | sys.exit() 912 | 913 | ##----------------------------------------------------------------------------- 914 | ## Per line functions 915 | ##----------------------------------------------------------------------------- 916 | 917 | def indentinator(leading_whitespace): 918 | log("Performing indentation management", LOGLEVEL.VERB) 919 | _f = state['file_chain'][-1] 920 | prev_ws = _f["prev_whitespace"] 921 | 922 | if leading_whitespace < sum(prev_ws): 923 | _f["command_block"] = False 924 | _reduce = 0 925 | for i in range(len(prev_ws), 0, -1): 926 | if leading_whitespace < sum(prev_ws[:i]): 927 | _reduce += 1 928 | else: 929 | break 930 | 931 | _f["cur_indent"] -= _reduce 932 | while _reduce >= 1: 933 | prev_ws.pop() 934 | if state["is_nvl_mode"]: 935 | state["is_nvl_mode"] -= 1 936 | if state["is_nvl_mode"] == 0: 937 | state["is_nvl_mode"] = False 938 | _reduce -= 1 939 | 940 | if leading_whitespace != sum(prev_ws): 941 | log("Inconsistent indentation detected", LOGLEVEL.ERROR) 942 | 943 | elif leading_whitespace > sum(prev_ws): 944 | _f["cur_indent"] += 1 945 | prev_ws.append(leading_whitespace - sum(prev_ws)) 946 | if state["is_nvl_mode"]: 947 | if state["is_nvl_mode"] is True: 948 | state["is_nvl_mode"] = 1 949 | else: 950 | state["is_nvl_mode"] += 1 951 | 952 | 953 | def parse_line(line): 954 | stats["in_lines"] += 1 955 | _f = state["file_chain"][-1] 956 | log('Parsing Line: "{}"'.format(line.strip()), LOGLEVEL.VERB) 957 | 958 | if empty_line_re.match(line): 959 | write_line() 960 | return 961 | 962 | # Comments 963 | _m = comment_re.match(line) 964 | if _m: 965 | line = _m.group(2).rstrip() 966 | if line[0] == config["copy_special_comments"]: 967 | write_line(_m.group(1)+'#'+line, indent=False) 968 | else: 969 | log("Non-copy comment detected; skipping.", LOGLEVEL.VERB) 970 | return 971 | 972 | indentinator(len(line) - len(line.lstrip())) 973 | line = line.strip() 974 | 975 | log("Checking for indentation errors", LOGLEVEL.VERB) 976 | if _f["new_indent"]: 977 | if _f["cur_indent"] <= _f["prev_indent"]: 978 | log("Expecting new indent", LOGLEVEL.ERROR) 979 | elif _f["cur_indent"] > _f["prev_indent"]: 980 | log("Line is indented, but not expecting a new indent", 981 | LOGLEVEL.ERROR) 982 | 983 | _f["prev_indent"] = _f["cur_indent"] 984 | _f["new_indent"] = 0 985 | 986 | # Inside command block 987 | if _f["command_block"]: 988 | parse_command_block(line.replace('"', r'\"')) 989 | return 990 | 991 | # Commands 992 | log("Checking for command", LOGLEVEL.VERB) 993 | for i in range(len(command_list)): 994 | _m = command_list[i].match(line.replace('"', r'\"')) 995 | if _m: 996 | if _m.group(1) == ':' and line[-1] == ':': 997 | log("New indent is now expected", LOGLEVEL.VERB) 998 | _f["new_indent"] = 1 999 | parse_command(_m.group(1), _m.groups()[0:], command_list[i-1]) 1000 | return 1001 | 1002 | log("Checking for new indent", LOGLEVEL.VERB) 1003 | if line[-1] == ':': 1004 | log("New indent is now expected", LOGLEVEL.VERB) 1005 | _f["new_indent"] = 1 1006 | 1007 | # $ starting python lines 1008 | _m = python_re.match(line) 1009 | if _m: 1010 | log("Python line command detected", LOGLEVEL.DEBUG) 1011 | write_line(_m.group(1)) 1012 | return 1013 | 1014 | line = line.replace('"', r'\"') 1015 | 1016 | # Line replacement 1017 | log("Checking for line replacement", LOGLEVEL.VERB) 1018 | for k, v in state["known_line_rep"].items(): 1019 | _m = k.match(line) 1020 | if _m: 1021 | log("Line replacement match", LOGLEVEL.VERB) 1022 | stats["line_replacements"] += 1 1023 | _s = re.sub('\\\\{|\\\\}', fix_brace, v) 1024 | _s = _s.format(*_m.groups()) 1025 | try: 1026 | write_line(re.sub('\xc0|\xc1', fix_brace, _s)) 1027 | except Exception as e: 1028 | raise 1029 | # log("Unable to replace line:\n {}\n {}".format( 1030 | # v, _m.groups()), LOGLEVEL.ERROR) 1031 | return 1032 | 1033 | # Character Replacement 1034 | log("Checking for character replacement", LOGLEVEL.VERB) 1035 | for k, v in state["known_character_rep"].items(): 1036 | _m = k.match(line) 1037 | if _m: 1038 | log("Character replacement match", LOGLEVEL.VERB) 1039 | _s = re.sub('\\\\{|\\\\}', fix_brace, v[0]) 1040 | if state["is_nvl_mode"]: 1041 | _n = config["nvl_prefix"]+_s+config["nvl_suffix"] 1042 | _line = ''.join((_n, v[1])) 1043 | else: 1044 | _line = ''.join(v) 1045 | stats["character_replacements"] += 1 1046 | stats["dialogue_lines"] += 1 1047 | try: 1048 | _line = _line.format(*_m.groups()) 1049 | write_line(re.sub('\xc0|\xc1', fix_brace, _line)) 1050 | except Exception as e: 1051 | raise 1052 | # log("Unable to replace prefix:\n {}\n {}".format( 1053 | # v[0], _m.groups()), LOGLEVEL.ERROR) 1054 | return 1055 | 1056 | # Unknown command 1057 | log("Checking for unknown command", LOGLEVEL.VERB) 1058 | _m = command_re.match(line) 1059 | if _m: 1060 | # TODO: implement unknow command outputs 1061 | log("Unknown command processing not yet implemented, sorry :/", 1062 | LOGLEVEL.WARN) 1063 | # log("Unknown command detected", LOGLEVEL.INFO) 1064 | # # TODO: Use current indent level 1065 | # # TODO: make use of this dict value 1066 | # _f["unknown_block"] = True 1067 | # # TODO: Write this function 1068 | # write_unknown_command_block() 1069 | # return 1070 | 1071 | # Else, its just a normal narration line 1072 | log("Normal narration line", LOGLEVEL.VERB) 1073 | if state["is_nvl_mode"]: 1074 | _nvl = config["nvl_character"]+' ' 1075 | else: 1076 | _nvl = '' 1077 | 1078 | stats["narration_lines"] += 1 1079 | if line[-1] == ':': 1080 | write_line(_nvl+'"{}":'.format(line[:-1])) 1081 | else: 1082 | write_line(_nvl+'"{}"'.format(line)) 1083 | 1084 | ##----------------------------------------------------------------------------- 1085 | ## Main execution 1086 | ##----------------------------------------------------------------------------- 1087 | 1088 | def main(argv): 1089 | global _debug, log 1090 | 1091 | if not argv: 1092 | usage("No input script file defined.") 1093 | if '-h' in argv or '--help' in argv: 1094 | usage() 1095 | 1096 | if len(argv) > 1: 1097 | try: 1098 | opts, args = getopt.getopt(argv[1:], 'ho:', 1099 | ['help', 'output=', 'debug', 'verbose']) 1100 | except getopt.GetoptError as e: 1101 | usage(str(e)) 1102 | else: 1103 | opts, args = {}, {} 1104 | 1105 | output_path = None 1106 | _debug = 0 1107 | 1108 | if opts: 1109 | log("Command line arguments: {}".format(opts), LOGLEVEL.VERB) 1110 | else: 1111 | log("No additional command line arguments detected", LOGLEVEL.VERB) 1112 | 1113 | for opt, arg in opts: 1114 | if opt in ('-o', '--output'): 1115 | output_path = arg 1116 | elif opt == '--debug': 1117 | if _debug: 1118 | log("Can not set --debug and --verbose options simultaniously", 1119 | LOGLEVEL.WARN) 1120 | else: 1121 | _debug = 1 1122 | log("Debug logging enabled") 1123 | elif opt == '--verbose': 1124 | if _debug: 1125 | log("Can not set --debug and --verbose options simultaniously", 1126 | LOGLEVEL.WARN) 1127 | else: 1128 | _debug = 2 1129 | log("Verbose mode is set. This can severly impact the speed " 1130 | "and performance of the script and may result in a huge " 1131 | "log file.", LOGLEVEL.WARN) 1132 | 1133 | in_file = path.abspath(path.expanduser(path.expandvars(argv[0]))) 1134 | 1135 | if path.isfile(in_file) is False: 1136 | log("{} is not an accessible file".format(argv[0]), 1137 | LOGLEVEL.ERROR, exit = False) 1138 | try: 1139 | log("initializing logger", LOGLEVEL.DEBUG) 1140 | os.chdir(path.dirname(in_file)) 1141 | except: 1142 | log("Failed to set directory for logger", LOGLEVEL.ERROR) 1143 | else: 1144 | log = _Logger(_tmp_log) 1145 | sys.exit(1) 1146 | 1147 | os.chdir(path.dirname(in_file)) 1148 | 1149 | setup_globals(output_path) 1150 | 1151 | loop_file(in_file) 1152 | 1153 | sys.exit() 1154 | 1155 | 1156 | def cleanup(): 1157 | log("Cleaning up", LOGLEVEL.VERB) 1158 | if state["control_file"]: 1159 | write_line("return", False, state["control_file"]) 1160 | for f in state['open_files']: 1161 | try: 1162 | f.close() 1163 | except ValueError: 1164 | pass 1165 | log.close() 1166 | 1167 | 1168 | if __name__ == "__main__": 1169 | print(_c.r) 1170 | log("Starting Build", LOGLEVEL.INFO) 1171 | try: 1172 | main(sys.argv[1:]) 1173 | except Exception: 1174 | log.log_traceback() 1175 | finally: 1176 | cleanup() 1177 | -------------------------------------------------------------------------------- /tests/output/audio.rpy: -------------------------------------------------------------------------------- 1 | label audio: 2 | label .play: 3 | play my_channel \"my_sound.ogg\" 4 | play music \"music.ogg\" fadeout 1.0 5 | play sound [\"sound.ogg\", \"sound_1.ogg\"] 6 | play audio \"audio.ogg\" 7 | 8 | label .voice: 9 | voice \"voice.ogg\" 10 | 11 | label .queue: 12 | queue music \"music_2.ogg\" 13 | 14 | label .stop: 15 | stop music 16 | 17 | -------------------------------------------------------------------------------- /tests/output/character_test.rpy: -------------------------------------------------------------------------------- 1 | label character_test: 2 | 3 | 4 | label character_test.single: 5 | CHARACTER_01 "Hello, World!" 6 | 7 | label .block: 8 | CHARACTER_02 "foobar1 a" 9 | CHARACTER_02 "foobar1 None" 10 | CHARACTER_03 "foobar2 abc" 11 | CHARACTER_04 "foobar3 xyz" 12 | CHARACTER_04 "foobar3 None" 13 | CHARACTER_05 (gg) "foobar4" 14 | CHARACTER_05 () "foobar4" 15 | CHARACTER_06 "foobar5" 16 | CHARACTER_07 "foobar6" 17 | CHARACTER_07 "foobar6" 18 | CHARA_CTER_xy08zz "foobar7" 19 | CHARACTER_ab08cd "foobar7" 20 | CHARAQCTER_08ef "foobar7" 21 | return 22 | 23 | -------------------------------------------------------------------------------- /tests/output/control.rpy: -------------------------------------------------------------------------------- 1 | label _control: 2 | call line_test 3 | call line_test.single 4 | call line_test.block 5 | call line_test.order 6 | call character_test 7 | call character_test.single 8 | call character_test.block 9 | call audio.play 10 | call audio.voice 11 | call audio.queue 12 | call audio.stop 13 | call flow_control.choice 14 | call flow_control.if_elif_else 15 | call files.import 16 | call files.file 17 | call other.comments 18 | call other.scene_show_with 19 | call other.nvl 20 | call other.misc 21 | return 22 | call other.logging 23 | -------------------------------------------------------------------------------- /tests/output/files.rpy: -------------------------------------------------------------------------------- 1 | label files: 2 | label .import: 3 | "This line will be imported" 4 | return 5 | 6 | label .file: 7 | -------------------------------------------------------------------------------- /tests/output/flow_control.rpy: -------------------------------------------------------------------------------- 1 | label flow_control: 2 | label .choice: 3 | menu: 4 | "This is the accompanying narration" 5 | "This is the first choice": 6 | jump flow_control._choice_01 7 | "This is the second choice": 8 | call ._c02 9 | "A third choice": 10 | jump other.this_is_ignored 11 | return 12 | 13 | label ._choice_01: 14 | "Choice 1" 15 | $ choice = 1 16 | return 17 | 18 | label ._c02: 19 | "Choice 2" 20 | $ choice = 2 21 | return \"foo\" 22 | 23 | label .if_elif_else: 24 | if choice == 1: 25 | "You chose the first option." 26 | elif choice == 2: 27 | "You chose the second option." 28 | else: 29 | "You chose the third option." 30 | return 31 | 32 | -------------------------------------------------------------------------------- /tests/output/foobar.rpy: -------------------------------------------------------------------------------- 1 | "This line should be in 'output/foobar.rpy'" 2 | return 3 | 4 | -------------------------------------------------------------------------------- /tests/output/line_test.rpy: -------------------------------------------------------------------------------- 1 | label line_test: 2 | 3 | 4 | label line_test.single: 5 | Line replacement 01 6 | return 7 | 8 | label .block: 9 | Line replacement 02 10 | Line replacement 02 11 | Line replacement 03 12 | Line replacement 04 13 | Line replacement 04 14 | Line replacement 05 (foobar) 15 | Line replacement 05 () 16 | Line replacement 06 17 | Line replacement 07 18 | Line replacement 07 19 | Line Q replacement $@ 08 (a) 20 | Line replacement #comment 08 (abc) 21 | Line _ replacement 08 (xyz) 22 | return 23 | 24 | label .order: 25 | 26 | Line order 01 (+) 27 | Line order 02 (?) 28 | Line order 02 (?) 29 | return 30 | 31 | -------------------------------------------------------------------------------- /tests/output/other.rpy: -------------------------------------------------------------------------------- 1 | label other: 2 | label .this_is_ignored: 3 | "Nothing here" 4 | $ choice = 3 5 | return 6 | 7 | label .comments: 8 | ## copy comment 9 | ## unindented copy comment 10 | #~ Oddly indented copy comment with new comment symbol 11 | return 12 | 13 | label .scene_show_with: 14 | scene scene_01 15 | show image_01 16 | with Dissolve(0.5) 17 | return 18 | 19 | label .nvl: 20 | play char: = CHARACTER 21 | "This narration is in ADV mode" 22 | "char: This dialog line is in ADV mode" 23 | NVL_test "This narration is in NVL mode" 24 | NVL_test "char: This dialog line is in NVL mode" 25 | nvl clear 26 | NVL_test "This NVL line is on the next page" 27 | "char: This line will be in ADV mode again" 28 | return 29 | 30 | label .misc: 31 | "Line indentation doesn't matter" 32 | "As long as it is consistent, just like python." 33 | "But this'll throw an error" 34 | "Likewise" 35 | return 36 | 37 | label .logging: 38 | -------------------------------------------------------------------------------- /tests/output/prefix_test.rpy: -------------------------------------------------------------------------------- 1 | label prefix_test: 2 | 3 | 4 | label line_test.single: 5 | PREFIX_01 "Hello, World!" 6 | 7 | label .block: 8 | PREFIX_02 "foobar1 a" 9 | PREFIX_02 "foobar1 None" 10 | PREFIX_03 "foobar2 abc" 11 | PREFIX_04 "foobar3 xyz" 12 | PREFIX_04 "foobar3 None" 13 | PREFIX_05 (gg) "foobar4" 14 | PREFIX_05 () "foobar4" 15 | PREFIX_06 "foobar5" 16 | PREFIX_07 "foobar6" 17 | PREFIX_07 "foobar6" 18 | PRE_FIX_xy08zz "foobar7" 19 | PREFIX_ab08cd "foobar7" 20 | PREqFIX_08ef "foobar7" 21 | return 22 | 23 | -------------------------------------------------------------------------------- /tests/test1.rps: -------------------------------------------------------------------------------- 1 | #! RenPy Script Builder test file 2 | 3 | # TEST: config (single) 4 | :config output_path = ./output 5 | 6 | # TEST: test config (block) 7 | :config: 8 | # TEST: all other config options 9 | create_parent_files = True 10 | create_flow_control_file = True 11 | flow_control_ignore = ["*_c+", "*_choice*", "*_ignore*"] 12 | copy_comments = False 13 | copy_special_comments = # 14 | nvl_character = NVL_test 15 | nvl_prefix = nvl_ 16 | nvl_suffix = _NVL_test 17 | auto_return = True 18 | abort_on_error = False 19 | 20 | # TEST: parent label (non-block) 21 | ::line_test 22 | 23 | # TEST: line replacement (single) 24 | :line LineRep_01 = Line replacement 01 25 | 26 | # TEST: sub label, with parent (non-block) 27 | ::line_test.single 28 | # TEST: single line replacement 29 | LineRep_01 30 | :r 31 | 32 | # TEST: line replacement (block) 33 | :line: 34 | # TEST: single character replacement 35 | LineRep_02(?) = Line replacement 02 36 | # TEST: one or more character replacement 37 | LineRep_03(+) = Line replacement 03 38 | # TEST: zero or more character replacement 39 | LineRep_04(*) = Line replacement 04 40 | # TEST: zero or more character replacement with capture 41 | LineRep_05({*}) = Line replacement 05 ({}) 42 | # TEST: escapes 43 | LineRep_06(\\\{\?\+\*\}) = Line replacement 06 44 | # TEST: multiple wildcards 45 | Line?Rep_*07(+) = Line replacement 07 46 | # TEST: multiple wildcards with captures 47 | Line{?}Rep_{*}08({+}) = Line {} replacement {} 08 ({}) 48 | 49 | # TEST: sub label, without parent (non-block) 50 | ::.block 51 | # TEST: block line replacements 52 | LineRep_02(a) 53 | LineRep_02() 54 | LineRep_03(abc) 55 | LineRep_04(abcd) 56 | LineRep_04() 57 | LineRep_05(foobar) 58 | LineRep_05() 59 | LineRep_06(\{?+*}) 60 | LineQRep_$@07(a) 61 | LineRep_07(abc) 62 | LineQRep_$@08(a) 63 | LineRep_#comment08(abc) 64 | Line_Rep_08(xyz) 65 | :r 66 | 67 | ::.order 68 | :line: 69 | # TEST: line replacement order 70 | LineOrder(+) = Line order 01 (+) 71 | LineOrder(?) = Line order 02 (?) 72 | LineOrder(*) = Line order 03 (*) 73 | LineOrder() = Line order 04 () 74 | 75 | # TEST: line order replacements 76 | LineOrder(abc) 77 | LineOrder(z) 78 | LineOrder() 79 | :r 80 | 81 | # TEST: parent label (non-block) 82 | ::character_test: 83 | 84 | # TEST: character replacement (single) 85 | :character character_01; = CHARACTER_01 86 | 87 | # TEST: sub label, with parent (block) 88 | ::character_test.single: 89 | # TEST: single character replacement 90 | character_01; Hello, World! 91 | 92 | # TEST: character replacement (block) 93 | :character: 94 | # TEST: single character replacement 95 | character_02?; = CHARACTER_02 96 | # TEST: one or more character replacement 97 | character_03+; = CHARACTER_03 98 | # TEST: zero or more character replacement 99 | character_04*; = CHARACTER_04 100 | # TEST: zero or more character replacement with capture 101 | character_05{*}; = CHARACTER_05 ({}) 102 | # TEST: escapes 103 | character_06\\\{\?\+\*\}; = CHARACTER_06 104 | # TEST: multiple wildcards 105 | chara?cter_*07+; = CHARACTER_07 106 | # TEST: multiple wildcards with captures 107 | chara{?}cter_{*}08{+}; = CHARA{}CTER_{}08{} 108 | 109 | # TEST: block character replacements 110 | ::.block: 111 | # TEST: block character replacements 112 | character_02a; foobar1 a 113 | character_02; foobar1 None 114 | character_03abc; foobar2 abc 115 | character_04xyz; foobar3 xyz 116 | character_04; foobar3 None 117 | character_05gg; foobar4 118 | character_05; foobar4 119 | character_06\{?+*}; foobar5 120 | chara_cter_$@07a; foobar6 121 | character_07abc; foobar6 122 | chara_cter_xy08zz; foobar7 123 | character_ab08cd; foobar7 124 | charaQcter_08ef; foobar7 125 | :r 126 | 127 | ::audio: 128 | ::.play: 129 | # TEST: play 130 | :p my_channel "my_sound.ogg" 131 | # TEST: play music 132 | :pm "music.ogg" fadeout 1.0 133 | # TEST: play sound 134 | :ps ["sound.ogg", "sound_1.ogg"] 135 | # TEST: play audio 136 | :pa "audio.ogg" 137 | 138 | ::.voice: 139 | # TEST: voice 140 | :v "voice.ogg" 141 | 142 | ::.queue: 143 | # TEST: queue 144 | :q music "music_2.ogg" 145 | 146 | ::.stop: 147 | # TEST: stop 148 | :stop music 149 | 150 | ::flow_control: 151 | ::.choice: 152 | # TEST: choice 153 | :choice: 154 | This is the accompanying narration 155 | This is the first choice: 156 | # TEST: jump 157 | :j flow_control._choice_01 158 | This is the second choice: 159 | # TEST call 160 | :c ._c02 161 | A third choice: 162 | # TEST: jump to different parent label 163 | :j other.this_is_ignored 164 | :r 165 | 166 | # TEST: Choice ignored label 167 | ::._choice_01: 168 | Choice 1 169 | # TEST: Python Line 170 | $ choice = 1 171 | # TEST: return 172 | :r 173 | 174 | # TEST: Custom ignore label 175 | ::._c02: 176 | Choice 2 177 | $ choice = 2 178 | # TEST: return with value 179 | :r "foo" 180 | 181 | ::.if_elif_else: 182 | # TEST: if 183 | :if choice == 1: 184 | You chose the first option. 185 | # TEST: elif 186 | :elif choice == 2: 187 | You chose the second option. 188 | # TEST: else 189 | :else: 190 | You chose the third option. 191 | :r 192 | 193 | ::files: 194 | ::.import: 195 | # TEST: import 196 | :import test2.rps 197 | :r 198 | 199 | ::.file: 200 | # TEST: New output file 201 | :file foobar.rpy 202 | This line should be in 'output/foobar.rpy' 203 | :r 204 | 205 | ::other: 206 | # TEST: Ignored label 207 | ::.this_is_ignored: 208 | Nothing here 209 | $ choice = 3 210 | :r 211 | 212 | ::.comments: 213 | # TEST: Comments 214 | # non-copy comment 215 | ## copy comment 216 | # unindented non-copy comment 217 | ## unindented copy comment 218 | :config copy_special_comments = ~ 219 | # Oddly indented non-copy comment 220 | #~ Oddly indented copy comment with new comment symbol 221 | :r 222 | 223 | ::.scene_show_with: 224 | # TEST: scene 225 | :sc scene_01 226 | # TEST: show 227 | :s image_01 228 | # TEST: with 229 | :w Dissolve(0.5) 230 | :r 231 | 232 | ::.nvl: 233 | :p char: = CHARACTER 234 | This narration is in ADV mode 235 | char: This dialog line is in ADV mode 236 | # TEST: NVL mode 237 | :nvl: 238 | This narration is in NVL mode 239 | char: This dialog line is in NVL mode 240 | # TEST: NVL clear 241 | :clear 242 | This NVL line is on the next page 243 | char: This line will be in ADV mode again 244 | :r 245 | 246 | ::.misc: 247 | # TEST: Odd indentation 248 | Line indentation doesn't matter 249 | As long as it is consistent, just like python. 250 | But this'll throw an error 251 | Likewise 252 | :r 253 | 254 | ::.logging: 255 | # TEST: logging verbose 256 | :log 0 Verbose 1 257 | :log VERBOSE Verbose 2 258 | # TEST: logging debug 259 | :log 1 Debug 1 260 | :log DEBUG Debug 261 | # TEST: logging info 262 | :log 2 Info 1 263 | :log INFO Info 2 264 | # TEST: logging warning 265 | :log 3 Warning 1 266 | :log WARN Warning 2 267 | :log WARNING Warning 3 268 | # TEST: logging error 269 | :log 4 Error 1 270 | :log ERROR Error 2 271 | # TEST: break 272 | :break 273 | :r 274 | -------------------------------------------------------------------------------- /tests/test2.rps: -------------------------------------------------------------------------------- 1 | This line will be imported 2 | --------------------------------------------------------------------------------