Modding - SuIT-pub/Mind-the-School GitHub Wiki
Audience: Developers who want to add content to Mind the School as a mod β events, situations, unlockables, pictograms, assets β without touching the base game. This is a quick start: follow it top to bottom and you end up with a working, enable-able mod.
Scope: How a mod is structured, registered, activated, and how it hooks its content into the game. The content systems have their own guides β Events, Building Situations, Building Unlockables β this page is the wrapper around them.
Set up a working copy of the game before you start modding:
- Ren'Py SDK β install Ren'Py 8.1.3 (the version the project is built against; a newer 8.1.x release may work, but use 8.1.3 if you hit compatibility issues).
-
Game source β clone from GitHub:
git clone https://github.com/SuIT-pub/Mind-the-School.git cd Mind-the-Schoolmasteralways tracks the latest released version. For the current in-development tree, check out the version branch β at the momentMTS-285/Version-0.2.2(each upcoming release gets its own branch):Mod againstgit checkout MTS-285/Version-0.2.2
masterif you want to match what players have from the last release; use the version branch to test against upcoming content. -
Python 3.9+ β needed for the asset download tools (
pip install -r requirements.txt). -
Game assets β the repository contains source code only (~21 GB of images are hosted
separately). From the repository root (the folder with
game/andtools/, not from insidegame/), download and install them:Default install mode is keep-existing (local files stay; cloud only adds missing paths). See Install modes forcd Mind-the-School # repository root pip install -r requirements.txt python tools/download_assets.py
overwrite-existingandfolder-swap. On Windows you can double-clicktools/Download Assets.batinstead. See Getting the game assets for details (where to run the script, failure handling, resume, and--cleanup). - Launch in Ren'Py β add the cloned project in the Ren'Py launcher and run the game once to confirm scripts and assets load.
Complete the requirements above, then:
-
Make a folder
game/mods/MyMod/and a filegame/mods/MyMod/my_mod.rpy. Ren'Py auto-loads every.rpyundergame/, mods included β no manifest needed. -
Register the mod very early so it appears in the in-game mod list:
init -97 python: register_mod("mymod_key", "My Mod", "1", "MyMod", description="Adds a small cafeteria scene.", author="YourName")
-
Claim the mod context and add content after the base pools exist:
init 1 python: set_current_mod('mymod_key') # redirects assets + gates registration my_event = Event(3, "mymod_scene", TimeCondition(daytime="d"), Pattern("main", "images/mymod_scene <step>.webp")) cafeteria_events["order_food"].add_event(my_event)
-
Write the scene label (
label mymod_scene(**kwargs): β¦) β see Events. -
Put assets under
game/mods/MyMod/images/β¦(referenced as plainimages/β¦). - Enable & test: launch β Journal β mod list β toggle My Mod on β restart β your content is now live.
The complete example at the bottom is a copy-paste starting point.
- Requirements
- How a mod plugs in
- Folder layout
- Step 1 β register the mod
- Step 2 β claim the mod context
- Step 3 β add content
- Step 4 β assets
- Step 5 β enable & test
- Why activation needs a restart
- A complete mod
- Distribution
- Conventions & gotchas
- Reference
A mod is just .rpy files under game/mods/<YourMod>/ that Ren'Py loads like any
other script. Nothing is special about the code β a mod uses the same APIs as the
base game (Event, Situation, register_situations, β¦). Two thin wrappers make it
a mod rather than base content:
-
register_mod(...)β announces the mod so it shows up in the player's mod list and gets a folder path for its assets. -
set_current_mod('key')β marks the following registrations as belonging to your mod. This does two things at once: it redirects asset paths into your mod folder, and it gates registration on the mod being active βEventStorage.add_event, an event's self-registration,register_start_method, and every content register function (register_situations,register_unlockables,load_person,add_pictogram,load_item,register_buildings) all quietly no-op while your mod is disabled. So you never write "if enabled" checks: claim the context and register normally.
Coupling to the base game is only through keys, conditions and progress, so your mod can gate on base state and be gated by it, and add to base event pools or situations without editing them.
game/mods/MyMod/
my_mod.rpy # your code (any name; multiple files allowed)
images/
mymod_scene 0.webp # assets, referenced as "images/mymod_scene 0.webp"
...
game/mods/* is git-ignored in the base repo (except the bundled CheatMod), so your
mod lives self-contained in its own folder and ships separately (see Β§10).
Call register_mod at a very early init (before content and before the mod-list
UI builds) β the bundled CheatMod uses init -97:
init -97 python:
register_mod(
"mymod_key", # unique key β your mod's identity everywhere (namespace it)
"My Mod", # display name shown in the mod list
"1", # version string
"MyMod", # folder name under mods/ (β assets resolve to mods/MyMod/)
description="Adds a small cafeteria scene.",
author="YourName",
# translations=["translations.csv"], # optional
)register_mod(key, name, version, path, **meta) writes the mod into
persistent.modList. A mod is registered inactive by default, and bumping the
version string resets it to inactive (players re-enable after an update).
At the top of every init block (and every loader label) that registers content, call:
set_current_mod('mymod_key')-
Asset redirection: image paths captured after this (event
Patterns, situationthumbnails, teaserimages,Pictoicons, item images) are prefixed with your mod's path (mods/MyMod/). Write plain paths relative to your mod root βPattern("main", "images/foo <step>.webp")β nevermods/MyMod/.... -
Active-state gating: registration functions check
is_mod_active('mymod_key')and no-op while your mod is off. That is why disabled mods inject nothing.
Base scripts do the same with set_current_mod('base'); you just use your own key.
There are two registration paths, and which one you use depends only on whether it's an event:
-
Events register at init (build the
Event,add_eventinto a pool). -
Everything else β situations, unlockables, characters, pictograms, items,
buildings β registers through a
register_start_methodloader label.
Both auto-gate on the active mod, so you never write "if enabled" checks.
Build after the base pools exist (init 1 works β base buildings build at init -1),
then add to a pool:
init 1 python:
set_current_mod('mymod_key')
my_event = Event(3, "mymod_scene",
TimeCondition(daytime="d"),
Pattern("main", "images/mymod_scene <step>.webp"))
cafeteria_events["order_food"].add_event(my_event) # no-op if the mod is offβ¦and write the scene label (label mymod_scene(**kwargs): β¦) β full scene grammar in
Events.
Queue one loader label into the start/after-load wave, and register whatever you like inside it after claiming the context. This one mechanism covers situations, unlockables, characters, pictograms, items and buildings:
init python:
register_start_method("load_mymod") # gated; runs in the start/after-load wave
label load_mymod:
$ set_current_mod('mymod_key') # claim context first (redirect + gating)
# situations / unlockables
$ register_situations( Situation("mymod_thing", β¦) )
$ register_unlockables( Unlockable("rule", "mymod_rule", β¦) )
# characters / persons
$ load_person("mymod_class", Person("mymod_girl", "Mira", "Sato", school_char, [β¦]))
# pictograms
$ load_pictograms( Pictogram("mymod_icon", "Label", "Tooltip", "images/icons/mymod.webp") )
# items
$ load_item( ItemData("mymod_item", "Name", "Desc", "images/mymod_item.png") )
# map buildings (registering alone doesn't draw it; add it to a map)
$ register_buildings( Building("mymod_building", "images/mymod_building.webp", β¦) )
$ map_manager.add_building_to_map("school", building_manager.get_building("mymod_building"))
# a map of your own (optional)
$ map_manager.load_map( Map("mymod_map", "images/mymod_map.webp", "mymod_building") )
returnWhy a start-method label rather than a bare init block: it runs inside the
lifecycle wave (after the base loaders), so reloads, the lifecycle registry, and
orphan revival all work β see Building Situations Β§8 and
Building Unlockables Β§13. Every register_* / load_*
function above also self-gates on the active mod, so a disabled mod registers
nothing even if its label somehow runs.
You can also add teasers/pictograms to base situations, events to base pools,
and gate base events on your progress β all without editing base files. Prefix your
keys (mymod_β¦) so they never collide.
Put images under your mod folder and reference them by the plain path you'd write relative to the mod root:
game/mods/MyMod/images/mymod_scene 0.webp
game/mods/MyMod/images/mymod_scene 1.webp
Pattern("main", "images/mymod_scene <step>.webp") # β mods/MyMod/images/mymod_scene <step>.webpThe redirect is baked in at construction (while set_current_mod points at your mod),
so it only works if the context is set before the object is built. Videos
(Movie images), thumbnails, teaser images and pictogram icons follow the same rule.
- Launch the game.
- Open the Journal β mod list and toggle your mod on.
- Restart the game (see Β§8 β content registers at startup based on the saved active state).
- Trigger it β e.g. go to the cafeteria, pick the action your event is in, at the
right time. Watch the log (category
event) for a missing-label or bad-key error.
Content is registered during init / the start wave, and set_current_mod gates
that registration on is_mod_active(...), which reads the saved persistent.modList
active flag. So the sequence is:
- First launch β your mod is registered but inactive β its
add_event/register_start_methodcalls no-op β no content. - You enable it in the mod list β the active flag is saved to
persistent. -
Restart β init runs again, now
is_mod_activeisTrueβ your content registers.
Toggling a mod therefore takes effect on the next start, not immediately β tell your players to restart after enabling, and after a version bump (which re-disables it).
Drop this in game/mods/SnackMod/snack_mod.rpy, add the two images, enable Snack
Mod, restart, and visit the cafeteria's order food action at daytime "d".
## game/mods/SnackMod/snack_mod.rpy
# 1) Register the mod (very early).
init -97 python:
register_mod(
"snackmod_suit", # unique key
"Snack Mod", # display name
"1", # version
"SnackMod", # folder under mods/ β assets at mods/SnackMod/
description="Adds a small chat event to the cafeteria.",
author="YourName",
)
# 2) Claim the context and register content (after base pools exist).
init 1 python:
set_current_mod('snackmod_suit')
snackmod_chat = Event(3, "snackmod_snack_chat",
TimeCondition(daytime="d"),
RandomListSelector("snack", "muffin", "apple", "bento"),
Pattern("main", "images/snack_chat <snack> <step>.webp"),
thumbnail="images/snack_chat muffin 0.webp")
cafeteria_events["order_food"].add_event(snackmod_chat) # auto-gated on active state
# 3) The scene label.
label snackmod_snack_chat(**kwargs):
$ begin_event(**kwargs)
$ snack = get_value("snack", **kwargs)
$ miwa = Person["miwa_igarashi"]
$ image = convert_pattern("main", **kwargs)
$ image.show(0)
miwa.say "Ooh, a [snack]! My favourite."
$ image.show(1)
miwa.think "...maybe I'll grab two."
call change_stats_with_modifier(happiness=SMALL) from _snackmod_1
$ end_event("new_daytime", **kwargs)Assets (referenced as images/β¦, resolved to mods/SnackMod/images/β¦):
game/mods/SnackMod/images/snack_chat muffin 0.webp
game/mods/SnackMod/images/snack_chat muffin 1.webp
game/mods/SnackMod/images/snack_chat apple 0.webp
β¦
That's a complete, self-contained mod: one registered mod, one ambient cafeteria event
with a rolled snack value driving both the image and the dialogue, its own art, and
a small happiness reward β all gated on the mod being enabled.
A mod is its game/mods/<YourMod>/ folder. Ship that folder; players drop it into
their own game/mods/. Because game/mods/* is git-ignored in the base repo, your mod
stays independent of the base game's version control. Keep everything (code + assets)
under your one folder so it's a single unit to install and remove. Bump the version
in register_mod on updates (players re-enable afterward).
-
Namespace everything. Prefix your mod key, event/situation/label keys and asset
folders (
mymod_β¦) so nothing collides with base or other mods. -
set_current_modfirst, always. Every contentinitblock and loader label must set it before building anything β otherwise assets don't redirect and registration may attach to the wrong (or no) mod. -
Definition name = label name. An
Event("mymod_scene", β¦)needs a matchinglabel mymod_scene. Missing/mismatched labels are logged under categoryevent. -
Register early, build late.
register_modatinit -97; events atinit 1(after base pools); everything else (situations, unlockables, characters, pictograms, items, buildings) via aregister_start_methodloader label. - Restart after enabling (and after a version bump) β see Β§8.
- Don't edit base files. Add to base pools / situations through keys and conditions instead; that keeps your mod portable and update-safe.
-
Avoid the deprecated temp-event classes (
TempEventStorage& friends) β they are highly unstable.
register_mod(key, name, version, path, description=β¦, author=β¦, translations=[β¦]) Β·
set_current_mod(key) Β· is_mod_active(key) Β· get_mod_path(key) /
get_current_mod_path() Β· register_start_method(label).
register_mod β init -97 (early) Β· events β init 1 (after base pools, via
add_event) Β· everything else (situations, unlockables, characters, pictograms,
items, buildings, maps) β a register_start_method loader label (lifecycle wave).
register_situations(...) Β· register_unlockables(...) Β· load_person(key, Person) Β·
load_pictograms(...) / add_pictogram(...) Β· load_item(ItemData) Β·
register_buildings(...) Β· map_manager.load_map(Map) Β· map_manager.add_building_to_map(map_key, building)
(Maps). Events use EventStorage.add_event(...) at init instead.
-
Events β scene definitions & labels,
Patterns, decisions, characters -
Maps β map buildings, extra maps,
add_building_to_map -
Images β path resolution, PNG/WebP, how
images/β¦becomesmods/MyMod/images/β¦ -
Building Situations / Building Unlockables β the
register_start_methodloader pattern - Selectors Β· Conditions Β· Effects Β· Modifiers Β· Options
-
game/mods/CheatMod/cheat_mod.rpyβ the bundled reference mod -
game/scripts/helper.rpyβregister_mod,set_current_mod,is_mod_active,get_mod_path