Skip to content

Courage.Assets#

require("Courage.Assets") maps files in your mod's assets/ folder to logical names used by the shader loader, Courage.Shaders, and CReact image / sprite primitives.

The game also exposes the same table on require("Courage.Game").assets.

Active mod context#

While Petrichor loads your mod (entry file and new() / init), the host sets the active mod folder and id. Courage.Assets only works during that time. Calling it from tick or a deferred callback raises an error.

API#

Courage.Assets is a callable table:

Form Returns Description
Assets(rel) string Logical name for a file under assets/ (strips .hlsl / .vs.hlsl, prefixes mod id)
Assets.path(rel) string Absolute filesystem path: <mod-dir>/assets/<rel>
Assets.mod_path() string Absolute filesystem path of the mod folder: <mod-dir>
Assets.set_override(virtualPath, realPath) boolean Map a virtual archive path to a real file on disk. Returns true on success, false if the file doesn't exist.
Assets.clear_override(virtualPath) boolean Remove a previously set override. Returns true if an entry was removed.

Logical name rules#

  • Slashes are normalized, and leading ./ and / are stripped.
  • Shader extensions .hlsl and .vs.hlsl are removed from the logical name.
  • Result is <mod-id>/<path/stem> unless the mod id is empty (then just the path).

Examples for mod id shader_demo:

Call Result
Assets("red_tint.hlsl") "shader_demo/red_tint"
Assets("filters/crt.hlsl") "shader_demo/filters/crt"
Assets.path("red_tint.hlsl") ".../mods/shader_demo/assets/red_tint.hlsl"

Use logical names with Courage.Shaders.set_enabled and list(). See Courage.Shaders.

Asset overrides#

There are two ways to redirect a game archive file to a mod file.

1. Filesystem overrides (passive, no Luau needed)#

Place a replacement file under mods/<mod-id>/assets/overrides/<archive>/<subdir>/<file>. The game checks every enabled mod folder in load-order on each resource fetch.

mods/my-mod/
└── assets/
    └── overrides/
        └── d_a_wolf/          (archive name, lowercase)
            └── body.bmd       (file to replace)

2. Assets.set_override (runtime, from Luau)#

Assets.set_override is checked before the filesystem scan. Use it for dynamic redirects: different files depending on mod state, save data, or player choice.

Assets.set_override(virtualPath, realPath)

virtualPath format: <archive>/<subdir>/<filename>, all lowercased, using forward slashes. This matches the key the engine builds internally from the archive name and the entry path.

realPath can be: - An absolute path: used as-is. - A relative path: resolved against the mods/ directory first, then the current working directory.

The function validates that the file exists before storing the mapping. It returns false (and logs a warning) if the file cannot be found.

Both set_override and clear_override automatically invalidate the mod resource cache for the affected virtual path, so the change takes effect on the next resource fetch without restarting.

local Assets = require("Courage.Assets")

local MyMod = mod("my-mod")

function MyMod:init()
    -- Redirect a specific archive file to a replacement on disk
    local ok = Assets.set_override(
        "d_a_wolf/body.bmd",                         -- virtual path (archive/file, lowercase)
        Assets.path("replacements/hero_wolf.bmd")    -- absolute path via Assets.path
    )
    if not ok then
        print("override failed: file not found")
    end
end

function MyMod:shutdown()
    Assets.clear_override("d_a_wolf/body.bmd")
end

return MyMod
import * as Assets from "Courage.Assets"

declare function mod(name: string): any

const MyMod = mod("my-mod")

MyMod.init = function () {
    const ok = Assets.set_override(
        "d_a_wolf/body.bmd",
        Assets.path("replacements/hero_wolf.bmd")
    )
    if (!ok) {
        print("override failed: file not found")
    }
}

MyMod.shutdown = function () {
    Assets.clear_override("d_a_wolf/body.bmd")
}

export = MyMod

[!TIP] Call Assets.clear_override in shutdown so overrides don't persist if the mod is disabled.

CReact textures#

CReact resolves asset / src on image, nineslice, and sprite through the engine asset registry:

Source Lookup
Memory assets RGBA pixels registered at runtime (for example by Courage.Layout capture / update)
Mod files mods/<mod-id>/assets/<path> when the key looks like <mod-id>/<path>
UI root Paths relative to the packaged CReact UI assets folder

Supported on-disk formats for mod files: PNG, JPEG, and BMP (decoded with stb_image). Retail TIMG textures inside loaded BLO screens are decoded when you rasterize through Courage.Layout.

Using assets in a panel#

local Assets = require("Courage.Assets")
local UI = require("Courage.UI")

function MyMod:init()
    local icon = Assets("icons/heart.png")  -- "my-mod/icons/heart"
    UI.setState("my-mod", { icon = icon })
    UI.showPopup("my-mod")
end
import * as Assets from "Courage.Assets"
import * as UI from "Courage.UI"

MyMod.init = function () {
    const icon = Assets("icons/heart.png")
    UI.setState("my-mod", { icon })
    UI.showPopup("my-mod")
}

Panel (creact/panel.tsx or popup.tsx)

local icon = Assets("icons/heart.png")
UI.setState("my-mod", { icon = icon })
<image asset={(state.icon as string) ?? ""} style={{ width: 32, height: 32 }} />

Place icons/heart.png under mods/my-mod/assets/. The string from Assets(...) must match the key CReact looks up.

Example#

local Assets = require("Courage.Assets")
local Shaders = require("Courage.Shaders")

local MyMod = mod("shader_demo")

function MyMod:init()
    local name = Assets("red_tint.hlsl")
    Shaders.set_enabled(name, true)
    Shaders.apply()
end

return MyMod
import * as Assets from "Courage.Assets"
import * as Shaders from "Courage.Shaders"

declare function mod(name: string): any

const MyMod = mod("shader_demo")

MyMod.init = function () {
    const name = Assets("red_tint.hlsl")
    Shaders.set_enabled(name, true)
    Shaders.apply()
}

export = MyMod

Asset layout#

mods/my-mod/
├── mod.json
├── main.luau
└── assets/
    ├── red_tint.hlsl
    ├── icons/
    │   └── heart.png
    └── filters/
        └── crt.hlsl

HLSL files under assets/ are discovered automatically on load and after Shaders.reload(). Image files are loaded on demand when a CReact panel references their logical asset key.

For vanilla archive and file names when planning asset overrides, see the Archives tab of the TP Ultimate Spreadsheet.