devolution

Author Topic: Extracting Ren'Pys scripting language  (Read 23286 times)

0 Members and 1 Guest are viewing this topic.

Offline rudistoned

  • Full Member
  • ***
  • Posts: 229
Extracting Ren'Pys scripting language
« on: November 20, 2013, 03:18:57 AM »
I'd like to be able to use renpy-like scripts in games not run by renpy. For this reason I am looking into renpys code to see how tightly the parser for renpy scripts is interwoven with the rest of the engine.


This is what I got so far:



The sequence of code called while renpy is launched that leads to loading and parsing the renpy script files (.rpy) in very broad strokes:


# error handling loop
renpy.bootstrap.bootstrap()


# main function called from error handling loop
renpy.main.main()


# load all .rpy files, which in their entirety together form "the script"
renpy.script.Script()
    """
    This class represents a Ren'Py script, which is parsed out of a
    collection of script files. Once parsing and initial analysis is
    complete, this object can be serialized out and loaded back in,
    so it shouldn't change at all after that has happened.


    @ivar namemap: A map from the name of an AST node to the AST node
    itself.  This is used for jumps, calls, and to find the current
    node when loading back in a save. The names may be strings or
    integers, strings being explicit names provided by the user, and
    integers being names synthesised by renpy.   


    @ivar initcode: A list of priority, Node tuples that should be
    executed in ascending priority order at init time.


    @ivar all_stmts: A list of all statements, that have been found
    in every file. Useful for lint, but tossed if lint is not performed
    to save memory.


    """
AST: http://en.wikipedia.org/wiki/Abstract_syntax_tree


All .rpy files are parsed at some point and their contents is represented by the Script class. Comments in renpy code calls this "the script".
"The script" is divided into nodes in an abstract syntax tree. Those nodes are defined in the renpy.ast module.
renpy.translation.Translator can sort nodes that require different treatment into different attributes. For example, python nodes go into the Translator.python dict and string nodes usable with StringTranslator go into the Translator.strings dict.


# find all files that will collectively form the script
renpy.script.Script.scan_script_files()
.rpy and .rpyc files go into renpy.script.Script.script_files
.rpym and .rpymc go into renpy.script.Script.module_files


# from renpy.script.Script.__init__ call Translator.chain_translates
As far as I can tell, Translator.chain_translates will do nothing because it "loops" over Translator.chain_worklist, which is initialized to an empty list. Translator.chain_worklist is filled when Script.load_script runs, which is obviously after the constructor of Script is done.


# from renpy.main.main call renpy.scrip.Script.load_script()
for each .rpy and .rpyc file:
    Run it through a series of loading methods, ending up at Script.load_file_core,
    which calls renpy.parser.parse


# the parser for the renpy script language: renpy.parser
# This module contains the parser for the Ren'Py script language. It's
# called when parsing is necessary, and creates an AST from the script.
The parser module does not import anything not in Pythons stdlib directly. It uses some parts of renpy 23 times by calling renpy.<something> (7 times renpy.ast and 4 times renpy.atl, which we probably need anyway; 4 times renpy.config, which is easy to replace).


Bottom line: renpy.parser seems easy to extract if renpy.ast and renpy.atl are easy to extract too, which I have not looked into yet.


Update1:
renpy.ast contains the classes implementing all the statements of renpys scripting language. Several of these statements make good use of objects outside of the renpy.ast module. In total, there are 130 calls to renpy.<something>, most of which we would have to replace (not all because we probably don't need all statements).


renpy.atl refers to the Ren'Py Animation and Transformation language. Since that is specific to the GUI toolkit used, I don't think we need it, at least not for the initial version of our event scripting language.


In conclusion, it appears as if renpy.parser and renpy.ast contain most of the code we need. Taking that, removing everything we don't need and replacing all calls to renpys infrastructure with a simpler, generic environment is IMHO likely to be faster than implementing something equally powerful on our own. The renpy scripting language has been around for a while and received its share of bugfixes and performance improvements. I think we could benefit from that.


« Last Edit: November 20, 2013, 08:18:40 AM by rudistoned »

Offline Xela

  • Global Moderator
  • *****
  • Posts: 6893
  • "It's like hunting cows"
Re: Extracting Ren'Pys scripting language
« Reply #1 on: November 20, 2013, 04:14:08 AM »
Interesting, I've been meaning to look under the Ren'Pys hood for a while but right now I don't even have time to work on PyTFall properly, please keep this thread updated :)
Like what we're doing?

Offline DocClox

  • Dev Team
  • *****
  • Posts: 1867
  • Messing Around With Python
Re: Extracting Ren'Pys scripting language
« Reply #2 on: November 20, 2013, 07:14:22 AM »
If not, I've got a couple of ideas on how we might render encounter trees using Python syntax, while keeping things reasonably clear. I'll back to hold off posting until I get back home tonight, but I think we can do this fairly cleanly.

Offline rudistoned

  • Full Member
  • ***
  • Posts: 229
Re: Extracting Ren'Pys scripting language
« Reply #3 on: November 20, 2013, 08:19:40 AM »
Added an update to the opening post. Since this looks better than I expected, I'll start ripping pieces out of renpy and see if I can revive them afterwards  ???

Offline DocClox

  • Dev Team
  • *****
  • Posts: 1867
  • Messing Around With Python
Re: Extracting Ren'Pys scripting language
« Reply #4 on: November 20, 2013, 08:52:11 AM »
and after I've been having such fun here, too!

Seriously, that sounds like great need. What I've got is workable, but renpy's syntax is much nicer to work with :)

Offline rudistoned

  • Full Member
  • ***
  • Posts: 229
Re: Extracting Ren'Pys scripting language
« Reply #5 on: November 20, 2013, 09:03:52 AM »
Sorry to crash your party  :D


You know, this was A LOT easier than I expected. I can already parse the renpy demo script (the_question) and get a list of abstract syntax tree instances as return value. 8)


Makes me think. I read somewhere that every time new technology works the first time you try somewhere 1000 kittens die a horribly death to keep the universe in balance  ???
Poor kittens...


I'm recording the changes I made, starting from the original renpy files, in a mercurial repository. I could upload it to source forge if you want to take a look at it.

Offline DocClox

  • Dev Team
  • *****
  • Posts: 1867
  • Messing Around With Python
Re: Extracting Ren'Pys scripting language
« Reply #6 on: November 20, 2013, 10:42:27 AM »
that sounds good :)

[edit]

Gah! I just spent an hour arguing with gmail that had become convinced that I had disabled cookies. I'm starting to mutter "bloody Google" in the same tones that I used to say "bloody Microsoft!"

Anyway... This is what I was doodling during my lunch hour.

Code: [Select]
encounter_list = {}


class Encounter:
        def __iadd__(self, enc_list):
                name = enc_list[0]
                encounter_list[name] = enc_list
                return self

        def __enter__(self):
                return self
        def __exit__(self, type, value, traceback):
                print "type = ", type
                print "value = ", value

#
# these are just so we can write chooser rather than "chooser"
#
# there are issues with namespace pollution, but I don't anticpate encounter
# definitions being mixed much with normal code, so probably not a
# problem
#
name    = "name"
speaker = "speaker"
setting = "setting"
lines   = "lines"
choose  = "choose"
func    = "func"
thumb   = "thumb"
encounter_end   = "encounter_end"

#
# the "with" construct is just to provide us with a block
# so we can define functions that we can later reference
# in the encounter list
#
with Encounter() as enc:

        def counter_offer(index):
                if index == 0:
                        show_encounter("best of luck")
                if index == 1:
                        if player.is_female():
                                show_encounter("you too")
                        else:
                                show_encounter("forty and my protection")
                if index ==2:
                        show_encounter("you should learn to haggle")

        def choose_you_too(index):
                # "Forty then, and you make do with just the girls",
                # "I don't see that I have much choice...",
                # "Sounds like a win-win scenario to me!"

                if index == 0:
                        show_encounter("bah!")
                if index == 1:
                        show_encounter("harsh business realities")
                if index ==2:
                        show_encounter("such enthusiasm")

        enc += [
                "Hamid raises a concern",
                {       speaker : "Hamid",
                        thumb   : "hamid_thumb.png",
                        setting : "hamids_tent.png",
                        lines   : [
                                "What are you doing buying slavegirls?",
                                "I said you could stay with me until you got your licence",
                                "I didn't say you should start building a harem!"
                        ]
                },
                {
                        lines   : [
                                "You to explain that the arrangement is only temporary\n"
                                "Just until you can get proper business premises.",
                                "Hamid seems unconvinced."
                        ]
                },
                {       speaker : "Hamid",
                        thumb   : "hamid_thumb.png",
                        lines   : [
                                "What? I can not believe my ears!",
                                "This is my home! You cannot turn it into a brothel!"
                        ]
                },
                {       lines   : "A calculating look crosses Hamid's face"
                },
                {       speaker : "Hamid",
                        thumb   : "hamid_thumb.png",
                        lines   : [
                                "If I run the House, I get to fuck all the girls for free, right?",
                                "And fifty percent of the take!"
                        ]
                },
                {       choose  : [
                                "Forget it! I don't need a business partner.",
                                "Fifty percent? Try twenty!",
                                "All right, you have a deal!",
                        ],
                        func    : counter_offer
                }
        ]

        enc += [
                "best of luck",
                {       speaker : "Hamid",
                        thumb   : "hamid_thumb.png",
                        setting : "hamids_tent.png",
                        lines   : [
                                "And I do not need such an ungracious guest "
                                "who would take such advantage of his host's generosity",
                                "",
                                "Do you think you will fare better sleeping in Element Square?",
                                "You are welcome to try!"
                        ]
                },
                {
                        lines   : [
                                "Thinking of what you have seen of the Souk, you are forced to agree",
                                "Reluctantly, you accept Hamid's offer"
                        ]
                },
                {       speaker : "Hamid",
                        thumb   : "hamid_thumb.png",
                        setting : "hamids_tent.png",
                        lines   : [
                                "Ha! I knew you would see sense!"
                        ]
                },
                encounter_end
      enc += [
                "you too",
                {       speaker : "Hamid",
                        thumb   : "hamid_thumb.png",
                        setting : "hamids_tent.png",
                        lines   : [
                                "I'll go as low as thirty, but for that I want you in my bed "
                                "as well as any of your girls whenever I want them"
                        ]
                },
                {       choose  : [
                                "Forty then, and you make do with just the girls",
                                "I don't see that I have much choice...",
                                "Sounds like a win-win scenario to me!"
                        ],
                        func    : choose_you_too
                }
        ]

        enc += [
                "you should learn to haggle",
                {       speaker : "Hamid",
                        thumb   : "hamid_thumb.png",
                        setting : "hamids_tent.png",
                        lines   : [
                                "We have a deal then! Though if we are to be partners "
                                "we shall have to work on your haggling skills!",
                                "No! Do not bother to thank me! it is all part of the service "
                                "now we are partners."
                        ]
                },
                encounter_end
        enc += [
                "forty and my protection",
                {       speaker : "Hamid",
                        thumb   : "hamid_thumb.png",
                        setting : "hamids_tent.png",
                        lines   : [
                                "Forty percent is as low as I will go.",
                                "But to sweeten the deal, I will offer my local expertise "
                                "as well as letting it be known that your girls are under "
                                "my protection",
                                "Take it or leave it, that's the best offer you'll get"
                        ]
                },
                {       lines   : [
                                "You have a feeling this is as good as it's going to get",
                                "You think back on the conditions in the Souk at night",
                                "It seems unlikely that you can do anything without some sort of base",
                                "",
                                "Reluctantly, you accept Hamid's offer",
                        ]
                },
                {       speaker : "Hamid",
                        thumb   : "hamid_thumb.png",
                        setting : "hamids_tent.png",
                        lines   : [
                                "Excellent! Let us drink to a long and profitable friendship!"
                        ]
                },
                encounter_end
        ]
        enc += [
                "bah!",
                {       speaker : "Hamid",
                        thumb   : "hamid_thumb.png",
                        setting : "hamids_tent.png",
                        lines   : [
                                "Bah! It is not good for a woman to raise a man's hopes like that!",
                                "Still, I should not grumble. If this enterprise goes well "
                                "I shall have all the pussy a man could dream of!",
                        ]
                },
                {
                        lines   : "Hamid begins to unbuckle his belt"
                },
                {       speaker : "Hamid",
                        thumb   : "hamid_thumb.png",
                        lines   : [
                                "Speaking of which, I think it is time I conducted some "
                                "basic quality control. You may watch if that is what you wish"
                        ]
                },
                {       lines   : "And so began my partnership with Hamid..."
                },
                encounter_end
        ]
        enc += [
                "harsh business realities",
                {       speaker : "Hamid",
                        thumb   : "hamid_thumb.png",
                        setting : "hamids_tent.png",
                        lines   : [
                                "It is good to meet a woman who understands the harsh realities of business! ",
                                "Far too many of you think only with your quim ... when you bother to think at all",
                                "Now I want you to get naked and break in your new slave",
                                "I shall watch and assess her technique ... and yours",
                                "",
                                "And don't give me that face! This is going to be fun! You'll see!"
                        ]
                },
                {       lines   : "And so began your association with Hamid..."
                },
                encounter_end
        ]
       enc += [
                "such enthusiasm",
                {       speaker : "Hamid",
                        thumb   : "hamid_thumb.png",
                        setting : "hamids_tent.png",
                        lines   : [
                                "Such enthusiasm! Have you ever consider you may be more suited to the workforce "
                                "than to management?",
                                "No matter! No matter! Let us seal the deal!"
                        ]
                },
                {
                        lines   : "Hamid begins to unbuckle his belt"
                },
                {       speaker : "Hamid",
                        thumb   : "hamid_thumb.png",
                        lines   : [
                                "Now I want you and your new slave naked and on my bed, right now!",
                                "I intend to satisfy both slave and mistress many times before the night is done"
                        ]
                },
                {       lines   : "And so began your friendship with Hamid..."
                },
                encounter_end
        ]

Which is probably as sane a way to do it as we'll find using vanilla python. That said, I still like the idea of Ren'Py's parser better.

i was going to work out the back end for all that, but there's not much point at the moment, so I'll go back to making widgets for a bit :)

« Last Edit: November 20, 2013, 02:17:00 PM by DocClox »

Offline rudistoned

  • Full Member
  • ***
  • Posts: 229
Re: Extracting Ren'Pys scripting language
« Reply #7 on: November 23, 2013, 08:37:21 AM »

I took a two day break from renpy, now I'm back at work again.


Turned out (of course...) that I could get the earlier code to run so easily because it basically skipped from start to finish without doing most of the work. Nevertheless, I filled in the pieces in between and I can now execute successfully labels and python code blocks. Scene and With statements are handled by printing a message for now. Next up: getting dialog lines to work.


What I can tell already is that this will be a "fork", not a "mod". Maintaining the delta and copying patches from newer versions of renpy will be difficult and might be too much trouble for their worth. I don't think that's a big problem though. Most of the code consists of features we don't need (rollback) or performance optimizations I deactivated to simplify things.


@DocCloxLooks interesting, thanks for posting it :)

Offline DocClox

  • Dev Team
  • *****
  • Posts: 1867
  • Messing Around With Python
Re: Extracting Ren'Pys scripting language
« Reply #8 on: November 23, 2013, 09:20:13 AM »
Sounds very promising.

I agree, we don't need renpy as a whole, just the scene player and DSL in module form.  Hell, I'd settle for just the parser :)

Offline rudistoned

  • Full Member
  • ***
  • Posts: 229
Re: Extracting Ren'Pys scripting language
« Reply #9 on: November 23, 2013, 10:07:39 AM »
My goal is to be able to parse a renpy script and execute it. If during execution something happens that is outside the scope of this scripting language module, it should emit some kind of signal, e.g. "show-image" and give a path. In the first iteration it will just print "show image", imagepath.


The dialogue handling code is integrated into character management, which uses all kinds of systems. When this thing is done, it will be one big, hairy mess of code that has no real meaning any more, interspersed with rare bits and pieces that actually get the job done. In any case, it will be A LOT smaller then renpy and it should be relatively easy to reduce it further from there.




UPDATE:


This is how far I've got. First, the renpy script:


Code: [Select]
# Declare images used by this game.
image bg lecturehall = "lecturehall.jpg"
image bg uni = "uni.jpg"
image bg meadow = "meadow.jpg"
image bg club = "club.jpg"

image sylvie normal = "sylvie_normal.png"
image sylvie giggle = "sylvie_giggle.png"
image sylvie smile = "sylvie_smile.png"
image sylvie surprised = "sylvie_surprised.png"

image sylvie2 normal = "sylvie2_normal.png"
image sylvie2 giggle = "sylvie2_giggle.png"
image sylvie2 smile = "sylvie2_smile.png"
image sylvie2 surprised = "sylvie2_surprised.png"

# Declare characters used by this game.
define s = Character('Sylvie', color="#c8ffc8")
define m = Character('Me', color="#c8c8ff")

# The game starts here.
label start:
    $ bl_game = False

    #play music "illurock.ogg"

    python:
        # test some python code
        r = range(10)
        print "Counting to 10"
        for i in r: print i

    scene bg lecturehall
    with fade

    "Well, professor Eileen's lecture was interesting."
    "But to be honest, I couldn't concentrate on it very much."
    "I had a lot of other thoughts on my mind."
    "And they all ended up with a question."
    "A question, I've been meaning to ask someone."

    scene bg uni
    with fade

    "When we came out of the university, I saw her."

    show sylvie normal
    with dissolve

    "She was a wonderful person."
    "I've known her ever since we were children."
    "And she's always been a good friend."
    "But..."
    "Recently..."
    "I think..."
    "... that I wanted more."
    "More just talking... more than just walking home together when our classes ended."
    "And I decided..."

    menu:

        "... to ask her right away.":

            jump rightaway

        "... to ask her later.":

            jump later


label rightaway:

    show sylvie smile

    s "Oh, hi, do we walk home together?"


And here is the output I get from the thing I have assembled here from parts of renpy:

Code: [Select]
Counting to 10
0
1
2
3
4
5
6
7
8
9
TODO [scene] display (u'bg', u'lecturehall') at layer master
TODO [with] fade transition
TODO [say] None Well, professor Eileen's lecture was interesting. {'interact': True, 'cb_args': {}}
TODO [say] None But to be honest, I couldn't concentrate on it very much. {'interact': True, 'cb_args': {}}
TODO [say] None I had a lot of other thoughts on my mind. {'interact': True, 'cb_args': {}}
TODO [say] None And they all ended up with a question. {'interact': True, 'cb_args': {}}
TODO [say] None A question, I've been meaning to ask someone. {'interact': True, 'cb_args': {}}
TODO [scene] display (u'bg', u'uni') at layer master
TODO [with] fade transition
TODO [say] None When we came out of the university, I saw her. {'interact': True, 'cb_args': {}}
TODO [show] (u'sylvie', u'normal')
TODO [with] dissolve transition
TODO [say] None She was a wonderful person. {'interact': True, 'cb_args': {}}
TODO [say] None I've known her ever since we were children. {'interact': True, 'cb_args': {}}
TODO [say] None And she's always been a good friend. {'interact': True, 'cb_args': {}}
TODO [say] None But... {'interact': True, 'cb_args': {}}
TODO [say] None Recently... {'interact': True, 'cb_args': {}}
TODO [say] None I think... {'interact': True, 'cb_args': {}}
TODO [say] None ... that I wanted more. {'interact': True, 'cb_args': {}}
TODO [say] None More just talking... more than just walking home together when our classes ended. {'interact': True, 'cb_args': {}}
TODO [say] None And I decided... {'interact': True, 'cb_args': {}}
TODO [menu] display choices [(u'... to ask her right away.', 'True', 0), (u'... to ask her later.', 'True', 1)] None
TODO [show] (u'sylvie', u'smile')

It then throws an exception complaining that it does not know who "s" is. Might be because I start execution at the start label, so the character and image definitions may have never run. Will have to look how renpy does this.






UPDATE2:

Character and image definitions work now. Menus/choices are currently ignored and the execution code always selects the first choice to continue. I'm hoping that's because I disabled the code in the menu statement and not because the block linking code does something it should not.




UPDATE3:

Menu statements are working now. The application using this scripting language receives a signal containing a list of choices and a callback method. It then selects a choice and hands it over to the callback, thus telling the script where to continue. Signals are transmitted via a pure-python signalling package.

« Last Edit: November 24, 2013, 08:38:47 AM by rudistoned »

Offline DocClox

  • Dev Team
  • *****
  • Posts: 1867
  • Messing Around With Python
Re: Extracting Ren'Pys scripting language
« Reply #10 on: November 25, 2013, 07:38:51 AM »
Cool! There can't be much left to do from the sound of it.

I'm  starting to look forward to having a play with this. I've got the into that I wrote for the abortive ren'py version of AC. it might be fun r to see if I can get that working :D

Offline rudistoned

  • Full Member
  • ***
  • Posts: 229
Re: Extracting Ren'Pys scripting language
« Reply #11 on: November 25, 2013, 07:52:45 AM »
Well, to get it working in the most basic sense, there is not much left to do. Basically, I need to add a few lines that emit a signal to the implementation of every statement that does something outside the scope of the scripting language itself (show an image, register a character, ...). That should be doable really fast.
Then, the scripting language should be refactored into an importable python package, so you don't have to have its files inside your main program directory. That was the last thing I was working on and even though it should have been easy, the whole thing stopped working after I made those changes.
It's not the first time Python's import system is messing with me. I suspect that the changed import statements now create new instances of the modules they import, instead of always importing the same instance (like the difference between  "from foo import FooClass" and "import foo.FooClass as FooClass"). I don't have time to look into that today though. Maybe tomorrow.
I could always upload it somewhere if you want to take a look on it yourself.

Offline DocClox

  • Dev Team
  • *****
  • Posts: 1867
  • Messing Around With Python
Re: Extracting Ren'Pys scripting language
« Reply #12 on: November 25, 2013, 08:01:34 AM »
That sounds good. It'll need to wait until I get home, but I should get some time tonight, all things being equal :)

Offline rudistoned

  • Full Member
  • ***
  • Posts: 229
Re: Extracting Ren'Pys scripting language
« Reply #13 on: November 25, 2013, 04:54:54 PM »
For simplicities sake, I uploaded the code into a new mercurial repository in my Pytherworld project. Here's the link to the Ren'Py Scripting Language. The example directory contains the first draft for a simple example application that demonstrates how to incorporate the RSL package into an application.

Offline DocClox

  • Dev Team
  • *****
  • Posts: 1867
  • Messing Around With Python
Re: Extracting Ren'Pys scripting language
« Reply #14 on: November 25, 2013, 06:11:41 PM »
Got it, thanks.

Had a quick stab at running the example - it seems to be missing a module called RSL - Ren'Py Script Language I assume rather than Remote Service Library. I can't find a file or folder with RSL in the name anywhere in the repo so I'm guessing that either the file(s) didn't get added in, or possibly the top level files need to be in a folder called RSL ... although __init__.py seems to suggest otherwise.

Anyway, it's getting towards my bedtime, further investigation will need to wait.  I've only had time for a quick glance at the code, so I'm probably missing all sorts of obvious here.

Anyway, more once I've had some sleep :)

[edit]

Actually, imports like RSL.Config and RSL.signals would probably work if config.py and signals were in a folder called RSL. Or maybe using relative imports, if I could just think a bit straighter. So that's probably part of what sprang loose when you repackaged it as standalone.

Not sure about RSL.base tho.

Anyway, sleep...
« Last Edit: November 25, 2013, 06:16:53 PM by DocClox »