Skip to content

Instantly share code, notes, and snippets.

@NichtJens
Created April 20, 2013 16:32
Show Gist options
  • Save NichtJens/5426532 to your computer and use it in GitHub Desktop.
Save NichtJens/5426532 to your computer and use it in GitHub Desktop.
PEP8 compliant modification of ZeldahackScrollText6.py from: http://kennastuff.blogspot.de/2013/03/zelda-starring-zelda-python-code.html
#!/usr/bin/python
# http://kennastuff.blogspot.de/2013/03/zelda-starring-zelda-python-code.html
# 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".
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]
# colors 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment