Difference between revisions of "Modding"

From Summertime Saga Wiki
Jump to: navigation, search
m
 
(216 intermediate revisions by 3 users not shown)
Line 1: Line 1:
= Modding API =
+
{{message box warning|This API is depreciated: the features are incomplete and will not be updated for future versions. '''USAGE IS HIGHLY DISCOURAGED.'''}}
  
{{AlertWarn|The API for Summertime Saga is currently under development, and this page is subject to many changes}}
+
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}}
  
== FSMs (Finite State Machines) ==
+
== Hooking into the game ==
 +
=== Registration and enabling of the mod ===
  
FSMs control the flow of the game for a particular character. They are basically linked chains of events (which are called States). The progress is achieved via a Trigger.
+
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.
  
<div class="center" style="width: auto; margin-left: auto; margin-right: auto;">[[File:Fsm1.png]]</div>
+
<u>'''Example:'''</u>
<div class="center" style="width: auto; margin-left: auto; margin-right: auto;">''A basic example of a FSM''</div>
 
  
=== Creating a FSM ===
+
<syntaxhighlight lang="Python">
Creating a FSM is done in 3 steps :
+
init python:
 +
    ModManager.register("ikarumod")
 +
</syntaxhighlight>
  
* Create the states, triggers and a Machine instance, providing the necessary arguments to each constructor. Triggers should be created in an init python block of priority 0 or later.
+
A manifest file named {{code|modname_manifest.json}} must be added in the {{code|scripts/data}} folder.
  
* Link the states together, with their '''State.add(Trigger t, State next_state)''' method.
+
=== Manifest file ===
  
* Add all the states to the machine with the '''Machine.add(*State states)''' method.
+
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.
  
State and machine definition and state linking should be done in a label named "character_fsm_init", character being the character's name. Additionally, further edits to the machine (such as adding the states) should be done in a label named "character_machine_init", there again, character being the name of the character.
+
==== Preferred mod load order ====
  
==== Machine specific ====
+
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 Machine constructor has plenty of arguments, here is the detail and what is expected :
+
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.
  
:: name : a name for the machine. Is used as a unique identifier for the machine. Will also be used to find the button dialogue for that machine.
+
==== Main function name ====
:: default_loc : a 4x7 matrix of Locations to be used as the "schedule" for where a given machine should be at any point during the week. A 4x1 or 2x4 matrix may be passed as well, and internally will be converted to a 4x7 matrix. If a 1x4 matrix is passed, it is assumed that the location only varies by time of day, and the day of the week doesn't matter. A 2x4 matrix will split the two, the first 4-list is for weekdays, and the second for weekend days.
 
:: description, states : (to be deprecated) alternate ways to add states to the machine, and a description that is never used anywhere.
 
:: vars : A dictionnary containing variables (as strings, the keys of that dictionnary), and init values for those variables. These variables are used with the state actions (see below) and the '''get(string variable)''' and '''set(string variable, object value)''' methods of the Machine class. When a specific state doesn't apply, or you need a variable to change based on the progress with a given path. The "sex speed" variable is assumed to be used only for setting the speed of the animation for sex scenes.
 
  
==== State specific ====
+
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)
  
At state creation, you may want to add a delay to that state using the delay optional argument. That delay is the number of days until that state is effectively reached once the trigger has been "pulled"
+
==== Init label name ====
  
When linking states together, you may want to add '''state actions''' to be executed when the state is triggered. Those actions are as follows :
+
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.<br>
 +
This label is called in a new context, and it must return, otherwise other mods won’t be loaded
  
:: ['set','flag_1']  set's the value of flag_1 to True
+
==== Adding to the game’s json files ====
:: ['clear','flag_1'] set's the value of flag_1 to False
 
:: ['toggle','flag_1'] toggle's the value of flag_1 between True and False
 
:: ['assign',['v1',100]] sets the value of v1 to 100
 
:: ['inc','v1'] increase the value of v1 by 1
 
:: ['dec','v1'] decrease the value of v1 by 1
 
:: ['triggeronzero':['v1',T_a_trigger]] sets v1 -= 1 and if
 
:::: v1 <= 0 it will fire the Trigger T_a_trigger
 
:: ['trigger',T_a_trigger], fire the Trigger T_a_trigger
 
:: ['call','label'], make a RenPy call to label. Label MUST return
 
:: ['location', [machine, {"tod":tod, "place":place}]], set the forced location for the
 
:::: machine to place (Moves the NPC). tod is 1-indexed (1=morning, 4=night)
 
:: ['force', [machine, {"tod": list or int, "flag": 4-list or bool}]]
 
::::  Says if the location is forced at tod or sets force flags according to the 4-list provided
 
:: ['unforce', None/machine] unforce the locations for machine or the machine specified forced.
 
:: ['exec', callable], calls the callable (function or method)
 
:: ['exec', [callable, *args]], calls the callable and pass in the args specified
 
:: ['condition', [condition_string, actions_list_true, actions_list_false, (optional) machine]],
 
:::: executes the actions in actions_list_true
 
:::: if condition_string evaluates to True, otherwise executes actions_list_false.
 
:::: conditions lists are assumed to be state actions.
 
:: ['action', [target_machine, action, target]] Executes the state action on another machine.
 
:: ['setdefaultloc', <nowiki>[[Location, Location, Location, Location]]</nowiki>] Sets the default locations for the current machine. The argument is in the same format as the default_loc argument in the Machine constructor (1x4, 2x4 or 7x4 matrices)
 
  
=== Using FSMs ===
+
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.
  
Using FSMs is suprisingly easy. In your location labels, you can just test the state a Machine is in with the '''is_state(State* states)''' method. Multiple states may be passed in that method, as well as a list of states. If the Machine is in any of the provided states, then the method will return True, and False otherwise. For convenience, other similar methods can be used. '''between_states(State state1, State state2)''' will return True if the Machine is in any state between state1 and state2 but is not in state state2. '''finished_state(State* states)''' will return True if the Machine has finished any of the provided states.
+
<u>'''Example of manifest:'''</u>
  
To advance to the next state, the '''trigger(Trigger t)''' method of the Machine class is used. It will advance the Machine to the next state associated with that trigger if the provided trigger is in the current state's table, and won't do anything otherwise.
+
<syntaxhighlight lang="json">
+
{
== Locations ==
+
    "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"{
 +
            ...
 +
        }
 +
    }
 +
}
 +
</syntaxhighlight>
  
Locations handle all the locations in the game. They are represented as a tree, with the possibility of multiple parents (like Smith's Bedroom, which has the frontyard and the hallway as its parents). A required attribute is the location name.
+
=== Mod init order ===
  
=== Creating a Location ===
+
{{message box information|Mods are assumed to be in an initial random order.}}
  
Locations are very simple to create. It's as simple as instanciating the Location class, and providing a name for that location to the contructor (which is the only required argument). Locations are mutable, so they should be instantiated in a "modname_locations_init" label.
+
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.
  
Furthermore, Locations have several optional arguments :
+
=== Text filter ===
  
:: name : the name for that location, as can be seen in the top-left corner of the UI. It's also the way to find and call the relevant location screen, as well as the location label.
+
The {{strong|text filter}} key in the manifest allows you to create your own filter function without overwriting other mods.
:: unlock_popup : the name of a renpy-defined displayable for the popup that should show up when the location is unlocked.
+
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.
:: background : the name of the background used for that location. The background name must follow several conventions to work properly.
 
:::: must be in images/backgrounds/
 
:::: name must start with location_
 
:::: name must end with _day.jpg. Variants may be made for night (_night.jpg) and evening (_evening.jpg)
 
:::: if the background is a halloween/christmas background, the strings "_halloween" and "_christmas" must be added before the "_day" (or "_night" etc). This will allow the game to find relevant backgrounds for the time period.
 
:::: Whatever is leftover should be provided in the background argument of the constructor. The full file path will be constructed by the background (resp. background_blur) properties.
 
:: parents : either a list of Location or a single Location instance. Will provide the parents of that Location. If left empty, the Location is assumed to be the root of the tree. The root should always be L_map (or L_NULL, but L_NULL should not have children)
 
:: locked : defaults to False. Whether the Location should be initially locked, and unaccessible at the beginning of the game.
 
:: label :  (to be deprecated) The label name for that location. the formatted_name property will be used later on.
 
  
{{underline|'''Example :'''}} <syntaxhighlight lang="Python">L_diane_barn = Location("Diane's Barn", unlock_popup="popup_diane_barn", background="barn_frontyard", parents=L_map, locked=True)</syntaxhighlight> will define a location named "Diane's Barn" (formatted name being dianes_barn), child of L_map, that shows "popup_diane_barn" on unlock, and that is initially locked.
+
=== Screens ===
  
The background is set as barn_frontyard, which means the game will look for files named "backgrounds/location_barn_frontyard_day.jpg" or "backgrounds/location_barn_frontyard_night.jpg" for instance.
+
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.
  
== User-defined Screen Actions ==
+
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.
  
These screen actions are specific to Summertime Saga, and should be used to keep the flow consistent.
+
{{note|Hookable screens are, at least for now, only the location screens, and do not include the user interface.}}
  
=== MoveTo ===
+
<u>'''Example:'''</u>
This action expects a Location to be passed. It will hide the current screen, move the player to that location, and call the corresponding screen as well as jump to the location label
 
  
=== BuyItem ===
+
<syntaxhighlight lang="Python">
 +
init python:
 +
    ModManager.register_main(ikarumod_main)
 +
</syntaxhighlight>
  
This action expects an item, or item identifier string to be passed to it. When activated, will buy the item, and show the appropriate failing popups should the transaction fail. Optionally, you may add a callback label with the callback_label optionnal argument. If the transaction succeeds, the game will then call that label (useful if you want some story element to trigger after buying an item).
+
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.
  
=== TalkTo ===
+
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.
  
To be used to talk to character. Will hide the screen and start the conversation with the given character. It expects a Machine instance, or a string to identify it, in which case it will attempt to find the machine in the store.machines dictionnary.
+
== Imported modules and directory structure ==
  
=== ClearPersistent ===
+
=== Third‐party modules ===
  
Will reset the player's persistent data (cookie jar, time spent playing, achievements etc). No arguments expected.
+
==== Platform agnostic ====
  
=== GetItem ===
+
* 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
  
Will get the item, and attempt to show the corresponding popup. No arguments required.
+
==== Desktop builds ====
  
== Items and inventory management ==
+
* certifi
 +
* requests
  
The inventory manager is fully automatic, but you may want to add some items to the game. Items are stored in a json file called items.json in the scripts/data/ folder. A typical item definition will look like this :
+
==== Mobile builds ====
  
:: "birth_control_pills": {
+
* android
:::: "id": "173",
+
* pyjnius
:::: "name": "{b}Birth Control Pills:{/b}",
 
:::: "cost": "0",
 
:::: "image": "objects/item_pills3.png",
 
:::: "description": "Makes you temporarily sterile.",
 
:::: "closeup": "",
 
:::: "dialogue_label": "birth_control_pills",
 
:::: "popup_image": ""
 
:: }
 
  
 +
=== Directory structure ===
  
In order :
+
* {{code|game}}
* The item identifier.
+
** {{code|audio}}: SFX and musics
* The item's id. Unused at this moment.
+
** {{code|fonts}}: fonts for the game.
* The item's name, as seen in the backpack.
+
** {{code|images}}
* The item's cost. Will be cast as an integer, and will prevent the player from picking up the item if he doesn't have sufficient money.
+
*** {{code|achievements}}: achievement‐related images.
* The item's miniature image, as seen in the backpack.
+
*** {{code|cookie_jar}}: cookie jar buttons, popups and thumbnails.
* The item's description, as seen in the backpack.
+
*** {{code|backgrounds}}: backgrounds and close‐ups.
* The item's closeup image, if there is one.
+
*** {{code|boxes}}: popups and general purpose buttons (like the go back button).
* The item's dialogue label, which will trigger if it exists, on clicking the item. The label should absolutely return, and take an item argument.
+
*** {{code|buttons}}: most minigame assets, and menu buttons.
* The item's popup image. Shown when first acquiring the item, leave the string empty if there is none.
+
*** {{code|cellphone}}: cellphone images assets.
 +
*** {{code|characters}}: character poses. One folder per character.
 +
*** {{code|map}}: map locations and map background.
 +
*** {{code|objects}}: character buttons, item buttons, doors, etc.
 +
*** {{code|vfx}}: special visual effects (like the rain in Roxxy’s trailing scene).
 +
** {{code|python-packages}}: third party python modules.
 +
** {{code|scripts}}
 +
*** {{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}}: core files are put there, mostly what has been documented in the Modding API section of this manifesto.
 +
*** {{code|data}}: JSON files that contain data about the game. Items, achievements, keymap or text messages are defined here.
 +
*** {{code|defines}}: general image definitions, transforms, etc.
 +
*** {{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}}: one folder for every minigame, the minigame dialogues and screen files are in that folder.
 +
*** {{code|script.rpy}}
 +
*** {{code|pregnancy_announcements.rpy}}
 +
** {{code|changelog.txt}}
 +
** {{code|pledge_list.txt}}
  
FUTURE : Put your items in a file named modname_items.json, in a scripts/data folder.
+
== Translating the game ==
  
== Game manager class ==
+
=== Label calling in the game ===
  
The Game manager class handles everything that is related to the gameplay itself, but is not planned to be extendable by mods. Its methods and attributes are still useable and should be used for your mod.
+
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.
  
:: language : a class-level attribute that sets the language of the game, which is used for translations. It defaults to "en" for english with other languages in the game untranslated (like spanish or french for instance)
+
<u>'''Example:'''</u>
:: cheat_mode : an attribute that is True if the game is currently in cheat mode (allowing minigames to be skipped)
 
:: CA_FILE : path to a certification file used to enable requests to websites. Is unused unless the player uses the "allow internet connection" checkbox in the options.
 
:: lock_ui(), unlock_ui() and ui_locked() : locks, unlocks and returns whether the ui is locked or not (i.e. grayed out)
 
:: dialog_select(string label_name) : classmethod that will choose a label based on its name and the language class attribute. To be used to split dialogues, and logic, and also allows your mod to be easily translated.
 
:: choose_label(string template) : classmethod that chooses a label at random that matches the template passed in the arguments.
 
:: main() : method that is to be called at the end of every label the player jumps to (and not '''called''' to). It calls the player's location screen, and checks for achievements and other stuff. This would be the only method expandable upon. It also clears the return stack, so that traceback are not undigestible. It may take two arguments, clear_return_stack to enable or not return stack clearing, and location, to call the screen of another location than the player's currently in.
 
:: is_christmas() and is_halloween() : classmethods that will check whether the system clock matches halloween/christmas.
 
  
== Player class ==
+
<syntaxhighlight lang="Python">
 +
init 1 python:
 +
    Game.language = "es" # for Spanish for instance, "fr" for French, etc
 +
</syntaxhighlight>
  
The Player class handles everything player related. It handles its inventory, grades, vehicle level and stats.
+
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.
Notable methods :
 
  
:: receive_message(string message_id) : player receives a message on the cellphone. Also sets up the alert icon on the UI.
+
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.
:: has_item(string* items) : checks if the player has any of the items provided at this moment.
 
:: has_picked_up_item(string* items) : checks if the player has ever picked up any of the items provided.
 
:: get_item(string item), remove_item(string item) : gets the item, if the item's cost is no higher than the amount of money the player has. Conversely, removes an item by it's string id.
 
:: get_money(int money) and spend_money(int money) : adds and substract money from the player.
 
:: has_required_str(int min_str) : checks if the player has the required str (similar for chr, int and dex)
 
:: go_to(Location location) : goes to the given location.
 
:: go_to_previous() : goes to the first parent location of the player's current location.
 
  
== Miscellaneous functions and classes ==
+
=== Cutscenes and minigame instructions ===
  
KeepRefs : A class used everywhere to make sure to keep a reference to any object instantiated, making sure '''is''' checks are kept correct.
+
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.
:: get_instances() : classmethod to get a generator of all the instances of this class' subclass.
 
  
LastUpdatedOrderedDict : A dictionnary-like structure that keeps its order, and store its items in the order the keys were last added (and not ''updated''). This is a subclass of Python's OrderedDict from the collections package.
+
<u>'''Example of translation:'''</u>
:: listvalues : returns a list of all the values of this dict
 
:: listkeys : returns a list of all the keys of this dict
 
:: lastkey : returns the last key added to this dict
 
:: lastvalue : returns the value of the last key added to this dict
 
:: isempty : returns whether the dict is empty
 
  
format_seconds_to_dhm(int seconds) : returns a formatted string in the form (x)d (y)h (z)m from the number of seconds passed in the arguments.
+
<syntaxhighlight lang="Python">
 +
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."}
  
insert_newlines(string string_to_format, int every) : returns a new string based on the passed in string, with newlines inserted every ''every'' character. ''every'' defaults to 30. Differs from splice_string() in that it will not cut a word in the middle if possible. splice_string() will insert a newline every ''every'' characters without question. insert_newline() is much safer as it will not cut a variable substitution, but is much slower than splice_string.
+
    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
 +
</syntaxhighlight>
  
text_identity(string text) : returns text (unmodified), useful for config.say_menu_text_filter if it is None.
+
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.
  
replace_bracket(string text) : returns text without the [ and ] characters (renpy variable formatting syntax).
+
<u>'''Example:'''</u>
 
 
gauss(float mean, float deviation, float lower, float upper) : returns a random integer number in a normal distribution, clamped between lower and upper.
 
 
 
get_angle_speeds(int angle_width, Iterator angle_range, Iterator speed_range) : returns two lists, true angles and false angles which are lists of tuples (initial_angle, initial_speed) for which the result land in or out of the angle_width. Used in the pregnancy minigame and spin the bottle minigame for instance.
 
 
 
= 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 will make the mod show up in the (upcoming) Mods menu on the main menu of the game. From then, the player may or may not enable the mod for his game.
 
  
 
<syntaxhighlight lang="Python">
 
<syntaxhighlight lang="Python">
init python:
+
def fr_text_filter(text):
     ModManager.register("ikarumod")
+
     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
 
</syntaxhighlight>
 
</syntaxhighlight>
  
In the future, it may be necessary to create a manifest file named "modname_manifest.json" in the scripts/data folder. At this point in time, it is unnecessary, as the API is still being under developement.
+
=== Location names ===
  
== Screens ==
+
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.
  
Create your screens with the following convention : modname + _ + in-game screen name (the renpy definition name).
+
<u>'''Example:'''</u>
Screens are being included into the main game screens with the use statement. If you wish to add new locations, you'll have to define a screen for it, in which case, you can inspire yourself with the existing screens in the game.
 
 
 
To add the proper background, start with the statement "add player.location.background", which will automatically show the proper background according to the time of day/period of the year. You can then add imagebuttons as you see fit. Please refer to the User-defined screen actions for more information on which screen actions the game defines.
 
 
 
== Labels ==
 
 
 
Only "main" labels can be hooked into. Those are labels that end in "$ game.main()"
 
 
 
== Main function ==
 
 
 
To hook into the main function, you must register your main function to the ModManager.
 
  
 
<syntaxhighlight lang="Python">
 
<syntaxhighlight lang="Python">
init python:
+
init -1 python:
     ModManager.register_main(ikarumod_main)
+
     L_map.display_name = "New Map Name"
 
</syntaxhighlight>
 
</syntaxhighlight>
  
In the above example, ikarumod_main is assumed to be a callable, which should be a python function. This function will be called with no arguments at the end of the game.main() method.
+
[[Category:Community]]
 
 
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
 
:::: fonts
 
:::: images
 
:::::: achievements
 
:::::: backgrounds
 
:::::: boxes
 
:::::: buttons
 
:::::: cellphone
 
:::::: characters
 
:::::: map
 
:::::: objects
 
:::::: vfx
 
:::: python-packages
 
:::: scripts
 
:::::: characters
 
:::::::: One folder for each character, containing an fsm file, a character.rpy file for misc 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. Stuff like 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
 

Latest revision as of 19: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"