Sunday, March 24, 2013

Zelda Starring Zelda: The Python Code

Update! I've made the .py file available on my google drive. IMPORTANT NOTE! You'll need to have a copy of Legend of Zelda named "original.nes" in the same folder as the .py file. Run the .py file and it'll spit out a file called "hack.nes". Have at it, my fellow script kiddies! :)

Here's the full code to edit the opening and closing screens, the colors of Zelda's tunic, and a few other things. Simon says it was no trouble at all to create, but I don't believe that for a minute. :P

You'll need Python installed and a Legend of Zelda ROM labeled "original.nes" to run it. It'll spit out a new file called "hack.nes" but remember that it won't alter the shape of the sprites. That's all Tile Layer Pro. If you have any questions about editing, like editing the colors, please post them here and I'll be happy to help. If you have a question about Python, you can ask in the comments, or ask it at my boyfriend's blog.

NOTE: The indentations didn't survive the copy/paste. :( I'll replace them later.   (Edit: Fixed-ish. The indents aren't as dramatic here as they are in my txt editor, but I hope it's easier for people to read)

Code after the jump





ORIGINAL_SCROLL_TEXT= [
"""402024_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 242420""",
"""602024_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 242420""",
"""802024_ e6e4e5_ _T_H_E_ _L_E_G_E_N_D_ _O_F_ _Z_E_L_D_A_ e5e4e5e6242420""",
"""a02024_ e2_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ e3242420""",
"""c02024_ e3_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ e2242420""",
"""e02024_ e2_ _M_A_N_Y_ _ _Y_E_A_R_S_ _ _A_G_O_ _ _P_R_I_N_C_E_ e3242421""",
"""002024_ e3_ _ _ _ _ _ _ _ _ _ #"_ _ _ _ _ _ _ #"_ _ _ _ _ _ _ e2242421""",
"""202024_ e2_ _D_A_R_K_N_E_S_S_ _ _ _G_A_N_N_O_N_ _ _S_T_O_L_E_ e3242421""",
"""402024_ e3_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ e2242421""",
"""602024_ e2_ _O_N_E_ _O_F_ _T_H_E_ _T_R_I_F_O_R_C_E_ _W_I_T_H_ e3242421""",
"""802024_ e3_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ e2242421""",
"""a02024_ e2_ _P_O_W_E_R#._ _ _ _ _P_R_I_N_C_E_S_S_ _Z_E_L_D_A_ e3242421""",
"""c02024_ e3_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ e2242421""",
"""e02024_ e2_ _H_A_D_ _ _O_N_E_ _O_F_ _T_H_E_ _T_R_I_F_O_R_C_E_ e3242422""",
"""002024_ e3_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ e2242422""",
"""202024_ e2_ _W_I_T_H_ _W_I_S_D_O_M#._ _S_H_E_ _D_I_V_I_D_E_D_ e2242422""",
"""402024_ e3_ _ _ _ _ _ _ _ #"_ _ #"_ _ _ _ _ _ _ _ _ _ _ _ _ _ e3242422""",
"""602024_ e2_ _I_T_ _I_N_T_O_ _ _8_ _U_N_I_T_S_ _T_O_ _H_I_D_E_ e3242422""",
"""802024_ e3_ _ _ _ _ _ _ _ _ #"_ _ _ _ _ _ _ #"_ _ _ _ _ _ _ _ e2242422""",
"""a02024_ e2_ _I_T_ _F_R_O_M_ _ _ _G_A_N_N_O_N_ _ _B_E_F_O_R_E_ e3242422""",
"""c02024_ e3_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ e2242422""",
"""e02024_ e2_ _S_H_E_ _W_A_S_ _C_A_P_T_U_R_E_D#._ _ _ _ _ _ _ _ e3242423""",
"""002024_ e3_ _ _ _ _ _ _ _ _ _ _ _ _ _ #"_ _ #"_ _ _ _ _ _ _ _ e2242423""",
"""202024_ e2_ _ _ _G_O_ _F_I_N_D_ _T_H_E_ _ _8_ _U_N_I_T_S_ _ _ e3242423""",
"""402024_ e3_ _ _ #"_ _ _ _ _ #"_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ e2242423""",
"""602024_ e2_ _ _ _ _ _L_I_N_K_ _ _T_O_ _S_A_V_E_ _H_E_R#._ _ _ e3242423""",
"""802024_ e3_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ e2242423""",
"""a02024_ e6e4e5e4e5e4e5e4e5e4e5e4e5e4e5e4e5e4e5e4e5e4e5e4e5e4e5e6242423""",
]
COLOR_TEXT= [
"""00ffffff0b0a0a0a0a0effff00004a5a5200ffff00000000585aff23e020ff00001000""",
"""0000ffff00000a0a0200fffffafabaaaaaaaffffffffffffffffff2bd002ffff2bd602""",
"""ffffff2000202424242424242424242424242424242424242424242424242424242424""",
]

NEW_SCROLL_TEXT= [
"""402024_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 242420""",
"""602024_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 242420""",
"""802024_ e6e4e5_ _ _L_E_G_E_N_D_ _O_F_ _Z_E_L_D_A_!_ _ _ e5e4e5e6242420""",
"""a02024_ e2_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ e3242420""",
"""c02024_ e3_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ e2242420""",
"""e02024_ e2_ _M_A_N_Y_ _ _Y_E_A_R_S_ _ _A_G_O_ _ _P_R_I_N_C_E_ e3242421""",
"""002024_ e3_ _ _ _ _ _ _ _ _ _ #"_ _ _ _ _ _ _ #"_ _ _ _ _ _ _ e2242421""",
"""202024_ e2_ _D_A_R_K_N_E_S_S_ _ _ _G_A_N_N_O_N_ _ _S_T_O_L_E_ e3242421""",
"""402024_ e3_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ e2242421""",
"""602024_ e2_ _T_H_E_ _T_R_I_F_O_R_C_E_ _O_F_ _P_O_W_E_R#._ _ _ e3242421""",
"""802024_ e3_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ e2242421""",
"""a02024_ e2_ _L_I_N_K_ _D_I_V_I_D_E_D_ _H_Y_R_U_L_E_'_S_ _ _ _ e3242421""",
"""c02024_ e3_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ e2242421""",
"""e02024_ e2_ _T_R_I_F_O_R_C_E_ _O_F_ _W_I_S_D_O_M_ _I_N_T_O_ _ e3242422""",
"""002024_ e3#"#"_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ e2242422""",
"""202024_ e2_ _8_ _P_A_R_T_S_ _A_N_D_ _H_I_D_ _T_H_E_M_ _ _ _ _ e2242422""",
"""402024_ e3_ _ _ _ _ #"_ _ _ _ _ _ #"_ _ _ _ _ _ _ _ _ _ _ _ _ e3242422""",
"""602024_ e2_ _F_R_O_M_ _G_A_N_N_O_N#._ _B_U_T_ _N_O_W_,_ _ _ _ e3242422""",
"""802024_ e3_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ e2242422""",
"""a02024_ e2_ _L_I_N_K_ _H_A_S_ _B_E_E_N_ _C_A_P_T_U_R_E_D#._ _ e3242422""",
"""c02024_ e3_ _ _ _ _ _ _ _ _ _ _ _ _ #"#"_ _ _ _ _ _ _ _ _ _ _ e2242422""",
"""e02024_ e2_ _ _ _ _F_I_N_D_ _T_H_E_ _ _8_ _P_A_R_T_S#._ _ _ _ e3242423""",
"""002024_ e3_ _ _ _ _ _ _ _ _ _ _ #"_ _ _ _ _ _ #"_ _ _ _ _ _ _ e2242423""",
"""202024_ e2_ _ _ _ _ _D_E_F_E_A_T_ _G_A_N_N_O_N#._ _ _ _ _ _ _ e3242423""",
"""402024_ e3_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ e2242423""",
"""602024_ e2_ _ _ _ _ _ _S_A_V_E_ _H_Y_R_U_L_E#._ _ _ _ _ _ _ _ e3242423""",
"""802024_ e3_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ e2242423""",
"""a02024_ e6e4e5e4e5e4e5e4e5e4e5e4e5e4e5e4e5e4e5e4e5e4e5e4e5e4e5e6242423""",
]
NEW_COLOR_TEXT= [
"""00fffffF0b0a0a0a0a0effff00505a0A0200ffff5F5000000000ff23e020ff01a0a000""",
"""0000ffff5f5f5f5f5f5ffffffa0a0a0a0aaaffffffffffffffffff2bd002ffff2bd602""",
"""ffffff2000202424242424242424242424242424242424242424242424242424242424""",
]

# rom memory locations for fun and pro$it
SCROLL_START, SCROLL_END= 0x1a455, 0x1A829
COLOR_START, COLOR_END= 0x1A830, 0x1A85D + (60) # 0x1A876
SKIP_QUEST_START, SKIP_QUEST_END= 0x9EFB,0x9EFF

# 1A81B - 1A85D
# Use of colours for the storyboard -
# every 2 bits sets the colour for a 2x2 block of text

# slots for different tunic colors
TUNIC_SPOTS= [0xa297,0xa298,0xa299]

# colros that
GREEN= 0x05
BLUE= 0x11
RED= 0x16
PURPL= 0x05

# list of characters in the order they are in the rom
# note how nice the programmers were: 0 is at offset 0x0, 'A' is at offset '0xa'
chars= """0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ ~~~,!'&."?-"""
# don't know why but the scrolling text has different sprites for these two guys
alts= { 0x63:'.', 0xf8:'"' }

def toRomRaw( text ):
  '''convert a simply human friendly string to rom friendly data
  '''
  x= [ chr( chars.index(d) ) for d in text ]
  return "".join( x )

OLD_SKIP_QUEST= toRomRaw("ZELDA")
NEW_SKIP_QUEST= toRomRaw("KENNA")
#print len(NEW_SKIP_QUEST), SKIP_QUEST_END-SKIP_QUEST_START+1
assert( len(NEW_SKIP_QUEST) == SKIP_QUEST_END-SKIP_QUEST_START+1 )

def hex( char ):
  '''convert a single byte(char) of rom data into a 2 digit hex number
  '''
  return "%2.2x" % ord(char)

def convert( char ):
  '''convert a single byte(char) of rom data into human friendly format
  '''
  charn= ord( char )
  if charn in alts:
    return "#" + alts[charn]
  if charn in range(0,len( chars )):
    return "_" + chars[charn]
  return hex( char )

def convertString( string, begin=0, end=0 ):
  '''convert a string of rom data into human friendly foramt
  '''
  if begin==end:
    converted_chars= [ hex(char) for char in string ]
  else:
    converted_chars= \
      [ hex(char) for char in string[0:begin] ]+\
      [ convert(char) for char in string[begin:end] ]+\
      [ hex(char) for char in string[end:]  ]
  return "".join( converted_chars )

def yieldScrollLines( scrollData, numbersOnly= False ):
  '''generate a series of human friendly strings from the passed rom data
  '''
  lineWidth=35           # lines appear to be 35 characters long
  # split the big string into separate lines
  for i in range( 0,len(scrollData),lineWidth ):
    line= scrollData[i:i+lineWidth]
    # the beginning and ending of the lines have special meaning
    if numbersOnly:
      yield convertString( line )  
    else:
      yield convertString( line, 3,-3 )  

def dumpScrollText( scrollData, numbersOnly= False ):
  '''print all of the scrollData to the screen in human friendly format
  '''
  for line in yieldScrollLines( scrollData, numbersOnly ):
    print '"""' + line + '""",'  

def yieldRomChars( textLines ):
  '''generate a series of rom friendly characters from the passed human friendly strings
  '''
  for text in textLines:
    for t in range(0,len(text), 2):
      one,two= text[t:t+2]
      value= None
      if one == '_':
        value= chars.index( two )
      elif one == '#':
        for k,v in alts.iteritems():
          if two == v:
            value= k
            break
        assert value is not None, "value starts with # but not in alts"
      else:      
        value= int(one+two,16)
      yield chr(value)      

def textToRomBlock( textLines ):
  '''convert a bunch of text lines into rom friendly format
  '''
  return "".join( [ v for v in yieldRomChars( textLines ) ] )

def verifyConversions( scrollData, scrollText=None  ):
  '''read the scroll data, convert it to text and back again, make sure it matches
  '''
  scrollText= scrollText or yieldScrollLines( scrollData )
  newData= textToRomBlock( scrollText )  #945
  for i in range(0,len(newData)):
    old= ord(scrollData[i])
    new= ord(newData[i])
    assert old == new, "FAILED char: %d, old value: %2x, new value: %2x" % ( i, old, new )

if __name__ == '__main__':
  # load the 'original.nes' file
  org= None
  with open("original.nes","rb") as f:
    org= f.read()
  newData= org
 
  ### SCROLL
  # extract the scroll data
  scrollData= org[SCROLL_START:SCROLL_END]
  #dumpScrollText( scrollData )
  # create the new scroll data from the new scroll text
  newScrollData= textToRomBlock( NEW_SCROLL_TEXT )
  # create new rom data with the new scroll data in place of the old
  newData= newData[:SCROLL_START] + newScrollData + newData[SCROLL_END:]
 
  ### COLOR
  # extract color data
  colorData= org[COLOR_START:COLOR_END]
  #dumpScrollText( colorData, numbersOnly=True )
  newColorData= textToRomBlock( NEW_COLOR_TEXT )
  newData= newData[:COLOR_START] + newColorData + newData[COLOR_END:]
 
  ### QUEST SKIP
  # replace the quest skip name
  newData= newData[:SKIP_QUEST_START] + NEW_SKIP_QUEST + newData[SKIP_QUEST_END+1:]
 
  ### tunic hacking
  spot= TUNIC_SPOTS[0]
  newData= newData[:spot] + chr(RED) + newData[spot+1:]
 
  spot= TUNIC_SPOTS[1]
  newData= newData[:spot] + chr(BLUE) + newData[spot+1:]
 
  spot= TUNIC_SPOTS[2]
  newData= newData[:spot] + chr(PURPL) + newData[spot+1:]
 

  QENDS= [ 0xA959+0xD, 0xAB07+0xD ]
  QEND_LENGTH= 56
  NEW_QENDS= [
  """e6_J60_ _ _ _ _Z_E_L_D_A_!_ _ _Y_O_U_'_R8e64_T_H_E_ _H_E_R_O_ _O_F_ _H_Y_R_U_L_Eec_W95a9a5adf0_6a95085_!e6_J60_X""",
  """575859_F_I_N_A_L_L_Y_,_P_E_A_C_E_ _R_E_T_U_R_N_S_ _T_O_ _H_Y_R_U_L_E_._T_H_I_S_ _E_N_D_S_ _T_H_E_ _S_T_O_R_Y_.ff"""
  ]


  for i in range(2):
    text, begin= NEW_QENDS[i], QENDS[i]
    data= textToRomBlock( [ text ] )
    assert len(data)==QEND_LENGTH, "%d != %d" % (len(data), QEND_LENGTH)
    newData= newData[:begin] + data + newData[begin+QEND_LENGTH:]
 
  ### VERIFY AND WRITE
  # verify
  assert len(newData) == len(org)
  # write the hack file to disk
  with open("hack.nes","wb") as hack:
    hack.write( newData )

5 comments:

  1. Hi!

    Would you repost the code, please?
    Preferably as a downloadable file...
    Without indention its really hard to read!

    Thanks!

    ReplyDelete
    Replies
    1. Hey :D I tried to paste the code again. How does that look? Easier to read?

      Delete
  2. Hey,

    thanks, much better indeed :)
    The code is a little unconventional though, is there a reason for that?!

    In the meantime I had cleaned up the code myself. And while reading I made it PEP8 compliant and put the result into my github. I hope that's okay?! Maybe it'll be useful for someone...

    https://gist.github.com/NichtJens/5426532

    Best!

    ReplyDelete
    Replies
    1. Hey Nicht, that looks great!

      I've fallen into the habit of using two spaces for all my personal code -- c, python, lua. No real reason other than i like the compact look. And, i mostly code via Windows, so i forget things like the #!/usr/bin/python header.

      Thanks for cleaning it up and putting it on github. Maybe it will help other people looking to hack Zelda!

      Delete
    2. Hey!

      No problem :) Thanks to you for writing it in the first place ;)

      While we talk about this stuff: where does the assignment style come from:

      name= value

      seems rather uncommon, I guess I've never seen anyone doing that. I have seen a fair share of

      name=value

      but I guess this is usually laziness in pressing space. You are very consistent in your assignment, so I suppose laziness can't be the answer.


      The spaces inside the brackets is rather common I guess...

      Best!

      Delete