-
Notifications
You must be signed in to change notification settings - Fork 684
Interfacing with the Submod Framework
The Submod Framework is currently under a big overhaul due to MAS Python 3 migration. This page will be updated as news come.
Submod Framework allows you to register submods (custom mini-modifications for MAS) with dependencies and have update scripts for different versions to help keep your submods compatible with the current version of MAS.
Additionally Submod Framework provides multiple utilities for you as a developer:
- utility to plug functions into labels or other functions
- utility to import Python packages from a path
- utility to dynamically load renpy modules
This page contains instructions on how to use the framework.
- You have to explicitly define your submod so MAS can detect and load it appropriately.
- All submods must be placed within the
game/Submods/
directory. Sub-folders are allowed. Valid examples: -
game/Submods/My Submod - more outfits/
-
game/Submods/My Submod Pack/My Submod - more dialogues/
-
game/Submods/My Submod Pack/My Submod - more backgrounds/
- All submods must include a header file with the submod definition
- All submods must have their renpy-script in
rpym
and/orrpymc
. Remember:rpy
/rpyc
are not supported and will be ignored by the submod framework -
- Submods may include assets with other file types -
png
,jpeg
,py
,txt
, etc - anything you need
- Submods may include assets with other file types -
In the root directory of your submod, you need to place a header file - header.json
, the name and extension matter!
We use the JSON format for submod header, you can find more info about it online, as well as JSON validators. Here we will just cover MAS-specific information.
The most basic header file:
{
"header_version": 1,
"author": "Monika After Story",
"name": "Example Submod",
"version": "0.0.3",
"modules": [
"main"
]
}
A more advanced header file:
{
"header_version": 1,
"author": "Monika After Story",
"name": "Example Submod",
"version": "0.0.3",
"modules": [
"main"
],
"description": "This is an example.",
"dependencies": {
"Some Other Submod": ["0.0.8", "0.1.5"]
},
"settings_pane": "example_submod_screen",
"version_updates": {
"monika_after_story_example_submod_v0_0_1": "monika_after_story_example_submod_v0_0_2"
},
"coauthors": ["Example Submod co-author"],
"priority": 0
}
Now we will explain all the fields. The above header will initialize a submod with the following information:
- Header version:
1
-
- This field is required!
-
- This is the version of MAS submod schema, in case we dramatically change the framework in the future (unlikely), we will change this number too, to avoid load submods that not yet support the new changes
-
- For now keep it 1
- Author:
Monika After Story
-
- This field is required!
-
- Make this unique!
-
- Submod author, change it to your name
- Name:
Example Submod
-
- This field is required!
-
- Make this unique!
-
- Your submod name, change it to how you want to name your submod
- Version:
0.0.3
-
- This field is required!
-
- Version of your submods, must be a string of 3 integers separated by a
.
- Version of your submods, must be a string of 3 integers separated by a
-
- Change it to whatever version of your submod is, after you update your submod, raise the version as you think is appropriate
-
- Read more about semantic versions
- Modules:
main
-
- This field is required!
-
- The modules of your submod
-
- Modules are
rpym
/rpymc
files with your code, they must be within your submod directory
- Modules are
-
- Name of a module is being defined by its relative path and its filename without extension
-
- For example:
main
means amain.rpym
in the root directory of your submod
- For example:
-
- Another example:
utils/logging
means alogging.rpym
inside autils/
folder which is inside the root directory of your submod
- Another example:
- Description:
This is an example.
-
- Describe your submod using this field
- Dependencies:
Some Other Submod
with version from0.0.8
to0.1.5
-
- This is an advanced feature
-
- If your submod requires another submod to be installed, you can specify that using this field
-
- Format:
dependency_name: [min_version, max_version]
- Format:
- Settings Pane:
example_submod_screen
-
- This is an advanced feature
-
- A screen to show in the submods menu (
Esc
->Submods
) under your submod name
- A screen to show in the submods menu (
-
- This can be useful if you want to add some settings for your submod, or show extra information that doesn't fit into the description
- Version Updates:
monika_after_story_example_submod_v0_0_1
tomonika_after_story_example_submod_v0_0_2
-
- This is an advanced feature
-
- You can specify labels to use as "update script" to run when user updates your submod
-
- The labels must follow the format
author_submodname_vversion
(notice extrav
)
- The labels must follow the format
-
- In this example MAS will run the
monika_after_story_example_submod_v0_0_1
and thenmonika_after_story_example_submod_v0_0_2
when the user updates
- In this example MAS will run the
- Co-authors: list of other people who worked on this submod
-
- In this example it will be
Example Submod co-author
- In this example it will be
-
- This field is completely optional, you don't have to include anyone or add this field at all
- Priority:
0
-
- This is an advanced feature
-
- Your submod loading priority, if you are not sure, don't specify this
-
- By default all submods have 0 priority, allowed values are integers in range from -999 to 999, lower - load sooner, higher - load later
-
- If your submod is some kind of a framework, you may want to load it before other submods
Hypothetically, let's say our Example Submod
submod required code from another submod (named Required Submod
) to work properly.
To add this submod as a dependency for yours, we want to add a key and a value to the dependencies
field. The format for this is as follows: "dependency submod name": ("minimum_version", "maximum_version")
So for our scenario here, we end up with the header:
{
"header_version": 1,
"author": "Monika After Story",
"name": "Example Submod",
"version": "0.0.3",
"description": "This is an example.",
"modules": [
"main"
],
"dependencies": {
"Some Other Submod": ["0.0.8", "0.1.5"],
"Required Submod": [null, null]
},
"settings_pane": "example_submod_screen",
"version_updates": {
"monika_after_story_example_submod_v0_0_1": "monika_after_story_example_submod_v0_0_2"
},
"coauthors": ["Example Submod co-author"],
"priority": 0
}
Which marks our Example Submod
as requiring a submod named Required Submod
with no specific version range to be initialized, otherwise MAS will not load our submod at all until the dependency is met.
- If there is no applicable minimum version and/or maximum version, they can be passed in as
null
. - Both the minimum version and maximum versions will be passed in like you passed in the version for your submod, semantic versioning as string.
- If a dependency fails, MAS will skip your submod during the loading process and log that there is a submod which is failing the dependency.
With submods like these, it wouldn't be ideal to clutter the main menus with submod settings or ways to get to the settings for your submod. This is where the settings_pane
field comes into play.
To create a settings pane, simply create a screen containing the settings for your submod. This can be done as you would for any other screen initialization in Ren'Py. To bind this to your submod, pass in the name of the screen as a string to the settings_pane
field.
For example, let's say we made the following screen our settings screen:
#Don't actually name your screen like this. Use something unique
screen example_submod_screen():
vbox:
box_wrap False
xfill True
xmaximum 1000
hbox:
style_prefix "check"
box_wrap False
textbutton _("Switch setting #1") action NullAction()
textbutton _("Switch setting #2") action NullAction()
textbutton _("Switch setting #3") action NullAction()
You can use tooltips for buttons on your setting pane, and they will be shown on the main submod screen. The submod screen already has a tooltip defined, all we need to do is get the screen, get the tooltip and adjust its value.
This is how we do it in our example:
screen example_submod_screen():
python:
submods_screen = store.renpy.get_screen("submods", "screens")
if submods_screen:
_tooltip = submods_screen.scope.get("tooltip", None)
else:
_tooltip = None
vbox:
box_wrap False
xfill True
xmaximum 1000
hbox:
style_prefix "check"
box_wrap False
if _tooltip:
textbutton _("Switch setting #1"):
action NullAction()
hovered SetField(_tooltip, "value", "You will see this text while hovering over the button")
unhovered SetField(_tooltip, "value", _tooltip.default)
else:
textbutton _("Switch setting #1"):
action NullAction()
It's quite a big structure, but it allows you to safely change tooltips for buttons on your screen.
As you can see, you'll need to define 2 variants for each button:
- With the tooltip
- Without the tooltip (as a fallback in case we fail to get the screen for some reason).
Notice that we change the tooltip via SetField
.
On the hovered
action, we'll set the tooltip to our value, on unhovered
, we'll set it back to its default value using _tooltip.default
.
Creating update scripts is likely the most complex aspect of the submod framework.
Submod update script labels must be precisely named in order to avoid conflicts due to Submods which are named the same.
The label formatting is as follows (fields corresponding to the values in the Submod
init)
<author>_<name>_v<version>
- Spaces in the
author
andname
parts will be replaced with underscores - All characters in the label will be forced to lowercase
- The periods in the version number will be replaced with underscores
- The
version_updates
dictionary works in a"from_version": "to_version"
approach, and will follow a chain that is present in the updates dictionary. (Starting from the current version number, going to the top) - Submod update labels must accept a parameter called
version
which defaults to the version the update label is for - Update scripts run at init 10
So with our Example Submod
, the formatting will be
monika_after_story_example_submod_v0_0_1(version="0.0.1")
as our initial update label.
If we needed to make changes from 0.0.1 to 0.0.2 that need to be migrated, we would have the following two labels:
monika_after_story_example_submod_v0_0_1(version="v0_0_1"):
return
monika_after_story_example_submod_v0_0_2(version="v0_0_2"):
#Make your changes here
return
Keeping in mind the "from_version": "to_version"
approach to these updates, to continue the chain if we were to move from 0.0.2 to 0.0.3 (or any other arbitrary version), we would simply add:
"monika_after_story_example_submod_v0_0_2": "monika_after_story_example_submod_v0_0_3"
after the entry we put above.
- Since there may be non-breaking changes or you may want to add a compatibility functionality for the scenario where another submod is installed, you can use the
mas_submod_utils.isSubmodInstalled()
function.
-
name
: The name of the submod you're checking for -
version
: A minimum version number (This defaults to None. And if None, is not regarded when checking if a submod is installed)
While overrides are useful, they are not always ideal when it comes to maintenance over updates, or especially when it comes to only adding one or two lines to a label or function. To address this, function plugins
was created
This framework allows you to register functions which can be plugged into any label you wish, and additionally other functions as well, if they are set up to run them.
Registering functions is rather flexible, the parameters are as follows:
-
key
- The label (or function name (as string) in which you want your function to be called in -
_function
- A pointer to the function you wish to plug-in -
args
- Arguments to supply to the function (Default:[]
) -
auto_error_handling
- Whether or not the function plugins framework should handle errors (will log them and not let MAS crash) or not. (Set this to False if your function performs arenpy.call
orrenpy.jump
) (Default:True
) -
priority
- The priority order in which functions should be run. These work like init levels, lower is earlier, higher is later. (For functions whichrenpy.call
orrenpy.jump
, usemas_submod_utils.JUMP_CALL_PRIORITY
as they should be done last) (Default:0
)
There are two ways to register functions. One way is using a decorator
on the function you wish to register.
Note that the decorator approach also removes the need to manually pass in a function pointer, as it takes it directly from the function which it's attached to.
The advantage of using this approach is that it takes up fewer lines to register a function and additionally, it does better in terms of self-documentation of your code.
It's done as follows:
init python:
@store.mas_submod_utils.functionplugin("ch30_minute")
def example_function():
"""
This is an example function to demonstrate registering a function plugin using the decorator approach.
"""
renpy.say(m, "I love you!")
Which initializes a function plugin in the ch30_minute
label that has Monika say "I love you!".
However, this works only if you have the function you wish to plug-in contained within your submod script files. If this is not the case, we use the second approach.
Assuming the same example_function
from above:
init python:
store.mas_submod_utils.registerFunction(
"ch30_minute",
example_function
)
Which is equivalent to the previous example.
Let's say you needed to use arguments for your function, the approach is the same, except now all we do is pass the arguments in order as a list
. Let's change example_function
to be this now:
init python:
def example_function(who, what):
"""
This is an example function to demonstrate registering a function but with arguments
"""
renpy.say(who, what)
To make this equivalent to the last example, we would need to pass m
and "I love you"
in as our arguments. Both approaches can be seen demonstrated below:
Decorator: @store.mas_submod_utils.functionplugin("ch30_minute", args=[m, "I love you"])
RegisterFunction():
init python:
store.mas_submod_utils.registerFunction(
"ch30_minute",
example_function,
args=[m, "I love you!"]
)
With args comes the following two functions:
- This function allows you to get the args of a function plugin. Simply pass in the label/function it's assigned to and the function pointer itself, and it will return its args as a list to you.
- This function allows you to set the args of a function plugin. Simply pass in the label/function it's assigned to, the function pointer itself, and a list of new args.
It is also possible to unregister a plugin. Simply use the following function:
- This function will allow you to unregister a function plugin. Simply pass in the label/function it's assigned to and the function pointer itself, and it will no longer be mapped to that label/function.
As mentioned above, it is possible to have function plugins also be mapped to a function.
This is an obscure case but is handled regardless, however not by everything. There is no function within MAS code by default which will run function plugins. If you wish to have them run in a custom function of your own, you may do so by adding a store.mas_submod_utils.getAndRunFunctions()
line.
It will automatically handle all functions plugged into your function.
- Function plugins can be registered at any time after init -980
- Function plugins by default (without
auto_error_handling
set toFalse
) will automatically handle any errors in your function and log it inmas_log.log
to avoid crashing MAS - Function plugins are run from the global store
- Function plugins will run at the start of the label it is registered in
- Function plugins will run if the label it is bound to is either
call
ed,jump
ed to, or fallen through to
Thanks to the implementation of Function Plugins, there were two new global variables added:
store.mas_submod_utils.current_label
:
- This variable holds the current label we're in
store.mas_submod_utils.last_label
:
- This variable holds the last label we were in.