Difference between revisions of "Modding"

From Summertime Saga Wiki
Jump to: navigation, search
m
m
 
(5 intermediate revisions by 2 users not shown)
Line 1: Line 1:
The modding of the game is based on the Summertime Saga API. It is recommended to read the [[Summertime Saga API|corresponding article]] beforehand in order to familiarize yourself with the specificities of the code.
+
{{message box warning|This API is depreciated: the features are incomplete and will not be updated for future versions. '''USAGE IS HIGHLY DISCOURAGED.'''}}
 +
 
 +
The {{strong|modding of the game}} is based on the Summertime Saga API. It is recommended to read the [[Summertime Saga API|corresponding article]] beforehand in order to familiarize yourself with the specificities of the code.
 
{{TOC limit|3}}
 
{{TOC limit|3}}
  
 
== Hooking into the game ==
 
== Hooking into the game ==
 
{{message box important|This feature is incomplete as of version 0.18.6, the ModManager class is present, but no hooking of any kind can be done at the moment.}}
 
 
 
=== Registration and enabling of the mod ===
 
=== Registration and enabling of the mod ===
  
Line 17: Line 16:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
A manifest file named <code>modname_manifest.json</code> must be added in the <code>scripts/data</code> folder.
+
A manifest file named {{code|modname_manifest.json}} must be added in the {{code|scripts/data}} folder.
  
 
=== Manifest file ===
 
=== Manifest file ===
  
The {{strong|manifest file}} details in which labels the mod should hook into the game, and which screens. It also defines the name of the main function you wish to use to hook into the <code>game.main()</code> function, if any.
+
The {{strong|manifest file}} details in which labels the mod should hook into the game, and which screens. It also defines the name of the main function you wish to use to hook into the {{code|game.main()}} function, if any.
  
 
==== Preferred mod load order ====
 
==== Preferred mod load order ====
  
Adding a key named <code>load_order</code> to the manifest will allow your mod to be loaded after a specific amount of mods, or before.<br>
+
Adding a key named {{code|load_order}} to the manifest will allow your mod to be loaded after a specific amount of mods, or before.
The value of this key must be a string leading with either <code>></code>, <code><</code> or <code>=</code> followed by an integer. Additionally, the values <code>inf</code> and <code>-inf</code> can be used to set the load order as last, and absolutely first. If several mods are defined with <code>-inf</code> in <code>load_order</code>, then a random order is chosen. The default value is <code>inf</code>, which means the mods are added last to the list in no particular order.
+
 
 +
The value of this key must be a string leading with either {{code|>}}, {{code|<}} or {{code|{{=}}}} followed by an integer. Additionally, the values {{code|inf}} and {{code|-inf}} can be used to set the load order as last, and absolutely first. If several mods are defined with {{code|-inf}} in {{code|load_order}}, then a random order is chosen. The default value is {{code|inf}}, which means the mods are added last to the list in no particular order.
  
 
==== Main function name ====
 
==== Main function name ====
  
This key should be a string containing the name of the main function of your mod. The function is searched for ini the <code>globals()</code> of the game (global namespace)
+
This key should be a string containing the name of the main function of your mod. The function is searched for ini the {{code|globals()}} of the game (global namespace)
  
 
==== Init label name ====
 
==== Init label name ====
Line 39: Line 39:
 
==== Adding to the game’s json files ====
 
==== Adding to the game’s json files ====
  
Keys in the manifest named <code>items</code>, <code>achievements</code> and <code>text_messages</code> can be used to add data to the game json files. Each of these keys expects a full json dictionary formatted in the same way as their respective models.
+
Keys in the manifest named {{code|items}}, {{code|achievements}} and {{code|text_messages}} can be used to add data to the game json files. Each of these keys expects a full json dictionary formatted in the same way as their respective models.
  
 
<u>'''Example of manifest:'''</u>
 
<u>'''Example of manifest:'''</u>
Line 83: Line 83:
 
{{message box information|Mods are assumed to be in an initial random order.}}
 
{{message box information|Mods are assumed to be in an initial random order.}}
  
For every mod that is enabled, create a Mod instance with that mod’s name, parse the load order from the manifest, then insert that mod in the <code>ModManager.mods</code> list in the proper position. Then, for every mod in the <code>ModManager.mods</code> list, call their init label, if defined. Finally, update the game stores (i.e. achievements, items and text_messages) with every data of the mod. Updates overwrite the keys, so the load order can be used to overwrite the game’s (although ill‐advised) or another mod’s items/text_messages/achievements.
+
For every mod that is enabled, create a Mod instance with that mod’s name, parse the load order from the manifest, then insert that mod in the {{code|ModManager.mods}} list in the proper position. Then, for every mod in the {{code|ModManager.mods}} list, call their init label, if defined. Finally, update the game stores (i.e. achievements, items and text_messages) with every data of the mod. Updates overwrite the keys, so the load order can be used to overwrite the game’s (although ill‐advised) or another mod’s items/text_messages/achievements.
  
 
=== Text filter ===
 
=== Text filter ===
  
 
The {{strong|text filter}} key in the manifest allows you to create your own filter function without overwriting other mods.
 
The {{strong|text filter}} key in the manifest allows you to create your own filter function without overwriting other mods.
If undefined, this defaults to {{nowrap|<code>lambda text:text</code>}}, otherwise the value of this key should be a string of the name of the function you wish to pass in as a text filter. If the function cannot be found in the global namespace (i.e. the <code>globals()</code> dictionary), then a <code>ModLoaderError("No Function named {text_filter} found in the global namespace. Is it defined properly?")</code> exception will be raised.
+
If undefined, this defaults to {{nowrap|{{code|lambda text:text}}}}, otherwise the value of this key should be a string of the name of the function you wish to pass in as a text filter. If the function cannot be found in the global namespace (i.e. the {{code|globals()}} dictionary), then a {{code|ModLoaderError("No Function named {text_filter} found in the global namespace. Is it defined properly?")}} exception will be raised.
  
 
=== Screens ===
 
=== Screens ===
  
Create your {{strong|screens}} with the following convention: <code>modname</code> + <code>_</code> + <code>in‐game screen name</code> (the Ren’Py definition name). Screens are being included into the main game screens with the use statement. If you wish to add new locations, you have to define a screen for it, in which case, you can inspire yourself with the existing screens in the game.<br>
+
Create your {{strong|screens}} with the following convention: {{code|modname}} + {{code|_}} + {{code|in‐game screen name}} (the Ren’Py definition name). Screens are being included into the main game screens with the use statement. If you wish to add new locations, you have to define a screen for it, in which case, you can inspire yourself with the existing screens in the game.<br>
 
To ensure proper game progression, you shoul only add imagebuttons. Please refer to {{section link|Summertime Saga API#User-defined screen actions}} for more information on which screen actions the game defines.
 
To ensure proper game progression, you shoul only add imagebuttons. Please refer to {{section link|Summertime Saga API#User-defined screen actions}} for more information on which screen actions the game defines.
  
The mod screen hook is quite simple as it uses RenPy’s <code>use</code> statement for every modded screen. The screen hook is called at the end of each screen, so background set up and imagebuttons for the game are already properly shown and defined. You can then add your own imagebuttons, and other screen features to it.
+
The mod screen hook is quite simple as it uses RenPy’s {{code|use}} statement for every modded screen. The screen hook is called at the end of each screen, so background set up and imagebuttons for the game are already properly shown and defined. You can then add your own imagebuttons, and other screen features to it.
  
Note : Hookable screens are, at least for now, only the location screens, and do not include the UI.
+
{{note|Hookable screens are, at least for now, only the location screens, and do not include the user interface.}}
 
 
=== Labels ===
 
 
 
{{Message box warning|This code is not yet implemented.}}
 
 
 
Only "main" {{strong|labels}} can be hooked into. Those are labels that end in {{nowrap|<code>$ game.main()</code>}}.
 
 
 
=== Main function ===
 
 
 
{{message box warning|This code is not yet implemented.}}
 
 
 
To hook into the {{strong|main function}}, you must register your main function to the ModManager.
 
  
 
<u>'''Example:'''</u>
 
<u>'''Example:'''</u>
Line 118: Line 106:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
In this example, <code>ikarumod_main</code> is assumed to be a callable, which should be a python function. The function is called with no arguments at the end of <code>game.main()</code> method.
+
In this example, {{code|ikarumod_main}} is assumed to be a callable, which should be a python function. The function is called with no arguments at the end of {{code|game.main()}} method.
  
 
Hooking into the main function is usually useful for code you want executed every time the game returns to a main screen, i.e. at the end of location labels for instance. This is where you can repeatedly check if the condition for an achievement has been fulfilled. If the provided function is not a callable, a ModLoaderError exception will be raised.
 
Hooking into the main function is usually useful for code you want executed every time the game returns to a main screen, i.e. at the end of location labels for instance. This is where you can repeatedly check if the condition for an achievement has been fulfilled. If the provided function is not a callable, a ModLoaderError exception will be raised.
Line 159: Line 147:
 
=== Directory structure ===
 
=== Directory structure ===
  
* <code>game</code>
+
* {{code|game}}
** <code>audio</code>: SFX and musics
+
** {{code|audio}}: SFX and musics
** <code>fonts</code>: fonts for the game.
+
** {{code|fonts}}: fonts for the game.
** <code>images</code>
+
** {{code|images}}
*** <code>achievements</code>: achievement‐related images.
+
*** {{code|achievements}}: achievement‐related images.
*** <code>cookie_jar</code>: cookie jar buttons, popups and thumbnails.
+
*** {{code|cookie_jar}}: cookie jar buttons, popups and thumbnails.
*** <code>backgrounds</code>: backgrounds and close‐ups.
+
*** {{code|backgrounds}}: backgrounds and close‐ups.
*** <code>boxes</code>: popups and general purpose buttons (like the go back button).
+
*** {{code|boxes}}: popups and general purpose buttons (like the go back button).
*** <code>buttons</code>: most minigame assets, and menu buttons.
+
*** {{code|buttons}}: most minigame assets, and menu buttons.
*** <code>cellphone</code>: cellphone images assets.
+
*** {{code|cellphone}}: cellphone images assets.
*** <code>characters</code>: character poses. One folder per character.
+
*** {{code|characters}}: character poses. One folder per character.
*** <code>map</code>: map locations and map background.
+
*** {{code|map}}: map locations and map background.
*** <code>objects</code>: character buttons, item buttons, doors, etc.
+
*** {{code|objects}}: character buttons, item buttons, doors, etc.
*** <code>vfx</code>: special visual effects (like the rain in Roxxy’s trailing scene).
+
*** {{code|vfx}}: special visual effects (like the rain in Roxxy’s trailing scene).
** <code>python-packages</code>: third party python modules.
+
** {{code|python-packages}}: third party python modules.
** <code>scripts</code>
+
** {{code|scripts}}
*** <code>characters</code>: one folder for each character, containing an fsm file, a character.rpy file for miscellaneous stuff, and a layeredimage definition file. May contain a file for the character’s button and the according dialogues.
+
*** {{code|characters}}: one folder for each character, containing an fsm file, a character.rpy file for miscellaneous stuff, and a layeredimage definition file. May contain a file for the character’s button and the according dialogues.
*** <code>core</code>: core files are put there, mostly what has been documented in the Modding API section of this manifesto.
+
*** {{code|core}}: core files are put there, mostly what has been documented in the Modding API section of this manifesto.
*** <code>data</code>: JSON files that contain data about the game. Items, achievements, keymap or text messages are defined here.
+
*** {{code|data}}: JSON files that contain data about the game. Items, achievements, keymap or text messages are defined here.
*** <code>defines</code>: general image definitions, transforms, etc.
+
*** {{code|defines}}: general image definitions, transforms, etc.
*** <code>locations</code>: one folder for every location, sorted in a tree‐like structure. Each location has a main file, a screen file and a dialogues file.
+
*** {{code|locations}}: one folder for every location, sorted in a tree‐like structure. Each location has a main file, a screen file and a dialogues file.
*** <code>minigames</code>: one folder for every minigame, the minigame dialogues and screen files are in that folder.
+
*** {{code|minigames}}: one folder for every minigame, the minigame dialogues and screen files are in that folder.
*** <code>script.rpy</code>
+
*** {{code|script.rpy}}
*** <code>pregnancy_announcements.rpy</code>
+
*** {{code|pregnancy_announcements.rpy}}
** <code>changelog.txt</code>
+
** {{code|changelog.txt}}
** <code>pledge_list.txt</code>
+
** {{code|pledge_list.txt}}
  
 
== Translating the game ==
 
== Translating the game ==
Line 199: Line 187:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Every time a dialogue is called, the game looks for the label name and the language string at the end. For instance, the label <code>bank_liu_account_info</code> has the English version in; and to overwrite that dialogue, you’d have <code>bank_liu_account_info_fr</code> or <code>bank_liu_account_info_es</code> depending on the value of the language string.
+
Every time a dialogue is called, the game looks for the label name and the language string at the end. For instance, the label {{code|bank_liu_account_info}} has the English version in; and to overwrite that dialogue, you’d have {{code|bank_liu_account_info_fr}} or {{code|bank_liu_account_info_es}} depending on the value of the language string.
  
Any dialogue can be translated with this method. Keeping a consistent directory structure is recommended, because of the large number of dialogues in the game. You also have to copy over the posing. For your convenience, every dialogue label is stored in a <code>dialogues.rpy</code> file for that location. Just copy the file and edit the dialogue and the label name.
+
Any dialogue can be translated with this method. Keeping a consistent directory structure is recommended, because of the large number of dialogues in the game. You also have to copy over the posing. For your convenience, every dialogue label is stored in a {{code|dialogues.rpy}} file for that location. Just copy the file and edit the dialogue and the label name.
  
 
=== Cutscenes and minigame instructions ===
 
=== Cutscenes and minigame instructions ===
  
You can’t edit the '''cutscenes''' directly. Well, you could, but you shouldn’t! Using the <code>config.say_menu_text_filter</code> variable is the best way. Just register a function to that variable with one argument, <code>text</code>, which contains the text of the displayable. Then edit the text how you see fit.
+
You can’t edit the '''cutscenes''' directly. Well, you could, but you shouldn’t! Using the {{code|config.say_menu_text_filter}} variable is the best way. Just register a function to that variable with one argument, {{code|text}}, which contains the text of the displayable. Then edit the text how you see fit.
  
 
<u>'''Example of translation:'''</u>
 
<u>'''Example of translation:'''</u>
Line 222: Line 210:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Keep in mind however that Ren’Py engine doesn’t handle large dictionaries. <code>elif</code> statements can be used to split the content into several dictionaries if necessary.
+
Keep in mind however that Ren’Py engine doesn’t handle large dictionaries. {{code|elif}} statements can be used to split the content into several dictionaries if necessary.
  
 
<u>'''Example:'''</u>
 
<u>'''Example:'''</u>
Line 235: Line 223:
 
         return text
 
         return text
 
</syntaxhighlight>
 
</syntaxhighlight>
 
In the future, registering to <code>config.say_menu_text_filter</code> variable will be done with the <code>+=</code> assignment, to allow several mods to subscribe to it. Mod load order will apply.
 
  
 
=== Location names ===
 
=== Location names ===
  
In an <code>init 1 python</code> (or later) block, you can set the display name of any location object in the game. This allows renaming and translation of any game location, thus displaying that name in relevant parts of the user interface.
+
In an {{code|init 1 python}} (or later) block, you can set the display name of any location object in the game. This allows renaming and translation of any game location, thus displaying that name in relevant parts of the user interface.
  
 
<u>'''Example:'''</u>
 
<u>'''Example:'''</u>

Latest revision as of 18:04, 11 July 2020

Warning red icon.pngThis API is depreciated: the features are incomplete and will not be updated for future versions. USAGE IS HIGHLY DISCOURAGED.

The modding of the game is based on the Summertime Saga API. It is recommended to read the corresponding article beforehand in order to familiarize yourself with the specificities of the code.

Hooking into the game

Registration and enabling of the mod

In an init -9 python or later, use the class method "register" of the ModManager class to register your mod to the game. This registration makes the mod show up in the (upcoming) Mods menu on the main menu so the players have the choice to enable or disable the mod for the game.

Example:

init python:
    ModManager.register("ikarumod")

A manifest file named modname_manifest.json must be added in the scripts/data folder.

Manifest file

The manifest file details in which labels the mod should hook into the game, and which screens. It also defines the name of the main function you wish to use to hook into the game.main() function, if any.

Preferred mod load order

Adding a key named load_order to the manifest will allow your mod to be loaded after a specific amount of mods, or before.

The value of this key must be a string leading with either >, < or = followed by an integer. Additionally, the values inf and -inf can be used to set the load order as last, and absolutely first. If several mods are defined with -inf in load_order, then a random order is chosen. The default value is inf, which means the mods are added last to the list in no particular order.

Main function name

This key should be a string containing the name of the main function of your mod. The function is searched for ini the globals() of the game (global namespace)

Init label name

This key should give the name of a label your mod uses at init time, which means after the game is fully initialized, either at the start of the game or after the load of the game.
This label is called in a new context, and it must return, otherwise other mods won’t be loaded

Adding to the game’s json files

Keys in the manifest named items, achievements and text_messages can be used to add data to the game json files. Each of these keys expects a full json dictionary formatted in the same way as their respective models.

Example of manifest:

{
    "name":"ikarumod",
    "version": "0.1.0",
    "load_order": ">3",
    "main_function_name": "ikarumod_main",
    "init_label_name":"ikarumod_init_label",
    "text_filter":"ikarumod_text_filter",
    "items": {
        "item1":{
            ...
        },
        "item2":{
            ...
        }
    },
    "text_messages":{
        "text_message1":{
            ...
        }
    },
    "screen_hooks":{
        "upstairs_bedroom":"upstairs_bedroom_ikarumod",
        "bedroom":"bedroom_ikarumod"
    },
    "label_hooks":{
        "bedroom":"bedroom_ikarumod"
    },
    "achievements":{
        "angler-2"{
            ...
        }
    }
}

Mod init order

Information icon.pngMods are assumed to be in an initial random order.

For every mod that is enabled, create a Mod instance with that mod’s name, parse the load order from the manifest, then insert that mod in the ModManager.mods list in the proper position. Then, for every mod in the ModManager.mods list, call their init label, if defined. Finally, update the game stores (i.e. achievements, items and text_messages) with every data of the mod. Updates overwrite the keys, so the load order can be used to overwrite the game’s (although ill‐advised) or another mod’s items/text_messages/achievements.

Text filter

The text filter key in the manifest allows you to create your own filter function without overwriting other mods. If undefined, this defaults to lambda text:text, otherwise the value of this key should be a string of the name of the function you wish to pass in as a text filter. If the function cannot be found in the global namespace (i.e. the globals() dictionary), then a ModLoaderError("No Function named {text_filter} found in the global namespace. Is it defined properly?") exception will be raised.

Screens

Create your screens with the following convention: modname + _ + in‐game screen name (the Ren’Py definition name). Screens are being included into the main game screens with the use statement. If you wish to add new locations, you have to define a screen for it, in which case, you can inspire yourself with the existing screens in the game.
To ensure proper game progression, you shoul only add imagebuttons. Please refer to Summertime Saga API § User-defined screen actions for more information on which screen actions the game defines.

The mod screen hook is quite simple as it uses RenPy’s use statement for every modded screen. The screen hook is called at the end of each screen, so background set up and imagebuttons for the game are already properly shown and defined. You can then add your own imagebuttons, and other screen features to it.

Hookable screens are, at least for now, only the location screens, and do not include the user interface.

Example:

init python:
    ModManager.register_main(ikarumod_main)

In this example, ikarumod_main is assumed to be a callable, which should be a python function. The function is called with no arguments at the end of game.main() method.

Hooking into the main function is usually useful for code you want executed every time the game returns to a main screen, i.e. at the end of location labels for instance. This is where you can repeatedly check if the condition for an achievement has been fulfilled. If the provided function is not a callable, a ModLoaderError exception will be raised.

Imported modules and directory structure

Third‐party modules

Platform agnostic

  • os
  • pygame
  • sys
  • from time : time, clock
  • from copy : copy, deepcopy
  • datetime
  • re
  • random
  • math
  • from collections : defaultdict, OrderedDict and Counter
  • weakref
  • codecs
  • hashlib
  • json
  • itertools
  • operator
  • textwrap
  • deuces

Desktop builds

  • certifi
  • requests

Mobile builds

  • android
  • pyjnius

Directory structure

  • game
    • audio: SFX and musics
    • fonts: fonts for the game.
    • images
      • achievements: achievement‐related images.
      • cookie_jar: cookie jar buttons, popups and thumbnails.
      • backgrounds: backgrounds and close‐ups.
      • boxes: popups and general purpose buttons (like the go back button).
      • buttons: most minigame assets, and menu buttons.
      • cellphone: cellphone images assets.
      • characters: character poses. One folder per character.
      • map: map locations and map background.
      • objects: character buttons, item buttons, doors, etc.
      • vfx: special visual effects (like the rain in Roxxy’s trailing scene).
    • python-packages: third party python modules.
    • scripts
      • characters: one folder for each character, containing an fsm file, a character.rpy file for miscellaneous stuff, and a layeredimage definition file. May contain a file for the character’s button and the according dialogues.
      • core: core files are put there, mostly what has been documented in the Modding API section of this manifesto.
      • data: JSON files that contain data about the game. Items, achievements, keymap or text messages are defined here.
      • defines: general image definitions, transforms, etc.
      • locations: one folder for every location, sorted in a tree‐like structure. Each location has a main file, a screen file and a dialogues file.
      • minigames: one folder for every minigame, the minigame dialogues and screen files are in that folder.
      • script.rpy
      • pregnancy_announcements.rpy
    • changelog.txt
    • pledge_list.txt

Translating the game

Label calling in the game

The game offers a function in the Game class to select different dialogues based on the language class attribute defined there. To change that attribute, it’s just a matter of writing this piece of code in a separate ".rpy" file.

Example:

init 1 python:
    Game.language = "es" # for Spanish for instance, "fr" for French, etc

Every time a dialogue is called, the game looks for the label name and the language string at the end. For instance, the label bank_liu_account_info has the English version in; and to overwrite that dialogue, you’d have bank_liu_account_info_fr or bank_liu_account_info_es depending on the value of the language string.

Any dialogue can be translated with this method. Keeping a consistent directory structure is recommended, because of the large number of dialogues in the game. You also have to copy over the posing. For your convenience, every dialogue label is stored in a dialogues.rpy file for that location. Just copy the file and edit the dialogue and the label name.

Cutscenes and minigame instructions

You can’t edit the cutscenes directly. Well, you could, but you shouldn’t! Using the config.say_menu_text_filter variable is the best way. Just register a function to that variable with one argument, text, which contains the text of the displayable. Then edit the text how you see fit.

Example of translation:

init 10 python:
    fr_translations = {"Using the key and stool, I was able to get into our attic.\nI had never been up there before.\nI was filled with excitement wondering what treasures {b}[deb_name]{/b} and dad had stashed away.": "En utilisant la clé et le tabouret, je pus aller dans le grenier.\nJe n'y avais jamais été auparavant.\nUn sentiment d'excitation m'envahissait alors que je me demandais quels trésors {b}[deb_name]{/b} et papa avaient caché là-haut."}

    def fr_text_filter(text):
        if text in fr_translations.keys():
            return fr_translations[text]
        else:
            return text
    
    config.say_menu_text_filter = fr_text_filter

Keep in mind however that Ren’Py engine doesn’t handle large dictionaries. elif statements can be used to split the content into several dictionaries if necessary.

Example:

def fr_text_filter(text):
    if text in fr_translations_1.keys():
        return fr_translations_1[text]
    elif text in fr_translations_2.keys():
        return fr_translations_2[text]
    else:
        return text

Location names

In an init 1 python (or later) block, you can set the display name of any location object in the game. This allows renaming and translation of any game location, thus displaying that name in relevant parts of the user interface.

Example:

init -1 python:
    L_map.display_name = "New Map Name"