Skip to content

Petrichor.Storage#

Petrichor.Storage is per-mod key/value storage, plus general file access scoped to your mod's save folder. Values stored via store() live in memory while your mod runs and are written to disk as JSON when you call save().

The host loads any existing store.json into memory when your mod starts, so retrieve can return data from a previous session.

Save location#

Each mod gets its own folder under the user data directory:

<user_data>/mod_saves/<mod_id>/

<user_data> is the Courage Reborn user folder (same root as mods/, saves/, and config.ini). The mod_saves folder name is configurable in config.ini as mod_saves (default: mod_saves).

mod_id matches the id field in your mod.json.

All Storage file operations (read_file, write_file, file_path, and the key/value store backed by store.json) are sandboxed to this folder. Mods cannot read or write outside mod_saves/<mod_id>/.

Bundled mod files live under <mod_dir>/assets/ and are read-only at runtime. Use Storage.asset(), Storage.asset_path(), and Storage.list_assets() to read them. There is no API to write into assets/. Ship anything static at build time instead.

Loading#

local Storage = require("Petrichor.Storage")
import * as Storage from "Petrichor.Storage"

Petrichor.Storage is registered per mod when the mod loads. It is not a global prelude.


API#

Key/value store#

These operate on an in-memory table that is persisted as a single JSON file, store.json, in your mod's save folder.

Storage.store(key, value)#

Sets a value in the in-memory store. Not written to disk until Storage.save().

Parameter Type Description
key string Storage key
value JSON value nil, boolean, number, string, or table (object or array)
Storage.store("volume", 0.8)
Storage.store("tags", { "cow", "debug" })
Storage.store("volume", 0.8)
Storage.store("tags", ["cow", "debug"])

Storage.retrieve(key) -> value?#

Returns the stored value, or nil if the key is missing.

local volume = Storage.retrieve("volume")
if volume == nil then
    volume = 1.0
end
let volume = Storage.retrieve("volume") as number | undefined
if (volume === undefined) {
    volume = 1.0
}

Storage.default(key, value) -> value#

If key is missing, sets it to value and returns value. If the key exists, returns the stored value without changing it.

local count = Storage.default("visit_count", 0)
Storage.store("visit_count", count + 1)
const count = Storage.default("visit_count", 0) as number
Storage.store("visit_count", count + 1)

Storage.save()#

This function is a no-op if nothing has changed since the last save.

Writes the in-memory store to disk as JSON (store.json in your mod's save folder).

Storage.store("enabled", true)
Storage.save()
Storage.store("enabled", true)
Storage.save()

You can also call self:save() on your mod class (from Petrichor.Mod) to persist storage from a hook or shutdown.

Storage.clear(key)#

Removes one key from the in-memory store. Call save() to persist the change.

Storage.clear_all()#

Empties the in-memory store. Call save() to overwrite store.json with an empty object.


General file access#

These let a mod read and write arbitrary named files inside its own save folder (mod_saves/<mod_id>/). This is useful for data that doesn't fit the single-JSON-blob model, like logs, exported data, or large/binary payloads.

Storage.read_file(name) -> string?#

Reads a file from your mod's save folder and returns its contents as a string. Returns nil if the file doesn't exist or name resolves outside the save folder.

Parameter Type Description
name string Filename, relative to your mod's save folder. Defaults to "store.json" if omitted.
local log = Storage.read_file("debug.log")
const log = Storage.read_file("debug.log")

Storage.write_file(name, content) -> boolean#

Writes a string to a file in your mod's save folder, creating it if needed. Returns true on success, false if content isn't a string or name resolves outside the save folder.

Parameter Type Description
name string Filename, relative to your mod's save folder. Defaults to "store.json" if omitted.
content string Data to write
local ok = Storage.write_file("debug.log", "started up\n")
const ok = Storage.write_file("debug.log", "started up\n")

Storage.file_path(name) -> string?#

Returns the absolute filesystem path for a file in your mod's save folder, without reading it. Use this when an API expects a file path rather than file contents. Returns nil if name resolves outside the save folder.

local Audio = require("Courage.Audio")
Audio.play(Storage.file_path("recording.mp3"))
import * as Audio from "Courage.Audio"
Audio.play(Storage.file_path("recording.mp3"))

Bundled assets (read-only)#

Storage.asset(path) -> string?#

Reads a bundled file from <mod_dir>/assets/<path> and returns its contents as a string. Returns nil if the file is missing.

local defaults = Storage.asset("defaults.json")
const defaults = Storage.asset("defaults.json")

Storage.list_assets(path?) -> string[]#

Lists filenames of files directly under <mod_dir>/assets/<path>. Defaults to assets root if path is omitted. Returns an empty array if the directory is missing.

local files = Storage.list_assets("configs")
for _, file in ipairs(files) do
    print(file)
end
const files = Storage.list_assets("configs")
for (const file of files) {
    print(file)
}

Storage.asset_path(path) -> string#

Returns the absolute filesystem path for a bundled asset file without reading it. Use this when an API expects a file path rather than file contents (e.g. Audio.play).

Parameter Type Description
path string Path relative to <mod_dir>/assets/
local Audio = require("Courage.Audio")
Audio.play(Storage.asset_path("sound.mp3"))
import * as Audio from "Courage.Audio"
Audio.play(Storage.asset_path("sound.mp3"))

JSON on disk#

Persistence uses Petrichor.Json. Only JSON-compatible values are safe to store via Storage.store() (booleans, numbers, strings, arrays, objects). Luau tables with mixed keys or non-string keys may not round-trip cleanly.


Example: remember a setting#

local Storage = require("Petrichor.Storage")

local MyMod = mod("my-mod")

function MyMod:init()
    self.enabled = Storage.default("enabled", true)
end

function MyMod:toggle()
    self.enabled = not self.enabled
    Storage.store("enabled", self.enabled)
    self:save()
end

function MyMod:shutdown()
    self:save()
end

return MyMod
import * as Storage from "Petrichor.Storage"

declare function mod(name: string): any

const MyMod = mod("my-mod")

MyMod.init = function () {
    this.enabled = Storage.default("enabled", true)
}

MyMod.toggle = function () {
    this.enabled = !this.enabled
    Storage.store("enabled", this.enabled)
    this.save()
}

MyMod.shutdown = function () {
    this.save()
}

export = MyMod

Example: writing a log file alongside store.json#

local Storage = require("Petrichor.Storage")

local function log(msg)
    local existing = Storage.read_file("debug.log") or ""
    Storage.write_file("debug.log", existing .. msg .. "\n")
end
import * as Storage from "Petrichor.Storage"

function log(msg: string) {
    const existing = (Storage.read_file("debug.log") as string | undefined) ?? ""
    Storage.write_file("debug.log", existing + msg + "\n")
}

Notes#

  • Changes made via Storage.store() are in memory until save(). If the game exits without saving, they are automatically saved via. self:save() on the mod instance. Note this is not a catch-all: due to the nature of our mixin-like system, mods can take down the process at any time. Save important changes eagerly.
  • Storage.write_file() writes immediately. It is not buffered like the key/value store, and has no separate save() step.
  • One save folder per mod id from mod.json. read_file, write_file, and file_path are sandboxed to that folder; a name that would resolve outside it fails (nil/false) rather than escaping.
  • assets/ is read-only. Use asset(), asset_path(), and list_assets() to read bundled files; use read_file() / write_file() / the key/value store for anything a mod needs to persist.
  • asset_path() and file_path() both return a path without reading the file. Use them when passing a path to another API such as Audio.play.