Skip to content

Create your first mod

This guide builds a mod called hello-cow. It runs before daCow_c::Execute and logs when a cow starts ticking.

1. Create the mod directory#

Inside your mods/ folder, create:

mods/
└── hello-cow/

2. Write mod.json#

Create mods/hello-cow/mod.json:

{
  "id": "hello-cow",
  "name": "Hello Cow",
  "type": "luau",
  "entry": "main.luau",
  "version": "1.0.0",
  "hostVersion": "0.8.0",
  "engineVersion": "1.0.0"
}
Field Description
id Unique id, no spaces
name Display name
type Use "luau" for Luau or TypeScript mods
entry Entry file path
version Semantic version of the mod package (for example, "1.0.0")
hostVersion Semantic version of the host game this mod requires (for example, "0.8.0")
engineVersion Semantic version of the Petrichor engine this mod requires (for example, "1.0.0")

See manifest reference for all fields.

3. Write main.luau#

Create mods/hello-cow/main.luau:

local Log = require("Petrichor.Log")

local HelloCow = mod("hello-cow")

function HelloCow:init()
    self:before("daCow_c::Execute", function(ctx)
        Log.info("hello-cow", "A cow is about to Execute!")
    end)
end

return HelloCow
import * as Log from "Petrichor.Log"

declare function mod(name: string): any

const HelloCow = mod("hello-cow")

HelloCow.init = function () {
    this.before("daCow_c::Execute", (ctx: HookCtx) => {
        Log.info("hello-cow", "A cow is about to Execute!")
    })
}

export = HelloCow

What's happening here#

  • mod("hello-cow") builds a class from Petrichor.Mod. See Script entry point.
  • init runs once when Petrichor calls new(). Register hooks here.
  • self:before(sym, fn) runs fn before the game function. ctx has ctx.self and parameters. Find symbols in Game Hooks. Actor names are on the Actors tab of the TP Ultimate Spreadsheet.
  • return HelloCow returns the class table. Petrichor creates an instance and calls tick / shutdown (empty defaults from Petrichor.Mod unless you override them).

4. Run the game#

Copy hello-cow/ into your mods folder (see Prerequisites), then launch the game. You should see:

[luau] loaded mod: hello-cow
[hello-cow] A cow is about to Execute!

Next steps: reading struct fields#

local Log = require("Petrichor.Log")

local HelloCow = mod("hello-cow")

function HelloCow:init()
    self:before("daCow_c::Execute", function(ctx)
        local cow = ctx.self
        Log.info("hello-cow", "Cow mode: " .. tostring(cow.mMode))
    end)
end

return HelloCow
import * as Log from "Petrichor.Log"

declare function mod(name: string): any

const HelloCow = mod("hello-cow")

HelloCow.init = function () {
    this.before("daCow_c::Execute", (ctx: HookCtx) => {
        const cow = ctx.self
        Log.info("hello-cow", `Cow mode: ${cow.mMode}`)
    })
}

export = HelloCow

Field names come from augment.bin. See hooks reference.

For TypeScript project setup, see TypeScript guide.


Shader mods

For visual effects without hooks, see Shader mod and the shader guides.