-
Notifications
You must be signed in to change notification settings - Fork 365
/
MQ.py
724 lines (571 loc) · 23.7 KB
/
MQ.py
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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
# mzxrules 2018
# In order to patch MQ to the existing data...
#
# Scenes:
#
# Ice Cavern (Scene 9) needs to have it's header altered to support MQ's path list. This
# expansion will delete the otherwise unused alternate headers command
#
# Transition actors will be patched over the old data, as the number of records is the same
# Path data will be appended to the end of the scene file.
#
# The size of a single path on file is NUM_POINTS * 6, rounded up to the nearest 4 byte boundary
# The total size consumed by the path data is NUM_PATHS * 8, plus the sum of all path file sizes
# padded to the nearest 0x10 bytes
#
# Collision:
# OoT's collision data consists of these elements: vertices, surface types, water boxes,
# camera behavior data, and polys. MQ's vertice and polygon geometry data is identical.
# However, the surface types and the collision exclusion flags bound to the polys have changed
# for some polygons, as well as the number of surface type records and camera type records.
#
# To patch collision, a flag denotes whether collision data cannot be written in place without
# expanding the size of the scene file. If true, the camera data is relocated to the end
# of the scene file, and the surface types are shifted down into where the camera types
# were situated. If false, the camera data isn't moved, but rather the surface type list
# will be shifted to the end of the camera data
#
# Rooms:
#
# Object file initialization data will be appended to the end of the room file.
# The total size consumed by the object file data is NUM_OBJECTS * 0x02, aligned to
# the nearest 0x04 bytes
#
# Actor spawn data will be appended to the end of the room file, after the objects.
# The total size consumed by the actor spawn data is NUM_ACTORS * 0x10
#
# Finally:
#
# Scene and room files will be padded to the nearest 0x10 bytes
#
# Maps:
# Jabu Jabu's B1 map contains no chests in the vanilla layout. Because of this,
# the floor map data is missing a vertex pointer that would point within kaleido_scope.
# As such, if the file moves, the patch will break.
from Utils import data_path
from Rom import LocalRom
import json
from struct import pack, unpack
SCENE_TABLE = 0xB71440
class File(object):
def __init__(self, file):
self.name = file['Name']
self.start = int(file['Start'], 16)
self.end = int(file['End'], 16)
self.remap = file['RemapStart']
# used to update the file's associated dmadata record
self.dma_key = self.start
if self.remap is not None:
self.remap = int(self.remap, 16)
def __repr__(self):
remap = "None"
if self.remap is not None:
remap = "{0:x}".format(self.remap)
return "{0}: {1:x} {2:x}, remap {3}".format(self.name, self.start, self.end, remap)
def relocate(self, rom:LocalRom):
if self.remap is None:
return
new_start = self.remap
offset = new_start - self.start
new_end = self.end + offset
rom.buffer[new_start:new_end] = rom.buffer[self.start:self.end]
self.start = new_start
self.end = new_end
class CollisionMesh(object):
def __init__(self, rom:LocalRom, start, offset):
self.offset = offset
self.poly_addr = rom.read_int32(start + offset + 0x18)
self.polytypes_addr = rom.read_int32(start + offset + 0x1C)
self.camera_data_addr = rom.read_int32(start + offset + 0x20)
self.polytypes = (self.poly_addr - self.polytypes_addr) // 8
def write_to_scene(self, rom:LocalRom, start):
addr = start + self.offset + 0x18
rom.write_int32s(addr, [self.poly_addr, self.polytypes_addr, self.camera_data_addr])
class ColDelta(object):
def __init__(self, delta):
self.is_larger = delta['IsLarger']
self.polys = delta['Polys']
self.polytypes = delta['PolyTypes']
self.cams = delta['Cams']
class Icon(object):
def __init__(self, data):
self.icon = data["Icon"];
self.count = data["Count"];
self.points = [IconPoint(x) for x in data["IconPoints"]]
def write_to_minimap(self, rom:LocalRom, addr):
rom.write_sbyte(addr, self.icon)
rom.write_byte(addr + 1, self.count)
cur = 2
for p in self.points:
p.write_to_minimap(rom, addr + cur)
cur += 0x03
def write_to_floormap(self, rom:LocalRom, addr):
rom.write_int16(addr, self.icon)
rom.write_int32(addr + 0x10, self.count)
cur = 0x14
for p in self.points:
p.write_to_floormap(rom, addr + cur)
cur += 0x0C
class IconPoint(object):
def __init__(self, point):
self.flag = point["Flag"]
self.x = point["x"]
self.y = point["y"]
def write_to_minimap(self, rom:LocalRom, addr):
rom.write_sbyte(addr, self.flag)
rom.write_byte(addr+1, self.x)
rom.write_byte(addr+2, self.y)
def write_to_floormap(self, rom:LocalRom, addr):
rom.write_int16(addr, self.flag)
rom.write_f32(addr + 4, float(self.x))
rom.write_f32(addr + 8, float(self.y))
class Scene(object):
def __init__(self, scene):
self.file = File(scene['File'])
self.id = scene['Id']
self.transition_actors = [convert_actor_data(x) for x in scene['TActors']]
self.rooms = [Room(x) for x in scene['Rooms']]
self.paths = []
self.coldelta = ColDelta(scene["ColDelta"])
self.minimaps = [[Icon(icon) for icon in minimap['Icons']] for minimap in scene['Minimaps']]
self.floormaps = [[Icon(icon) for icon in floormap['Icons']] for floormap in scene['Floormaps']]
temp_paths = scene['Paths']
for item in temp_paths:
self.paths.append(item['Points'])
def write_data(self, rom:LocalRom):
# write floormap and minimap data
self.write_map_data(rom)
# move file to remap address
self.file.relocate(rom)
start = self.file.start
headcur = self.file.start
room_list_offset = 0
code = rom.read_byte(headcur)
loop = 0x20
while loop > 0 and code != 0x14: #terminator
loop -= 1
if code == 0x03: #collision
col_mesh_offset = rom.read_int24(headcur + 5)
col_mesh = CollisionMesh(rom, start, col_mesh_offset)
self.patch_mesh(rom, col_mesh);
elif code == 0x04: #rooms
room_list_offset = rom.read_int24(headcur + 5)
elif code == 0x0D: #paths
path_offset = self.append_path_data(rom)
rom.write_int32(headcur + 4, path_offset)
elif code == 0x0E: #transition actors
t_offset = rom.read_int24(headcur + 5)
addr = self.file.start + t_offset
write_actor_data(rom, addr, self.transition_actors)
headcur += 8
code = rom.read_byte(headcur)
# update file references
self.file.end = align16(self.file.end)
update_dmadata(rom, self.file)
update_scene_table(rom, self.id, self.file.start, self.file.end)
# write room file data
for room in self.rooms:
room.write_data(rom)
if self.id == 6 and room.id == 6:
patch_spirit_temple_mq_room_6(rom, room.file.start)
cur = self.file.start + room_list_offset
for room in self.rooms:
rom.write_int32s(cur, [room.file.start, room.file.end])
cur += 0x08
def write_map_data(self, rom:LocalRom):
if self.id >= 10:
return
# write floormap
floormap_indices = 0xB6C934
floormap_vrom = 0xBC7E00
floormap_index = rom.read_int16(floormap_indices + (self.id * 2))
floormap_index //= 2 # game uses texture index, where two textures are used per floor
cur = floormap_vrom + (floormap_index * 0x1EC)
for floormap in self.floormaps:
for icon in floormap:
Icon.write_to_floormap(icon, rom, cur)
cur += 0xA4
# fixes jabu jabu floor B1 having no chest data
if self.id == 2:
cur = floormap_vrom + (0x08 * 0x1EC + 4)
kaleido_scope_chest_verts = 0x803A3DA0 # hax, should be vram 0x8082EA00
rom.write_int32s(cur, [0x17, kaleido_scope_chest_verts, 0x04])
# write minimaps
map_mark_vrom = 0xBF40D0
map_mark_vram = 0x808567F0
map_mark_array_vram = 0x8085D2DC # ptr array in map_mark_data to minimap "marks"
array_vrom = map_mark_array_vram - map_mark_vram + map_mark_vrom
map_mark_scene_vram = rom.read_int32(self.id * 4 + array_vrom)
mark_vrom = map_mark_scene_vram - map_mark_vram + map_mark_vrom
cur = mark_vrom
for minimap in self.minimaps:
for icon in minimap:
Icon.write_to_minimap(icon, rom, cur)
cur += 0x26
def patch_mesh(self, rom:LocalRom, mesh:CollisionMesh):
start = self.file.start
final_cams = []
# build final camera data
for cam in self.coldelta.cams:
data = cam['Data']
pos = cam['PositionIndex']
if pos < 0:
final_cams.append((data, 0))
else:
addr = start + (mesh.camera_data_addr & 0xFFFFFF)
seg_off = rom.read_int32(addr + (pos * 8) + 4)
final_cams.append((data, seg_off))
types_move_addr = 0
# if data can't fit within the old mesh space, append camera data
if self.coldelta.is_larger:
types_move_addr = mesh.camera_data_addr
# append to end of file
self.write_cam_data(rom, self.file.end, final_cams)
mesh.camera_data_addr = get_segment_address(2, self.file.end - self.file.start)
self.file.end += len(final_cams) * 8
else:
types_move_addr = mesh.camera_data_addr + (len(final_cams) * 8)
# append in place
addr = self.file.start + (mesh.camera_data_addr & 0xFFFFFF)
self.write_cam_data(rom, addr, final_cams)
# if polytypes needs to be moved, do so
if (types_move_addr != mesh.polytypes_addr):
a_start = self.file.start + (mesh.polytypes_addr & 0xFFFFFF)
b_start = self.file.start + (types_move_addr & 0xFFFFFF)
size = mesh.polytypes * 8
rom.buffer[b_start:b_start + size] = rom.buffer[a_start:a_start + size]
mesh.polytypes_addr = types_move_addr
# patch polytypes
for item in self.coldelta.polytypes:
id = item['Id']
high = item['High']
low = item['Low']
addr = self.file.start + (mesh.polytypes_addr & 0xFFFFFF) + (id * 8)
rom.write_int32s(addr, [high, low])
# patch poly data
for item in self.coldelta.polys:
id = item['Id']
t = item['Type']
flags = item['Flags']
addr = self.file.start + (mesh.poly_addr & 0xFFFFFF) + (id * 0x10)
vert_bit = rom.read_byte(addr + 0x02) & 0x1F # VertexA id data
rom.write_int16(addr, t)
rom.write_byte(addr + 0x02, (flags << 5) + vert_bit)
# Write Mesh to Scene
mesh.write_to_scene(rom, self.file.start)
def write_cam_data(self, rom:LocalRom, addr, cam_data):
for item in cam_data:
data, pos = item
rom.write_int32s(addr, [data, pos])
addr += 8
# appends path data to the end of the rom
# returns segment address to path data
def append_path_data(self, rom:LocalRom):
start = self.file.start
cur = self.file.end
records = []
for path in self.paths:
nodes = len(path)
offset = get_segment_address(2, cur - start)
records.append((nodes, offset))
#flatten
points = [x for points in path for x in points]
rom.write_int16s(cur, points)
path_size = align4(len(path) * 6)
cur += path_size
records_offset = get_segment_address(2, cur - start)
for node, offset in records:
rom.write_byte(cur, node)
rom.write_int32(cur + 4, offset)
cur += 8
self.file.end = cur
return records_offset
class Room(object):
def __init__(self, room):
self.file = File(room['File'])
self.id = room['Id']
self.objects = [int(x, 16) for x in room['Objects']]
self.actors = [convert_actor_data(x) for x in room['Actors']]
def write_data(self, rom:LocalRom):
# move file to remap address
self.file.relocate(rom)
headcur = self.file.start
code = rom.read_byte(headcur)
loop = 0x20
while loop != 0 and code != 0x14: #terminator
loop -= 1
if code == 0x01: # actors
offset = self.file.end - self.file.start
write_actor_data(rom, self.file.end, self.actors)
self.file.end += len(self.actors) * 0x10
rom.write_byte(headcur + 1, len(self.actors))
rom.write_int32(headcur + 4, get_segment_address(3, offset))
elif code == 0x0B: # objects
offset = self.append_object_data(rom, self.objects)
rom.write_byte(headcur + 1, len(self.objects))
rom.write_int32(headcur + 4, get_segment_address(3, offset))
headcur += 8
code = rom.read_byte(headcur)
# update file reference
self.file.end = align16(self.file.end)
update_dmadata(rom, self.file)
def append_object_data(self, rom:LocalRom, objects):
offset = self.file.end - self.file.start
cur = self.file.end
rom.write_int16s(cur, objects)
objects_size = align4(len(objects) * 2)
self.file.end += objects_size
return offset
def patch_files(rom:LocalRom, mq_scenes:list):
data = get_json()
scenes = [Scene(x) for x in data]
for scene in scenes:
if scene.id in mq_scenes:
if scene.id == 9:
patch_ice_cavern_scene_header(rom)
scene.write_data(rom)
def get_json():
with open(data_path('mqu.json'), 'r') as stream:
data = json.load(stream)
return data
def convert_actor_data(str):
spawn_args = str.split(" ")
return [ int(x,16) for x in spawn_args ]
def get_segment_address(base, offset):
offset &= 0xFFFFFF
base *= 0x01000000
return base + offset
def patch_ice_cavern_scene_header(rom):
rom.buffer[0x2BEB000:0x2BEB038] = rom.buffer[0x2BEB008:0x2BEB040]
rom.write_int32s(0x2BEB038, [0x0D000000, 0x02000000])
def patch_spirit_temple_mq_room_6(rom:LocalRom, room_addr):
cur = room_addr
actor_list_addr = 0
cmd_actors_offset = 0
# scan for actor list and header end
code = rom.read_byte(cur)
while code != 0x14: #terminator
if code == 0x01: # actors
actor_list_addr = rom.read_int32(cur + 4)
cmd_actors_offset = cur - room_addr
cur += 8
code = rom.read_byte(cur)
cur += 8
# original header size
header_size = cur - room_addr
# set alternate header data location
alt_data_off = header_size + 8
# set new alternate header offset
alt_header_off = align16(alt_data_off + (4 * 3)) # alt header record size * num records
# write alternate header data
# the first 3 words are mandatory. the last 3 are just to make the binary
# cleaner to read
rom.write_int32s(room_addr + alt_data_off,
[0, get_segment_address(3, alt_header_off), 0, 0, 0, 0])
# clone header
a_start = room_addr
a_end = a_start + header_size
b_start = room_addr + alt_header_off
b_end = b_start + header_size
rom.buffer[b_start:b_end] = rom.buffer[a_start:a_end]
# make the child header skip the first actor,
# which avoids the spawning of the block while in the hole
cmd_addr = room_addr + cmd_actors_offset
actor_list_addr += 0x10
actors = rom.read_byte(cmd_addr + 1)
rom.write_byte(cmd_addr+1, actors - 1)
rom.write_int32(cmd_addr + 4, actor_list_addr)
# move header
rom.buffer[a_start + 8:a_end + 8] = rom.buffer[a_start:a_end]
# write alternate header command
seg = get_segment_address(3, alt_data_off)
rom.write_int32s(room_addr, [0x18000000, seg])
def verify_remap(scenes):
def test_remap(file:File):
if file.remap is not None:
if file.start < file.remap:
return False
return True
print("test code: verify remap won't corrupt data")
for scene in scenes:
file = scene.file
result = test_remap(file)
print("{0} - {1}".format(result, file))
for room in scene.rooms:
file = room.file
result = test_remap(file)
print("{0} - {1}".format(result, file))
def update_dmadata(rom:LocalRom, file:File):
key, start, end = file.dma_key, file.start, file.end
rom.update_dmadata_record(key, start, end)
def update_scene_table(rom:LocalRom, sceneId, start, end):
cur = sceneId * 0x14 + SCENE_TABLE
rom.write_int32s(cur, [start, end])
def write_actor_data(rom:LocalRom, cur, actors):
for actor in actors:
rom.write_int16s(cur, actor)
cur += 0x10
def align4(value):
return ((value + 3) // 4) * 4
def align16(value):
return ((value + 0xF) // 0x10) * 0x10
# This function inserts space in a ovl section at the section's offset
# The section size is expanded
# Every relocation entry in the section after the offet is moved accordingly
# Every relocation value that is after the inserted space is increased accordingly
def insert_space(rom, file, vram_start, insert_section, insert_offset, insert_size):
sections = []
val_hi = {}
adr_hi = {}
# get the ovl header
cur = file.end - rom.read_int32(file.end - 4)
section_total = 0
for i in range(0, 4):
# build the section offsets
section_size = rom.read_int32(cur)
sections.append(section_total)
section_total += section_size
# increase the section to be expanded
if insert_section == i:
rom.write_int32(cur, section_size + insert_size)
cur += 4
# calculate the insert address in vram
insert_vram = sections[insert_section] + insert_offset + vram_start
insert_rom = sections[insert_section] + insert_offset + file.start
# iterate over the relocation table
relocate_count = rom.read_int32(cur)
cur += 4
for i in range(0, relocate_count):
entry = rom.read_int32(cur)
# parse relocation entry
section = ((entry & 0xC0000000) >> 30) - 1
type = (entry & 0x3F000000) >> 24
offset = entry & 0x00FFFFFF
# calculate relocation address in rom
address = file.start + sections[section] + offset
# move relocation if section is increased and it's after the insert
if insert_section == section and offset >= insert_offset:
# rebuild new relocation entry
rom.write_int32(cur,
((section + 1) << 30) |
(type << 24) |
(offset + insert_size))
# value contains the vram address
value = rom.read_int32(address)
raw_value = value
if type == 2:
# Data entry: value is the raw vram address
pass
elif type == 4:
# Jump OP: Get the address from a Jump instruction
value = 0x80000000 | (value & 0x03FFFFFF) << 2
elif type == 5:
# Load High: Upper half of an address load
reg = (value >> 16) & 0x1F
val_hi[reg] = (value & 0x0000FFFF) << 16
adr_hi[reg] = address
# Do not process, wait until the lower half is read
value = None
elif type == 6:
# Load Low: Lower half of the address load
reg = (value >> 21) & 0x1F
val_low = value & 0x0000FFFF
val_low = unpack('h', pack('H', val_low))[0]
# combine with previous load high
value = val_hi[reg] + val_low
else:
# unknown. OoT does not use any other types
value = None
# update the vram values if it's been moved
if value != None and value >= insert_vram:
# value = new vram address
new_value = value + insert_size
if type == 2:
# Data entry: value is the raw vram address
rom.write_int32(address, new_value)
elif type == 4:
# Jump OP: Set the address in the Jump instruction
op = rom.read_int32(address) & 0xFC000000
new_value = (new_value & 0x0FFFFFFC) >> 2
new_value = op | new_value
rom.write_int32(address, new_value)
elif type == 6:
# Load Low: Lower half of the address load
op = rom.read_int32(address) & 0xFFFF0000
new_val_low = new_value & 0x0000FFFF
rom.write_int32(address, op | new_val_low)
# Load High: Upper half of an address load
op = rom.read_int32(adr_hi[reg]) & 0xFFFF0000
new_val_hi = (new_value & 0xFFFF0000) >> 16
if new_val_low >= 0x8000:
# add 1 if the lower part is negative for borrow
new_val_hi += 1
rom.write_int32(adr_hi[reg], op | new_val_hi)
cur += 4
# Move rom bytes
rom.buffer[(insert_rom + insert_size):(file.end + insert_size)] = rom.buffer[insert_rom:file.end]
rom.buffer[insert_rom:(insert_rom + insert_size)] = [0] * insert_size
file.end += insert_size
def add_relocations(rom, file, addresses):
relocations = []
sections = []
header_size = rom.read_int32(file.end - 4)
header = file.end - header_size
cur = header
# read section sizes and build offsets
section_total = 0
for i in range(0, 4):
section_size = rom.read_int32(cur)
sections.append(section_total)
section_total += section_size
cur += 4
# get all entries in relocation table
relocate_count = rom.read_int32(cur)
cur += 4
for i in range(0, relocate_count):
relocations.append(rom.read_int32(cur))
cur += 4
# create new enties
for address in addresses:
if isinstance(address, tuple):
# if type provided use it
type, address = address
else:
# Otherwise, try to infer type from value
value = rom.read_int32(address)
op = value >> 26
type = 2 # default: data
if op == 0x02 or op == 0x03: # j or jal
type = 4
elif op == 0x0F: # lui
type = 5
elif op == 0x08: # addi
type = 6
# Calculate section and offset
address = address - file.start
section = 0
for section_start in sections:
if address >= section_start:
section += 1
else:
break
offset = address - sections[section - 1]
# generate relocation entry
relocations.append((section << 30)
| (type << 24)
| (offset & 0x00FFFFFF))
# Rebuild Relocation Table
cur = header + 0x10
relocations.sort(key = lambda val: val & 0xC0FFFFFF)
rom.write_int32(cur, len(relocations))
cur += 4
for relocation in relocations:
rom.write_int32(cur, relocation)
cur += 4
# Add padded 0?
rom.write_int32(cur, 0)
cur += 4
# Update Header and File size
new_header_size = (cur + 4) - header
rom.write_int32(cur, new_header_size)
file.end += (new_header_size - header_size)