Interactable Texture Configuration

Configure textures for placeable, interactable objects like doors, chests, and furnaces.

Interactables are objects the player can place and interact with: doors, chests, furnaces, crafting benches, and any custom interactable added by a mod. Their textures are declared in a YAML config file, one per mod.


File Location

Config file:

mods/<your-mod>/assets/textures/interactables/interactables.yaml

Texture images go in the same folder:

mods/<your-mod>/assets/textures/interactables/<name>.png

The file name must match the name field exactly (case-sensitive), with spaces kept as spaces in the file name.


Minimal Example

textures:
  - name: my_bench
    sprites:
      - name: idle
        frame-count: 1
    frame-width: 32
    frame-height: 16

This registers an interactable named my_bench from my_bench.png. It will be referred to as YourMod:my_bench anywhere an interactable name is expected.


Full Field Reference

textures:
  - name: my_door              # Required. Registered as YourMod:my_door
    override: Creation:door    # Optional. Replace another mod's registered interactable (see below)
    light-mask: my_door_mask   # Optional. Light mask texture (see below)
    frame-width: 16            # Required. Width of one frame in pixels
    frame-height: 64           # Required. Height of one frame in pixels
    sprites:                # Required. One or more named animation sequences
      - name: door_swing       # Required. Animation name
        frame-count: 2         # Required. Number of frames in this animation
        frame-time-ms: 100     # Optional. Milliseconds per frame (default: 100)
        frame-durations-ms:    # Optional. Per-frame timing override (see below)
          - count: 2
            duration-ms: 150
    states:                    # Optional. Named static poses
      - name: door_closed      # State name used by server scripts
        sprite: door_swing     # Which sprite this state uses
        frame: first           # first, last, or a frame number (0-based)
      - name: door_open
        sprite: door_swing
        frame: last
FieldRequiredDescription
nameYesIdentifier for this interactable. Registered as ModName:name.
overrideNoFull name of another mod’s interactable to replace (e.g. Creation:door). See Overrides.
light-maskNoName of a separate PNG used as the light emission mask. See Light Masks.
frame-widthYesWidth of a single frame in pixels.
frame-heightYesHeight of a single frame in pixels.
spritesYesList of named animation sequences. At least one required.
sprites[].nameYesAnimation name, used by server scripts to trigger playback.
sprites[].frame-countYesHow many frames this animation plays through.
sprites[].frame-time-msNoMilliseconds per frame for uniform timing. Default: 100.
sprites[].frame-durations-msNoPer-frame timing override. See Per-Frame Timing.
statesNoNamed static poses the server can snap the interactable to without animation.
states[].nameYesState name, used by server scripts via entity:set_state(...).
states[].spriteYesWhich sprite this state uses.
states[].frameYesfirst, last, or a zero-based frame index.

Sprite Sheet Format

All sprites for an interactable share one sprite sheet image. The image is a grid:

  • Columns — frames, read left to right. Total image width = frame-width × column count.
  • Rows — sprites, stacked top to bottom. Total image height = frame-height × sprite count.

The first sprite declared in the YAML corresponds to the top row; the second to the row below it, and so on.

[ var 0, frame 0 ][ var 0, frame 1 ][ var 0, frame 2 ]   ← row 0 (first sprite)
[ var 1, frame 0 ][ var 1, frame 1 ][ var 1, frame 2 ]   ← row 1 (second sprite)

All rows must have the same number of columns. If a sprite uses fewer frames than the column count, fill the unused columns with transparent/empty pixels.


Per-Frame Timing

By default all frames display for the same duration (frame-time-ms). To give individual frames different hold times, add frame-durations-ms to the sprite.

Two entry formats are supported and can be mixed freely:

Flat — one value per frame:

frame-durations-ms: [80, 80, 120, 120, 200]

Batch — a duration applied to several consecutive frames:

frame-durations-ms:
  - count: 3
    duration-ms: 80
  - count: 2
    duration-ms: 200

Both produce the same result: frames 0–2 last 80 ms each, frames 3–4 last 200 ms each.

Note: The total number of entries (after expanding batches) must equal frame-count exactly. If the counts do not match, frame-durations-ms is ignored and frame-time-ms is used for all frames instead.


States

States let the server snap an interactable to a specific visual pose without playing an animation. Use frame: first for the first frame, frame: last for the last frame, or a zero-based number for any specific frame.

states:
  - name: door_closed
    sprite: door_swing
    frame: first
  - name: door_open
    sprite: door_swing
    frame: last

Server scripts set the state with entity:set_state("door_closed") — the string must match the state’s name exactly (state keys are stored verbatim and matched literally).

animate direction, and why it has to agree with your states

entity:animate("sprite_name", reverse) plays a sprite once:

reversePlaysEnds on
falsefirst frame → lastthe last frame
truelast frame → firstthe first frame

The state decides the resting pose; the animation only decides how you get there. When an animation finishes, the object snaps to the frame its current state names — so set_state is what determines how the object is drawn once it settles, and reverse only picks which way the sprite travels on the way. Set both:

-- door_swing runs closed (frame 0) -> open (frame 3)
entity:animate("door_swing", collidable)   -- closing = reverse, opening = forward
if collidable then
    entity:set_state("door_closed")         -- frame: first — the resting pose
else
    entity:set_state("door_open")           -- frame: last
end

Choose reverse so the sprite travels toward the state’s frame: a forward pass for a state on frame: last, a reverse pass for one on frame: first. Get it backwards and the swing plays the wrong way and then snaps — visibly odd, but the object still comes to rest drawn correctly.

This used to be load-bearing rather than cosmetic. Nothing re-applied the state to the sprite, so the frame an animation stopped on stayed forever, and a mismatch left an object drawn as its own opposite indefinitely — a door standing open but rendered shut and still solid. The state is now authoritative for the resting pose, so a mismatch is a visual glitch rather than a permanent lie.

State names are matched exactly

Three files have to agree on these names, and nothing cross-checks them at load — both the state and sprite lookups are exact string matches:

Written inAs
this texture configa states[].name and a sprites[].name
interactable-config.yamldefault-state
your Luaentity:set_state("…") and entity:animate("…")

A name that matches nothing is not an error — the object simply keeps the sprite it already had. It now logs a warning naming the object and the unresolved name, so check the client log first when an interactable’s art never moves.

Case and separators are load-bearing here, unlike elsewhere in the schema. These are names you invent, not closed-set values, so DOOR OPEN, door-open and door_open are three different names. Nothing normalizes them. The shipped door and chest were broken this way for exactly this reason: their scripts called set_state("DOOR CLOSED") and animate("DOOR SWING") against door_closed and door_swing, so neither the pose nor the swing animation ever played. Pick one spelling per object and use it in all three files.

A missing default-state counts as a mismatch. Omitting it leaves the object on the engine fallback default, which is only special-cased to mean “variation 0, frame 0” — if that isn’t the pose you want, declare a state and name it in default-state. The shipped chest had this bug too.


Light Masks

A light mask controls how an interactable interacts with the lighting system. It must be a PNG the same size as the full sprite sheet. White pixels (255, 255, 255) emit full light; black pixels (0, 0, 0) block light entirely.

  - name: christmas_tree
    sprites:
      - name: christmas_tree
        frame-count: 1
    frame-width: 80
    frame-height: 112
    light-mask: christmas_tree_mask

Place christmas_tree_mask.png in the same folder as the main texture. If no light-mask is specified, a fully opaque white mask is generated automatically (the interactable emits no special light).


Reusing Configuration with YAML Anchors

Interactables that share animation sets, states, or timing (e.g. several doors or chests with the same swing/open cycle) don’t need to duplicate that YAML by hand. Standard YAML anchors (&name) and aliases (*name) let you define a block once and reuse it anywhere in the same file — the parser resolves the alias to a full copy of the anchored block before the interactable config is read, so no special loader support is needed.

Anchor whichever block is actually shared — a single sprite list, a states block, or anything else — and leave the rest of the entry free to differ:

textures:
  - name: door
    sprites: &door_swing
      - name: door_swing
        frame-count: 2
        frame-durations-ms:
          - count: 2
            duration-ms: 150
    states: &door_states
      - name: door_closed
        sprite: door_swing
        frame: first
      - name: door_open
        sprite: door_swing
        frame: last
    frame-width: 16
    frame-height: 64
  - name: vault_door
    sprites: *door_swing   # identical swing animation, reused
    states: *door_states      # identical open/closed states, reused
    frame-width: 32           # its own frame size
    frame-height: 96

Note: An alias is a full substitution, not a merge — *door_swing copies the entire anchored block as-is. You cannot reuse a block and then tweak just one field inside it; anchor only the smallest block that is genuinely identical across entries, and declare anything that varies outside of it.


Overrides

Your mod can replace the texture used for any interactable from another mod without changing that mod’s files. Add override with the full mod-namespaced name of the interactable to replace:

textures:
  - name: door
    override: Creation:door
    sprites:
      - name: door_swing
        frame-count: 2
        frame-durations-ms:
          - count: 2
            duration-ms: 150
    states:
      - name: door_closed
        sprite: door_swing
        frame: first
      - name: door_open
        sprite: door_swing
        frame: last
    frame-width: 16
    frame-height: 64
  • The name field is still required (it is used to locate your replacement PNG in your mod’s folder).
  • Your texture file must match the frame dimensions of the original interactable.
  • Your override entry fully replaces the animation configuration of the original, so you must redeclare all sprites, states, and timing you want to keep.
  • The states and timing declared in your override must be compatible with server scripts that reference that interactable by name.

Complete Example

A door with two sprites (a single animated swing plus an alternate locked state):

textures:
  - name: door
    sprites:
      - name: door_swing
        frame-count: 2
        frame-durations-ms:
          - count: 2
            duration-ms: 150
      - name: door_locked
        frame-count: 1
        frame-time-ms: 100
    states:
      - name: door_closed
        sprite: door_swing
        frame: first
      - name: door_open
        sprite: door_swing
        frame: last
      - name: door_locked
        sprite: door_locked
        frame: first
    frame-width: 16
    frame-height: 64

Sprite sheet layout for this door (16 px wide × 64 px tall frames, 2 columns × 2 rows):

[ CLOSED frame ][ OPEN frame ]    ← row 0: DOOR SWING
[ LOCKED frame ][ (empty)    ]    ← row 1: DOOR LOCKED (second column unused)

Total image size: 32 × 128 px.


Ambient Particle Emission

Note: this section configures behavior, not textures — it belongs in the interactable/tree definition YAML (for interactables: the file referenced by interactable-config in your mod’s manifest, the one declaring each interactable’s name, hitbox, time-to-break-ms, etc.; for trees: tree-config.yaml), not in the interactables.yaml/trees.yaml texture files described above.

Both interactables and trees can ambiently emit particles at random, mirroring the particles config blocks support (e.g. a campfire crackling, a furnace venting smoke, a magic crystal glowing, leaves drifting off a tree). Add an optional particles section to an entry:

interactables:
  - name: campfire
    hitbox: { ... }
    particles:
      - emit-random:
          - name: smoke               # Required. Particle name (namespaced automatically if no ':')
            chance: 0.4               # Optional, default 0.1. Roll 0.0–1.0 each time the cooldown elapses
            cooldown-ms: 800          # Optional, default 1000. Milliseconds between emission attempts
            lifetime-ms: 1500         # Optional, default 1. Particle lifetime in milliseconds
            has-lifetime: true        # Optional, default true. Whether the lifetime applies (smart particles only)
            x-offset-min: -0.4     # Optional, default 0.0. Random x offset range (world units) from the object's location
            x-offset-max: 0.4
            y-offset-min: 0.0      # Optional, default 0.0. Random y offset range (world units) from the object's location
            y-offset-max: 0.6
trees:
  - name: cherry_tree
    type: cherry_tree
    particles:                        # Tree-level default, applies to every sprite below
      - emit-random:
          - name: cherry_leaf
            chance: 0.1
            cooldown-ms: 5000
            lifetime-ms: 10000
            x-offset-min: -3.0
            x-offset-max: 3.0
            y-offset-min: 0.0
            y-offset-max: 3.0
    sprites:
      - name: cherry_tree             # No 'particles' here — uses the tree-level config above
        hitbox: { ... }
      - name: cherry_tree_2
        hitbox: { ... }
        particles:                    # Sprite-level override — replaces the tree-level config for this sprite only
          - emit-random:
              - name: cherry_leaf
                chance: 0.2
                cooldown-ms: 3000
                lifetime-ms: 10000
                x-offset-min: -3.0
                x-offset-max: 3.0
                y-offset-min: 0.0
                y-offset-max: 3.0

Each emit-random entry tracks its own cooldown independently per instance, with a randomized initial phase so multiple instances of the same tree/interactable don’t emit in lockstep. Particles are positioned at the object’s location plus the rolled x-offset/y-offset. Emission is purely cosmetic and entirely client-driven — no client-update hook is required for particles to emit (trees have none at all).

A particles block on a tree sprite entirely replaces the tree-level one for that sprite (it does not merge). Sprites that omit particles fall back to the tree-level config, if any. Interactables have no sprite concept and only support the top-level particles block.