diff --git a/asset_dumper.py b/asset_dumper.py
index 811ff69..d360f36 100644
--- a/asset_dumper.py
+++ b/asset_dumper.py
@@ -52,7 +52,7 @@ def dump_all_textures_in_gcm(self, gcm: GCM, out_dir):
for _ in self.dump_all_textures_in_rarc(rarc, out_path, display_path_prefix=file_path):
continue
elif file_ext == ".bti":
- out_path = os.path.join(out_dir, rel_dir, base_name + ".png")
+ out_path = os.path.join(out_dir, rel_dir, base_name)
bti = BTI(gcm.get_changed_file_data(file_path))
self.dump_texture(bti, out_path)
elif file_ext in [".bmd", ".bdl", ".bmt"]:
@@ -85,7 +85,7 @@ def dump_all_textures_in_rarc(self, rarc: RARC, out_dir, display_path_prefix=Non
try:
if file_ext == ".bti":
- out_path = os.path.join(out_dir, rel_dir, base_name + ".png")
+ out_path = os.path.join(out_dir, rel_dir, base_name)
bti = rarc.get_file(file_entry.name, BTI)
self.dump_texture(bti, out_path)
elif file_ext in [".bmd", ".bdl", ".bmt"]:
@@ -112,7 +112,7 @@ def dump_all_textures_in_tex1(self, tex1: TEX1, out_dir):
if len(textures) == 0:
continue
- images: list[Image.Image] = []
+ unique_image_count = 0
for i, texture in enumerate(textures):
is_duplicate = False
for prev_texture in textures[:i]:
@@ -122,34 +122,31 @@ def dump_all_textures_in_tex1(self, tex1: TEX1, out_dir):
break
if is_duplicate:
continue
-
- image = texture.render()
-
- if image in images:
- # A duplicate not detected by is_visually_equal_to().
- # For example, if one version uses C8 format with RGB565 palette and the other uses RGB565 format, they can be identical, but is_visually_equal_to() will not detect this.
- continue
-
- images.append(image)
+ unique_image_count += 1
- has_different_duplicates = len(images) > 1
- for i, image in enumerate(images):
+ has_different_duplicates = unique_image_count > 1
+ for i, texture in enumerate(textures):
+ dupe_tex_name = texture_name
if has_different_duplicates:
# If there's more than one unique version of this texture, append _dupe0 _dupe1 etc to all the images.
- out_path = os.path.join(out_dir, texture_name + "_dupe%d.png" % i)
- else:
- out_path = os.path.join(out_dir, texture_name + ".png")
+ dupe_tex_name += ".dupe%d" % i
- image.save(out_path)
+ out_path = os.path.join(out_dir, dupe_tex_name)
- self.succeeded_file_count += 1
+ self.dump_texture(texture, out_path)
def dump_texture(self, bti: BTI, out_path):
out_dir = os.path.dirname(out_path)
- if not os.path.isdir(out_dir):
- os.makedirs(out_dir)
+ os.makedirs(out_dir, exist_ok=True)
- image = bti.render()
- image.save(out_path)
+ for i in range(bti.mipmap_count):
+ mip_out_path = out_path
+ if (bti.mipmap_count) > 1:
+ mip_out_path += ".mip%d" % i
+ mip_out_path += ".png"
+ # if os.path.isfile(mip_out_path):
+ # continue
+ image = bti.render_mipmap(i)
+ image.save(mip_out_path)
self.succeeded_file_count += 1
diff --git a/gcft_ui/bti_tab.py b/gcft_ui/bti_tab.py
index a778c77..be0466a 100644
--- a/gcft_ui/bti_tab.py
+++ b/gcft_ui/bti_tab.py
@@ -28,6 +28,7 @@
("min_lod", 1),
("max_lod", 1),
("lod_bias", 2),
+ ("mipmap_count", 1),
]
class BTITab(QWidget):
@@ -41,6 +42,8 @@ def __init__(self):
self.ui.export_bti.setDisabled(True)
self.ui.export_bti_image.setDisabled(True)
+ self.ui.bti_curr_mipmap.setDisabled(True)
+ self.ui.replace_bti_mipmap.setDisabled(True)
self.ui.bti_file_size.setText("")
self.ui.bti_resolution.setText("")
@@ -57,6 +60,8 @@ def __init__(self):
self.ui.import_bti_image.clicked.connect(self.import_bti_image)
self.ui.export_bti_image.clicked.connect(self.export_bti_image)
self.ui.import_bti_from_bnr.clicked.connect(self.import_bti_from_bnr)
+ self.ui.bti_curr_mipmap.currentIndexChanged.connect(self.reload_bti_image)
+ self.ui.replace_bti_mipmap.clicked.connect(self.replace_bti_mipmap_image)
for field_name, field_enum in BTI_ENUM_FIELDS:
widget_name = "bti_" + field_name
@@ -116,6 +121,13 @@ def import_bti_from_bnr(self):
file_type="BNR", file_filter="All files (*.*)"
)
+ def replace_bti_mipmap_image(self):
+ self.window().generic_do_gui_file_operation(
+ op_callback=self.replace_bti_mipmap_image_by_path,
+ is_opening=True, is_saving=False, is_folder=False,
+ file_type="image", file_filter="PNG Files (*.png)"
+ )
+
def import_bti_by_path(self, bti_path):
with open(bti_path, "rb") as f:
@@ -126,6 +138,8 @@ def import_bti_by_path(self, bti_path):
self.import_bti_by_data(data, bti_name)
def import_bti_by_data(self, data, bti_name):
+ self.ui.bti_curr_mipmap.setCurrentIndex(0)
+
prev_bti = self.bti
self.bti = BTI(data)
@@ -187,9 +201,15 @@ def import_bti_by_data(self, data, bti_name):
self.ui.export_bti.setDisabled(False)
self.ui.export_bti_image.setDisabled(False)
+ self.ui.replace_bti_mipmap.setDisabled(False)
+ self.ui.bti_curr_mipmap.setDisabled(False)
def reload_bti_image(self):
- self.bti_image = self.bti.render()
+ selected_mipmap_index = self.ui.bti_curr_mipmap.currentIndex()
+ selected_mipmap_index = max(0, selected_mipmap_index)
+ selected_mipmap_index = min(self.bti.mipmap_count-1, selected_mipmap_index)
+
+ self.bti_image = self.bti.render_mipmap(selected_mipmap_index)
image_bytes = self.bti_image.tobytes('raw', 'BGRA')
qimage = QImage(image_bytes, self.bti_image.width, self.bti_image.height, QImage.Format_ARGB32)
@@ -210,6 +230,14 @@ def reload_bti_image(self):
self.ui.bti_image_label.setFixedWidth(self.bti_image.width)
self.ui.bti_image_label.setFixedHeight(self.bti_image.height)
self.ui.bti_image_label.show()
+
+ self.ui.bti_curr_mipmap.blockSignals(True)
+ self.ui.bti_curr_mipmap.clear()
+ for i in range(self.bti.mipmap_count):
+ _, _, mipmap_width, mipmap_height = self.bti.get_mipmap_offset_and_size(i)
+ self.ui.bti_curr_mipmap.addItem(f"{i}: {mipmap_width}x{mipmap_height}")
+ self.ui.bti_curr_mipmap.setCurrentIndex(selected_mipmap_index)
+ self.ui.bti_curr_mipmap.blockSignals(False)
def export_bti_by_path(self, bti_path):
self.bti.save_changes()
@@ -290,6 +318,26 @@ def import_bti_from_bnr_by_data(self, data, bti_name):
self.import_bti_by_data(data, bti_name)
+ def replace_bti_mipmap_image_by_path(self, image_path):
+ _, _, width, height = self.bti.get_mipmap_offset_and_size(self.ui.bti_curr_mipmap.currentIndex())
+ image = Image.open(image_path)
+ if image.width != width or image.height != height:
+ QMessageBox.warning(self,
+ "Invalid mipmap size",
+ "The image you selected has the wrong size for this mipmap.\n" +
+ "Each mipmap must be exactly half the size of the previous one.\n" +
+ f"Expected size: {width}x{height}\n" +
+ f"Instead got: {image.width}x{image.height}\n\n" +
+ "If you want to completely replace all mipmaps with an image of a different size, select \"Import Image\" instead."
+ )
+
+ self.bti.replace_mipmap(self.ui.bti_curr_mipmap.currentIndex(), image)
+
+ self.bti.save_changes()
+ print(self.bti.num_colors)
+
+ self.reload_bti_image()
+
def bti_header_field_changed(self):
for field_name, field_enum in BTI_ENUM_FIELDS:
widget_name = "bti_" + field_name
@@ -324,13 +372,20 @@ def bti_header_field_changed(self):
QMessageBox.warning(self, "Invalid value", "\"%s\" is not a valid decimal number." % new_str_value)
new_value = old_value
- if new_value < 0:
- QMessageBox.warning(self, "Invalid value", "Value cannot be negative.")
+ if field_name == "mipmap_count":
+ min_value = 1
+ max_value = self.bti.get_max_valid_mipmap_count()
+ else:
+ min_value = 0
+ max_value = max_value = (2**(byte_size*8)) - 1
+
+ if new_value < min_value:
+ QMessageBox.warning(self, "Invalid value", f"Value is too small (minimum value: 0x{min_value:X})")
new_value = old_value
- if new_value >= 2**(byte_size*8):
+ if new_value > max_value:
QMessageBox.warning(
self, "Invalid value",
- "Value is too large to fit in field %s (maximum value: 0x%X)" % (field_name, (2**(byte_size*8))-1)
+ "Value is too large to fit in field %s (maximum value: 0x%X)" % (field_name, max_value)
)
new_value = old_value
diff --git a/gcft_ui/bti_tab.ui b/gcft_ui/bti_tab.ui
index 6cdeff3..cfbf2a9 100644
--- a/gcft_ui/bti_tab.ui
+++ b/gcft_ui/bti_tab.ui
@@ -30,10 +30,17 @@
+ -
+
+
+ Replace Mipmap
+
+
+
-
- Import From GameCube Banner (.bnr)
+ Import From Banner (.bnr)
@@ -58,14 +65,17 @@
-
+ -
+
+
-
-
+
-
-
+
0
-
@@ -145,8 +155,8 @@
0
0
- 591
- 433
+ 570
+ 431
@@ -393,6 +403,40 @@
+ -
+
+
+ Num Mipmaps
+
+
+
+ -
+
+
+
+ 35
+ 16777215
+
+
+
+
+ -
+
+
+ Selected Mipmap
+
+
+
+ -
+
+
+
+ 80
+ 0
+
+
+
+
diff --git a/gcft_ui/uic/ui_bti_tab.py b/gcft_ui/uic/ui_bti_tab.py
index d7ade99..1815b5a 100644
--- a/gcft_ui/uic/ui_bti_tab.py
+++ b/gcft_ui/uic/ui_bti_tab.py
@@ -3,7 +3,7 @@
################################################################################
## Form generated from reading UI file 'bti_tab.ui'
##
-## Created by: Qt User Interface Compiler version 6.4.0
+## Created by: Qt User Interface Compiler version 6.5.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
@@ -39,6 +39,11 @@ def setupUi(self, BTITab):
self.horizontalLayout_5.addWidget(self.import_bti_image)
+ self.replace_bti_mipmap = QPushButton(BTITab)
+ self.replace_bti_mipmap.setObjectName(u"replace_bti_mipmap")
+
+ self.horizontalLayout_5.addWidget(self.replace_bti_mipmap)
+
self.import_bti_from_bnr = QPushButton(BTITab)
self.import_bti_from_bnr.setObjectName(u"import_bti_from_bnr")
@@ -64,6 +69,11 @@ def setupUi(self, BTITab):
self.horizontalLayout.addWidget(self.widget)
+ self.widget_2 = QWidget(BTITab)
+ self.widget_2.setObjectName(u"widget_2")
+
+ self.horizontalLayout.addWidget(self.widget_2)
+
self.verticalLayout.addLayout(self.horizontalLayout)
@@ -92,7 +102,7 @@ def setupUi(self, BTITab):
self.bti_image_scroll_area.setWidgetResizable(True)
self.scrollAreaWidgetContents = QWidget()
self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents")
- self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 591, 433))
+ self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 570, 431))
self.gridLayout = QGridLayout(self.scrollAreaWidgetContents)
self.gridLayout.setObjectName(u"gridLayout")
self.bti_image_label = QLabel(self.scrollAreaWidgetContents)
@@ -272,9 +282,32 @@ def setupUi(self, BTITab):
self.formLayout.setWidget(13, QFormLayout.FieldRole, self.bti_lod_bias)
+ self.label_14 = QLabel(BTITab)
+ self.label_14.setObjectName(u"label_14")
+
+ self.formLayout.setWidget(14, QFormLayout.LabelRole, self.label_14)
+
+ self.bti_mipmap_count = QLineEdit(BTITab)
+ self.bti_mipmap_count.setObjectName(u"bti_mipmap_count")
+ self.bti_mipmap_count.setMaximumSize(QSize(35, 16777215))
+
+ self.formLayout.setWidget(14, QFormLayout.FieldRole, self.bti_mipmap_count)
+
+ self.label_16 = QLabel(BTITab)
+ self.label_16.setObjectName(u"label_16")
+
+ self.formLayout.setWidget(15, QFormLayout.LabelRole, self.label_16)
+
+ self.bti_curr_mipmap = QComboBox(BTITab)
+ self.bti_curr_mipmap.setObjectName(u"bti_curr_mipmap")
+ self.bti_curr_mipmap.setMinimumSize(QSize(80, 0))
+
+ self.formLayout.setWidget(15, QFormLayout.FieldRole, self.bti_curr_mipmap)
+
self.horizontalLayout_8.addLayout(self.formLayout)
+ self.horizontalLayout_8.setStretch(0, 1)
self.verticalLayout.addLayout(self.horizontalLayout_8)
@@ -288,7 +321,8 @@ def retranslateUi(self, BTITab):
BTITab.setWindowTitle(QCoreApplication.translate("BTITab", u"Form", None))
self.import_bti.setText(QCoreApplication.translate("BTITab", u"Import BTI", None))
self.import_bti_image.setText(QCoreApplication.translate("BTITab", u"Import Image", None))
- self.import_bti_from_bnr.setText(QCoreApplication.translate("BTITab", u"Import From GameCube Banner (.bnr)", None))
+ self.replace_bti_mipmap.setText(QCoreApplication.translate("BTITab", u"Replace Mipmap", None))
+ self.import_bti_from_bnr.setText(QCoreApplication.translate("BTITab", u"Import From Banner (.bnr)", None))
self.export_bti.setText(QCoreApplication.translate("BTITab", u"Export BTI", None))
self.export_bti_image.setText(QCoreApplication.translate("BTITab", u"Export Image", None))
self.bti_image_label.setText("")
@@ -310,5 +344,7 @@ def retranslateUi(self, BTITab):
self.label_8.setText(QCoreApplication.translate("BTITab", u"Min LOD", None))
self.label_9.setText(QCoreApplication.translate("BTITab", u"Max LOD", None))
self.label_10.setText(QCoreApplication.translate("BTITab", u"LOD Bias", None))
+ self.label_14.setText(QCoreApplication.translate("BTITab", u"Num Mipmaps", None))
+ self.label_16.setText(QCoreApplication.translate("BTITab", u"Selected Mipmap", None))
# retranslateUi
diff --git a/gclib b/gclib
index 59aed74..7e6e587 160000
--- a/gclib
+++ b/gclib
@@ -1 +1 @@
-Subproject commit 59aed743a5768001c848822994eb0bd65f7e27f1
+Subproject commit 7e6e587a8e747dab439c112c25370a9146f12539