Skip to content

Instantly share code, notes, and snippets.

@infval
Last active December 23, 2024 14:32
Show Gist options
  • Save infval/3d12781f57e891d2905212f5aaebc6c5 to your computer and use it in GitHub Desktop.
Save infval/3d12781f57e891d2905212f5aaebc6c5 to your computer and use it in GitHub Desktop.
[Wii] Coraline | dxt Converter / Unpacker / Packer / Extractor | Magic: 30 81 80 00 / \x30\x81\x80\x00 / 30818000 / 308180
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
[Wii] Coraline
.dxt <-> PNG
Requirements:
pip install -U pillow
Usage:
# ./*.dxt -> ./*.dxt.png
script.py
# filename.dxt -> filename.dxt.png
script.py filename.dxt
# filename.dxt.png + filename.dxt (header) -> filename.dxt
script.py filename.dxt.png
Useful Tool:
Color quantizer - http://x128.ho.ua/color-quantizer.html
"""
__version__ = "1.1"
__author__ = "infval"
import sys
from pathlib import Path
from struct import unpack
from collections import OrderedDict
from PIL import Image
HEADER_SIZE = 0x80
class Texture:
"""
[Wii] Color Index 8-bits (C8) + Palette RGBA8
"""
def __init__(self, path, offset, w, h, pal_offset):
self.path = path
self.offset = offset
self.w = w
self.h = h
self.pal_offset = pal_offset
def to_png(self, png_path):
im = Image.new("RGBA", (self.w, self.h))
pixels = im.load()
with open(self.path, "rb") as f:
f.seek(self.offset)
b = f.read(self.w * self.h)
f.seek(self.pal_offset)
b_pal = f.read(256 * 4)
pal = []
for i in range(256):
index = i * 2
pal.append((b_pal[index+1], # R
b_pal[index+0], # G
b_pal[index+1+0x200], # B
b_pal[index+0+0x200])) # A
tw = 8
th = 4
bytes_in_tile = tw * th
bytes_in_row = self.w * th
x = 0
y = 0
for i in range(self.w * self.h):
index = i // bytes_in_row * bytes_in_row
index += x // tw * bytes_in_tile
index += (y % th) * tw + x % tw
c = b[index]
pixels[x, y] = pal[c]
x += 1
if x == self.w:
x = 0
y += 1
im.save(png_path, "PNG")
print("Unpacked: {} -> {}".format(self.path, png_path))
def from_png(self, bin_path, base_path=None):
im = Image.open(self.path).convert("RGBA")
pixels = im.load()
self.w, self.h = im.size
if not base_path:
# Raw
self.offset = 0
self.pal_offset = self.w * self.h
b = bytearray(self.w * self.h + 256 * 4)
else:
with open(base_path, "rb") as f:
b = bytearray(f.read())
pal = OrderedDict()
pal_i = 0
b_img = bytearray(self.w * self.h)
tw = 8
th = 4
bytes_in_tile = tw * th
bytes_in_row = self.w * th
i = 0
for y in range(self.h):
for x in range(self.w):
c = pixels[x, y]
if c not in pal:
if len(pal) >= 256:
print("Too many colors! Max: 256.", self.path)
sys.exit(1)
else:
pal[c] = pal_i
pal_i += 1
index = i // bytes_in_row * bytes_in_row
index += x // tw * bytes_in_tile
index += (y % th) * tw + x % tw
b_img[index] = pal[c]
i += 1
b[self.offset: self.offset + self.w * self.h] = b_img
pal = list(pal.keys())
pal += [(0, 0, 0, 0)] * (256 - len(pal)) # Unused colors
for i in range(256):
index1 = self.pal_offset + i * 2
index2 = self.pal_offset + i * 2 + 0x200
cR, cG, cB, cA = pal[i]
b[index2+0] = cA
b[index2+1] = cB
b[index1+0] = cG
b[index1+1] = cR
with open(bin_path, "wb") as f:
f.write(b)
print("Packed: {} -> {}".format(self.path, bin_path))
def dxt_to_png(path):
with open(path, "rb") as f:
f.seek(0x0A)
w, h = unpack("<BB", f.read(2))
w = 1 << w
h = 1 << h
png_path = path.with_name(path.name + ".png")
Texture(path, HEADER_SIZE, w, h, HEADER_SIZE + w * h).to_png(png_path)
def png_to_dxt(path):
bin_path = path.with_suffix('')
with open(bin_path, "rb") as f:
f.seek(0x0A)
w, h = unpack("<BB", f.read(2))
w = 1 << w
h = 1 << h
Texture(path, HEADER_SIZE, None, None, HEADER_SIZE + w * h).from_png(bin_path, bin_path)
if __name__ == '__main__':
if len(sys.argv) == 1:
path = Path(".")
for p in path.glob("*.dxt"):
dxt_to_png(p)
else:
p = Path(sys.argv[1])
if p.suffix == ".png":
png_to_dxt(p)
else:
dxt_to_png(p)
@Rypie109
Copy link

Rypie109 commented Jul 8, 2024

I downloaded the script, but I do not think I am using python 3. I will try to load the right python version somehow.

@romhacking
Copy link

I will try to load the right python version somehow

I think another reason. I checked now on version 3.12.4 (https://www.python.org), there is no error.

@Rypie109
Copy link

Rypie109 commented Jul 8, 2024

What are you running the script through? Cmd? Python from cmd? Shell?

@romhacking
Copy link

What are you running the script through?

cmd. PowerShell works too.

@Rypie109
Copy link

Rypie109 commented Jul 8, 2024

Got it, now I just have to figure out how to switch my py version from 3.11 to 3.12 in cmd. Thanks for answering all my questions, I really appreciate this, and the script!

@Rypie109
Copy link

Rypie109 commented Jul 8, 2024

I figured it out!

@Rypie109
Copy link

Rypie109 commented Jul 8, 2024

I am a bit confused on what I should be doing on color quantizer, as when I select 256 from the dropdown menu, and press, ok, it switches to 146. Also, you mention that the script wont work if I dont reduce it to 256, I am assuming you are referring to your python script, but that doesnt seem to have any issues, maybe im understanding it wrong.

@romhacking
Copy link

what I should be doing on color quantizer, as when I select 256 from the dropdown menu, and press, ok, it switches to 146

"Color quantizer" is needed if you want to convert the modified PNG back to a .dxt file and the PNG will have more than 256 colors (my script will display the message "Too many colors! Max: 256."). You have fewer than 256 colors, so the program is not needed in this case.

@Rypie109
Copy link

Rypie109 commented Jul 9, 2024

Ok, I have no need to recompress the textures. I have figured this all out, and now I need to find out how to make a script to automaticly split at certain points. Any ideas where to start/where to ask?

@romhacking
Copy link

I need to find out how to make a script to automaticly split at certain points

Extracting .dxt files from .wii is easy. Here is the script, name it wii_to_dxt.py:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
[Wii] Coraline
  .wii -> .dxt

Usage:
# ./*.wii -> ./*.dxt
  script.py
# filename.wii -> filename_*.dxt
  script.py filename.wii
"""
__version__ = "1.0"
__author__  = "infval"

import sys
from pathlib import Path

def wii_to_dxt(path):
    print(f"# {path}")
    data = path.read_bytes()
    ind = -1
    while True:
        ind = data.find(bytes.fromhex("30 81 80 00"), ind + 1)
        if ind == -1:
            break
        w, h = data[ind + 0xA], data[ind + 0xB]
        size = (1 << w) * (1 << h) + 0x480
        p = path.with_name(path.stem + f"_{ind:06X}.dxt")
        p.write_bytes(data[ind : ind + size])
        print(f"> {p}")

if __name__ == '__main__':
    if len(sys.argv) == 1:
        path = Path(".")
        for p in path.glob("*.wii"):
            wii_to_dxt(p)
    else:
        p = Path(sys.argv[1])
        wii_to_dxt(p)

To extract all .wii in the script folder, run the script:
wii_to_dxt.py
To unpack a single .wii, pass the file path as a command line argument:
wii_to_dxt.py global.wii

@Rypie109
Copy link

...YOU MADLAD, thank you so much!

@Rypie109
Copy link

You have been monumental in this project, and I made sure to tell people that this stuff was by you. The last thing for me to do to technically finish this 235 day project is to get the rigs, but I do not expect you to do that at all, as I don't want to waste your time, and you have already done so much for me already.

@Rypie109
Copy link

@romhacking I have some questions. What made you create this script? Any motive? Are you also interested in datamining this game?

@romhacking
Copy link

@Rypie109 The script dxt_to_png.py was made for the Russian translation of the game Coraline, that's all. I mostly do small hacks for NES games.

@Rypie109
Copy link

@romhacking That is very interesting! I am assuming this was a fan translation?

@romhacking
Copy link

@Rypie109 Of course it's an unofficial translation. I only made this script.

@Rypie109
Copy link

Hi, it's me again. Is there a way to make the dxt_to_png script work with a game that has the exact same form
l_ben1_t_2F1100
at, yet has a slightly different formatting? Here is a file I copied out, same header same everthing, and when converted it looks like this. Heres a sample dxt file, just rename it from .svg to .dxt
l_ben1_t_2F1100 dxt

@Rypie109
Copy link

The image I attached is not properly converted, open it in a new tab or save it to see

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment