Skip to content

Commit 6934f24

Browse files
Implemented exporting texture and model files (Models untested)
1 parent fcf5d04 commit 6934f24

15 files changed

Lines changed: 565 additions & 78 deletions

docs/release-notes/0.0.2-beta.1.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- Select an active project to quick add files to
1111
- Font viewer
1212
- Set project root
13+
- Full export support in various formats (Textures, Models (untested))
1314

1415
# TKMM Integration
1516
- TKMM option support

icons/outline-view-active.svg

Lines changed: 0 additions & 12 deletions
This file was deleted.

icons/outline-view-inactive.svg

Lines changed: 0 additions & 12 deletions
This file was deleted.

package.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,10 @@
364364
"command": "totk-editor.dumpExport",
365365
"title": "Export..."
366366
},
367+
{
368+
"command": "totk-editor.explorerExport",
369+
"title": "Export..."
370+
},
367371
{
368372
"command": "totk-editor.exportYaml",
369373
"title": "TKVSC: Export Current File to YAML"
@@ -621,12 +625,12 @@
621625
},
622626
{
623627
"command": "totk-editor.archiveExport",
624-
"when": "view == totk-editor.archives && viewItem == archiveVirtualFile",
628+
"when": "view == totk-editor.archives && (viewItem == archiveVirtualFile || viewItem == archiveFile || viewItem == archivePackage)",
625629
"group": "5_export@1"
626630
},
627631
{
628632
"command": "totk-editor.dumpExport",
629-
"when": "view == totk-editor.gameDump && viewItem == dumpVirtualFile",
633+
"when": "view == totk-editor.gameDump && (viewItem == dumpVirtualFile || viewItem == dumpFile || viewItem == dumpArchive)",
630634
"group": "5_export@1"
631635
},
632636
{
@@ -665,6 +669,11 @@
665669
"command": "totk-editor.archivePaste",
666670
"when": "totk-editor.archiveClipboardNotEmpty",
667671
"group": "9_cutcopypaste"
672+
},
673+
{
674+
"command": "totk-editor.explorerExport",
675+
"when": "resourceExtname == .txtg || resourceExtname == .bntx || resourceExtname == .bftex || resourceExtname == .byml || resourceExtname == .bgyml || resourceExtname == .bfmdl || resourceExtname == .bfska",
676+
"group": "5_export@1"
668677
}
669678
]
670679
},

python/bntx_renderer.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,3 +487,29 @@ def render_texture_to_png(bntx_data: bytes, texture_name: str) -> str | None:
487487

488488
_log(f"Saved PNG: {tmp_path}")
489489
return tmp_path
490+
491+
def extract_texture_linear(bntx_data: bytes, texture_name: str) -> tuple[int, int, str, bytes, int, int] | None:
492+
"""Extract raw deswizzled linear bytes and metadata for native export."""
493+
textures = _parse_textures(bntx_data)
494+
tex = None
495+
for t in textures:
496+
if t.name == texture_name:
497+
tex = t
498+
break
499+
if tex is None:
500+
return None
501+
502+
fmt_name, bpp, blk_w, blk_h, decoder_key = _fmt_info(tex.format_id)
503+
if tex.data_offset <= 0 or tex.data_size <= 0:
504+
return None
505+
506+
raw_data = bntx_data[tex.data_offset : tex.data_offset + tex.data_size]
507+
508+
if tex.tile_mode == 1:
509+
linear = _deswizzle_pitch_linear(tex.width, tex.height, blk_w, blk_h, bpp, raw_data)
510+
else:
511+
linear = _deswizzle_block_linear(
512+
tex.width, tex.height, blk_w, blk_h, bpp, tex.block_height_log2, raw_data
513+
)
514+
515+
return tex.width, tex.height, decoder_key, linear, tex.mip_count, tex.format_id

python/totk_bridge.py

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,13 @@ def _read_txtg_texture_result(file_data: bytes, texture_name: str, logical_path:
106106

107107

108108
def export_archive_file_to_temp(archive_path: str, internal_path: str, romfs_path: str = "") -> str:
109-
file_data = read_archive_file_bytes(archive_path, internal_path, romfs_path)
110-
file_name = Path(internal_path).name or "file.bin"
109+
if not internal_path:
110+
file_data = Path(archive_path).read_bytes()
111+
file_name = Path(archive_path).name or "file.bin"
112+
else:
113+
file_data = read_archive_file_bytes(archive_path, internal_path, romfs_path)
114+
file_name = Path(internal_path).name or "file.bin"
115+
111116
safe_name = "".join(ch if ch.isalnum() or ch in "._-" else "_" for ch in file_name)
112117
fd, tmp_path = tempfile.mkstemp(prefix="totk-tool-", suffix=f"-{safe_name}")
113118
with os.fdopen(fd, "wb") as out:
@@ -587,6 +592,142 @@ def main():
587592
)
588593
)
589594

595+
elif command == "export-converted":
596+
internal_path = sys.argv[3]
597+
target_ext = sys.argv[4].lower()
598+
if internal_path:
599+
file_data = read_archive_file_bytes(archive_path, internal_path, romfs_path)
600+
logical_path = internal_path
601+
else:
602+
file_data = Path(archive_path).read_bytes()
603+
logical_path = archive_path
604+
605+
kind = _file_kind(logical_path, file_data, romfs_path)
606+
607+
out_path = export_archive_file_to_temp(archive_path, internal_path, romfs_path)
608+
609+
if target_ext in [".png", ".jpg", ".jpeg", ".bmp", ".tga", ".dds"]:
610+
png_path = None
611+
is_txtg = kind == "txtg" or logical_path.lower().endswith(".txtg") or file_data[4:8] == b"6PK0"
612+
613+
bntx_data = None
614+
tex_name = None
615+
616+
if is_txtg:
617+
from txtg_reader import read_txtg_texture_result
618+
tex_name = Path(logical_path).name or "texture"
619+
res = read_txtg_texture_result(file_data, tex_name)
620+
png_path = res.get("pngPath")
621+
else:
622+
bntx_ctx = _resolve_bntx_for_read(archive_path, internal_path, romfs_path)
623+
if bntx_ctx is not None:
624+
bntx_data, tex_name = bntx_ctx
625+
from bntx_renderer import render_texture_to_png
626+
png_path = render_texture_to_png(bntx_data, tex_name)
627+
628+
if png_path:
629+
try:
630+
from PIL import Image
631+
img = Image.open(png_path)
632+
fd, cvt_path = tempfile.mkstemp(prefix="totk-cvt-", suffix=target_ext)
633+
os.close(fd)
634+
635+
if target_ext == ".dds":
636+
native_dds_bytes = None
637+
if not is_txtg and bntx_data is not None and tex_name is not None:
638+
from bntx_renderer import extract_texture_linear
639+
extracted = extract_texture_linear(bntx_data, tex_name)
640+
if extracted is not None:
641+
tex_width, tex_height, decoder_key, linear, mip_count, format_id = extracted
642+
if decoder_key.startswith("bc"):
643+
dxgi_map = {
644+
0x1A: 72 if (format_id & 0xFF) == 0x06 else 71,
645+
0x1B: 75 if (format_id & 0xFF) == 0x06 else 74,
646+
0x1C: 78 if (format_id & 0xFF) == 0x06 else 77,
647+
0x1D: 81 if (format_id & 0xFF) == 0x02 else 80,
648+
0x1E: 84 if (format_id & 0xFF) == 0x02 else 83,
649+
0x1F: 96 if (format_id & 0xFF) == 0x02 else 95,
650+
0x20: 99 if (format_id & 0xFF) == 0x06 else 98,
651+
}
652+
dxgi_format = dxgi_map.get(format_id >> 8)
653+
if dxgi_format is not None:
654+
header = bytearray(148)
655+
header[0:4] = b"DDS "
656+
header[4:8] = (124).to_bytes(4, "little")
657+
header[8:12] = (0x1 | 0x2 | 0x4 | 0x1000 | 0x80000).to_bytes(4, "little")
658+
header[12:16] = tex_height.to_bytes(4, "little")
659+
header[16:20] = tex_width.to_bytes(4, "little")
660+
header[20:24] = len(linear).to_bytes(4, "little")
661+
header[24:28] = (1).to_bytes(4, "little")
662+
header[28:32] = (mip_count).to_bytes(4, "little")
663+
664+
header[76:80] = (32).to_bytes(4, "little")
665+
header[80:84] = (0x4).to_bytes(4, "little")
666+
header[84:88] = b"DX10"
667+
668+
header[108:112] = (0x1000).to_bytes(4, "little")
669+
670+
header[128:132] = dxgi_format.to_bytes(4, "little")
671+
header[132:136] = (3).to_bytes(4, "little")
672+
header[136:140] = (0).to_bytes(4, "little")
673+
header[140:144] = (1).to_bytes(4, "little")
674+
header[144:148] = (0).to_bytes(4, "little")
675+
676+
native_dds_bytes = bytes(header) + linear
677+
678+
if native_dds_bytes is not None:
679+
with open(cvt_path, "wb") as f:
680+
f.write(native_dds_bytes)
681+
else:
682+
img = img.convert("RGBA")
683+
width, height = img.size
684+
pixels = img.tobytes("raw", "RGBA")
685+
header = bytearray(128)
686+
header[0:4] = b"DDS "
687+
header[4:8] = (124).to_bytes(4, "little")
688+
header[8:12] = (0x100F).to_bytes(4, "little")
689+
header[12:16] = height.to_bytes(4, "little")
690+
header[16:20] = width.to_bytes(4, "little")
691+
header[20:24] = (width * 4).to_bytes(4, "little")
692+
header[24:28] = (1).to_bytes(4, "little")
693+
header[28:32] = (1).to_bytes(4, "little")
694+
header[76:80] = (32).to_bytes(4, "little")
695+
header[80:84] = (0x41).to_bytes(4, "little")
696+
header[88:92] = (32).to_bytes(4, "little")
697+
header[92:96] = (0x000000FF).to_bytes(4, "little")
698+
header[96:100] = (0x0000FF00).to_bytes(4, "little")
699+
header[100:104] = (0x00FF0000).to_bytes(4, "little")
700+
header[104:108] = (0xFF000000).to_bytes(4, "little")
701+
header[108:112] = (0x1000).to_bytes(4, "little")
702+
with open(cvt_path, "wb") as f:
703+
f.write(header)
704+
f.write(pixels)
705+
else:
706+
if target_ext in [".jpg", ".jpeg"] and img.mode in ("RGBA", "P"):
707+
img = img.convert("RGB")
708+
img.save(cvt_path)
709+
710+
img.close()
711+
os.unlink(png_path)
712+
if os.path.exists(out_path):
713+
os.unlink(out_path)
714+
out_path = cvt_path
715+
except Exception:
716+
import traceback
717+
traceback.print_exc()
718+
out_path = png_path
719+
720+
elif target_ext in [".yaml", ".yml"]:
721+
if kind == "byml" or kind == "bgyml":
722+
yaml_text = read_byml_content(file_data, internal_path, romfs_path)
723+
fd, yaml_path = tempfile.mkstemp(prefix="totk-cvt-", suffix=target_ext)
724+
os.close(fd)
725+
Path(yaml_path).write_text(yaml_text, encoding="utf-8")
726+
os.unlink(out_path)
727+
out_path = yaml_path
728+
729+
print(json.dumps({"path": out_path}))
730+
590731
elif command == "write-raw":
591732
internal_path = sys.argv[3]
592733
encoded = sys.stdin.read()

python/txtg_reader.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
0x202: ("BC1_UNORM", 8, 4, 4, "bc1"),
2121
0x203: ("BC1_SRGB", 8, 4, 4, "bc1"),
2222
0x302: ("BC1_UNORM", 8, 4, 4, "bc1"),
23+
0x303: ("BC1_SRGB", 8, 4, 4, "bc1"),
2324
0x505: ("BC3_SRGB", 16, 4, 4, "bc3"),
2425
0x602: ("BC4_UNORM", 8, 4, 4, "bc4"),
2526
0x606: ("BC4_UNORM", 8, 4, 4, "bc4"),
@@ -429,19 +430,25 @@ def format_id(self, value: int):
429430

430431
@property
431432
def texture_setting2(self) -> int:
433+
if len(self._data) < 0x48:
434+
return 0
432435
return struct.unpack_from("<I", self._data, 0x44)[0]
433436

434437
@texture_setting2.setter
435438
def texture_setting2(self, value: int):
436-
struct.pack_into("<I", self._data, 0x44, value)
439+
if len(self._data) >= 0x48:
440+
struct.pack_into("<I", self._data, 0x44, value)
437441

438442
@property
439443
def texture_setting4(self) -> int:
444+
if len(self._data) < 0x50:
445+
return 0
440446
return struct.unpack_from("<I", self._data, 0x4C)[0]
441447

442448
@texture_setting4.setter
443449
def texture_setting4(self, value: int):
444-
struct.pack_into("<I", self._data, 0x4C, value)
450+
if len(self._data) >= 0x50:
451+
struct.pack_into("<I", self._data, 0x4C, value)
445452

446453
def replace_image_data(self, raw_surfaces: list[bytes]):
447454
"""

scratch.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import sys
2+
import tempfile
3+
from pathlib import Path
4+
5+
# Add the python directory to sys.path
6+
sys.path.append(str(Path('e:/Nintendo Stuff/TOTK/TOTK Searcher Dev/totk-vscode/totk-vscode/python').resolve()))
7+
8+
from bntx_renderer import get_texture_metadata, render_texture_to_png
9+
from txtg_reader import read_txtg_texture_result
10+
11+
def test_render():
12+
romfs_path = "e:/Nintendo Stuff/ROMS/Switch/Tears Dump/romfs"
13+
bntx_path = romfs_path + "/TexToGo/Armor_001_Lower_Alb.1.txtg"
14+
try:
15+
data = Path(bntx_path).read_bytes()
16+
res = read_txtg_texture_result(data, "Armor_001_Lower_Alb.1")
17+
if res.get("pngPath"):
18+
print(f"Generated PNG at: {res['pngPath']}")
19+
else:
20+
print(f"Failed to generate PNG: {res.get('error')}")
21+
except Exception as e:
22+
print(f"Error: {e}")
23+
24+
if __name__ == "__main__":
25+
test_render()

scratch_bntx.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import sys
2+
from pathlib import Path
3+
4+
# Add the python directory to sys.path
5+
sys.path.append(str(Path('e:/Nintendo Stuff/TOTK/TOTK Searcher Dev/totk-vscode/totk-vscode/python').resolve()))
6+
7+
from bntx_renderer import get_texture_metadata, render_texture_to_png
8+
9+
def test_render():
10+
romfs_path = "e:/Nintendo Stuff/ROMS/Switch/Tears Dump/romfs"
11+
bntx_path = romfs_path + "/Armor/Armor_001_Lower.bntx"
12+
try:
13+
data = Path(bntx_path).read_bytes()
14+
from bntx_renderer import _parse_textures
15+
textures = _parse_textures(data)
16+
for tex in textures:
17+
print(f"Texture: {tex.name}, {tex.width}x{tex.height}")
18+
png_path = render_texture_to_png(data, tex.name)
19+
20+
from PIL import Image
21+
img = Image.open(png_path)
22+
print(f" Generated PNG size: {img.size}")
23+
24+
meta = get_texture_metadata(data, tex.name)
25+
print(f" Metadata size: {meta['imageInfo']['width']}x{meta['imageInfo']['height']}")
26+
break
27+
28+
except Exception as e:
29+
print(f"Error: {e}")
30+
31+
if __name__ == "__main__":
32+
test_render()

scratch_check.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import sys
2+
from PIL import Image
3+
4+
def check():
5+
img = Image.open(r"C:\Users\Aiden\AppData\Local\Temp\totk-txtg-46ue49e_-Armor_001_Lower_Alb.1.png")
6+
pixels = img.load()
7+
w, h = img.size
8+
9+
# Check last 10 columns for transparency
10+
for x in range(w - 20, w):
11+
transparent = True
12+
for y in range(h):
13+
if img.mode == 'RGBA':
14+
if pixels[x, y][3] > 0:
15+
transparent = False
16+
break
17+
if transparent:
18+
print(f"Column {x} is fully transparent!")
19+
else:
20+
print(f"Column {x} has data.")
21+
22+
if __name__ == "__main__":
23+
check()

0 commit comments

Comments
 (0)