Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Quiet-Wolfe/Rustic-Engine/llms.txt

Use this file to discover all available pages before exploring further.

Rustic Engine reads four primary asset formats. All of them are the same formats used by Psych Engine, so existing mods and tools work without conversion.

Sprite atlases

Sprites are packed into PNG spritesheets with Sparrow XML sidecar files (the format produced by TexturePacker). Each atlas consists of a .png image and a .xml file with the same base name, placed in the images/ directory.

XML structure

<TextureAtlas imagePath="BOYFRIEND.png">
  <SubTexture name="BF idle dance0000" x="0" y="0" width="154" height="183"
    frameX="-27" frameY="-5" frameWidth="214" frameHeight="204"/>
  <SubTexture name="BF idle dance0001" x="154" y="0" width="160" height="186"
    frameX="-24" frameY="-2" frameWidth="214" frameHeight="204"/>
  <SubTexture name="BF NOTE LEFT0000" x="0" y="183" width="130" height="173"
    frameX="-20" frameY="0" frameWidth="214" frameHeight="204"/>
</TextureAtlas>

SubTexture attributes

SubTexture.name
string
required
Frame name including a zero-padded index suffix (e.g. BF idle dance0000). The engine strips trailing digits to derive the animation prefix (BF idle dance).
SubTexture.x
number
required
X position of the frame within the spritesheet, in pixels.
SubTexture.y
number
required
Y position of the frame within the spritesheet, in pixels.
SubTexture.width
number
required
Width of the source frame region, in pixels.
SubTexture.height
number
required
Height of the source frame region, in pixels.
SubTexture.frameX
number
default:"0"
Horizontal offset applied when placing the trimmed frame back into the original canvas. Typically negative, representing trimmed left margin.
SubTexture.frameY
number
default:"0"
Vertical offset applied when placing the trimmed frame back into the original canvas. Typically negative, representing trimmed top margin.
SubTexture.frameWidth
number
Width of the original (untrimmed) canvas for this frame. Omit if no trimming was applied.
SubTexture.frameHeight
number
Height of the original (untrimmed) canvas for this frame. Omit if no trimming was applied.
SubTexture.rotated
boolean
default:"false"
Whether the frame was rotated 90 degrees clockwise in the sheet.

Animation name derivation

The engine groups frames into animations by stripping trailing digits from each SubTexture name:
  • BF idle dance0000 → animation prefix BF idle dance
  • BF NOTE LEFT0000 → animation prefix BF NOTE LEFT
A character JSON then maps these prefixes to named animations (see character format below).

Charts

Charts use the Psych Engine JSON format. The file is wrapped in a top-level "song" object.

Outer wrapper

{
  "song": {
    "song": "Bopeebo",
    "bpm": 100.0,
    ...
  }
}
The engine also handles the double-nested form { "song": { "song": { ... } } } produced by some older editors.

Song fields

body.song
string
Song name used for display and asset lookup.
body.bpm
number
required
Starting BPM. Individual sections can override this with changeBPM + bpm.
body.speed
number
default:"1.0"
Note scroll speed multiplier.
body.offset
number
default:"0"
Global audio offset in milliseconds applied to all note timing.
body.player1
string
default:"bf"
Character name for the player (boyfriend) slot.
body.player2
string
default:"dad"
Character name for the opponent slot.
body.gfVersion
string
default:"gf"
Character name for the girlfriend slot.
body.stage
string
default:"stage"
Stage name. The engine loads stages/{stage}.json.
body.needsVoices
boolean
default:"true"
Whether the song has a separate vocals track (Voices.ogg).
body.format
string
Format identifier. When set to psych_v1 or psych_v1_convert, note directions are treated as absolute (0–3 = player, 4–7 = opponent) and the legacy direction-flip conversion is skipped.
body.notes
object[]
required
Array of chart sections. See section format below.
body.events
array
Top-level events array. See events format below.
body.arrowSkin
string
Custom note skin path override for both sides.
body.arrowSkinDAD
string
Custom note skin override for the opponent side (VS Retrospecter extension).
body.arrowSkinBF
string
Custom note skin override for the player side (VS Retrospecter extension).

Section format

Each entry in the notes array is a section:
{
  "sectionNotes": [
    [0.0, 0, 0],
    [500.0, 5, 200.0],
    [1000.0, 2, 0, "Alt Animation"]
  ],
  "mustHitSection": true,
  "sectionBeats": 4.0,
  "altAnim": false,
  "gfSection": false,
  "bpm": 0.0,
  "changeBPM": false
}
body.sectionNotes
array[]
required
Array of individual notes. Each note is a JSON array: [strum_time, direction, sustain_length] or [strum_time, direction, sustain_length, note_type].
body.mustHitSection
boolean
default:"false"
In legacy (non-psych_v1) charts, controls how directions 0–7 are remapped to player vs opponent. In psych_v1 charts this field is ignored for direction calculation.
body.sectionBeats
number
default:"4.0"
Number of beats in this section.
body.altAnim
boolean
default:"false"
If true, all notes in this section use the alternate character animation.
body.gfSection
boolean
default:"false"
If true, all notes in this section animate the girlfriend character instead.
body.changeBPM
boolean
default:"false"
If true, the bpm field takes effect at the start of this section.

Note format

Each note inside sectionNotes is a JSON array:
[strum_time, direction, sustain_length, note_type?]
IndexFieldTypeDescription
0strum_timenumberNote spawn time in milliseconds
1directionnumber0–7. In psych_v1: 0–3 = player lanes (left/down/up/right), 4–7 = opponent lanes
2sustain_lengthnumberHold note length in milliseconds. 0 = tap note
3note_typestringOptional. Built-in values: "Alt Animation", "Hey!", "Hurt Note", "GF Sing", "No Animation". Any other string is a custom note type

Events format

Events in the top-level events array use this structure:
[
  2000.0,
  [
    ["Hey!", "BF", "0.6"],
    ["Add Camera Zoom", "0.04", "0.03"]
  ]
]
Each entry is [strum_time, [[name, value1, value2], ...]]. Multiple events can share the same strum time by listing them together in the inner array.

psych_v1 vs legacy format

Note directions are absolute: 0–3 always refer to player lanes and 4–7 always refer to opponent lanes, regardless of mustHitSection. Charts saved by Psych Engine 0.7+ use this format. The format field is set to "psych_v1" or "psych_v1_convert".
Note directions are relative to mustHitSection. When mustHitSection is false, directions 0–3 belong to the opponent and 4–7 belong to the player. The engine automatically converts these to the absolute layout before processing. Older Psych Engine charts and the base Friday Night Funkin’ charts use this format.

Characters

Character files live at characters/{name}.json.

Example

{
  "animations": [
    {
      "anim": "idle",
      "name": "BF idle dance",
      "fps": 24,
      "loop": false,
      "indices": [],
      "offsets": [-5, 0]
    },
    {
      "anim": "singLEFT",
      "name": "BF NOTE LEFT0",
      "fps": 24,
      "loop": false,
      "indices": [],
      "offsets": [5, -6]
    },
    {
      "anim": "singDOWN",
      "name": "BF NOTE DOWN0",
      "fps": 24,
      "loop": false,
      "indices": [],
      "offsets": [-10, -50]
    }
  ],
  "image": "characters/BOYFRIEND",
  "scale": 1.0,
  "sing_duration": 4.0,
  "healthicon": "bf",
  "position": [0, 350],
  "camera_position": [0, 0],
  "flip_x": true,
  "no_antialiasing": false,
  "healthbar_colors": [49, 176, 209]
}

Top-level fields

body.animations
object[]
required
List of animation entries. See animation fields below.
body.image
string
Path to the sprite atlas relative to images/, without extension (e.g. characters/BOYFRIEND). If empty, the engine falls back to images["baseSheet"].
body.images
object
Multi-atlas map of sheet name to image path. The "baseSheet" key is the primary atlas. Used when image is empty.
body.scale
number
default:"1.0"
Uniform scale multiplier applied to the character sprite.
body.sing_duration
number
default:"4.0"
How many steps the sing animation holds before the character returns to idle.
body.healthicon
string
Health icon name. The engine resolves images/icons/icon-{name}.png or images/icons/{name}.png.
body.position
number[]
default:"[0, 0]"
[x, y] spawn position of the character in the stage.
body.camera_position
number[]
default:"[0, 0]"
[x, y] camera offset applied when the camera is focusing on this character.
body.flip_x
boolean
default:"false"
Mirror the sprite horizontally. Typically true for the player character.
body.no_antialiasing
boolean
default:"false"
Disable bilinear filtering. Use for pixel-art characters.
body.healthbar_colors
number[]
default:"[161, 161, 161]"
[r, g, b] color of this character’s side of the health bar.
body.vocals_file
string
Custom vocals filename stem (without extension) inside songs/{songName}/.
body.skin
string
Custom note skin path override (e.g. notes/Notes_Ace).
body.healthBarImg
string
Custom health bar image name. Resolves to healthBars/{name}/bar.png.
body.stage_scale
object
Per-stage scale overrides. Keys are stage names (e.g. "nightflaid": 0.774).
body.stage_camera
object
Per-stage camera offset overrides. Keys are stage names, values are [x, y].

Animation fields

Each entry in the animations array:
body.anim
string
required
Internal animation name used by the engine (e.g. idle, singLEFT, singDOWN, singUP, singRIGHT, singLEFTmiss).
body.name
string
required
Sparrow XML prefix to match (trailing digits are stripped automatically). For example, "BF NOTE LEFT0" matches frames named BF NOTE LEFT0000, BF NOTE LEFT0001, etc.
body.fps
number
default:"24"
Playback speed in frames per second.
body.loop
boolean
default:"false"
Whether the animation loops when it reaches the last frame.
body.indices
number[]
Optional subset of frame indices to play. Empty array plays all frames in sequence.
body.offsets
number[]
default:"[0, 0]"
[x, y] pixel offset applied when this animation is active.
The engine looks for danceLeft in the animation list to determine whether a character uses the two-step idle (danceLeft / danceRight) instead of a single idle animation.

Stages

Stage files live at stages/{name}.json.

Example

{
  "directory": "",
  "defaultZoom": 0.9,
  "isPixelStage": false,
  "boyfriend": [770, 100],
  "girlfriend": [400, 130],
  "opponent": [100, 100],
  "hide_girlfriend": false,
  "camera_boyfriend": [0, 0],
  "camera_opponent": [0, 0],
  "camera_girlfriend": [0, 0],
  "camera_speed": 1.0,
  "disableZooming": false,
  "objects": [
    {
      "type": "sprite",
      "name": "stageback",
      "image": "stageback",
      "x": -600,
      "y": -200,
      "scale": [1.0, 1.0],
      "scroll": [0.9, 0.9],
      "alpha": 1.0,
      "color": "#FFFFFF",
      "angle": 0.0,
      "flipX": false,
      "flipY": false,
      "antialiasing": true,
      "animations": [],
      "firstAnimation": ""
    }
  ]
}

Stage fields

body.directory
string
default:""
Subdirectory name under the search roots used to locate stage images. Leave empty to use the root images/ path.
body.defaultZoom
number
default:"0.9"
Initial camera zoom when the song starts.
body.isPixelStage
boolean
default:"false"
Disables antialiasing on all stage objects and characters for pixel-art stages.
body.boyfriend
number[]
default:"[770, 100]"
[x, y] spawn position for the player character.
body.girlfriend
number[]
default:"[400, 130]"
[x, y] spawn position for the girlfriend character.
body.opponent
number[]
default:"[100, 100]"
[x, y] spawn position for the opponent character.
body.hide_girlfriend
boolean
default:"false"
If true, the girlfriend character is not shown on this stage.
body.camera_boyfriend
number[]
default:"[0, 0]"
[x, y] camera offset applied when the camera focuses on the player.
body.camera_opponent
number[]
default:"[0, 0]"
[x, y] camera offset applied when the camera focuses on the opponent.
body.camera_girlfriend
number[]
default:"[0, 0]"
[x, y] camera offset applied when the camera focuses on the girlfriend.
body.camera_speed
number
default:"1.0"
Multiplier for how fast the camera moves toward its target.
body.camera_start
number[]
Optional [x, y] starting camera position. When set, the camera opens here instead of focusing on the opponent.
body.disableZooming
boolean
default:"false"
If true, disables beat-driven camera zoom pulses on this stage.
body.objects
object[]
Array of sprite objects to draw as the stage background and foreground.

Stage object fields

body.type
string
Object type identifier. Use "sprite" for standard image sprites.
body.name
string
Unique name for the object. Lua scripts can reference objects by this name.
body.image
string
Image path relative to images/ (and optionally the stage directory), without extension.
body.x
number
default:"0"
Horizontal position in the stage world.
body.y
number
default:"0"
Vertical position in the stage world.
body.scale
number[]
default:"[1.0, 1.0]"
[x, y] scale factors.
body.scroll
number[]
default:"[1.0, 1.0]"
[x, y] parallax scroll factors. 0.9 makes the object scroll slower than the camera (background depth effect).
body.alpha
number
default:"1.0"
Opacity from 0.0 (transparent) to 1.0 (opaque).
body.color
string
default:"#FFFFFF"
Hex color tint applied to the sprite (e.g. "#FF0000" for red).
body.angle
number
default:"0"
Rotation in degrees.
body.flipX
boolean
default:"false"
Mirror the sprite horizontally.
body.flipY
boolean
default:"false"
Mirror the sprite vertically.
body.antialiasing
boolean
default:"true"
Enable bilinear filtering on this object. Set to false for pixel art.
body.animations
object[]
Optional list of animation definitions for animated stage sprites. Uses the same structure as character animations (anim, name, fps, loop, indices, offsets).
body.firstAnimation
string
Name of the animation to play when the stage loads (matches an anim field in animations).

Build docs developers (and LLMs) love