Skip to content

Commit c997714

Browse files
committed
Merge 'Extend space available for message data' (#1837)
2 parents c401b0b + db26898 commit c997714

File tree

11 files changed

+22477
-22257
lines changed

11 files changed

+22477
-22257
lines changed

ASM/build/asm_symbols.txt

Lines changed: 436 additions & 434 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ASM/src/build.asm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ RANDO_CONTEXT:
111111
.include "drop_overrides/obj_comb.asm"
112112
.include "drop_overrides/actor.asm"
113113
.include "rand_seed.asm"
114+
.include "messages.asm"
114115

115116
.align 0x10
116117
.importobj "../build/bundle.o"

ASM/src/hacks.asm

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2999,6 +2999,32 @@ skip_GS_BGS_text:
29992999
.orga 0xB575C8
30003000
sw t6, 0x00(a1)
30013001

3002+
; Dynamically load the en/jp message files for text lookup. Both files are utilized to make room
3003+
; for additional text. The jp file is filled first. The segment value for the requested text ID
3004+
; is used to manipulate the language bit to tell Message_OpenText (func_800DC838) which file
3005+
; to load and search. Hook at VRAM 0x800DCB60 in message.s
3006+
.orga 0xB52AC0
3007+
jal set_message_file_to_search
3008+
nop
3009+
3010+
; The message lookup function uses a fixed reference to the first entry's segment to strip it from
3011+
; the combined segment/offset word. Since we have mixed segments in the table now, this is no
3012+
; longer safe. Load the correct segment into register a2 from the current message.
3013+
.orga 0xB4C980
3014+
j load_correct_message_segment
3015+
nop
3016+
3017+
; Since message lookup already occurs in the above hook, remove the lookup from both the JP and EN
3018+
; branches.
3019+
.orga 0xB52AD0 ; JP branch
3020+
nop
3021+
nop
3022+
nop
3023+
nop
3024+
.orga 0xB52B64 ; EN branch
3025+
nop
3026+
nop
3027+
30023028
;==================================================================================================
30033029
; Null Boomerang Pointer in Links Instance
30043030
;==================================================================================================

ASM/src/messages.asm

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
; Manipulates the save context language bit based
2+
; on the segment value for the requested text ID.
3+
; Message lookup is moved here from the vanilla
4+
; branches.
5+
; t6 used for branching to JP/EN file loading
6+
; in parent function func_800DC838 at VRAM 0x800DCB68
7+
; 0 == JP
8+
; 1 == EN
9+
set_message_file_to_search:
10+
; displaced code
11+
lhu a1, 0x0046($sp)
12+
lw a0, 0x0040($sp)
13+
14+
; Saved by vanilla code prior to jump to
15+
; message lookup function. Called here so
16+
; we can safely store $ra.
17+
sw t0, 0x002C($sp)
18+
19+
; Message lookup saves to the stack without
20+
; changing the stack pointer. Save to an
21+
; unused variable to avoid changing the stack.
22+
or t4, $zero, $ra
23+
24+
; call JP message lookup function exclusively
25+
; since the JP/EN tables are merged.
26+
jal 0x800D69EC
27+
nop
28+
29+
; a1 contains the segment/offset word, formatted as
30+
; ssoooooo
31+
; where "s" is the segment and "o" is the offset.
32+
; Vanilla crashes on a failed text lookup already,
33+
; so we don't need to worry about bad values.
34+
srl t0, a1, 0x18
35+
andi t0, t0, 0x08
36+
sltiu t6, t0, 0x0001
37+
38+
or $ra, $zero, t4
39+
jr $ra
40+
nop
41+
42+
load_correct_message_segment:
43+
; displaced code
44+
lbu t6, 0x0002(v1)
45+
lw a1, 0x0004(v1)
46+
47+
; shift out the offset bits
48+
srl a2, a1, 0x18
49+
sll a2, a2, 0x18
50+
51+
; Remaining code from func_800D69EC since
52+
; it doesn't save $ra
53+
lw a1, 0x0004(v1)
54+
addiu v0, a0, 0x2200
55+
sb t6, 0x0008(v0)
56+
lw a3, 0x000C(v1)
57+
subu t7, a1, a2
58+
addiu v1, v1, 0x0008
59+
subu t8, a3, a1
60+
sw t7, 0x0000(v0)
61+
jr $ra
62+
sw t8, 0x0004(v0)

Hints.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,7 +1037,7 @@ def get_junk_hint(spoiler, world, checked):
10371037
def get_important_check_hint(spoiler, world, checked):
10381038
top_level_locations = []
10391039
for location in world.get_filled_locations():
1040-
if (HintArea.at(location).text(world.settings.clearer_hints) not in top_level_locations
1040+
if (HintArea.at(location).text(world.settings.clearer_hints) not in top_level_locations
10411041
and (HintArea.at(location).text(world.settings.clearer_hints) + ' Important Check') not in checked
10421042
and "pocket" not in HintArea.at(location).text(world.settings.clearer_hints)):
10431043
top_level_locations.append(HintArea.at(location).text(world.settings.clearer_hints))
@@ -1059,7 +1059,7 @@ def get_important_check_hint(spoiler, world, checked):
10591059
or (location.item.type == 'SmallKey' and not (world.settings.shuffle_smallkeys == 'dungeon' or world.settings.shuffle_smallkeys == 'vanilla'))
10601060
or (location.item.type == 'HideoutSmallKey' and not world.settings.shuffle_hideoutkeys == 'vanilla')
10611061
or (location.item.type == 'BossKey' and not (world.settings.shuffle_bosskeys == 'dungeon' or world.settings.shuffle_bosskeys == 'vanilla'))
1062-
or (location.item.type == 'GanonBossKey' and not (world.settings.shuffle_ganon_bosskey == 'vanilla'
1062+
or (location.item.type == 'GanonBossKey' and not (world.settings.shuffle_ganon_bosskey == 'vanilla'
10631063
or world.settings.shuffle_ganon_bosskey == 'dungeon' or world.settings.shuffle_ganon_bosskey == 'on_lacs'
10641064
or world.settings.shuffle_ganon_bosskey == 'stones' or world.settings.shuffle_ganon_bosskey == 'medallions'
10651065
or world.settings.shuffle_ganon_bosskey == 'dungeons' or world.settings.shuffle_ganon_bosskey == 'tokens'))):

Messages.py

Lines changed: 100 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
from TextBox import line_wrap
66
from Utils import find_last
77

8-
TEXT_START = 0x92D000
8+
ENG_TEXT_START = 0x92D000
9+
JPN_TEXT_START = 0x8EB000
910
ENG_TEXT_SIZE_LIMIT = 0x39000
10-
JPN_TEXT_SIZE_LIMIT = 0x3A150
11+
JPN_TEXT_SIZE_LIMIT = 0x3B000
1112

1213
JPN_TABLE_START = 0xB808AC
1314
ENG_TABLE_START = 0xB849EC
@@ -19,6 +20,9 @@
1920
EXTENDED_TABLE_START = JPN_TABLE_START # start writing entries to the jp table instead of english for more space
2021
EXTENDED_TABLE_SIZE = JPN_TABLE_SIZE + ENG_TABLE_SIZE # 0x8360 bytes, 4204 entries
2122

23+
EXTENDED_TEXT_START = JPN_TABLE_START # start writing text to the jp table instead of english for more space
24+
EXTENDED_TEXT_SIZE_LIMIT = JPN_TEXT_SIZE_LIMIT + ENG_TEXT_SIZE_LIMIT # 0x74000 bytes
25+
2226
# name of type, followed by number of additional bytes to read, follwed by a function that prints the code
2327
CONTROL_CODES = {
2428
0x00: ('pad', 0, lambda _: '<pad>' ),
@@ -455,14 +459,14 @@ def size(self):
455459
return size
456460

457461
# writes the code to the given offset, and returns the offset of the next byte
458-
def write(self, rom, offset):
459-
rom.write_byte(TEXT_START + offset, self.code)
462+
def write(self, rom, text_start, offset):
463+
rom.write_byte(text_start + offset, self.code)
460464

461465
extra_bytes = 0
462466
if self.code in CONTROL_CODES:
463467
extra_bytes = CONTROL_CODES[self.code][1]
464468
bytes_to_write = int_to_bytes(self.data, extra_bytes)
465-
rom.write_bytes(TEXT_START + offset + 1, bytes_to_write)
469+
rom.write_bytes(text_start + offset + 1, bytes_to_write)
466470

467471
return offset + 1 + extra_bytes
468472

@@ -498,7 +502,7 @@ def get_python_string(self):
498502

499503
# check if this is an unused message that just contains it's own id as text
500504
def is_id_message(self):
501-
if self.unpadded_length != 5:
505+
if self.unpadded_length != 5 or self.id == 0xFFFC:
502506
return False
503507
for i in range(4):
504508
code = self.text_codes[i].code
@@ -609,20 +613,20 @@ def transform(self, replace_ending=False, ending=None, always_allow_skip=True, s
609613

610614
# writes a Message back into the rom, using the given index and offset to update the table
611615
# returns the offset of the next message
612-
def write(self, rom, index, offset):
616+
def write(self, rom, index, text_start, offset, bank):
613617
# construct the table entry
614618
id_bytes = int_to_bytes(self.id, 2)
615619
offset_bytes = int_to_bytes(offset, 3)
616-
entry = id_bytes + bytes([self.opts, 0x00, 0x07]) + offset_bytes
620+
entry = id_bytes + bytes([self.opts, 0x00, bank]) + offset_bytes
617621
# write it back
618622
entry_offset = EXTENDED_TABLE_START + 8 * index
619623
rom.write_bytes(entry_offset, entry)
620624

621625
for code in self.text_codes:
622-
offset = code.write(rom, offset)
626+
offset = code.write(rom, text_start, offset)
623627

624628
while offset % 4 > 0:
625-
offset = Text_Code(0x00, 0).write(rom, offset) # pad to 4 byte align
629+
offset = Text_Code(0x00, 0).write(rom, text_start, offset) # pad to 4 byte align
626630

627631
return offset
628632

@@ -651,8 +655,14 @@ def __init__(self, raw_text, index, id, opts, offset, length):
651655

652656
# read a single message from rom
653657
@classmethod
654-
def from_rom(cls, rom, index):
655-
entry_offset = ENG_TABLE_START + 8 * index
658+
def from_rom(cls, rom, index, eng=True):
659+
if eng:
660+
table_start = ENG_TABLE_START
661+
text_start = ENG_TEXT_START
662+
else:
663+
table_start = JPN_TABLE_START
664+
text_start = JPN_TEXT_START
665+
entry_offset = table_start + 8 * index
656666
entry = rom.read_bytes(entry_offset, 8)
657667
next = rom.read_bytes(entry_offset + 8, 8)
658668

@@ -661,7 +671,7 @@ def from_rom(cls, rom, index):
661671
offset = bytes_to_int(entry[5:8])
662672
length = bytes_to_int(next[5:8]) - offset
663673

664-
raw_text = rom.read_bytes(TEXT_START + offset, length)
674+
raw_text = rom.read_bytes(text_start + offset, length)
665675

666676
return cls(raw_text, index, id, opts, offset, length)
667677

@@ -922,19 +932,50 @@ def read_messages(rom):
922932
index += 1
923933
table_offset += 8
924934

935+
# Also grab 0xFFFC entry from JP table.
936+
messages.append(read_fffc_message(rom))
925937
return messages
926938

939+
# The JP text table is the only source for ID 0xFFFC, which is used by the
940+
# title and file select screens. Preserve this table entry and text data when
941+
# overwriting the JP data. The regular read_messages function only reads English
942+
# data.
943+
def read_fffc_message(rom):
944+
table_offset = JPN_TABLE_START
945+
index = 0
946+
while True:
947+
entry = rom.read_bytes(table_offset, 8)
948+
id = bytes_to_int(entry[0:2])
949+
950+
if id == 0xFFFC:
951+
message = Message.from_rom(rom, index, eng=False)
952+
break
953+
954+
index += 1
955+
table_offset += 8
956+
957+
return message
958+
927959
# write the messages back
928960
def repack_messages(rom, messages, permutation=None, always_allow_skip=True, speed_up_text=True):
929961

930-
rom.update_dmadata_record(TEXT_START, TEXT_START, TEXT_START + ENG_TEXT_SIZE_LIMIT)
962+
rom.update_dmadata_record(ENG_TEXT_START, ENG_TEXT_START, ENG_TEXT_START + ENG_TEXT_SIZE_LIMIT)
963+
rom.update_dmadata_record(JPN_TEXT_START, JPN_TEXT_START, JPN_TEXT_START + JPN_TEXT_SIZE_LIMIT)
931964

932965
if permutation is None:
933966
permutation = range(len(messages))
934967

935968
# repack messages
936969
offset = 0
937-
text_size_limit = ENG_TEXT_SIZE_LIMIT
970+
text_start = JPN_TEXT_START
971+
text_size_limit = EXTENDED_TEXT_SIZE_LIMIT
972+
text_bank = 0x08 # start with the Japanese text bank
973+
jp_bytes = 0
974+
# An extra dummy message is inserted after exhausting the JP text file.
975+
# Written message IDs are independent of the python list index, but the
976+
# index has to be maintained for old/new lookups. This wouldn't be an
977+
# issue if text shuffle didn't exist.
978+
jp_index_offset = 0
938979

939980
for old_index, new_index in enumerate(permutation):
940981
old_message = messages[old_index]
@@ -943,21 +984,57 @@ def repack_messages(rom, messages, permutation=None, always_allow_skip=True, spe
943984
new_message.id = old_message.id
944985

945986
# modify message, making it represent how we want it to be written
946-
new_message.transform(True, old_message.ending, always_allow_skip, speed_up_text)
987+
if new_message.id != 0xFFFC:
988+
new_message.transform(True, old_message.ending, always_allow_skip, speed_up_text)
989+
990+
# check if there is space to write the message
991+
message_size = new_message.size()
992+
if message_size + offset > JPN_TEXT_SIZE_LIMIT and text_start == JPN_TEXT_START:
993+
# Add a dummy entry to the table for the last entry in the
994+
# JP file. This is used by the game to calculate message
995+
# length. Since the next entry in the English table has an
996+
# offset of zero, which would lead to a negative length.
997+
# 0xFFFD is used as the text ID for this in vanilla.
998+
# Text IDs need to be in order across the table for the
999+
# split to work.
1000+
entry = bytes([0xFF, 0xFD, 0x00, 0x00, text_bank]) + int_to_bytes(offset, 3)
1001+
entry_offset = EXTENDED_TABLE_START + 8 * old_index
1002+
rom.write_bytes(entry_offset, entry)
1003+
# if there is no room then switch to the English text bank
1004+
text_bank = 0x07
1005+
text_start = ENG_TEXT_START
1006+
jp_bytes = offset
1007+
jp_index_offset = 1
1008+
offset = 0
1009+
1010+
# Special handling for text ID 0xFFFC, which has hard-coded offsets to
1011+
# the JP file in function Font_LoadOrderedFont in z_kanfont.c
1012+
if new_message.id == 0xFFFC:
1013+
# hard-coded offset including segment
1014+
rom.write_int16(0xAD1CE2, (text_bank << 8) + ((offset & 0xFFFF0000) >> 16) + (1 if offset & 0xFFFF > 0x8000 else 0))
1015+
rom.write_int16(0xAD1CE6, offset & 0XFFFF)
1016+
# hard-coded message length, represented by offset of end of message
1017+
rom.write_int16(0xAD1D16, (text_bank << 8) + (((offset + new_message.size()) & 0xFFFF0000) >> 16) + (1 if (offset + new_message.size()) & 0xFFFF > 0x8000 else 0))
1018+
rom.write_int16(0xAD1D1E, (offset + new_message.size()) & 0XFFFF)
1019+
# hard-coded segment, default JP file (0x08)
1020+
rom.write_int16(0xAD1D12, (text_bank << 8))
1021+
# hard-coded text file start address in rom, default JP
1022+
rom.write_int16(0xAD1D22, ((text_start & 0xFFFF0000) >> 16) + (1 if text_start & 0xFFFF > 0x8000 else 0))
1023+
rom.write_int16(0xAD1D2E, text_start & 0XFFFF)
9471024

9481025
# actually write the message
949-
offset = new_message.write(rom, old_index, offset)
1026+
offset = new_message.write(rom, old_index + jp_index_offset, text_start, offset, text_bank)
9501027

9511028
new_message.id = remember_id
9521029

9531030
# raise an exception if too much is written
9541031
# we raise it at the end so that we know how much overflow there is
955-
if offset > text_size_limit:
956-
raise(TypeError("Message Text table is too large: 0x" + "{:x}".format(offset) + " written / 0x" + "{:x}".format(ENG_TEXT_SIZE_LIMIT) + " allowed."))
1032+
if jp_bytes + offset > text_size_limit:
1033+
raise(TypeError("Message Text table is too large: 0x" + "{:x}".format(jp_bytes + offset) + " written / 0x" + "{:x}".format(EXTENDED_TEXT_SIZE_LIMIT) + " allowed."))
9571034

958-
# end the table
959-
table_index = len(messages)
960-
entry = bytes([0xFF, 0xFD, 0x00, 0x00, 0x07]) + int_to_bytes(offset, 3)
1035+
# end the table, accounting for additional entry for file split
1036+
table_index = len(messages) + (1 if text_bank == 0x07 else 0)
1037+
entry = bytes([0xFF, 0xFD, 0x00, 0x00, text_bank]) + int_to_bytes(offset, 3)
9611038
entry_offset = EXTENDED_TABLE_START + 8 * table_index
9621039
rom.write_bytes(entry_offset, entry)
9631040
table_index += 1
@@ -982,6 +1059,7 @@ def is_exempt(m):
9821059
)
9831060
shuffle_exempt = [
9841061
0x208D, # "One more lap!" for Cow in House race.
1062+
0xFFFC, # Character data from JP table used on title and file select screens
9851063
]
9861064
is_hint = (except_hints and m.id in hint_ids)
9871065
is_error_message = (m.id == ERROR_MESSAGE)

Notes/message_table_notes.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Message_FindMessage (debug) -> func_800D6A90 (1.0 EN) / func_800D69EC (1.0 JP)
2+
save context + 0x2200 = play->msgCtx.font
3+
line 12279 in data.s in disassembly
4+
80112E34 = pointer to JP table (8010A94C) line 10152
5+
80112E38 = pointer to EN table (8010EA8C) line 11196
6+
credit table? 80112CAC line 12254
7+
findmessage called near lbl_800DCB60 (JP) / lbl_800DCC04 (EN)
8+
9+
text ID FFFC hard-coded offsets for JP file in func_8005BD78 / Font_LoadOrderedFont in kanfont.s / z_kanfont.c
10+
DMA request needs to be modified to point to EN file if FFFC relocated there
11+
a3 = 0803A150 change to actual FFFC offset, modify segment to 07 if in EN file
12+
t6 = 08000000 change to 07000000 if FFFC in EN file
13+
t7 = 0803A340 change to actual FFFD offset, modify segment to 07 if in EN file (used for FFFC length)
14+
t8 = 008EB000 change to 0092D000 for start of file pointer
15+
16+

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ issue. You should always Hard Reset to avoid this issue entirely.
128128
* The model used for Ruto's Letter is now rotated onto its side to better differentiate from other bottles.
129129
* The dummy boss key chest on the wall of Forest Temple's twisted hallway now matches the real version of the chest in the straightened version of the room.
130130
* The Kakariko Well water will no longer be up as adult to facilitate a glitch strategy for entering the well.
131+
* The message table has been extended to allow further developments which require more added textboxes.
131132

132133
#### Plandomizer
133134
* Plandomizer now allows you to specify locations that are valid but do not exist in your current seed, for example, an MQ-specific location when that dungeon is Vanilla.

0 commit comments

Comments
 (0)