Table of Contents

Copyable recipes

Each example below is a complete mod.lua, not an isolated API fragment. Create a new folder under Mods/S1Lua, paste one example into that folder as mod.lua, and change the mod ID before sharing your version.

Important

Every installed mod needs a unique lowercase id. Treat that ID as permanent once a save depends on it.

Pay a welcome bonus once per save

This mod gives the player $250 after a save loads, remembers that it paid the bonus, and requests a save. Restarting the game does not pay it twice.

local mod = s1.mod {
    id = "s1lua.welcome-bonus",
    name = "Welcome Bonus",
    version = "1.0.0",
    author = "S1Lua"
}

mod:on("game_loaded", function()
    if mod:get("bonus_paid", false) then
        return
    end

    mod:change_cash(250, true, true)
    mod:set("bonus_paid", true)
    mod:save()
    s1.log("Paid the one-time $250 welcome bonus.")
end)

Change 250 to adjust the bonus. Keep the bonus_paid check unless you intentionally want to pay on every load.

Report player, money, and rank status

This read-only example shows how to handle player information that may not be ready yet and how to react to events. Its output appears in the MelonLoader console.

local mod = s1.mod {
    id = "s1lua.status-watcher",
    name = "Status Watcher",
    version = "1.0.0",
    author = "S1Lua"
}

mod:on("game_loaded", function()
    local player = s1.player()
    local progress = s1.progress()
    local money = s1.money()

    if player ~= nil then
        s1.log("Current region: " .. player.region .. "; health: " .. player.health)
    end
    if progress ~= nil then
        s1.log("Current rank: " .. progress.rank .. " tier " .. progress.tier)
    end
    s1.log("Cash on hand: " .. money.cash)
end)

mod:on("rank_up", function()
    local progress = s1.progress()
    if progress ~= nil then
        s1.log("Rank advanced to " .. progress.rank .. " tier " .. progress.tier .. "!")
    end
end)

mod:on("player_died", function()
    s1.warn("The local player died.")
end)

mod:on("player_revived", function()
    s1.log("The local player is back.")
end)

These information functions may return nil until the related part of the game is ready:

  • s1.player() returns nil until the local player exists;
  • s1.progress() returns nil until progression exists;
  • s1.money() returns numeric balances and uses zeros before the money manager exists.

Award XP for recycling trash

This example adds 5 XP for each trash object processed by the recycler. The normal cash reward remains unchanged.

local XP_PER_ITEM = 5

local mod = s1.mod {
    id = "s1lua.recycle-for-xp",
    name = "Recycle for XP",
    version = "1.0.0",
    author = "S1Lua"
}

mod:on("trash_recycled", function(item_count)
    local xp = item_count * XP_PER_ITEM
    if mod:add_xp(xp) then
        s1.log("Recycled " .. item_count .. " trash item(s) and requested " .. xp .. " XP.")
    else
        s1.warn("Trash was recycled, but progression is not ready yet.")
    end
end)

Change XP_PER_ITEM to tune the reward. A filled trash bag counts as one recycled object. XP awards work in single-player and for the lobby host.

Create a shop item

The starter example copies an existing item, changes how it appears, and adds it to suitable shops.

local mod = s1.mod {
    id = "yourname.my-first-mod",
    name = "My First Mod",
    version = "1.0.0",
    author = "Your Name"
}

mod:item {
    id = "golden_cuke",
    clone = "cuke",
    name = "Golden Cuke",
    description = "A suspiciously expensive energy drink.",
    price = 250,
    stack = 10,
    shops = "compatible"
}

mod:on("game_loaded", function()
    s1.log("My First Mod is ready!")
end)

Change one field at a time and restart the game. Keep id stable. You can freely change name, description, price, or stack while testing.

Split a mod into modules and delay work

The ModularTimer example loads a helper file from its own folder, then runs a function after the game loads.

local mod = s1.mod {
    id = "bars.modular-timer",
    name = "Modular Timer",
    version = "1.0.0",
    author = "Bars",
    description = "Demonstrates safe modules and delayed callbacks."
}

local messages = mod:require("messages")

mod:on("game_loaded", function()
    mod:after(1, function()
        s1.log(messages.loaded())
    end)
end)

Keep messages.lua beside mod.lua. For larger mods, use relative paths such as mod:require("features/reminders"); paths cannot leave the mod folder.

Combine recipes

Use one s1.mod { ... } declaration per mod.lua. To combine examples, keep the declaration from your own mod and copy only the mod:on(...) or mod:item { ... } blocks you need.

Next, use the Lua API reference to check accepted fields and event names. If the script does not load, the troubleshooting guide maps common console messages to fixes.