Refactored code into classes for clearer grouping
This commit is contained in:
parent
f7881ea49f
commit
a12e04866d
4
Rom.py
4
Rom.py
|
@ -7,7 +7,7 @@ import struct
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from Dungeons import dungeon_music_addresses
|
from Dungeons import dungeon_music_addresses
|
||||||
from Text import string_to_alttp_text, text_addresses, Credits
|
from Text import MultiByteTextMapper, text_addresses, Credits
|
||||||
from Text import Uncle_texts, Ganon1_texts, PyramidFairy_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, BombShop2_texts
|
from Text import Uncle_texts, Ganon1_texts, PyramidFairy_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, BombShop2_texts
|
||||||
from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
|
from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
|
||||||
from Utils import local_path
|
from Utils import local_path
|
||||||
|
@ -955,7 +955,7 @@ def write_sprite(rom, sprite):
|
||||||
|
|
||||||
def write_string_to_rom(rom, target, string):
|
def write_string_to_rom(rom, target, string):
|
||||||
address, maxbytes = text_addresses[target]
|
address, maxbytes = text_addresses[target]
|
||||||
rom.write_bytes(address, string_to_alttp_text(string, maxbytes))
|
rom.write_bytes(address, MultiByteTextMapper.convert(string, maxbytes))
|
||||||
|
|
||||||
|
|
||||||
def write_strings(rom, world):
|
def write_strings(rom, world):
|
||||||
|
|
326
Text.py
326
Text.py
|
@ -437,177 +437,183 @@ class SceneLargeCreditLine(SceneCreditLine):
|
||||||
buf += LargeCreditBottomMapper.convert(self.text)
|
buf += LargeCreditBottomMapper.convert(self.text)
|
||||||
return buf
|
return buf
|
||||||
|
|
||||||
|
class MultiByteTextMapper(object):
|
||||||
|
@classmethod
|
||||||
|
def convert(cls, text, maxbytes=256):
|
||||||
|
outbuf = MultiByteCoreTextMapper.convert(text)
|
||||||
|
|
||||||
def string_to_alttp_text(s, maxbytes=256):
|
# check for max length
|
||||||
outbuf = string_to_alttp_core(s)
|
if len(outbuf) > maxbytes - 2:
|
||||||
|
outbuf = outbuf[:maxbytes - 2]
|
||||||
|
# Note: this could crash if the last byte is part of a two byte command
|
||||||
|
# depedning on how well the command handles a value of 0x7F.
|
||||||
|
# Should probably do something about this.
|
||||||
|
|
||||||
# check for max length
|
outbuf.append(0x7F)
|
||||||
if len(outbuf) > maxbytes - 2:
|
outbuf.append(0x7F)
|
||||||
outbuf = outbuf[:maxbytes - 2]
|
return outbuf
|
||||||
# Note: this could crash if the last byte is part of a two byte command
|
|
||||||
# depedning on how well the command handles a value of 0x7F.
|
|
||||||
# Should probably do something about this.
|
|
||||||
|
|
||||||
outbuf.append(0x7F)
|
class MultiByteCoreTextMapper(object):
|
||||||
outbuf.append(0x7F)
|
special_commands = {
|
||||||
return outbuf
|
"{SPEED0}": [0x7A, 0x00],
|
||||||
|
"{SPEED2}": [0x7A, 0x02],
|
||||||
|
"{SPEED6}": [0x7A, 0x06],
|
||||||
|
"{PAUSE1}": [0x78, 0x01],
|
||||||
|
"{PAUSE3}": [0x78, 0x03],
|
||||||
|
"{PAUSE5}": [0x78, 0x05],
|
||||||
|
"{PAUSE7}": [0x78, 0x07],
|
||||||
|
"{PAUSE9}": [0x78, 0x09],
|
||||||
|
"{INPUT}": [0x7E],
|
||||||
|
"{CHOICE}": [0x68],
|
||||||
|
"{ITEMSELECT}": [0x69],
|
||||||
|
"{CHOICE2}": [0x71],
|
||||||
|
"{CHOICE3}": [0x72],
|
||||||
|
"{HARP}": [0x79, 0x2D],
|
||||||
|
"{MENU}": [0x6D, 0x00],
|
||||||
|
"{BOTTOM}": [0x6D, 0x00],
|
||||||
|
"{NOBORDER}": [0x6B, 0x02],
|
||||||
|
"{CHANGEPIC}": [0x67, 0x67],
|
||||||
|
"{CHANGEMUSIC}": [0x67],
|
||||||
|
"{INTRO}": [0x6E, 0x00, 0x77, 0x07, 0x7A, 0x03, 0x6B, 0x02, 0x67],
|
||||||
|
"{NOTEXT}": [0x6E, 0x00, 0x6B, 0x04],
|
||||||
|
"{IBOX}": [0x6B, 0x02, 0x77, 0x07, 0x7A, 0x03],
|
||||||
|
}
|
||||||
|
|
||||||
special_commands = {
|
@classmethod
|
||||||
"{SPEED0}": [0x7A, 0x00],
|
def convert(cls, text, pause=True, wrap=14):
|
||||||
"{SPEED2}": [0x7A, 0x02],
|
text = text.upper()
|
||||||
"{SPEED6}": [0x7A, 0x06],
|
lines = text.split('\n')
|
||||||
"{PAUSE1}": [0x78, 0x01],
|
outbuf = bytearray()
|
||||||
"{PAUSE3}": [0x78, 0x03],
|
lineindex = 0
|
||||||
"{PAUSE5}": [0x78, 0x05],
|
is_intro = '{INTRO}' in text
|
||||||
"{PAUSE7}": [0x78, 0x07],
|
|
||||||
"{PAUSE9}": [0x78, 0x09],
|
|
||||||
"{INPUT}": [0x7E],
|
|
||||||
"{CHOICE}": [0x68],
|
|
||||||
"{ITEMSELECT}": [0x69],
|
|
||||||
"{CHOICE2}": [0x71],
|
|
||||||
"{CHOICE3}": [0x72],
|
|
||||||
"{HARP}": [0x79, 0x2D],
|
|
||||||
"{MENU}": [0x6D, 0x00],
|
|
||||||
"{BOTTOM}": [0x6D, 0x00],
|
|
||||||
"{NOBORDER}": [0x6B, 0x02],
|
|
||||||
"{CHANGEPIC}": [0x67, 0x67],
|
|
||||||
"{CHANGEMUSIC}": [0x67],
|
|
||||||
"{INTRO}": [0x6E, 0x00, 0x77, 0x07, 0x7A, 0x03, 0x6B, 0x02, 0x67],
|
|
||||||
"{NOTEXT}": [0x6E, 0x00, 0x6B, 0x04],
|
|
||||||
"{IBOX}": [0x6B, 0x02, 0x77, 0x07, 0x7A, 0x03],
|
|
||||||
}
|
|
||||||
|
|
||||||
def string_to_alttp_core(s, pause=True, wrap=14):
|
while lines:
|
||||||
s = s.upper()
|
linespace = wrap
|
||||||
lines = s.split('\n')
|
line = lines.pop(0)
|
||||||
outbuf = bytearray()
|
if line.startswith('{'):
|
||||||
lineindex = 0
|
outbuf.extend(cls.special_commands[line])
|
||||||
is_intro = '{INTRO}' in s
|
continue
|
||||||
|
|
||||||
while lines:
|
words = line.split(' ')
|
||||||
linespace = wrap
|
outbuf.append(0x74 if lineindex == 0 else 0x75 if lineindex == 1 else 0x76) # line starter
|
||||||
line = lines.pop(0)
|
|
||||||
if line.startswith('{'):
|
|
||||||
outbuf.extend(special_commands[line])
|
|
||||||
continue
|
|
||||||
|
|
||||||
words = line.split(' ')
|
while words:
|
||||||
outbuf.append(0x74 if lineindex == 0 else 0x75 if lineindex == 1 else 0x76) # line starter
|
word = words.pop(0)
|
||||||
|
# sanity check: if the word we have is more than 14 characters, we take as much as we can still fit and push the rest back for later
|
||||||
|
if cls.wordlen(word) > wrap:
|
||||||
|
if linespace < wrap:
|
||||||
|
word = ' ' + word
|
||||||
|
(word_first, word_rest) = cls.splitword(word, linespace)
|
||||||
|
words.insert(0, word_rest)
|
||||||
|
lines.insert(0, ' '.join(words))
|
||||||
|
|
||||||
while words:
|
outbuf.extend(RawMBTextMapper.convert(word_first))
|
||||||
word = words.pop(0)
|
break
|
||||||
# sanity check: if the word we have is more than 14 characters, we take as much as we can still fit and push the rest back for later
|
|
||||||
if wordlen(word) > wrap:
|
|
||||||
if linespace < wrap:
|
|
||||||
word = ' ' + word
|
|
||||||
(word_first, word_rest) = splitword(word, linespace)
|
|
||||||
words.insert(0, word_rest)
|
|
||||||
lines.insert(0, ' '.join(words))
|
|
||||||
|
|
||||||
outbuf.extend(RawMBTextMapper.convert(word_first))
|
if cls.wordlen(word) <= (linespace if linespace == wrap else linespace - 1):
|
||||||
|
if linespace < wrap:
|
||||||
|
word = ' ' + word
|
||||||
|
linespace -= cls.wordlen(word)
|
||||||
|
outbuf.extend(RawMBTextMapper.convert(word))
|
||||||
|
else:
|
||||||
|
# ran out of space, push word and lines back and continue with next line
|
||||||
|
words.insert(0, word)
|
||||||
|
lines.insert(0, ' '.join(words))
|
||||||
|
break
|
||||||
|
|
||||||
|
if is_intro and lineindex < 3:
|
||||||
|
outbuf.extend([0xFF]*linespace)
|
||||||
|
|
||||||
|
has_more_lines = len(lines) > 1 or (lines and not lines[0].startswith('{'))
|
||||||
|
|
||||||
|
lineindex += 1
|
||||||
|
if pause and lineindex % 3 == 0 and has_more_lines:
|
||||||
|
outbuf.append(0x7E)
|
||||||
|
if lineindex >= 3 and has_more_lines:
|
||||||
|
outbuf.append(0x73)
|
||||||
|
return outbuf
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def wordlen(cls, word):
|
||||||
|
l = 0
|
||||||
|
offset = 0
|
||||||
|
while offset < len(word):
|
||||||
|
c_len, offset = cls.charlen(word, offset)
|
||||||
|
l += c_len
|
||||||
|
return l
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def splitword(cls, word, length):
|
||||||
|
l = 0
|
||||||
|
offset = 0
|
||||||
|
while True:
|
||||||
|
c_len, new_offset = cls.charlen(word, offset)
|
||||||
|
if l+c_len > length:
|
||||||
break
|
break
|
||||||
|
l += c_len
|
||||||
|
offset = new_offset
|
||||||
|
return (word[0:offset], word[offset:])
|
||||||
|
|
||||||
if wordlen(word) <= (linespace if linespace == wrap else linespace - 1):
|
@classmethod
|
||||||
if linespace < wrap:
|
def charlen(cls, word, offset):
|
||||||
word = ' ' + word
|
c = word[offset]
|
||||||
linespace -= wordlen(word)
|
if c in ['>', '¼', '½', '♥']:
|
||||||
outbuf.extend(RawMBTextMapper.convert(word))
|
return (2, offset+1)
|
||||||
else:
|
if c in ['@']:
|
||||||
# ran out of space, push word and lines back and continue with next line
|
return (4, offset+1)
|
||||||
words.insert(0, word)
|
if c in ['ᚋ', 'ᚌ', 'ᚍ', 'ᚎ']:
|
||||||
lines.insert(0, ' '.join(words))
|
return (2, offset+1)
|
||||||
break
|
return (1, offset+1)
|
||||||
|
|
||||||
if is_intro and lineindex < 3:
|
class CompressedTextMapper(object):
|
||||||
outbuf.extend([0xFF]*linespace)
|
two_byte_commands = [
|
||||||
|
0x6B, 0x6C, 0x6D, 0x6E,
|
||||||
|
0x77, 0x78, 0x79, 0x7A
|
||||||
|
]
|
||||||
|
specially_coded_commands = {
|
||||||
|
0x73: 0xF6,
|
||||||
|
0x74: 0xF7,
|
||||||
|
0x75: 0xF8,
|
||||||
|
0x76: 0xF9,
|
||||||
|
0x7E: 0xFA,
|
||||||
|
0x7A: 0xFC,
|
||||||
|
}
|
||||||
|
|
||||||
has_more_lines = len(lines) > 1 or (lines and not lines[0].startswith('{'))
|
@classmethod
|
||||||
|
def convert(cls, text, pause=True, max_bytes_expanded=0x800, wrap=14):
|
||||||
|
inbuf = MultiByteCoreTextMapper.convert(text, pause, wrap)
|
||||||
|
|
||||||
lineindex += 1
|
# Links name will need 8 bytes in the target buffer
|
||||||
if pause and lineindex % 3 == 0 and has_more_lines:
|
# and two will be used by the terminator
|
||||||
outbuf.append(0x7E)
|
# (Variables will use 2 bytes, but they start as 2 bytes)
|
||||||
if lineindex >= 3 and has_more_lines:
|
bufsize = len(inbuf) + 7 * inbuf.count(0x6A) + 2
|
||||||
outbuf.append(0x73)
|
if bufsize > max_bytes_expanded:
|
||||||
return outbuf
|
raise ValueError("Uncompressed string too long for buffer")
|
||||||
|
inbuf.reverse()
|
||||||
two_byte_commands = [
|
outbuf = bytearray()
|
||||||
0x6B, 0x6C, 0x6D, 0x6E,
|
outbuf.append(0xfb) # terminator for previous record
|
||||||
0x77, 0x78, 0x79, 0x7A
|
while inbuf:
|
||||||
]
|
val = inbuf.pop()
|
||||||
specially_coded_commands = {
|
if val == 0xFF:
|
||||||
0x73: 0xF6,
|
|
||||||
0x74: 0xF7,
|
|
||||||
0x75: 0xF8,
|
|
||||||
0x76: 0xF9,
|
|
||||||
0x7E: 0xFA,
|
|
||||||
0x7A: 0xFC,
|
|
||||||
}
|
|
||||||
|
|
||||||
def string_to_alttp_text_compressed(s, pause=True, max_bytes_expanded=0x800, wrap=14):
|
|
||||||
inbuf = string_to_alttp_core(s, pause, wrap)
|
|
||||||
|
|
||||||
# Links name will need 8 bytes in the target buffer
|
|
||||||
# and two will be used by the terminator
|
|
||||||
# (Variables will use 2 bytes, but they start as 2 bytes)
|
|
||||||
bufsize = len(inbuf) + 7 * inbuf.count(0x6A) + 2
|
|
||||||
if bufsize > max_bytes_expanded:
|
|
||||||
raise ValueError("Uncompressed string too long for buffer")
|
|
||||||
inbuf.reverse()
|
|
||||||
outbuf = bytearray()
|
|
||||||
outbuf.append(0xfb) # terminator for previous record
|
|
||||||
while inbuf:
|
|
||||||
val = inbuf.pop()
|
|
||||||
if val == 0xFF:
|
|
||||||
outbuf.append(val)
|
|
||||||
elif val == 0x00:
|
|
||||||
outbuf.append(inbuf.pop())
|
|
||||||
elif val == 0x01: #kanji
|
|
||||||
outbuf.append(0xFD)
|
|
||||||
outbuf.append(inbuf.pop())
|
|
||||||
elif val >= 0x67:
|
|
||||||
if val in specially_coded_commands:
|
|
||||||
outbuf.append(specially_coded_commands[val])
|
|
||||||
else:
|
|
||||||
outbuf.append(0xFE)
|
|
||||||
outbuf.append(val)
|
outbuf.append(val)
|
||||||
if val in two_byte_commands:
|
elif val == 0x00:
|
||||||
outbuf.append(inbuf.pop())
|
outbuf.append(inbuf.pop())
|
||||||
else:
|
elif val == 0x01: #kanji
|
||||||
raise ValueError("Unexpected byte found in uncompressed string")
|
outbuf.append(0xFD)
|
||||||
return outbuf
|
outbuf.append(inbuf.pop())
|
||||||
|
elif val >= 0x67:
|
||||||
|
if val in cls.specially_coded_commands:
|
||||||
|
outbuf.append(cls.specially_coded_commands[val])
|
||||||
|
else:
|
||||||
|
outbuf.append(0xFE)
|
||||||
|
outbuf.append(val)
|
||||||
|
if val in cls.two_byte_commands:
|
||||||
|
outbuf.append(inbuf.pop())
|
||||||
|
else:
|
||||||
|
raise ValueError("Unexpected byte found in uncompressed string")
|
||||||
|
return outbuf
|
||||||
|
|
||||||
|
class CharTextMapper(object):
|
||||||
|
|
||||||
def wordlen(word):
|
|
||||||
l = 0
|
|
||||||
offset = 0
|
|
||||||
while offset < len(word):
|
|
||||||
c_len, offset = charlen(word, offset)
|
|
||||||
l += c_len
|
|
||||||
return l
|
|
||||||
|
|
||||||
def splitword(word, length):
|
|
||||||
l = 0
|
|
||||||
offset = 0
|
|
||||||
while True:
|
|
||||||
c_len, new_offset = charlen(word, offset)
|
|
||||||
if l+c_len > length:
|
|
||||||
break
|
|
||||||
l += c_len
|
|
||||||
offset = new_offset
|
|
||||||
return (word[0:offset], word[offset:])
|
|
||||||
|
|
||||||
def charlen(word, offset):
|
|
||||||
c = word[offset]
|
|
||||||
if c in ['>', '¼', '½', '♥']:
|
|
||||||
return (2, offset+1)
|
|
||||||
if c in ['@']:
|
|
||||||
return (4, offset+1)
|
|
||||||
if c in ['ᚋ', 'ᚌ', 'ᚍ', 'ᚎ']:
|
|
||||||
return (2, offset+1)
|
|
||||||
return (1, offset+1)
|
|
||||||
|
|
||||||
class TextMapper(object):
|
|
||||||
number_offset = None
|
number_offset = None
|
||||||
alpha_offset = 0
|
alpha_offset = 0
|
||||||
char_map = {}
|
char_map = {}
|
||||||
|
@ -627,7 +633,7 @@ class TextMapper(object):
|
||||||
buf.append(cls.map_char(char))
|
buf.append(cls.map_char(char))
|
||||||
return buf
|
return buf
|
||||||
|
|
||||||
class RawMBTextMapper(TextMapper):
|
class RawMBTextMapper(CharTextMapper):
|
||||||
char_map = {' ': 0xFF,
|
char_map = {' ': 0xFF,
|
||||||
'『': 0xC4,
|
'『': 0xC4,
|
||||||
'』': 0xC5,
|
'』': 0xC5,
|
||||||
|
@ -1098,7 +1104,7 @@ class RawMBTextMapper(TextMapper):
|
||||||
return buf
|
return buf
|
||||||
|
|
||||||
|
|
||||||
class GoldCreditMapper(TextMapper):
|
class GoldCreditMapper(CharTextMapper):
|
||||||
char_map = {' ': 0x9F,
|
char_map = {' ': 0x9F,
|
||||||
',': 0x34,
|
',': 0x34,
|
||||||
"'": 0x35,
|
"'": 0x35,
|
||||||
|
@ -1107,16 +1113,16 @@ class GoldCreditMapper(TextMapper):
|
||||||
alpha_offset = -0x47
|
alpha_offset = -0x47
|
||||||
|
|
||||||
|
|
||||||
class GreenCreditMapper(TextMapper):
|
class GreenCreditMapper(CharTextMapper):
|
||||||
char_map = {' ': 0x9F,
|
char_map = {' ': 0x9F,
|
||||||
'·': 0x52}
|
'·': 0x52}
|
||||||
alpha_offset = -0x29
|
alpha_offset = -0x29
|
||||||
|
|
||||||
class RedCreditMapper(TextMapper):
|
class RedCreditMapper(CharTextMapper):
|
||||||
char_map = {' ': 0x9F}
|
char_map = {' ': 0x9F}
|
||||||
alpha_offset = -0x61
|
alpha_offset = -0x61
|
||||||
|
|
||||||
class LargeCreditTopMapper(TextMapper):
|
class LargeCreditTopMapper(CharTextMapper):
|
||||||
char_map = {' ': 0x9F,
|
char_map = {' ': 0x9F,
|
||||||
"'": 0x77,
|
"'": 0x77,
|
||||||
'!': 0x78,
|
'!': 0x78,
|
||||||
|
@ -1137,7 +1143,7 @@ class LargeCreditTopMapper(TextMapper):
|
||||||
number_offset = 0x23
|
number_offset = 0x23
|
||||||
|
|
||||||
|
|
||||||
class LargeCreditBottomMapper(TextMapper):
|
class LargeCreditBottomMapper(CharTextMapper):
|
||||||
char_map = {' ': 0x9F,
|
char_map = {' ': 0x9F,
|
||||||
"'": 0x9D,
|
"'": 0x9D,
|
||||||
'!': 0x9E,
|
'!': 0x9E,
|
||||||
|
|
Loading…
Reference in New Issue