• To improve security, we will soon start forcing password resets for any account that uses a weak password on the next login. If you have a weak password or a defunct email, please update it now to prevent future disruption.

Ren'Py How can I set multiple variables at once using a spreadsheet? [SOLVED]

JohnDupont

Active Member
Modder
May 26, 2017
802
2,698
I wanted to have up to 4 buttons, with multiple images inside, which would create and set multiple variables at once. So I tried adapting some ren'py/python stuff.
You don't have permission to view the spoiler content. Log in or register now.



Right now, all I have to do is create a spreadsheet per character per set of personnality/outfit.
You don't have permission to view the spoiler content. Log in or register now.



Then, call this label:
Code:
label variables+images_spreadsheetX:
    $ variables+images("spreadsheetX.tsv")
    call variables+images
    return


Which itself calls this monstrosity, which I'll divide in 3 parts:

1) The init:
Code:
init -1 python:
   
    def clothingchoice(filename):
       
        # Creates the variables and defines them as []      
        # Variable for all the data
        global ALLTHEDATA
        # Variable for line 0
        global LISTLINE0
       
        ALLTHEDATA = []
        LISTLINE0 = []
       
        # From the file defined by $ clothingchoice(filename)
        with renpy.file(filename) as INTERNALNAMEFORTHETSVFILE:
           
            # For each line:
            for line in INTERNALNAMEFORTHETSVFILE:
           
                line = line.decode("utf-8")
                data = line.rstrip().split("\t")
               
                # Ignore empty lines and lines starting with "#"
                if data == "" or data[0][0] == "#": continue
               
                # Add all the data to 'ALLTHEDATA'
                ALLTHEDATA.append([data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11]])    
               
                # Add the data from column0 to 'LISTLINE0'              
                LISTLINE0.append(data[0])  
               
                # Then, for each line in column 1 create the variables 1 to 5
                for i in range(0,len(data[1].split(","))):
                    try:
                        globals()["{}".format(data[7].split(",")[i].split()[0])]
                    except KeyError:
                        globals()["{}".format(data[7].split(",")[i].split()[0])] = []
                   
                    try:
                        globals()["{}".format(data[8].split(",")[i].split()[0])]
                    except KeyError:
                        globals()["{}".format(data[8].split(",")[i].split()[0])] = []
                       
                    try:
                        globals()["{}".format(data[9].split(",")[i].split()[0])]
                    except KeyError:
                        globals()["{}".format(data[9].split(",")[i].split()[0])] = []
                       
                    try:
                        globals()["{}".format(data[10].split(",")[i].split()[0])]
                    except KeyError:
                        globals()["{}".format(data[10].split(",")[i].split()[0])] = []
                       
                    try:
                        globals()["{}".format(data[11].split(",")[i].split()[0])]
                    except KeyError:
                        globals()["{}".format(data[11].split(",")[i].split()[0])] = []

init -1:
    default LINE0 = []
   
   
    # Transform for the images
    transform transform_images:
        size (320,536)

2) The label 'variables+images', which calls the screen 'SCREEN0':
Code:
label variables+images:       
    $ LINE0 = LISTLINE0   
    call screen SCREEN0

3) The screen SCREEN0, which :
- sets all the variables to False
- show all the images
- creates the buttons

When clicked the button sets some variables to True then returns to the first label:
Code:
screen SCREEN0:
    modal True
   
    for i in range(0,len(LINE0)):
        $ SetVariable("{}".format(ALLTHEDATA[i][7]) , "False")
        $ SetVariable("{}".format(ALLTHEDATA[i][8]) , "False")
        $ SetVariable("{}".format(ALLTHEDATA[i][9]) , "False")
        $ SetVariable("{}".format(ALLTHEDATA[i][10]) , "False")
        $ SetVariable("{}".format(ALLTHEDATA[i][11]) , "False")    
   
    # Shows the images 1 to 5  
    hbox:
        xfill
        ysize 536
        anchor (0.5,1.0) align (0.5,1.0)
        # For each data in 'LINE0'
        for i in range(0,len(LINE0)):
            if ALLTHEDATA[i][2] != 'jdmod':
                add "images/{}.png".format(ALLTHEDATA[i][2]) at transform_images
    hbox:
        xfill
        ysize 536
        anchor (0.5,1.0) align (0.5,1.0)
        # For each data in 'LINE0'
        for i in range(0,len(LINE0)):
            if ALLTHEDATA[i][3] != 'jdmod':
                add "images/{}.png".format(ALLTHEDATA[i][3]) at transform_images
    hbox:
        xfill
        ysize 536
        anchor (0.5,1.0) align (0.5,1.0)
        # For each data in 'LINE0'
        for i in range(0,len(LINE0)):
            if ALLTHEDATA[i][4] != 'jdmod':
                add "images/{}.png".format(ALLTHEDATA[i][4]) at transform_images
    hbox:
        xfill
        ysize 536
        anchor (0.5,1.0) align (0.5,1.0)
        # For each data in 'LINE0'
        for i in range(0,len(LINE0)):
            if ALLTHEDATA[i][5] != 'jdmod':
                add "images/{}.png".format(ALLTHEDATA[i][5]) at transform_images
    hbox:
        xfill
        ysize 536
        anchor (0.5,1.0) align (0.5,1.0)
        # For each data in 'LINE0'
        for i in range(0,len(LINE0)):
            if ALLTHEDATA[i][6] != 'jdmod':
                add "images/{}.png".format(ALLTHEDATA[i][6]) at transform_images
   
    # Set the variables
    hbox:
        xfill
        ysize 536      
        anchor (0.5,1.0) align (0.5,1.0)                  
        # For each data in 'LINE0'
        for i in range(0,len(LINE0)):

            # Creates a button
            button:
                xysize (320,536)
                #padding (0,0)
               
                # Add on image on top (foreground) when hovered
                hover_foreground Frame(highlight.png", left = 10, top = 10)
               
                # When CLICKED, SetVariables to True and Return
                clicked [SetVariable("{}".format(ALLTHEDATA[i][7]) , True) , SetVariable("{}".format(ALLTHEDATA[i][8]) , True) , SetVariable("{}".format(ALLTHEDATA[i][9]) , True) , SetVariable("{}".format(ALLTHEDATA[i][10]) , True) , SetVariable("{}".format(ALLTHEDATA[i][11]) , True) , Return]
   
    # Add text
    hbox:
        ysize 720
        anchor (0.5,1.0) align (0.5,1.0)      
        for i in range(0,len(LINE0)):
            frame:
                background Solid("#0000")
                xsize 320 xmargin 60 anchor (0.5,0.5) align (0.5,0.15)
                text "{}".format(ALLTHEDATA[i][1]) text_align 0.5 size 20 at truecenter


It works but my problem is:
Since I am currently using 1 column per variable, I can't use more than 5 variables without expanding initial the code and I also have to fill every empty column with an ugly 'jdmod'. I'd like to use only 1 column for all the variables.

Same thing for the images.
I have tried multiple things but way past my knowledge of ren'py and python. It's a miracle this thing works in the first place.

Also I'd like to create only one spreadsheet per character.
I have some ideas but if you can help for this one too, it would be great.

Thank you. You deserve it for getting this far.
 

Rich

Old Fart
Modder
Respected User
Donor
Game Developer
Jun 25, 2017
2,472
6,941
First, one way you could get rid of the 'jdmod' bit is to take advantage of the fact that Python considers an empty string to be "falsey" and a non-empty string to be "truthy". Thus, instead of:
Code:
 if ALLTHEDATA[i][3] != 'jdmod':
      ...
you could just do
Code:
 if ALLTHEDATA[i][3]:
      ...
Thus, the indented block will only be executed if the variable is non-empty, since an empty string will be evaluated as if it was a boolean False. If you have problems with spaces getting in, you can use ".strip()" on the string to remove white space.

So if you want to reduce the number of columns, it sounds like you might need to have your sheet structured with three columns:
  • style index
  • variable name
  • variable value
Obviously, this will mean that there will be multiple rows for each style.
Code:
style index,variable name,variable value
0, style_name, good
0, image, evag
0, Variable1, evanormal
1, style_name, sexy
1, image, evab
1, Variable1, evasexy
...
(showing the rows comma-separated instead of tab-separated, but you get the drift)

So your top-level array would be indexed by the style index, and then you have "variable name/variable value" pairs in a dictionary at that index in the array. I'm banging the following out without checking it in Python for syntax, but something like:
Code:
ALLTHEDATA = []
with renpy.file(filename) as INTERNALNAMEFORTHETSVFILE:
      
    # For each line:
    for line in INTERNALNAMEFORTHETSVFILE:
      
        line = line.decode("utf-8")
        data = line.rstrip().split("\t")
          
        # Ignore empty lines and lines starting with "#"
        if data == "" or data[0][0] == "#": continue

        style_index = int(data[0])
        variable_name = data[1]
        variable_value = data[2]

        # make sure that ALL_THE_DATA has a dictionary at this index
        # by appending as many new dictionaries as required
        while len(ALL_THE_DATA) < style_index + 1
            ALL_THE_DATA.append({})

        # set the variable name in the style dictionary to the variable value
        ALLTHEDATA[style_index][variable_name] = variable_value
So, after this ALLTHEDATA[1] gives you a dictionary of the form
Code:
{
    'style_name': 'sexy',
    'image': 'evab',
    'Variable1': 'evasexy'
}
So, to find the image for style 1, you'd do ALL_THE_DATA[1]['image']. To find out whether or not a style has an image, you'd do:
Code:
if 'image' in ALL_THE_DATA[1]:
    ....
Of course, all that assumes I've understood what your goal is...
 
  • Like
Reactions: JohnDupont

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,193
14,923
Honestly I didn't really understood what you try to do. Really looks like you tried to make something simple, but make it really complex.

What I understood is that you have multiples buttons, all filled by a layered picture. So, my question is, why not using dynamics displayable ?
Something like this (made on the fly) :
Code:
default buttonLayers = []

# a fully transparent image
image empty = Solid( "#00000000" )

image button one:
    contains:
        DynamicImage( buttonLayers[0]["base"] )
    contains:
        DynamicImage( buttonLayers[0]["tatoo"] )
    contains:
        DynamicImage( buttonLayers[0]['top"] )
    contains:
        DynamicImage( buttonLayers[0]["bottom"] )
image button two:
    contains:
        DynamicImage( buttonLayers[1]["base"] )
    contains:
        DynamicImage( buttonLayers[1]["tatoo"] )
    contains:
        DynamicImage( buttonLayers[1]['top"] )
    contains:
        DynamicImage( buttonLayers[1]["bottom"] )

label updateButton:
    python:
        buttonLayers[0]["base"] = "whatever picture you want for this case"
        # No tatoo for this time
        buttonLayers[0]["tatoo"] = empty
        buttonLayers[0]["top"] = "whatever picture you want for this case"
        buttonLayers[0]["bottom"] = "whatever picture you want for this case"
        [...]

screen showButtons:
     vbox:
         text "text for this button this time"
         imagebutton:
             idle button one
             action [whatever you want to do]

     vbox:
         text "text for this button this time"
         imagebutton:
             idle button two
             action [whatever you want to do]

Since I am currently using 1 column per variable, I can't use more than 5 variables without expanding initial the code
I still don't understand. You used a dynamic value for the first index of the array, just use one for the second since it's the only thing which change in the code you provided :
Code:
    # Shows the images 1 to 5  
    for idx in range(2,7):	
        hbox:
	    xfill
	    ysize 536
	    anchor (0.5,1.0) align (0.5,1.0)
	    # For each data in 'LINE0'
	    for i in range(0,len(LINE0)):
	        if ALLTHEDATA[i][idx] != 'jdmod':
		    add "images/{}.png".format(ALLTHEDATA[i][idx]) at transform_images
All this said, you seemed to struggle while describing what you want to do, and that's not a good thing. If you don't know how to describe the code you want to write, you'll never achieve to write it. The first thing to do is to take the time to peacefully think about what you want to do, until in your mind it's no more "something like this", but a precise description. Then it will be more easy for you to find the way it should be done.


Code:
 if ALLTHEDATA[i][3] != 'jdmod':
      ...
you could just do
Code:
 if ALLTHEDATA[i][3]:
      ...
Thus, the indented block will only be executed if the variable is non-empty,
Er... Being empty and not being equal to "jdmod" are not the same thing.
 
  • Like
Reactions: JohnDupont

JohnDupont

Active Member
Modder
May 26, 2017
802
2,698
Thank you both.

All this said, you seemed to struggle while describing what you want to do, and that's not a good thing. If you don't know how to describe the code you want to write, you'll never achieve to write it. The first thing to do is to take the time to peacefully think about what you want to do, until in your mind it's no more "something like this", but a precise description. Then it will be more easy for you to find the way it should be done.
You are one hundred percent right. I'm sorry ; I made this thing with very little knowledge of what I was doing and most of the original post was me trying to explain what I did, rather than explain what I wanted to achieve.

What I'd like to do is:
From a spreadsheet like this:
You don't have permission to view the spoiler content. Log in or register now.

Write something like that:
Code:
label:
    $ spreadsheet_is("character.tsv")
    $ options_selected_are("option1", "option2", ... from 2 to 4)
    call monstrosity
    return
   
label monstrosity:
    call screen
   
screen:
    for each option selected:
        set all the variables in the options selected to False
 
    hbox:
        for each option selected:
            add all the images for this option          

    hbox:
        for each option selected:
            button:
                clicked [set all the variables in this option to True, Return]
           
    hbox:
        for each option selected:
            text "description" for this option
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,193
14,923
You are one hundred percent right. I'm sorry
No worry, I needed years to learn this ;)


From a spreadsheet like this:
So :
  • An undefined number of row ; which mean a list at first.
  • Three different named fields for each row ; which mean a dict.
  • Two of the fields have an undefined number of columns ; which mean yet a list for them.
All this give something like this :
Code:
AllOptions = []

# create option 1
AllOptions.append( {} )

# Now fill it with description
AllOptions[0]["Description"] = "some description"

# Then create the images list for it
AllOptions[0]["Images"] = []
# And fill it with the first image
AllOptions[0]["Images"].append( "A displayable or image" )
# then the second, and so on
AllOptions[0]["Images"].append( "Another displayable or image" )

# And finally do the same with the variables list
AllOptions[0]["Variables"] = []
AllOptions[0]["Variables"].append( "Some variable" )
AllOptions[0]["Variables"].append( "Some other variable" )
I aimed for an explanation more than a demonstration, so wrote it as simple as possible.


Now for the screen, it will be a little more complicated.
You have a fixed number of rows, but their size are variables. So you can't display them by row, because they'll not be aligned. What you need is to display each column one by one, then Ren'py should use the wider one to define the size of the ensemble.
It lead to something like this :
Code:
screen theScreen:

    # The rows must be side by side, so grouped in a hbox
    hbox:
        # Column "options"
        vbox:
            for i in range( 0, len( AllOptions ) ):
                text "Option number {}".format( i )

        # Column "Description"
        vbox:
            # There can be long text, limit the maximal size.
            xmaximum 400
            for i in range( 0, len( AllOptions ) ):
                text "{}".format( AllOptions[i]["Description"] )

        # Column "Images"
        #  Here it starts to be difficult, so it will be differed in an external
        # screen. In the same time the same screen global definition also
        # works for the last row, so it will simplify.
        vbox:
            use externalScreen( "Images" )

        # Row "Variables"
        vbox:
            use externalScreen( "Variables" )

screen externalScreen( target ):

    #  Format the column according to the number of entries in the biggest
    # 'target' list. And obviously, find this number.
    $ max = 0
    for i in range( 0, len( AllOptions ):
        if len( AllOptions[i][target] ) > max:
            $ max = len( AllOptions[i][target] )

    #  This column is an ensemble of row of columns. And like previously, we
    # need to display them by column to let Ren'py align everything.
    hbox:
        # Display the number of columns find above.
        for n in range(0, max ):
            vbox:
                for i in range( 0, len( AllOptions ) ):
                    # Some list will be shorter, fill them with an empty value.
                    if len( AllOptions[i][target] ) >= n: # Not sure if ">=" or just ">"
                        text ""
                    else:
                        text "{}".format( AllOptions[i][target][n] )
Keep in mind that I write it on the fly and that it's already 11:30PM here. So it's more the spirit of the screen than the screen itself.
Also, for the example I used text as display to simplify. Change it by what you need. If the type of content from "Images" and "Variables" is different, the two last lines of the screen must be replaced by these four ones:
Code:
                        elif target == "Images":
                            text "{}".format( AllOptions[i][target][n]
                        else:  # imply "Variables"
                            text "{}".format( AllOptions[i][target][n]
Once again, I used a text, change by what is needed.

Edit: Corrected a big error in the screen
 
  • Like
Reactions: JohnDupont

JohnDupont

Active Member
Modder
May 26, 2017
802
2,698
Thank you for the help.
Even though I tried some of your solutions, I didn't end up using any. But you made me realized that most of what I thought I knew about python was wrong and that motivated me to actually read some Python documentation.

I started from scratch ( ), rewrote everything with what I wanted in mind, spent hours to find a solution for the variables but I think I finished it.

Code:
label gallery_variableselection_character:
    $ spreadsheet("JDMOD/Database/TEST.tsv")
    $ options_selected = [0, 1]
    call JDMOD_TEST
    return
Code:
init python:
    def spreadsheet(filename):
        f = file(config.gamedir + "/" + filename)
        global rv
        rv = [ ]
    
        for l in f:
            if l == "" or l[0][0] == "#": continue
            l = l[:-1]
            rv.append(l.split('\t'))
        f.close()
    
    def splitted(line,column,number):   
        return rv[line][column].split(",")[number].split()
    
    def vartrue(l):
        for z in range(0,len(options_selected)):
            if choice == z:
                for true in range(0,len(rv[options_selected[z]][3].split(","))):
                    globals()["{}".format(rv[options_selected[z]][3].split(",")[true])] = True
            else:
                for true in range(0,len(rv[options_selected[z]][3].split(","))):
                    globals()["{}".format(rv[options_selected[z]][3].split(",")[true])] = False   
    
init -1:
    $ choice = None
    transform transform_clothingchoice_characters:
        size (320,536)

label JDMOD_TEST:
    call screen SCREEN

 
screen SCREEN:
    modal True
 
    # Text background
    hbox:
        ysize 720
        anchor (0.5,1.0) align (0.5,1.0)   
        for o in range(0,len(options_selected)):       
            frame:                   
                ysize 120 xsize 320 xmargin 20 anchor (0.5,0.5) align (0.5,0.15)
                background Frame("gui/button/choice_idle_background.png", left=0, top=0)
            
    # Description
    hbox:
        ysize 720
        anchor (0.5,1.0) align (0.5,1.0)   
        for o in range(0,len(options_selected)):
            frame:           
                xsize 320 xmargin 60 anchor (0.5,0.5) align (0.5,0.15)
                background Solid("#0000")
                text rv[options_selected[o]][1] text_align 0.5 size 20 at truecenter

    # Button
    hbox:
        xfill
        ysize 536   
        anchor (0.5,1.0) align (0.5,1.0) 
        for o in range(0,len(options_selected)):
            frame:
                background Solid("#5555")
                button:
                    xysize (320,536)
                    padding (0,0)
                    hovered SetVariable("choice", o)
                    # Images
                    for i in range(0,len(rv[int(options_selected[o])][2].split(","))):
                        add splitted(options_selected[o],2,i) at transform_clothingchoice_characters                   
                    hover_foreground Frame("JDMOD/thumbnails/highlight.png", left = 10, top = 10)
                    # Variables to True
                    clicked [ vartrue(o), Return ]
This was the spread I used for testing :
Capture.PNG

The images works fine (except I have to write "images/" and ".png" in the spreadsheet), it only creates variables I need and gives them the right value.

EDIT: It seems like I don't have a problem when I don't write "images/" and ".png" but I don't know why. Maybe because the images are defined with the same name in the script?
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,193
14,923
Thank you for the help.
Even though I tried some of your solutions, I didn't end up using any.
Not a problem. It's part of what I explained it more than giving a code actually really doing everything. When it's your own code, you understand it better and so use it more easily. You needed guidance, and apparently the one I gave was enough ; why asking for more than your actual, "yes, I made it !" ?


EDIT: It seems like I don't have a problem when I don't write "images/" and ".png" but I don't know why. Maybe because the images are defined with the same name in the script?
Because of the way Ren'py works.
Basically speaking, there's two kind of images, the "external" ones, that are a file somewhere inside the "images/" folder, and the "internal" ones, that are created with the image statement. Since some times Ren'py can works with both in the same way. Something like this :
If the name/tag correspond to a "internal" image, display it. Else, search if an "external" image correspond to this name/tag. If yes, display it, else well do nothing there's an error.

It's not as clever as it look, but it let you forget about the 'images/' and extension.
This said, I don't recommend to works this way. Here it's not a real problem since it's an automated process. But when you use it manually, try to put at least the extension. It change nothing to the performance and act as a reminder ; this is an "external" image.