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.


Requirements

Set up a working copy of the game before you start modding:

  1. 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).
  2. Game source β€” clone from GitHub:
    git clone https://github.com/SuIT-pub/Mind-the-School.git
    cd Mind-the-School
    master always tracks the latest released version. For the current in-development tree, check out the version branch β€” at the moment MTS-285/Version-0.2.2 (each upcoming release gets its own branch):
    git checkout MTS-285/Version-0.2.2
    Mod against master if you want to match what players have from the last release; use the version branch to test against upcoming content.
  3. Python 3.9+ β€” needed for the asset download tools (pip install -r requirements.txt).
  4. Game assets β€” the repository contains source code only (~21 GB of images are hosted separately). From the repository root (the folder with game/ and tools/, not from inside game/), download and install them:
    cd Mind-the-School          # repository root
    pip install -r requirements.txt
    python tools/download_assets.py
    Default install mode is keep-existing (local files stay; cloud only adds missing paths). See Install modes for overwrite-existing and folder-swap. On Windows you can double-click tools/Download Assets.bat instead. See Getting the game assets for details (where to run the script, failure handling, resume, and --cleanup).
  5. Launch in Ren'Py β€” add the cloned project in the Ren'Py launcher and run the game once to confirm scripts and assets load.

Quick start (the whole path)

Complete the requirements above, then:

  1. Make a folder game/mods/MyMod/ and a file game/mods/MyMod/my_mod.rpy. Ren'Py auto-loads every .rpy under game/, mods included β€” no manifest needed.
  2. 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")
  3. 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)
  4. Write the scene label (label mymod_scene(**kwargs): …) β€” see Events.
  5. Put assets under game/mods/MyMod/images/… (referenced as plain images/…).
  6. 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.


Contents

  1. Requirements
  2. How a mod plugs in
  3. Folder layout
  4. Step 1 β€” register the mod
  5. Step 2 β€” claim the mod context
  6. Step 3 β€” add content
  7. Step 4 β€” assets
  8. Step 5 β€” enable & test
  9. Why activation needs a restart
  10. A complete mod
  11. Distribution
  12. Conventions & gotchas
  13. Reference

1. How a mod plugs in

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.


2. Folder layout

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).


3. Step 1 β€” register the mod

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).


4. Step 2 β€” claim the mod context

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, situation thumbnails, teaser images, Picto icons, 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") β€” never mods/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.


5. Step 3 β€” add content

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_event into a pool).
  • Everything else β€” situations, unlockables, characters, pictograms, items, buildings β€” registers through a register_start_method loader label.

Both auto-gate on the active mod, so you never write "if enabled" checks.

Events (the init path)

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.

Everything else (the register_start_method path)

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") )
    return

Why 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.

Extending base content

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.


6. Step 4 β€” assets

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>.webp

The 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.


7. Step 5 β€” enable & test

  1. Launch the game.
  2. Open the Journal β†’ mod list and toggle your mod on.
  3. Restart the game (see Β§8 β€” content registers at startup based on the saved active state).
  4. 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.

8. Why activation needs a restart

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_method calls 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_active is True β†’ 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).


9. A complete mod

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.


10. Distribution

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).


11. Conventions & gotchas

  • Namespace everything. Prefix your mod key, event/situation/label keys and asset folders (mymod_…) so nothing collides with base or other mods.
  • set_current_mod first, always. Every content init block 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 matching label mymod_scene. Missing/mismatched labels are logged under category event.
  • Register early, build late. register_mod at init -97; events at init 1 (after base pools); everything else (situations, unlockables, characters, pictograms, items, buildings) via a register_start_method loader 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.

12. Reference

Mod API

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).

Init timing

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).

Content register functions (all mod-gated)

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.

Related pages

Related files

  • 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
⚠️ **GitHub.com Fallback** ⚠️