pokecrystal/extras
yenatch 9fbe7ea423 make careless use of gfx.py less fatal 2013-01-13 00:40:13 -05:00
..
pkmnasm simple lexer 2012-04-26 00:31:53 -05:00
README.md improve extras/README.md opening sentence 2012-05-14 10:42:43 -05:00
__init__.py gah forgot a file to let preprocessor import macros 2012-04-26 14:16:01 -05:00
chars.py rename textpre.py -> preprocessor.py 2012-04-26 16:58:20 -05:00
comparator.py find functions based on first few similar bytes 2012-05-31 02:02:23 -05:00
crystal.py clean up whitespace 2012-12-28 12:37:58 -06:00
gbz80disasm.py Merge remote branch 'remotes/yenatch/master' 2012-12-23 02:57:21 -06:00
gfx.py make careless use of gfx.py less fatal 2013-01-13 00:40:13 -05:00
graph.py better graph-generating routines 2012-07-01 08:41:38 -05:00
item_constants.py redo item constants (some were missed?) 2012-04-27 15:52:57 -05:00
labels.py more de-dependency-looping 2012-06-20 01:42:33 -05:00
move_constants.py fix quick syntax error 2012-05-20 03:11:22 -05:00
old_parse_scripts.py add encoding to old_parse_scripts.py 2012-04-18 20:55:16 -05:00
pksv.py Move some data back to avoid a circular import. 2012-04-17 23:33:23 -05:00
pointers.py more de-dependency-looping 2012-06-20 01:42:33 -05:00
pokemon_constants.py Move a bunch of constant data into separate files. 2012-04-17 21:37:46 -05:00
romstr.py get an address given a label 2012-10-11 18:52:54 -05:00
sym.py add symfile generation 2013-01-10 03:00:46 -05:00
trainers.py SUPERNERD -> SUPER_NERD in trainer constants 2012-05-21 08:39:11 -05:00

README.md

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 crystal. Once the module is loaded a first time, it must be reloaded if the file changes and the updates are desired.

import 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.

import crystal
crystal.run_tests()

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.

import 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.

db $0, "Hm? That's a #-", $4f
db "DEX, isn't it?", $55
; ...

However, this is not how that TextScript object would appear in the final ASM. To see how it would appear in main.asm once inserted, you would run print crystal.to_asm(text) to get the following.

UnknownText_0x580c7: ; 0x580c7
    db $0, "Hm? That's a #-", $4f
    db "DEX, isn't it?", $55
    db "May I see it?", $51
    db "There are so many", $4f
    db "kinds of #MON.", $51
    db "Hm? What's this?", $51
    db "What is this", $4f
    db "#MON?", $51
    db "It looks like the", $4f
    db "strange writing on", $51
    db "the walls of the", $4f
    db "RUINS.", $51
    db "If those drawings", $4f
    db "are really #-", $55
    db "MON, there should", $55
    db "be many more.", $51
    db "I know! Let me up-", $4f
    db "grade your #-", $55
    db "DEX. Follow me.", $57
; 0x581e5

Figuring out where a script appears based on a known address

Another approach is to parse the entire ROM, then check a script at a particular address. This has the advantage that the script object will have the map_group and map_id variables set.

import crystal

# parse the ROM
crystal.run_main()

# get the parsed script
script = crystal.script_parse_table[0x58043]

# read its attributes to figure out map group / map id
map_group = script.map_group
map_id = script.map_id

# MapHeader is not given all the info yet
# in the mean time "map_names" contains some metadata
map_dict = crystal.map_names[map_group][map_id]
map_header = map_dict["header_new"]

print map_dict["name"]
# Ruins of Alph Outside

While the above doesn't show this, it turns out that the script at 0x58043 is referenced in the MapEventHeader as a person-event.

print map_header.second_map_header.event_header.to_asm()

This will show a structure roughly like:

person_event $3c, 19, 15, $7, $0, 255, 255, $0, 0, UnknownScript_0x58043, $0703

within this:

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, UnknownScript_0x58031, $0, $0
    xy_trigger 1, $f, $a, $0, UnknownScript_0x5803a, $0, $0

    ; signposts
    db 3
    signpost 8, 16, $0, UnknownScript_0x580b1
    signpost 16, 12, $0, UnknownScript_0x580b4
    signpost 12, 18, $0, UnknownScript_0x580b7

    ; people-events
    db 5
    person_event $27, 24, 8, $6, $0, 255, 255, $2, 1, Trainer_0x58089, $ffff
    person_event $3c, 19, 15, $7, $0, 255, 255, $0, 0, UnknownScript_0x58043, $0703
    person_event $3a, 21, 17, $3, $0, 255, 255, $a0, 0, UnknownScript_0x58061, $078e
    person_event $27, 15, 18, $2, $11, 255, 255, $b0, 0, UnknownScript_0x58076, $078f
    person_event $27, 12, 16, $7, $0, 255, 255, $80, 0, UnknownScript_0x5807e, $078f
; 0x58560

Helpful ROM investigation tools

import 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:

"""
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!
...
"""