Skip to main content

Tools for compiling and disassembling Pokémon Red and Pokémon Crystal.

Project description

Pokémon Crystal utilities and extras
==============================

`crystal.py` parses the ROM and provides convenient classes to dump human-readable ASM with the global `to_asm()` method. This ASM can then be compiled back into the original ROM. Currently it parses map headers, "second" map headers, map event headers, map script headers, map triggers, map "callbacks", map blockdata, xy triggers, warps, people-events, texts and scripts.

#### Simple ASM generation example

Note: throughout these examples it is possible to use `reload(crystal)` instead of `import pokemontools.crystal`. Once the module is loaded a first time, it must be reloaded if the file changes and the updates are desired.

```python
import pokemontools.crystal as crystal

# parse the ROM
crystal.run_main()

# create a new dump
asm = crystal.Asm()

# insert the first 10 maps
x = 10
asm.insert_with_dependencies(crystal.all_map_headers[:x])

# dump to extras/output.txt
asm.dump()
```

After running those lines, `cp extras/output.txt main.asm` and run `git diff main.asm` to confirm that changes to `main.asm` have occurred. To test whether or not the newly inserted ASM compiles into the same ROM, use: `make clean && make`. This will complain very loudly if something is broken.

#### Testing

Unit tests cover most of the classes.

```bash
python tests.py
```

#### Parsing a script at a known address

Here is a demo of how to investigate a particular script, starting with only an address to a known script (0x58043). In this case, the script calls the `2writetext` command to show some dialog. This dialog will be shown at the end of the example.

```python
import pokemontools.crystal as crystal

# parse the script at 0x58043 from the map event header at 0x584c3
# from the second map header at 0x958b8
# from the map header at 0x941ae
# for "Ruins of Alph Outside" (map_group=3 map_id=0x16)
script = Script(0x58043)

# show the script
print script.to_asm()

# what labels does it point to in the to_asm output?
# these must be present in the final asm file for rgbasm to compile the file
objdeps = script.get_dependencies()
print str(objdeps)

# the individual commands that make up the script
commands = script.commands
print str(commands)

# the 3rd command is 2writetext and points to a text
thirdcommand = script.commands[2]
print thirdcommand
# <crystal.2writetextCommand instance at 0x8ad4c0c>

# look at the command parameters
params = thirdcommand.params
print params
# {0: <crystal.RawTextPointerLabelParam instance at 0x8ad4b0c>}

# 2writetext always has a single parameter
definition_param_count = len(getattr(crystal, "2writetextCommand").param_types.keys())
current_param_count = len(params.keys())
assert definition_param_count == current_param_count, "this should never " + \
"happen: instance of a command has more parameters than the " + \
"definition of the command allows"

# get the first parameter (the text pointer)
param = params[0]
print param
# <crystal.RawTextPointerLabelParam instance at 0x8ad4b0c>

# RawTextPointerLabelParam instances point to their text
text = param.text
print text
# <crystal.TextScript instance at 0x8ad47ec>

# now investigate this text appearing in this script in "Ruins of Alph Outside"
print text.to_asm()
```

The final output will be the following text.

```asm
db You can't use 'macro parameter character #' in math mode4f
db "DEX, isn't it?", 55;...However,thisisnothowthatTextScriptobjectwouldappearinthefinalASM.Toseehowitwouldappearinmain.asmonceinserted,youwouldrunprintcrystal.toasm(text)togetthefollowing.asmUnknownText0x580c7:;0x580c7db0, "Hm? That's a #-", 4fdb"DEX,isntit?",55
db "May I see it?", 51db"Therearesomany",4f
db "kinds of #MON.", 51db"Hm?Whatsthis?",51
db "What is this", You can't use 'macro parameter character #' in math mode51
db "It looks like the", 4fdb"strangewritingon",51
db "the walls of the", 4fdb"RUINS.",51
db "If those drawings", You can't use 'macro parameter character #' in math mode55
db "MON, there should", 55db"bemanymore.",51
db "I know! Let me up-", You can't use 'macro parameter character #' in math mode55
db "DEX. Follow me.", You can't use 'macro parameter character #' in math mode3c, 19, 15, 7,0, 255, 255, 0,0,UnknownScript0x58043,0703
```

within this:

```asm
MapEventHeader_0x584c3: ; 0x584c3
; filler
db 0, 0

; warps
db 11
warp_def 11,2, 1, GROUP_RUINS_OF_ALPH_HO_OH_CHAMBER, MAP_RUINS_OF_ALPH_HO_OH_CHAMBER
warp_def 7,e, 1, GROUP_RUINS_OF_ALPH_KABUTO_CHAMBER, MAP_RUINS_OF_ALPH_KABUTO_CHAMBER
warp_def 1d,2, 1, GROUP_RUINS_OF_ALPH_OMANYTE_CHAMBER, MAP_RUINS_OF_ALPH_OMANYTE_CHAMBER
warp_def 21,10, 1, GROUP_RUINS_OF_ALPH_AERODACTYL_CHAMBER, MAP_RUINS_OF_ALPH_AERODACTYL_CHAMBER
warp_def d,a, 1, GROUP_RUINS_OF_ALPH_INNER_CHAMBER, MAP_RUINS_OF_ALPH_INNER_CHAMBER
warp_def b,11, 1, GROUP_RUINS_OF_ALPH_RESEARCH_CENTER, MAP_RUINS_OF_ALPH_RESEARCH_CENTER
warp_def 13,6, 1, GROUP_UNION_CAVE_B1F, MAP_UNION_CAVE_B1F
warp_def 1b,6, 2, GROUP_UNION_CAVE_B1F, MAP_UNION_CAVE_B1F
warp_def 5,7, 3, GROUP_ROUTE_36_RUINS_OF_ALPH_GATE, MAP_ROUTE_36_RUINS_OF_ALPH_GATE
warp_def 14,d, 1, GROUP_ROUTE_32_RUINS_OF_ALPH_GATE, MAP_ROUTE_32_RUINS_OF_ALPH_GATE
warp_def 15,d, 2, GROUP_ROUTE_32_RUINS_OF_ALPH_GATE, MAP_ROUTE_32_RUINS_OF_ALPH_GATE

; xy triggers
db 2
xy_trigger 1, e,b, 0,UnknownScript0x58031,0, 0xytrigger1,f, a,0, UnknownScript_0x5803a, 0,0

; signposts
db 3
signpost 8, 16, 0,UnknownScript0x580b1signpost16,12,0, UnknownScript_0x580b4
signpost 12, 18, 0,UnknownScript0x580b7;peopleeventsdb5personevent27, 24, 8, 6,0, 255, 255, 2,1,Trainer0x58089,ffff
person_event 3c,19,15,7, 0,255,255,0, 0, UnknownScript_0x58043, 0703personevent3a, 21, 17, 3,0, 255, 255, a0,0,UnknownScript0x58061,078e
person_event 27,15,18,2, 11,255,255,b0, 0, UnknownScript_0x58076, 078fpersonevent27, 12, 16, 7,0, 255, 255, 80,0,UnknownScript0x5807e,078f
; 0x58560
```

#### Helpful ROM investigation tools

```python
import pokemontools.crystal as crystal

# load the bytes
crystal.load_rom()

# get a sequence of bytes
crystal.rom_interval(0x112116, 10)
# ['0x48', '0x54', '0x54', '0x50', '0x2f', '0x31', '0x2e', '0x30', '0xd', '0xa']
crystal.rom_interval(0x112116, 10, strings=False)
# [72, 84, 84, 80, 47, 49, 46, 48, 13, 10]

# get bytes until a certain byte
crystal.rom_until(0x112116, 0x50, strings=False)
# ['0x48', '0x54', '0x54']
# [72, 84, 84]

# or just look at the encoded characters directly
crystal.rom[0x112116:0x112116+10]
# 'HTTP/1.0\r\n'

# look at a text at 0x197186
text = crystal.parse_text_at2(0x197186, 601, debug=False)
print text
```

That last text at 0x197186 will look like:

```python
"""
OAK: Aha! So
you're !
I'm OAK! A #MON
researcher.
I was just visit-
ing my old friend
MR.#MON.
I heard you were
running an errand
for PROF.ELM, so I
waited here.
Oh! What's this?
A rare #MON!
...
"""
```

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page