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.
- What This Project Does
- Project Status
- Quick Start
- Example: Rotom Syntax
- Design Philosophy
- Contributing
- License
- Related Projects
Rotom provides a complete compiler toolchain for the Gen 4 Pokémon scripting engine:
- High-level syntax with control flow (
if/else,while,Jump) - Full legacy tool support: DSPRE
.scriptand Decomp.stranslation/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)
- 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: ... EndMovementorApplyMovement 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(...)andMenuGlobal(...)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 xis equivalent toif x != 0as an extension of this
- truthiness for variables and flags:
- 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/#definefor decomp header integration - GlobalScript Resolution: Symbolic cross-file global script references keyed by module, e.g.:
CallCommonScript CommonScripts::NewGame
- 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, likeUseVar 0x40F0+312. needs liveness analysis but could be implemented with before-and-after increment/decrement - Load
scripts.orderin 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/ifreconstruction - Derive includes for utilized text banks from map headers and GlobalScript table (DSPRE)
- installing it as a submodule
- jetbrains plugin
See INSTALL.md for setting up rotom for DSPRE/decomp/hge projects and extension support.
rotom init /path/to/projectrotom init creates rotom.toml and seeds .rotom/command_database/. Project compile/decompile commands use that config by default.
rotom convertMigrates 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.
rotom compilerotom compile -d .rotom/command_database/platinum_v2.json -i script.rotom -o script.binrotom decompile -d .rotom/command_database/platinum_v2.json -i script.bin -o script.rotomImportant
The single-file commands also accept folders for batch compilation/decompilation.
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.
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
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 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
Commands that return a result (those with a destVar/destVarID parameter defaulting to VAR_RESULT) can be used directly in conditions. The compiler automatically:
- Emits the command with
VAR_RESULTas the destination - 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
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.
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
Rotom is built with two core principles:
- Fidelity to source: Compile back to byte-matching binaries that match the original game scripts for smaller patch sizes and decomp compatibility.
- Developer experience: Clean syntax, rich error messages, seamless decomp integration
see CONTRIBUTING.MD for details.
MIT License. See LICENSE for details.
- 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
