Skip to content

Repository files navigation

Animated sprite of Rotom from Pokemon Black and White

codecov

rotom

rotom is a high-level scripting language and toolchain for Pokémon Generation 4 (Diamond/Pearl/Platinum/HGSS) romhacking/modding projects. Inspired by poryscript for Gen 3.

Important

Rotom is still a work in progress. Expect things to change and break.


Table of Contents


What This Project Does

Rotom provides a complete compiler toolchain for the Gen 4 Pokémon scripting engine:

Core Features

  • High-level syntax with control flow (if/else, while, Jump)
  • Full legacy tool support: DSPRE .script and Decomp .s translation/conversion layer
  • JSON Levelscripts: levelscripts now exist in declarative JSON format
  • Byte-Matching Compilation: de- and compilation preserve all semantics and oddities from original script files.
  • Decomp integration - Automatically loads constants from your pokeplatinum/pokediamond headers/jsons
  • Fall-through semantics - Preserves the game engine's organization where scripts flow into each other
  • Decompiler - Disassemble binary scripts back to source (normal scripts to .rotom, levelscripts to JSON)
  • Editor support - LSP support and editor integrations for diagnostics, completion, hover, go-to-definition, inlay hints, syntax highlighting, etc. (see Editor Support)

Language Features

  • Scripts with explicit jump table slots, callable from events/levelscripts: script Main #1:, or #[1-3, 5, 6] for multiple slots at once
  • Private labels for internal code organization (these were called functions in DSPRE): HelperCode:
  • Aliases for constants: alias 0x800C as VAR_RESULT
  • Actions for named or inline movement data: action WalkPattern: ... EndMovement or ApplyMovement LOCALID_PLAYER, action(WalkFastEast 2)
  • Rich control flow: Nested if/else/endif, while/endwhile, match/endmatch, break
  • Menu builders: Easily define list menus with Menu(...) and MenuGlobal(...) builders
  • Autovar: Commands that return results can be used directly in conditions (e.g., if CheckPlayerOnBike() then), inspired by the feature of the same name from PoryScript
    • truthiness for variables and flags: if x is equivalent to if x != 0 as an extension of this
  • String literals: Write message text directly in your script without needing to touch text archives. Use format() to let the compiler handle word wrapping
  • Preprocessor: #include / #define for decomp header integration
  • GlobalScript Resolution: Symbolic cross-file global script references keyed by module, e.g.: CallCommonScript CommonScripts::NewGame

To Do

  • Vendor JSON DBs for CI purposes
  • using autovar as variable params like PlayCry GetPlayerStarterSpecies()
  • Format string argument support, e.g. format("Hello, {}!", BufferPlayerName) for easy string formatting
  • runtime arithmetic via IncrementVar/DecrementVar, like UseVar 0x40F0+312. needs liveness analysis but could be implemented with before-and-after increment/decrement
  • Load scripts.order in DSPRE if present to allow for semantic file naming
  • Graph colouring for variable liveness analysis, which will allow for:
    • Variable allocation for automatic assignment
    • Fully-featured for loops, will need graph colouring for counting
  • Decompiler pattern matching for match/while/if reconstruction
  • Derive includes for utilized text banks from map headers and GlobalScript table (DSPRE)

Ideas from poryscript that havent made it to rotom yet (may or may not be implemented)

  • installing it as a submodule
  • jetbrains plugin

Quick Start

Installation

See INSTALL.md for setting up rotom for DSPRE/decomp/hge projects and extension support.

Initialize a project

rotom init /path/to/project

rotom init creates rotom.toml and seeds .rotom/command_database/. Project compile/decompile commands use that config by default.

Convert legacy scripts

rotom convert

Migrates a project's existing legacy scripts to Rotom source: DSPRE .script files and decomp .s files become .rotom (levelscripts become .json), with the originals backed up first. rotom init reports how many convertible files it finds and asks you whether to convert them rightaway, without needing to run this; pass --dry-run to preview the conversions without writing anything.

Note: for DSPRE, it actually freshly disassembles from the binaries. This is done to avoid database and symbol conflicts.

Compile a project

rotom compile

Compile a single script with an explicit database

rotom compile -d .rotom/command_database/platinum_v2.json -i script.rotom -o script.bin

Decompile a single binary with an explicit database

rotom decompile -d .rotom/command_database/platinum_v2.json -i script.bin -o script.rotom

Important

The single-file commands also accept folders for batch compilation/decompilation.

Editor Support

Rotom includes rotom-lsp, a Language Server Protocol server for editor features such as diagnostics, autocomplete, hover, go-to-definition, inlay hints, signature help, and code lenses.

Editor integrations for VS Code, Zed, and Neovim are being developed alongside Rotom in rotom-extensions. Tree-sitter grammar and highlighting support live in tree-sitter-rotom.


Example: Rotom Syntax

in-depth technical information about the rotom spec can be found in the spec

// === Constants ===
alias 0x800C as VAR_RESULT // this is just an example, these constants are both included in the standard database
alias 0x800D as VAR_LASTTALKED

// === Public script (in jump table) ===
script Main #1:
    // If it's an NPC
    if VAR_LASTTALKED != 0 then
        Call TalkToNPC
    else
        Message 1  // It's a sign
    endif
    End

// === Private label (helper code) ===
TalkToNPC:
    FacePlayer
    Message 2
    WaitButton
    Return

// === Movement action ===
action NPC_WalkAway:
    WalkDown 3
    WalkLeft 2
    FaceDown
EndMovement

Match Statements

Use match to dispatch based on a variable's value:

Important

The branches are exclusive and have no fall-through semantics. A case whose body is a single Call or Jump is optimized into a CallIf or GoToIf to the target respectively; any other case body uses exclusive Jump branching. If you really need fall-through semantics, that effect can be achieved with labels.

script HandleChoice #1:
    match VAR_RESULT with
        case 0:
            Message 1
        case 1, 2:
            Message 2
        else:
            Message 3
    endmatch
    End

Menu Builders

Menu builders turn a list of entries into the game-specific menu commands and dispatch the selected entry to its target label:

script ChooseAction #1:
    Menu(
        "Talk" -> Talk,
        ("Leave", "Are you sure?") -> Leave,
    ).cancel(Leave)
    End

Talk:
    Message "Hello!"
    End

Leave:
    End

Menu stores the entry text in the script's local text archive. MenuGlobal uses the game's global menu-entry archive (not recommended, as this only has very limited space). A builder accepts 1 to 28 entries. The optional (label, hover) form adds help text that changes with the selected entry. In DPPt this turns the menu into a list menu; HGSS displays the help text on the top screen.

label -> target, .position(), .cursor(), .prompt(), .cancel(target), and .cancel(label -> target) work in every game and menu type.

The other available syntax by game and menu type is:

Syntax D/P normal D/P list Platinum normal Platinum list HGSS touch/list
(label, hover) -> target No Yes No Yes Yes
.cancel((label, hover) -> target) No Yes No Yes Yes
.scrollable() / .scrollable(bool) No Yes No Yes No
.columns(count) Yes No Yes No No
.width(tiles) No No No Yes No
.anchor(left | right) No No Yes* Yes* No

Hover entries, scrolling and .width(...) require list mode and combining either with .scrollable(false) emits a warning. DPPt normal menus emit warnings when the list is too long to display without messing up the prompt. Multi-column menus require the total entry count to be divisible by the column count. Platinum right anchoring is limited to one-column normal menus and auto-width list menus, so .anchor(right) cannot be combined with .columns(...) or .width(...). .cancel(target) sets the B-button fallback without adding an entry. The dispatch form (text -> target) also adds the supplied label, and optional hover text, as a selectable final entry.

script ChooseItem #1:
    MenuGlobal(
        ("Potion", "Restore HP") -> UsePotion,
    )
    .position(3, 2)
    .prompt("Choose an item.")
    .width(8)
    .cancel(("Cancel", "Go back") -> Cancel)
    End

Autovar: Commands in Conditions

Commands that return a result (those with a destVar/destVarID parameter defaulting to VAR_RESULT) can be used directly in conditions. The compiler automatically:

  1. Emits the command with VAR_RESULT as the destination
  2. Compares the result appropriately
script BikeCheck #1:
    // Bare call - equivalent to: CheckPlayerOnBike VAR_RESULT; if VAR_RESULT == 1
    if CheckPlayerOnBike() then
        Message 1
    endif

    // With explicit comparison
    if ShowYesNoMenu() == 0 then
        Message 2
    endif

    // In match statements
    match ShowYesNoMenu() with
        case 0:
            Call HandleNo // note: these get optimized into CallIf instructions
        case 1:
            Call HandleYes
    endmatch
    End

Commands with additional parameters work too:

script ItemCheck #1:
    // AddItem(item, amount) - destVarID is automatically VAR_RESULT
    if AddItem(ITEM_POTION, 5) then
        Message 1  // Success
    else
        Message 2  // Bag full
    endif
    End

String Literals

Instead of managing a separate text file and referencing messages by number, you can write text directly in your script:

script NPC #1:
    Message "Hello, trainer!"
    WaitButton
    End

This works with any command that takes a text argument: menu entries, bank-specific messages, and more:

AddMenuEntryImm "Option A", 4
MessageFromBank 1, "text here"

The compiler takes care of storing the text in the right place automatically.

Strings can span multiple lines. Leading whitespace at the start of each new line is stripped, so you can indent freely without it showing up in-game:

script LongSpeech #1:
    Message "Hello there! I've been waiting
             for a trainer as strong as you
             to come along."
    WaitButton
    End

Wrap a string in format() to have the compiler automatically insert word-wrap breaks so the text fits the in-game dialog box:

script Explanation #1:
    Message format("This text is too long for one line but format will handle the wrapping for you automatically.")
    WaitButton
    End

The formatter also respects manually inserted line breaks, so if you dont like a certain line break placement or want to end a line early, you can insert \n or \r to force a line break at that point.

Important

String literals and format() only work when compiling as part of a project; single-file compilation doesn't have the context to know where to store the text.

Break Statement

Use break to exit a while loop early:

script SearchLoop #1:
    while VAR_COUNTER < 10 do
        if VAR_RESULT == TARGET_VALUE then
            break
        endif
        AddVar VAR_COUNTER, 1
    endwhile
    End

Design Philosophy

Rotom is built with two core principles:

  1. Fidelity to source: Compile back to byte-matching binaries that match the original game scripts for smaller patch sizes and decomp compatibility.
  2. Developer experience: Clean syntax, rich error messages, seamless decomp integration

Contributing

see CONTRIBUTING.MD for details.


License

MIT License. See LICENSE for details.


Related Projects

  • uxie - Data fetching library for Gen 4 romhacking. Used heavily by Rotom.
  • chatot - Text processing library for Gen 4 romhacking
  • scrcmd-database - Reference script command databases derived from DSPRE and decomps.
  • poryscript - High-level scripting for Gen 3 (inspiration)
  • pokeplatinum - Pokemon Platinum decompilation
  • pokeheartgold - Pokemon HeartGold decompilation

"This bizarre Pokémon appears to be a will-o'-the-wisp powered by electricity. Be wary, as Rotom is both smart and mischievous." -- Pokédex entry in Pokémon: Legends Arceus

About

A pokemon script assembler/disassembler

Resources

Contributing

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages