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
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
|
# SPDX-FileCopyrightText: © 2025 ZeldaRET
# SPDX-License-Identifier: CC0-1.0
from pathlib import Path
import abc
from dataclasses import dataclass
import enum
import reprlib
import io
from typing import TYPE_CHECKING, Sequence, Optional, Union, Any, Iterable, Generator
from pprint import pprint
if TYPE_CHECKING:
from .memorymap import MemoryContext
# 0: nothing, 1: in progress & waiting, 2: all
VERBOSE_FILE_TRY_PARSE_DATA = 0
VERBOSE_REPORT_RESBUF = False
DUMP_REPORTERS_IN_SOURCE = False
DUMP_XML_IN_SOURCE = False
DONT_MERGE_RESOURCE_BUFFERS_FROM_DIFFERENT_USERS = False
#
# file
#
# sentinel value
RESOURCE_PARSE_SUCCESS = object()
class ResourceParseException(Exception):
pass
class ResourceParseWaiting(ResourceParseException):
"""Resource has nothing to do and can't be parsed yet, try again later.
For example, another resource may need to be parsed first.
If all resources fail to parse in this way, it means parsing has come to a deadlock.
"""
def __init__(self, *args, waiting_for: list[Any]):
"""
waiting_for should be a non-empty list of things the resource is currently waiting for.
(one item per meaningful thing that is waited for)
It is only meant for informational purpose (such as debugging).
It may contain more than the strict minimum the resource needs to make some progress,
and less than everything the resource needs to be fully parsed.
"""
assert waiting_for
super().__init__(*args, waiting_for)
self.waiting_for = waiting_for
class ResourceParseInProgress(ResourceParseException):
"""Resource can't be parsed yet, try again later.
But there was progress (as opposed to ResourceParseWaiting indicating no progress).
"Progress" includes:
- other resources were reported
- information was passed to or made available to other resources
- data was partially parsed
"""
def __init__(self, *args, new_progress_done: list[Any], waiting_for: list[Any]):
"""
new_progress_done, waiting_for: see ResourceParseWaiting.__init__
"""
assert new_progress_done and waiting_for
super().__init__(*args, new_progress_done, waiting_for)
self.new_progress_done = new_progress_done
self.waiting_for = waiting_for
class ResourceParseImpossible(ResourceParseException):
"""Resource cannot be parsed, neither now nor later.
For example, there is unexpected data or values that cannot be handled properly.
"""
pass
class GetResourceAtResult(enum.Enum):
DEFINITIVE = enum.auto()
PERHAPS = enum.auto()
NONE_YET = enum.auto()
@dataclass
class ResourceBufferMarker:
name: str
file_start: int
file_end: int
users: set["Resource"]
@dataclass
class ResourcesOverlapError(Exception):
overlaps: list[tuple["Resource", "Resource"]]
class File:
"""A file is a collection of resources
It typically corresponds to a rom file (segment) but doesn't have to
It doesn't even need to correspond to the entirety of one or more .c files,
it can be only a fraction
It can also correspond to how some memory is laid out,
for example what a segment "contains" in the context of a file or resource.
"""
def __init__(
self,
name: str,
*,
data: Optional[memoryview] = None,
size: Optional[int] = None,
alignment: int = 8,
):
self.name = name
if data is None != size is None:
raise Exception(
"Exactly one of either data or size must be provided", data, size
)
if data is not None:
self.data = data
self.size = len(data)
else:
assert size is not None
self.data = None
self.size = size
self.alignment = alignment
self._resources: list[Resource] = []
self._is_resources_sorted = True
self.referenced_files: set[File] = set()
self.resource_buffer_markers_by_resource_type: dict[
type[Resource], list[ResourceBufferMarker]
] = dict()
def add_resource(self, resource: "Resource"):
assert resource is not None
assert resource not in self._resources
self._resources.append(resource)
self._is_resources_sorted = False
def extend_resources(self, resources: Sequence["Resource"]):
self._resources.extend(resources)
self._is_resources_sorted = False
def sort_resources(self):
self._resources.sort(key=lambda resource: resource.range_start)
self._is_resources_sorted = True
def get_resource_at(self, offset: int):
if __debug__:
# FIXME when resources overlap during parse, nothing catches that
# (and nothing can really catch it because of the unk-size resources)
# probably implement this check properly by finding all resources at the offset below,
# hopefully there is at most one and do normal operation,
# or if there are >=2 then error (or best_effort: pick the furthest resource?)
self.sort_resources()
self.check_overlapping_resources()
assert offset < self.size
# Resources may use a defined range with both start and end defined,
# or a range that only has its start defined.
# While looking for a resource with a defined range the request offset
# belongs to, also keep track of the last resource that starts at or before
# offset (note: that resource may or may not have an end range defined).
last_resource_before_offset: Union[Resource, None] = None
for resource in self._resources:
if resource.range_start <= offset:
if (
last_resource_before_offset is None
or last_resource_before_offset.range_start < resource.range_start
):
last_resource_before_offset = resource
if resource.range_end is not None:
# If the requested offset falls within a defined range, return that
# resource with GetResourceAtResult.DEFINITIVE .
if resource.range_start <= offset < resource.range_end:
return GetResourceAtResult.DEFINITIVE, resource
# If the loop exits normally, without returning a defined range resource,
# check if the last resource starting at or before the requested offset
# (if any) has an undefined range.
if (
last_resource_before_offset is not None
and last_resource_before_offset.range_end is None
):
if last_resource_before_offset.range_start == offset:
# Resources are always more than 0 bytes in size, so if the resource
# starts exactly at the requested offset, then it is guaranteed to
# cover (at least) that offset.
return GetResourceAtResult.DEFINITIVE, last_resource_before_offset
else:
# Return it with GetResourceAtResult.PERHAPS , as it may extend up to
# and beyond the requested offset (or not).
return GetResourceAtResult.PERHAPS, last_resource_before_offset
else:
# No (potential) resource at that offset (currently).
return GetResourceAtResult.NONE_YET, None
def get_overlapping_resources(self):
if not self._is_resources_sorted:
raise Exception("sort resources first")
overlaps: list[tuple[Resource, Resource]] = []
for i in range(1, len(self._resources)):
resource_a = self._resources[i - 1]
if resource_a.range_end is not None:
for j in range(i, len(self._resources)):
resource_b = self._resources[j]
# This should hold true, as resources are sorted
assert resource_a.range_start <= resource_b.range_start
if resource_a.range_end > resource_b.range_start:
overlaps.append((resource_a, resource_b))
else:
break
else:
for j in range(i, len(self._resources)):
resource_b = self._resources[j]
assert resource_a.range_start <= resource_b.range_start
if resource_a.range_start == resource_b.range_start:
overlaps.append((resource_a, resource_b))
else:
break
return overlaps
def check_overlapping_resources(self):
try:
overlaps = self.get_overlapping_resources()
if overlaps:
raise ResourcesOverlapError(overlaps)
except:
print(self.str_report())
raise
def get_non_parsed_resources(self):
return [resource for resource in self._resources if not resource.is_data_parsed]
def check_non_parsed_resources(self):
try:
resources_data_not_parsed = self.get_non_parsed_resources()
if resources_data_not_parsed:
print(len(resources_data_not_parsed), "resource(s) not parsed:")
for resource in resources_data_not_parsed:
print(resource)
if hasattr(resource, "last_parse_waiting_e"):
print(
" last_parse_waiting_e =",
repr(resource.last_parse_waiting_e),
)
else:
print("??? no last_parse_waiting_e ???")
print("then why has", resource.name, "not been parsed?")
BEST_EFFORT = False # TODO move
if BEST_EFFORT:
print("BEST_EFFORT: removing non-parsed resources")
for resource in resources_data_not_parsed:
self._resources.remove(resource)
self.add_resource(
BinaryBlobResource(
self,
resource.range_start,
(
resource.range_end
if resource.range_end is not None
else (
resource.range_start
+ 4 # TODO 4 if I_D_OMEGALUL else 1
)
),
f"{resource.name}_bin_placeholder",
)
)
return
raise Exception(
"resources not parsed",
len(resources_data_not_parsed),
[(r.name, r.__class__) for r in resources_data_not_parsed],
resources_data_not_parsed,
)
except:
print(self.str_report())
raise
def try_parse_resources_data(self, file_memory_context: "MemoryContext"):
"""Returns true if any progress was made between the method being called and the method returning."""
any_progress = False
while True:
any_data_parsed = False
# Parsing resources may add more, copy the list
# to avoid concurrent modification while iterating
resources_copy = self._resources.copy()
for resource in resources_copy:
if resource.is_data_parsed:
pass
else:
resource.last_parse_waiting_e = None
resource_memory_context = file_memory_context # TODO
try:
ret_try_parse_data = resource.try_parse_data(
resource_memory_context
)
except ResourceParseInProgress as e:
any_progress = True
if VERBOSE_FILE_TRY_PARSE_DATA >= 1:
pprint(
(
"(in progress) Defering parsing",
resource,
"progress:",
e.new_progress_done,
"waiting:",
e.waiting_for,
)
)
except ResourceParseWaiting as e:
if VERBOSE_FILE_TRY_PARSE_DATA >= 1:
pprint(
(
"(waiting) Defering parsing",
resource,
"waiting:",
e.waiting_for,
)
)
resource.last_parse_waiting_e = e
except (
ResourceParseException
) as e: # TODO ResourceParseImpossible ?
# TODO replace resource with binblob or something idk
print("Error while attempting to parse data", resource)
raise
except:
# TODO replace resource with binblob or something idk
print("Error while attempting to parse data", resource)
raise
else:
# Catch try_parse_data mistakenly returning successfully
# (instead of raising) by enforcing having it return a sentinel value
assert ret_try_parse_data is RESOURCE_PARSE_SUCCESS, (
"Resources should return RESOURCE_PARSE_SUCCESS when parsing is successful, "
"or raise ResourceParseInProgress/ResourceParseWaiting if parsing is unsuccessful",
resource,
resource.try_parse_data,
)
# resource parsed successfully
if VERBOSE_FILE_TRY_PARSE_DATA >= 2:
pprint(("(success) Done parsing", resource))
any_progress = True
assert resource.range_end is not None, (
resource,
resource.__class__,
)
resource.is_data_parsed = True
any_data_parsed = True
any_resource_added = len(self._resources) != len(resources_copy)
keep_looping = any_data_parsed or any_resource_added
if not keep_looping:
break
return any_progress
def mark_resource_buffer(
self,
reporter: "Resource",
resource_type: type["Resource"],
name: str,
file_start: int,
file_end: int,
):
# Ignore markers falling within existing resources
result, resource = self.get_resource_at(file_start)
if result == GetResourceAtResult.DEFINITIVE:
assert resource is not None
assert resource.range_end is not None
if resource.range_start <= file_start < file_end <= resource.range_end:
assert isinstance(resource, resource_type)
resource.reporters.add(reporter)
return
# Check for intersection with existing resources
for resource in self._resources:
# TODO and when resource.range_end is None ?
if resource.range_end is not None and (
resource.range_start <= file_start < resource.range_end
or resource.range_start < file_end <= resource.range_end
):
raise Exception(
f"Resource buffer {name=} of {resource_type=}"
f" to be marked at {file_start:#X}-{file_end:#X}"
f" intersects with existing {resource=}"
)
resource_buffer_markers = (
self.resource_buffer_markers_by_resource_type.setdefault(resource_type, [])
)
resource_buffer_markers.append(
ResourceBufferMarker(name, file_start, file_end, {reporter})
)
resource_buffer_markers.sort(key=lambda rbm: rbm.file_start)
def do_fuse(i_start, i_end):
assert i_start < i_end
if i_start + 1 == i_end:
return False
fused = resource_buffer_markers[i_start:i_end]
users = set()
for rbm in fused:
users.update(rbm.users)
resource_buffer_markers[i_start:i_end] = [
ResourceBufferMarker(
fused[0].name.removesuffix("_fused_") + "_fused_", # TODO
min(map(lambda _f: _f.file_start, fused)),
max(map(lambda _f: _f.file_end, fused)),
users,
)
]
return True
def fuse_more():
stride_first_i = None
for i, rbm in enumerate(resource_buffer_markers):
if stride_first_i is None:
stride_first_i = i
else:
assert i > 0
prev = resource_buffer_markers[i - 1]
if prev.file_end < rbm.file_start or (
DONT_MERGE_RESOURCE_BUFFERS_FROM_DIFFERENT_USERS
and prev.file_end == rbm.file_start
and prev.users != rbm.users
):
# disjointed
if do_fuse(stride_first_i, i):
return True
stride_first_i = i
if stride_first_i is not None:
return do_fuse(stride_first_i, len(resource_buffer_markers))
else:
return False
while fuse_more():
pass
def commit_resource_buffers(self):
# TODO rework resource buffers handling
# this won't play well with manually defined vtx arrays
# probably can't merge the vbuf refs so quick
for (
resource_type,
resource_buffer_markers,
) in self.resource_buffer_markers_by_resource_type.items():
if VERBOSE_REPORT_RESBUF:
print(resource_type, resource_buffer_markers)
for rbm in resource_buffer_markers:
result, resource = self.get_resource_at(rbm.file_start)
assert (
result != GetResourceAtResult.PERHAPS
), "commit_resource_buffers should be called at a point where all resources have a definitive range"
if result == GetResourceAtResult.NONE_YET:
resource = resource_type(
self, rbm.file_start, rbm.file_end, rbm.name
)
resource.reporters.update(rbm.users)
self.add_resource(resource)
else:
assert result == GetResourceAtResult.DEFINITIVE
assert resource is not None
assert resource.range_end is not None
assert (
resource.range_start
<= rbm.file_start
< rbm.file_end
<= resource.range_end
), (
"marked resource buffer overlaps with existing resource but also extends out of that resource",
hex(rbm.file_start),
hex(rbm.file_end),
rbm,
resource,
)
# TODO ? not really reporters but at least users, figure out reporters/users
resource.reporters.update(rbm.users)
self.resource_buffer_markers_by_resource_type = dict()
def add_unaccounted_resources(self, *, I_D_OMEGALUL: bool):
assert self._is_resources_sorted
assert self.data is not None
unaccounted_resources: list[Resource] = []
def add_unaccounted(range_start, range_end):
assert self.data is not None
if I_D_OMEGALUL:
# IDO aligns every declaration to 4, so declaring zeros
# that is actually padding for that purpose throws off matching.
# This block strips such zeros from the unaccounted range.
# Compute the amount of bytes from range_start to the
# next multiple of 4.
pad_bytes = (4 - range_start % 4) % 4
if pad_bytes != 0:
pad_range_end = range_start + pad_bytes
assert pad_range_end <= range_end
pad_data = self.data[range_start:pad_range_end]
if set(pad_data) != {0}:
raise Exception(
"Expected pad bytes to be 0",
hex(range_start),
hex(pad_range_end),
set(pad_data),
bytes(pad_data),
)
pad_resource = ZeroPaddingResource(
self,
range_start,
pad_range_end,
f"{self.name}_zero_padding_{range_start:06X}",
include_in_source=False,
)
unaccounted_resources.append(pad_resource)
range_start += pad_bytes
if range_start == range_end:
# It turns out the whole unaccounted range is
# zero padding, so do nothing else
return
assert range_start < range_end
unaccounted_data = self.data[range_start:range_end]
if set(unaccounted_data) == {0}:
unaccounted_resource = ZeroPaddingResource(
self,
range_start,
range_end,
f"{self.name}_zeros_{range_start:06X}",
)
else:
unaccounted_resource = BinaryBlobResource(
self,
range_start,
range_end,
f"{self.name}_unaccounted_{range_start:06X}",
)
unaccounted_resources.append(unaccounted_resource)
if self._resources:
# Add unaccounted if needed at the start of the file
resource_first = self._resources[0]
if resource_first.range_start > 0:
add_unaccounted(
0,
resource_first.range_start,
)
# Add unaccounted if needed at the end of the file
resource_last = self._resources[-1]
assert resource_last.range_end is not None, (
"add_unaccounted_resources should be called once all"
" resources are parsed and have a definitive range"
)
if resource_last.range_end < len(self.data):
add_unaccounted(
resource_last.range_end,
len(self.data),
)
else:
# Add unaccounted for the whole file
add_unaccounted(0, len(self.data))
for i in range(1, len(self._resources)):
resource_a = self._resources[i - 1]
resource_b = self._resources[i]
assert resource_a.range_end is not None, (
"add_unaccounted_resources should be called once all"
" resources are parsed and have a definitive range"
)
assert resource_a.range_end <= resource_b.range_start
# Add unaccounted if needed between two successive resources
if resource_a.range_end < resource_b.range_start:
try:
add_unaccounted(
resource_a.range_end,
resource_b.range_start,
)
except:
print(
"Could not add an unaccounted resource between the two resources:"
)
print(resource_a)
print(resource_b)
raise
self.extend_resources(unaccounted_resources)
def set_resources_paths(self, extracted_path, build_path, out_path):
for resource in self._resources:
resource.set_paths(extracted_path, build_path, out_path)
def write_resources_extracted(self, file_memory_context: "MemoryContext"):
for resource in self._resources:
assert resource.is_data_parsed, resource
resource_memory_context = file_memory_context # TODO
try:
resource.extract_to_path.parent.mkdir(parents=True, exist_ok=True)
resource.write_extracted(resource_memory_context)
except:
print("Couldn't write extracted resource", resource)
raise
# These two are set by calling set_source_path
source_c_path: Path
source_h_path: Path
def set_source_path(self, source_path: Path):
file_name = self.name
# May catch random problems but not a hard requirement otherwise
assert file_name and all(
(c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789")
for c in file_name
), file_name
self.source_file_name = file_name
self.source_c_path = source_path / f"{file_name}.c"
self.source_h_path = source_path / f"{file_name}.h"
def write_source(self):
def strip_extracted_prefix(path: Path) -> Path:
parts = path.parts
if parts[0] == "extracted":
return Path(*parts[2:]) # Skip first two parts
return path # Return original path if condition not met
assert hasattr(
self, "source_c_path"
), "set_source_path must be called before write_source"
assert hasattr(self, "source_h_path")
self.source_c_path.parent.mkdir(parents=True, exist_ok=True)
self.source_h_path.parent.mkdir(parents=True, exist_ok=True)
with self.source_c_path.open("w") as c:
with self.source_h_path.open("w") as h:
# Paths to files to be included
file_include_paths_complete: list[Path] = []
file_include_paths_complete.append(self.source_h_path)
for referenced_file in self.referenced_files:
assert isinstance(referenced_file, File), referenced_file
assert hasattr(referenced_file, "source_c_path"), (
"set_source_path must be called on all files before any write_source call",
referenced_file,
)
assert hasattr(referenced_file, "source_h_path")
file_include_paths_complete.append(
strip_extracted_prefix(referenced_file.source_h_path)
)
# Same as file_include_paths_complete,
# but paths that can be are made relative to the source C.
file_include_paths: list[Path] = []
for path_complete in file_include_paths_complete:
try:
path = path_complete.relative_to(self.source_c_path.parent)
except ValueError:
# Included path is not relative to this file's source C folder.
# Just use the complete path.
path = path_complete
file_include_paths.append(path)
included_headers_in_c = set()
included_headers_in_h = set()
for resource in self._resources:
incls = resource.get_c_includes()
assert not isinstance(incls, str)
included_headers_in_c.update(incls)
incls = resource.get_h_includes()
assert not isinstance(incls, str)
included_headers_in_h.update(incls)
for file_include_path in file_include_paths:
c.write(f'#include "{file_include_path}"\n')
c.write("\n")
c.writelines(
f'#include "{_h}"\n' for _h in sorted(included_headers_in_c)
)
c.write("\n")
INCLUDE_GUARD = self.source_file_name.upper() + "_H"
h.writelines(
(
f"#ifndef {INCLUDE_GUARD}\n",
f"#define {INCLUDE_GUARD}\n",
"\n",
)
)
h.writelines(
f'#include "{_h}"\n' for _h in sorted(included_headers_in_h)
)
h.write("\n")
if not self._is_resources_sorted:
self.sort_resources()
for resource in self._resources:
if resource.write_c_definition(c):
c.write("\n")
resource.write_c_declaration(h)
h.writelines(
(
"\n",
"#endif\n",
)
)
def str_report(self):
return "\n".join(
f"0x{resource.range_start:06X}-"
+ (
f"0x{resource.range_end:06X}"
if resource.range_end is not None
else "..."
)
+ f" {resource.name}"
for resource in self._resources
)
@reprlib.recursive_repr()
def __repr__(self):
return (
self.__class__.__qualname__
+ f"({self.name!r}, data is None={self.data is None}, size={self.size}, {self._resources!r})"
)
def __rich_repr__(self):
yield "name", self.name
yield "data is None", self.data is None
yield "size", self.size
yield "resources", self._resources
__rich_repr__.angular = True
#
# resources
#
class Resource(abc.ABC):
"""A resource is a blob of data inside a file.
(at least for now,) one resource = one symbol
Examples:
- a struct-defined piece of data, such as a SkeletonHeader
- an array of data, such as a display list Gfx[], or a texture u64[]
"""
braces_in_source = True
def __init_subclass__(cls, /, can_size_be_unknown=False, **kwargs):
super().__init_subclass__(**kwargs)
cls.can_size_be_unknown = can_size_be_unknown
def __init__(
self,
file: File,
range_start: int,
range_end: Optional[int],
name: str,
):
assert (
0 <= range_start < file.size
), f"{range_start=:#08X} out of range [0,{file.size=:#08X})"
if range_end is None:
assert self.can_size_be_unknown
else:
assert (
range_start < range_end <= file.size
), f"{range_end=:#08X} out of range [{range_start=:#08X},{file.size=:#08X})"
self.file = file
self.range_start = range_start
"""Offset in the file data this resource starts at (inclusive)
Example:
range_start = 1 and range_end = 3
means the resource is two bytes, the second and third bytes in the file
"""
self.range_end = range_end
"""Offset in the file data this resource end at (exclusive)
May be None if the resource size isn't known yet
(only if can_size_be_unknown is True, see __init_subclass__)
Must be set at the latest before try_parse_data returns normally (without raising)
See range_start
"""
self.name = name
"""Name of this resource, for logging/reporting.
This member is NOT to be used as a C identifier, symbol name or file name.
See also:
- symbol_name
- get_filename_stem
"""
self.symbol_name = name
"""Name of the symbol to use to reference this resource"""
self.is_data_parsed = False
"""Will be set to true when the resource is successfully parsed
(after a successful try_parse_data call)
"""
self.reporters: set[Resource] = set()
"""Collection of all the resources having reported this resource
TODO figure out what to do with this, for now thinking debugging"""
@abc.abstractmethod
def try_parse_data(self, memory_context: "MemoryContext") -> object:
"""Parse this resource's data bytes
This can typically result in finding more resources,
for example from pointer types.
If data can't be parsed yet, ResourceParseInProgress or ResourceParseWaiting should be raised.
Then this will be called again later.
Raising other ResourceParseException s abandons parsing this resource.
Other exceptions raised are not caught.
Note this can both add found resources to the file,
and wait before further parsing its own data (by raising ResourceParseInProgress).
If data is successfully parsed, the method should return normally
and RESOURCE_PARSE_SUCCESS should be returned.
Then this will not be called again.
"""
...
@abc.abstractmethod
def get_c_reference(self, resource_offset: int) -> str:
"""Get a C expression for referencing data in this resource (as a pointer)
The offset `resource_offset` is relative to the resource:
0 means the start of the resource data,
and NOT the start of the file the resource is in.
Should raise `ValueError` if the `resource_offset` isn't meaningful.
Examples:
- `StructData data`, `get_c_reference(0)` -> `&data`
- `u8 array[]`, `get_c_reference(0)` -> `&array[0]`
- `u8 array[]`, `get_c_reference(6)` -> `&array[6]`
- `u16 array[]`, `get_c_reference(6)` -> `&array[3]`
- `u16 array[]`, `get_c_reference(1)` -> raises `ValueError`
- `u64 texture[]`, `get_c_reference(0)` -> `texture`
"""
...
def get_c_expression_length(self, resource_offset: int) -> str:
"""Get a C expression for referencing the length of data in this resource
The offset `resource_offset` is relative to the resource, as in get_c_reference.
Should raise `ValueError` if the `resource_offset` isn't meaningful.
Examples:
- `StructData data`, `get_c_expression_length(0)` -> raises `ValueError`
- `u8 array[]`, `get_c_reference(0)` -> `ARRAY_COUNT(array)`
- `u8 array[]`, `get_c_reference(1)` -> raises `ValueError`
"""
# Override in children classes if needed
raise ValueError(
"This resource has no data with a length that can be referenced",
self.__class__,
self,
)
needs_build = False
"""Whether this resource needs processing by the build system.
If False, it is extracted directly as .inc.c and included as is.
See set_paths
"""
extracted_path_suffix = ".inc.c"
"""The file extension for constructing the path to extract this resource to.
See set_paths
"""
def get_filename_stem(self):
"""Stem (name without suffix) for the file to write this resource to
See set_paths
"""
return self.name
# These two are set by calling set_paths
extract_to_path: Path
inc_c_path: Path
def set_paths(self, extracted_path: Path, build_path: Path, out_path: Path):
"""Compute and set `self.extract_to_path` and `self.inc_c_path`
Examples with
extracted_path: `extracted/VERSION/`
build_path: `build/VERSION/`
out_path: `assets/.../`
Binary: extracted_path_suffix = ".bin"
with get_filename_stem() = "blob"
extract_to_path: `extracted/VERSION/assets/.../blob.bin`
inc_c_path: `assets/.../blob.inc.c`
C: extracted_path_suffix = ".inc.c"
with get_filename_stem() = "data"
extract_to_path: `extracted/VERSION/assets/.../data.inc.c`
inc_c_path: `assets/.../data.inc.c`
rgba16 image: extracted_path_suffix = ".png"
with get_filename_stem() = "img.rgba16"
extract_to_path: `extracted/VERSION/assets/.../img.rgba16.png`
inc_c_path: `assets/.../img.rgba16.inc.c`
"""
filename_stem = self.get_filename_stem()
extract_to_path = (
extracted_path / out_path / (filename_stem + self.extracted_path_suffix)
)
if not self.needs_build:
assert self.extracted_path_suffix == ".inc.c"
inc_c_path = out_path / (filename_stem + ".inc.c")
self.extract_to_path = extract_to_path
self.inc_c_path = inc_c_path
def get_c_includes(self) -> Iterable[str]:
return ()
def get_h_includes(self) -> Iterable[str]:
return ()
@abc.abstractmethod
def write_extracted(self, memory_context: "MemoryContext") -> None:
"""Write the extracted resource data to self.extract_to_path"""
...
@abc.abstractmethod
def get_c_declaration_base(self) -> str:
"""Get the base source for declaring this resource's symbol in C.
For example:
- "u8 blob[]", `return f"u8 {self.symbol_name}[]"`
- "DataStruct data", `return f"DataStruct {self.symbol_name}"`
"""
...
def get_as_xml(self) -> str:
raise NotImplementedError()
def write_c_definition(self, c: io.TextIOBase) -> bool:
"""
Returns True if something was written
"""
if DUMP_REPORTERS_IN_SOURCE:
c.write(f"//R: {' '.join(_r.name for _r in self.reporters)}\n")
if DUMP_XML_IN_SOURCE:
try:
xml = self.get_as_xml()
except NotImplementedError:
pass
else:
c.write("/*\n")
c.write(xml)
c.write("\n")
c.write("*/\n")
if hasattr(self, "HACK_IS_STATIC_ON"):
c.write("static ")
c.write(self.get_c_declaration_base())
if self.braces_in_source:
c.write(" = {\n")
else:
c.write(" =\n")
c.write(f'#include "{self.inc_c_path}"\n')
if self.braces_in_source:
c.write("};\n")
else:
c.write(";\n")
return True
def write_c_declaration(self, h: io.TextIOBase) -> None:
if hasattr(self, "HACK_IS_STATIC_ON"):
h.write("static ")
else:
h.write("extern ")
h.write(self.get_c_declaration_base())
h.write(";\n")
@reprlib.recursive_repr()
def __repr__(self):
return (
self.__class__.__qualname__
+ "("
+ ", ".join(
(
repr(self.name),
(
f"0x{self.range_start:08X}-"
+ (
f"0x{self.range_end:08X}"
if self.range_end is not None
else "..."
)
),
f"file.name={self.file.name!r}",
)
)
+ ")"
)
def __rich_repr__(self) -> Generator[str | tuple[str, Any], None, None]:
yield self.name
yield (
f"0x{self.range_start:08X}-"
+ (f"0x{self.range_end:08X}" if self.range_end is not None else "...")
)
yield "file.name", self.file.name
__rich_repr__.angular = True
class ZeroPaddingResource(Resource):
def __init__(
self,
file: File,
range_start: int,
range_end: int,
name: str,
*,
include_in_source=True,
):
super().__init__(file, range_start, range_end, name)
self.include_in_source = include_in_source
def try_parse_data(self, memory_context):
# Check the data is 0s
assert self.file.data is not None
if set(self.file.data[self.range_start : self.range_end]) != {0}:
raise ResourceParseImpossible("Not zero padding")
return RESOURCE_PARSE_SUCCESS
def get_c_reference(self, resource_offset):
raise ValueError("Referencing zero padding should not happen")
def get_c_includes(self):
return ("ultra64.h",)
def write_extracted(self, memory_context):
# No need to extract zeros
pass
def get_c_declaration_base(self):
assert self.range_end is not None
length_bytes = self.range_end - self.range_start
assert length_bytes > 0
return f"u8 {self.symbol_name}[{length_bytes}]"
def write_c_definition(self, c: io.TextIOBase):
if self.include_in_source:
c.write(self.get_c_declaration_base())
c.write(" = { 0 };\n")
return True
else:
return False
def write_c_declaration(self, h: io.TextIOBase):
# No need to declare zeros
pass
class BinaryBlobResource(Resource):
needs_build = True
extracted_path_suffix = ""
def try_parse_data(self, memory_context):
# Nothing specific to do
return RESOURCE_PARSE_SUCCESS
def get_as_xml(self):
assert self.range_end is not None
return f"""\
<Blob Name="{self.symbol_name}" Size="0x{self.range_end - self.range_start:X}" Offset="0x{self.range_start:X}"/>"""
def get_c_reference(self, resource_offset):
return f"&{self.symbol_name}[{resource_offset}]"
def get_filename_stem(self):
return super().get_filename_stem() + ".bin"
def get_h_includes(self):
return ("ultra64.h",)
def write_extracted(self, memory_context):
assert self.file.data is not None
assert self.range_end is not None
data = self.file.data[self.range_start : self.range_end]
assert len(data) == self.range_end - self.range_start
self.extract_to_path.write_bytes(data)
def get_c_declaration_base(self):
return f"u8 {self.symbol_name}[]"
def get_c_expression_length(self, resource_offset: int):
raise Exception(
"A binary blob resource could support returning a C expression for its length, "
"but it would be error-prone due to the 'anything goes' nature of binary blobs. "
"Make a dedicated resource instead"
)
|