-
Notifications
You must be signed in to change notification settings - Fork 12
/
text_database.rb
178 lines (146 loc) · 7.01 KB
/
text_database.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
require 'csv'
class TextDatabase
attr_reader :fs,
:text_list
def initialize(fs)
@fs = fs
@text_list = TextList.new(fs) # Lazy loader
end
def write_to_rom
# Writes all text back into ROM, repointing everything in case any strings are longer than they were originally.
# For DoS, note that you can't go past where strings would normally end in RAM for the original game - that would either overwrite or be overwritten by other data.
# However, this function packs the new strings in more snugly than they were originally in DoS, leaving no free space between them. This means that there is a significant amount of extra room for increasing the size of strings at the end. But not unlimited, so raise an error if even that extra space isn't enough to hold the new strings.
# For OoE, the strings are already packed snugly, so we can't gain any space that way. But we can expand the overlay file here since nothing immediately follows it in RAM.
# For PoR, the strings are split across two different overlay files, so we have to handle those separately.
if REGION == :jp || GAME == "hod"
header_footer_length = 4
else
header_footer_length = 3
end
# Clear all text that is currently using free space beforehand.
text_list.each do |text|
if GAME == "por"
overlay_allowable_end = STRING_DATABASE_ALLOWABLE_END_OFFSET_FOR_OVERLAY[text.overlay_id]
else
overlay_allowable_end = STRING_DATABASE_ALLOWABLE_END_OFFSET
end
if text.string_ram_pointer < STRING_DATABASE_START_OFFSET || text.string_ram_pointer >= overlay_allowable_end
# This text isn't currently in the original string database. It's in free space.
# Therefore, we tell the free space manager to clear it, so this space can be used again.
string_length = text.original_encoded_string_length + header_footer_length
if string_length % 4 != 0
# Because the free space manager pads the free spaces it gives up to 4 bytes, we have to clear up to the padded length to fully clear each of these.
string_length = ((string_length / 4) * 4) + 4
end
fs.free_unused_space(text.string_ram_pointer, string_length)
end
end
overlays = TEXT_REGIONS_OVERLAYS.values.uniq
overlays.each do |overlay|
fs.load_overlay(overlay) if overlay
if GAME == "por"
overlay_original_end = STRING_DATABASE_ORIGINAL_END_OFFSET_FOR_OVERLAY[overlay]
overlay_allowable_end = STRING_DATABASE_ALLOWABLE_END_OFFSET_FOR_OVERLAY[overlay]
else
overlay_original_end = STRING_DATABASE_ORIGINAL_END_OFFSET
overlay_allowable_end = STRING_DATABASE_ALLOWABLE_END_OFFSET
end
# Remove nonzero free spaces just once for each overlay, instead of once for each string.
fs.automatically_remove_nonzero_free_spaces_for_overlay(overlay)
text_list_for_overlay = text_list.select{|text| text.overlay_id == overlay}
next_string_ram_pointer = STRING_DATABASE_START_OFFSET
writing_to_end_of_file = false
using_free_space_manager = false
text_list_for_overlay.each do |text|
string_length = text.encoded_string.length + header_footer_length
if next_string_ram_pointer + string_length >= overlay_allowable_end
# Writing strings past this point would result in something being overwritten, so start using the free space manager instead.
using_free_space_manager = true
end
region_name = TEXT_REGIONS.find{|name, range| range.include?(text.text_id)}[0]
if using_free_space_manager
next_string_ram_pointer = fs.get_free_space(string_length, overlay, remove_nonzero_spaces = false)
# Write null bytes to where the string will take up so the free space manager doesn't consider this space free.
fs.write(next_string_ram_pointer, "\0"*string_length)
else
if !writing_to_end_of_file && GAME == "ooe" && next_string_ram_pointer + string_length >= overlay_original_end
# Reached the end of where strings were in the original game, but in OoE we can expand the file.
writing_to_end_of_file = true
end
if writing_to_end_of_file
next_string_ram_pointer = fs.expand_overlay_and_get_end(overlay, string_length)
end
# System strings and AoS strings must be aligned to the nearest 4 bytes or they won't be displayed.
region_name = TEXT_REGIONS.find{|name, range| range.include?(text.text_id)}[0]
if GAME == "aos" || (region_name == "System" && next_string_ram_pointer % 4 != 0)
next_string_ram_pointer = ((next_string_ram_pointer / 4) * 4) + 4
end
end
text.string_ram_pointer = next_string_ram_pointer
if !writing_to_end_of_file && !using_free_space_manager
next_string_ram_pointer += string_length
end
end
end
overlays.each do |overlay|
fs.load_overlay(overlay)
text_list_for_overlay = text_list.select{|text| text.overlay_id == overlay}
text_list_for_overlay.each do |text|
text.write_to_rom()
end
end
end
def export_to_csv(output_folder)
FileUtils.mkdir_p(output_folder)
TEXT_REGIONS.each do |region_name, text_index_range|
output_path = File.join(output_folder, "%s.csv" % region_name)
CSV.open(output_path, "wb", encoding: "UTF-8") do |csv|
text_list[text_index_range].each do |text|
csv << ["0x%03X" % text.text_id, text.decoded_string]
end
end
end
end
def import_from_csv(input_folder)
TEXT_REGIONS.each do |region_name, text_index_range|
input_path = File.join(input_folder, "%s.csv" % region_name)
rows = CSV.read(input_path, encoding: "UTF-8")
rows.each do |text_id, string|
text_id = text_id.to_i(16)
if !text_index_range.include?(text_id)
raise "Text ID 0x%03X does not belong in text region \"%s\"" % [text_id, region_name]
end
text = text_list[text_id]
text.decoded_string = string
end
end
end
end
class TextList
include Enumerable
attr_reader :fs
def initialize(fs)
@fs = fs
@text_list = [nil]*TEXT_RANGE.size
end
# Only create and cache the texts that were specifically requested.
def [](text_id_or_range)
if text_id_or_range.is_a?(Integer)
text_id = text_id_or_range
@text_list[text_id] ||= Text.new(text_id, fs)
elsif text_id_or_range.is_a?(Range)
range = text_id_or_range
@text_list[range].each_index do |text_id_offset|
text_id = range.begin + text_id_offset
@text_list[text_id] ||= Text.new(text_id, fs)
end
@text_list[range]
else
raise "Invalid argument to text list: #{text_id_or_range.inspect}"
end
end
def each(&block)
self[TEXT_RANGE] # Need to initialize the full text list before iterating over it.
@text_list.each(&block)
end
end